diff --git a/stacklets/memory/Dockerfile b/stacklets/memory/Dockerfile index 70b67c9c..06a58f14 100644 --- a/stacklets/memory/Dockerfile +++ b/stacklets/memory/Dockerfile @@ -88,6 +88,7 @@ COPY quartz/components/FamilyCrumbs.tsx ./quartz/components/FamilyCrumbs.tsx COPY quartz/components/scripts/familyNav.inline.ts ./quartz/components/scripts/familyNav.inline.ts COPY quartz/plugins/familyLists.tsx ./quartz/plugins/emitters/familyLists.tsx COPY quartz/plugins/vaultLinks.ts ./quartz/plugins/transformers/vaultLinks.ts +COPY quartz/plugins/listTitles.ts ./quartz/plugins/transformers/listTitles.ts # The shipped ontology, for the topic areas a vault installed earlier # does not name yet (see readOntology in familyModel.ts). COPY seeds/ontology.toml ./ontology.seed.toml diff --git a/stacklets/memory/quartz/components/familyModel.ts b/stacklets/memory/quartz/components/familyModel.ts index f3ad2906..ec18ded3 100644 --- a/stacklets/memory/quartz/components/familyModel.ts +++ b/stacklets/memory/quartz/components/familyModel.ts @@ -31,6 +31,8 @@ const LABELS = { menu: "Menu", start: "Start", recent: "Recent", + lists: "Lists", + list: "List", diary: "Diary", people: "People", topics: "Topics", @@ -75,6 +77,8 @@ const LABELS = { menu: "Menü", start: "Start", recent: "Aktuell", + lists: "Listen", + list: "Liste", diary: "Tagebuch", people: "Personen", topics: "Themen", @@ -118,6 +122,11 @@ const LABELS = { export const L = LABELS[lang] +// A list is often named for what it is about ("Camping"), which reads as +// the topic. Its title says it is a list, unless the family's name for it +// already does ("Einkaufsliste", "Packing list"). +export const listTitle = (title: string) => (/list/i.test(title) ? title : `${title} ${L.list}`) + export const locale = lang === "de" ? "de-DE" : "en-US" const dateFormat = new Intl.DateTimeFormat(locale, { day: "numeric", month: "short", year: "numeric" }) export const fmtDate = (d: Date) => dateFormat.format(d) @@ -392,6 +401,7 @@ function build(ctx: BuildCtx, allFiles: QuartzPluginData[]): Model { const records: Rec[] = [] const recTopics = new Map() const people: NavNode[] = [] + const familyLists: NavNode[] = [] const diaryYears = new Map() let diaryRoot: string | undefined @@ -419,6 +429,16 @@ function build(ctx: BuildCtx, allFiles: QuartzPluginData[]): Model { continue } + // family lists: /lists/, counted by their open items; + // the archive of ticked items below it is not a list of its own + if (segs[0] === bucket && segs[1] === "lists") { + if (segs.length === 3 && meta.type === "list") { + const open = openTasks(readText(ctx, f)).length + familyLists.push({ label: String(meta.title ?? segs[2]), slug, count: open || undefined }) + } + continue + } + // topic folders: about page and task list if (segs[0] === bucket && segs.length === 3 && !KIND_FOLDERS.has(segs[1])) { const t = folderTopic(segs[1]) @@ -632,6 +652,8 @@ function build(ctx: BuildCtx, allFiles: QuartzPluginData[]): Model { // ── assemble, in the agreed order ── const nav: NavNode[] = [{ key: "start", label: L.start, slug: "index" }] if (recent.length) nav.push({ key: "recent", label: L.recent, open: true, children: recent.map(topicNode) }) + if (familyLists.length) + nav.push({ key: "lists", label: L.lists, children: familyLists.sort((a, b) => a.label.localeCompare(b.label, lang)) }) if (diaryRoot || diaryYears.size) { const yearsDesc = [...diaryYears.values()].sort((a, b) => Number(b.label) - Number(a.label)) nav.push({ diff --git a/stacklets/memory/quartz/plugins/listTitles.ts b/stacklets/memory/quartz/plugins/listTitles.ts new file mode 100644 index 00000000..2890d57e --- /dev/null +++ b/stacklets/memory/quartz/plugins/listTitles.ts @@ -0,0 +1,22 @@ +import { QuartzTransformerPlugin } from "../types" +import { listTitle } from "../../components/familyModel" + +// NEW TRANSFORMER (not an upstream override). +// +// A family list is titled the way the family named it, often for what it +// is about: "Camping". On its own page that reads as the topic, so a list +// page says it is a list ("Camping Liste"). The title is set here, once, +// so the page heading, the browser tab, search and the menu all agree. +// The file in the vault keeps the family's own title. + +export const ListTitles: QuartzTransformerPlugin = () => ({ + name: "ListTitles", + markdownPlugins() { + return [ + () => (_tree, file) => { + const fm = file.data.frontmatter + if (fm?.type === "list" && typeof fm.title === "string") fm.title = listTitle(fm.title) + }, + ] + }, +}) diff --git a/stacklets/memory/quartz/quartz.config.ts b/stacklets/memory/quartz/quartz.config.ts index 9e3f6b2a..170b605e 100644 --- a/stacklets/memory/quartz/quartz.config.ts +++ b/stacklets/memory/quartz/quartz.config.ts @@ -15,6 +15,7 @@ import { QuartzConfig } from "./quartz/cfg" import * as Plugin from "./quartz/plugins" import { FamilyLists } from "./quartz/plugins/emitters/familyLists" import { VaultLinks } from "./quartz/plugins/transformers/vaultLinks" +import { ListTitles } from "./quartz/plugins/transformers/listTitles" // Runtime env. `WIKI_HOST` is "memory." in domain mode and // the empty-domain rendering "memory." in port mode — we treat the @@ -123,6 +124,8 @@ const config: QuartzConfig = { plugins: { transformers: [ Plugin.FrontMatter(), + // A list page's title says it is a list; needs the frontmatter read. + ListTitles(), // The vault is a git repo — let git timestamps win over // filesystem mtime so a fresh clone shows real commit dates. Plugin.CreatedModifiedDate({