refactor(notes): rename title font to heading font

These fonts apply to markdown headings (h1-h6), so "heading" is more
accurate than "title". Renames the chosen*Font field, store action, and
the --heading-font-family CSS variable.

Keeps backward compatibility: .remanso.json still honors the legacy `t`
key (alongside the new `h`), and a saved `chosenTitleFont` in
localStorage is migrated on load so existing preferences survive.
This commit is contained in:
Julien Calixte
2026-06-30 00:41:54 +02:00
parent b8c9d07930
commit 4104e138c1
9 changed files with 116 additions and 29 deletions

View File

@@ -25,16 +25,16 @@ const fontFamilies = computed(
const sortedFontFamilies = computed(() => {
const base = fontFamilies.value
const extras = [
store.userSettings?.chosenTitleFont,
store.userSettings?.chosenHeadingFont,
store.userSettings?.chosenBodyFont
].filter((f): f is string => !!f && !base.includes(f))
return [...base, ...extras].sort((a, b) => a.localeCompare(b))
})
const fontSizes = Array.from({ length: 7 }, (_, i) => `${9 + i * 2}pt`)
const titleFont = computed({
get: () => store.userSettings?.chosenTitleFont,
set: (value) => store.setTitleFont(value!)
const headingFont = computed({
get: () => store.userSettings?.chosenHeadingFont,
set: (value) => store.setHeadingFont(value!)
})
const bodyFont = computed({
get: () => store.userSettings?.chosenBodyFont,
@@ -49,8 +49,8 @@ const fontSize = computed({
<template>
<div class="font-change">
<div>
<label for="title-font" class="font-label">t</label>
<select id="title-font" class="select" v-model="titleFont">
<label for="heading-font" class="font-label">h</label>
<select id="heading-font" class="select" v-model="headingFont">
<option v-for="font in sortedFontFamilies" :key="font" :value="font">
{{ font }}
</option>

View File

@@ -8,7 +8,7 @@ export interface UserSettings extends Model<DataType.UserSettings> {
fontSize?: string
chosenFontSize?: string
backlink?: boolean
chosenTitleFont?: string
chosenHeadingFont?: string
chosenBodyFont?: string
pageWidth?: string
}

View File

@@ -25,6 +25,7 @@ import { getOctokit, runWithAuthRetry } from "./octo"
import {
getFiles,
getRepoPermission,
getUserSettingsContent,
queryFileContent
} from "./repo"
@@ -165,3 +166,58 @@ describe("queryFileContent", () => {
expect(warn).toHaveBeenCalled()
})
})
describe("getUserSettingsContent", () => {
beforeEach(() => {
vi.mocked(runWithAuthRetry).mockReset()
})
afterEach(() => {
vi.restoreAllMocks()
})
const configFiles = [
{ path: ".remanso.json", type: "blob", sha: "CFG" }
] as never
const withConfig = (json: object) => {
const base64 = btoa(JSON.stringify(json))
vi.mocked(runWithAuthRetry).mockImplementation(async (call) => {
const octokit = {
request: vi.fn().mockResolvedValue({ data: { content: base64 } })
}
return call(octokit as never)
})
}
it("maps the `h` key to chosenHeadingFont and `p` to chosenBodyFont", async () => {
withConfig({ h: "Lora", p: "Inter" })
const settings = await getUserSettingsContent("owner", "repo", configFiles)
expect(settings?.chosenHeadingFont).toBe("Lora")
expect(settings?.chosenBodyFont).toBe("Inter")
})
it("falls back to the legacy `t` key for chosenHeadingFont", async () => {
withConfig({ t: "Merriweather", p: "Inter" })
const settings = await getUserSettingsContent("owner", "repo", configFiles)
expect(settings?.chosenHeadingFont).toBe("Merriweather")
})
it("prefers `h` over the legacy `t` when both are present", async () => {
withConfig({ h: "Lora", t: "Merriweather" })
const settings = await getUserSettingsContent("owner", "repo", configFiles)
expect(settings?.chosenHeadingFont).toBe("Lora")
})
it("returns null when there is no .remanso.json", async () => {
expect(
await getUserSettingsContent("owner", "repo", [] as never)
).toBeNull()
})
})

View File

@@ -123,12 +123,14 @@ export const getUserSettingsContent = async (
const raw = JSON.parse(atob(content)) as UserSettings & {
t?: string
h?: string
p?: string
}
const { t, p, ...rest } = raw
const { t, h, p, ...rest } = raw
return {
...rest,
chosenTitleFont: t,
// `h` (heading) is the current key; `t` (title) is kept for back-compat.
chosenHeadingFont: h ?? t,
chosenBodyFont: p
}
}

View File

@@ -131,20 +131,20 @@ describe("userRepo store — synchronous mutations", () => {
expect(persisted.chosenFontFamily).toBe("Inter")
})
it("setFontSize, setTitleFont, setBodyFont each persist their respective field", () => {
it("setFontSize, setHeadingFont, setBodyFont each persist their respective field", () => {
const store = useUserRepoStore()
store.user = "alice"
store.repo = "notes"
store.setFontSize("18px")
store.setTitleFont("Serif")
store.setHeadingFont("Serif")
store.setBodyFont("Sans")
const persisted = JSON.parse(
localStorage.getItem("remanso:layout:alice:notes") as string
)
expect(persisted.chosenFontSize).toBe("18px")
expect(persisted.chosenTitleFont).toBe("Serif")
expect(persisted.chosenHeadingFont).toBe("Serif")
expect(persisted.chosenBodyFont).toBe("Sans")
})
})
@@ -173,6 +173,25 @@ describe("userRepo store — setUserRepo", () => {
expect(store.loadError).toBeNull()
})
it("migrates the legacy chosenTitleFont localStorage key to chosenHeadingFont", async () => {
localStorage.setItem(
"remanso:layout:alice:notes",
JSON.stringify({ chosenTitleFont: "Lora" })
)
const store = useUserRepoStore()
await store.setUserRepo("alice", "notes")
await flushAsync()
await flushAsync()
expect(store.userSettings?.chosenHeadingFont).toBe("Lora")
const persisted = JSON.parse(
localStorage.getItem("remanso:layout:alice:notes") as string
)
expect(persisted.chosenHeadingFont).toBe("Lora")
expect(persisted.chosenTitleFont).toBeUndefined()
})
it("populates files from getFiles on success", async () => {
vi.mocked(getFiles).mockResolvedValue([
{ sha: "a", path: "x.md", type: "blob" } as never

View File

@@ -56,7 +56,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
if (!this.userSettings) return
try {
const {
chosenTitleFont,
chosenHeadingFont,
chosenBodyFont,
chosenFontSize,
chosenFontFamily,
@@ -65,7 +65,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
localStorage.setItem(
`remanso:layout:${this.user}:${this.repo}`,
JSON.stringify({
chosenTitleFont,
chosenHeadingFont,
chosenBodyFont,
chosenFontSize,
chosenFontFamily,
@@ -86,7 +86,17 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
let lsLayout: Partial<UserSettings> = {}
try {
const lsRaw = localStorage.getItem(`remanso:layout:${user}:${repo}`)
if (lsRaw) lsLayout = JSON.parse(lsRaw)
if (lsRaw) {
const parsed = JSON.parse(lsRaw) as Partial<UserSettings> & {
chosenTitleFont?: string
}
// Migrate the pre-rename key so a saved heading font survives.
if (parsed.chosenTitleFont && !parsed.chosenHeadingFont) {
parsed.chosenHeadingFont = parsed.chosenTitleFont
}
delete parsed.chosenTitleFont
lsLayout = parsed
}
} catch {
// ignore
}
@@ -157,9 +167,9 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
: userSettings?.fontFamily
const chosenFontSize =
this.userSettings?.chosenFontSize ?? userSettings?.fontSize
const chosenTitleFont =
this.userSettings?.chosenTitleFont ??
userSettings?.chosenTitleFont ??
const chosenHeadingFont =
this.userSettings?.chosenHeadingFont ??
userSettings?.chosenHeadingFont ??
chosenFontFamily
const chosenBodyFont =
this.userSettings?.chosenBodyFont ??
@@ -175,14 +185,14 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
chosenFontFamily ?? this.userSettings.fontFamily
this.userSettings.chosenFontSize =
chosenFontSize ?? this.userSettings.fontSize
this.userSettings.chosenTitleFont = chosenTitleFont
this.userSettings.chosenHeadingFont = chosenHeadingFont
this.userSettings.chosenBodyFont = chosenBodyFont
this._persistLayout()
// Persist only repo config fields — chosen* are localStorage-only
const {
chosenTitleFont: _t,
chosenHeadingFont: _h,
chosenBodyFont: _b,
chosenFontSize: _s,
chosenFontFamily: _f,
@@ -294,11 +304,11 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
this.userSettings.chosenFontSize = fontSize
this._persistLayout()
},
setTitleFont(font: string) {
setHeadingFont(font: string) {
if (!this.userSettings) {
this.userSettings = { $type: DataType.UserSettings }
}
this.userSettings.chosenTitleFont = font
this.userSettings.chosenHeadingFont = font
this._persistLayout()
},
setBodyFont(font: string) {

View File

@@ -16,12 +16,12 @@ export const useUserSettings = () => {
const fontSize = store.userSettings?.chosenFontSize
const bodyFont = store.userSettings?.chosenBodyFont
const titleFont = store.userSettings?.chosenTitleFont
const headingFont = store.userSettings?.chosenHeadingFont
downloadFont(bodyFont || DEFAULT_FONT_POLICY, "--font-family")
downloadFont(
titleFont || bodyFont || DEFAULT_FONT_POLICY,
"--title-font-family"
headingFont || bodyFont || DEFAULT_FONT_POLICY,
"--heading-font-family"
)
root.style.setProperty("--font-size", fontSize || DEFAULT_FONT_SIZE)

View File

@@ -6,7 +6,7 @@
:root {
--primary-color: #ffa4c0;
--font-family: "Libertinus Serif", serif;
--title-font-family: "Libertinus Serif", serif;
--heading-font-family: "Libertinus Serif", serif;
--font-size: 13pt;
--font-color: #4a4a4a;
--link: #445fb9;

View File

@@ -3,7 +3,7 @@ const dotenv = require("dotenv")
dotenv.config()
const defaultTitleStyles = Array.from(
const defaultHeadingStyles = Array.from(
{ length: 6 },
(_, k) => `h${k + 1}`
).reduce(
@@ -12,7 +12,7 @@ const defaultTitleStyles = Array.from(
[heading]: {
"margin-top": "0",
"margin-bottom": "0.5em",
"font-family": "var(--title-font-family)"
"font-family": "var(--heading-font-family)"
}
}),
{}
@@ -27,7 +27,7 @@ module.exports = {
typography: () => ({
DEFAULT: {
css: {
...defaultTitleStyles,
...defaultHeadingStyles,
"font-size": "13pt",
"font-family": '"Libertinus Serif", serif',
p: {