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