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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user