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

View File

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

View File

@@ -25,6 +25,7 @@ import { getOctokit, runWithAuthRetry } from "./octo"
import { import {
getFiles, getFiles,
getRepoPermission, getRepoPermission,
getUserSettingsContent,
queryFileContent queryFileContent
} from "./repo" } from "./repo"
@@ -165,3 +166,58 @@ describe("queryFileContent", () => {
expect(warn).toHaveBeenCalled() 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 & { const raw = JSON.parse(atob(content)) as UserSettings & {
t?: string t?: string
h?: string
p?: string p?: string
} }
const { t, p, ...rest } = raw const { t, h, p, ...rest } = raw
return { return {
...rest, ...rest,
chosenTitleFont: t, // `h` (heading) is the current key; `t` (title) is kept for back-compat.
chosenHeadingFont: h ?? t,
chosenBodyFont: p chosenBodyFont: p
} }
} }

View File

@@ -131,20 +131,20 @@ describe("userRepo store — synchronous mutations", () => {
expect(persisted.chosenFontFamily).toBe("Inter") 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() const store = useUserRepoStore()
store.user = "alice" store.user = "alice"
store.repo = "notes" store.repo = "notes"
store.setFontSize("18px") store.setFontSize("18px")
store.setTitleFont("Serif") store.setHeadingFont("Serif")
store.setBodyFont("Sans") store.setBodyFont("Sans")
const persisted = JSON.parse( const persisted = JSON.parse(
localStorage.getItem("remanso:layout:alice:notes") as string localStorage.getItem("remanso:layout:alice:notes") as string
) )
expect(persisted.chosenFontSize).toBe("18px") expect(persisted.chosenFontSize).toBe("18px")
expect(persisted.chosenTitleFont).toBe("Serif") expect(persisted.chosenHeadingFont).toBe("Serif")
expect(persisted.chosenBodyFont).toBe("Sans") expect(persisted.chosenBodyFont).toBe("Sans")
}) })
}) })
@@ -173,6 +173,25 @@ describe("userRepo store — setUserRepo", () => {
expect(store.loadError).toBeNull() 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 () => { it("populates files from getFiles on success", async () => {
vi.mocked(getFiles).mockResolvedValue([ vi.mocked(getFiles).mockResolvedValue([
{ sha: "a", path: "x.md", type: "blob" } as never { 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 if (!this.userSettings) return
try { try {
const { const {
chosenTitleFont, chosenHeadingFont,
chosenBodyFont, chosenBodyFont,
chosenFontSize, chosenFontSize,
chosenFontFamily, chosenFontFamily,
@@ -65,7 +65,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
localStorage.setItem( localStorage.setItem(
`remanso:layout:${this.user}:${this.repo}`, `remanso:layout:${this.user}:${this.repo}`,
JSON.stringify({ JSON.stringify({
chosenTitleFont, chosenHeadingFont,
chosenBodyFont, chosenBodyFont,
chosenFontSize, chosenFontSize,
chosenFontFamily, chosenFontFamily,
@@ -86,7 +86,17 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
let lsLayout: Partial<UserSettings> = {} let lsLayout: Partial<UserSettings> = {}
try { try {
const lsRaw = localStorage.getItem(`remanso:layout:${user}:${repo}`) 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 { } catch {
// ignore // ignore
} }
@@ -157,9 +167,9 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
: userSettings?.fontFamily : userSettings?.fontFamily
const chosenFontSize = const chosenFontSize =
this.userSettings?.chosenFontSize ?? userSettings?.fontSize this.userSettings?.chosenFontSize ?? userSettings?.fontSize
const chosenTitleFont = const chosenHeadingFont =
this.userSettings?.chosenTitleFont ?? this.userSettings?.chosenHeadingFont ??
userSettings?.chosenTitleFont ?? userSettings?.chosenHeadingFont ??
chosenFontFamily chosenFontFamily
const chosenBodyFont = const chosenBodyFont =
this.userSettings?.chosenBodyFont ?? this.userSettings?.chosenBodyFont ??
@@ -175,14 +185,14 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
chosenFontFamily ?? this.userSettings.fontFamily chosenFontFamily ?? this.userSettings.fontFamily
this.userSettings.chosenFontSize = this.userSettings.chosenFontSize =
chosenFontSize ?? this.userSettings.fontSize chosenFontSize ?? this.userSettings.fontSize
this.userSettings.chosenTitleFont = chosenTitleFont this.userSettings.chosenHeadingFont = chosenHeadingFont
this.userSettings.chosenBodyFont = chosenBodyFont this.userSettings.chosenBodyFont = chosenBodyFont
this._persistLayout() this._persistLayout()
// Persist only repo config fields — chosen* are localStorage-only // Persist only repo config fields — chosen* are localStorage-only
const { const {
chosenTitleFont: _t, chosenHeadingFont: _h,
chosenBodyFont: _b, chosenBodyFont: _b,
chosenFontSize: _s, chosenFontSize: _s,
chosenFontFamily: _f, chosenFontFamily: _f,
@@ -294,11 +304,11 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
this.userSettings.chosenFontSize = fontSize this.userSettings.chosenFontSize = fontSize
this._persistLayout() this._persistLayout()
}, },
setTitleFont(font: string) { setHeadingFont(font: string) {
if (!this.userSettings) { if (!this.userSettings) {
this.userSettings = { $type: DataType.UserSettings } this.userSettings = { $type: DataType.UserSettings }
} }
this.userSettings.chosenTitleFont = font this.userSettings.chosenHeadingFont = font
this._persistLayout() this._persistLayout()
}, },
setBodyFont(font: string) { setBodyFont(font: string) {

View File

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

View File

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

View File

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