@thjodann/wk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +197 -0
  3. package/dist/cli/commands/article.d.ts +2 -0
  4. package/dist/cli/commands/article.js +47 -0
  5. package/dist/cli/commands/article.js.map +1 -0
  6. package/dist/cli/commands/closeout.d.ts +2 -0
  7. package/dist/cli/commands/closeout.js +37 -0
  8. package/dist/cli/commands/closeout.js.map +1 -0
  9. package/dist/cli/commands/compile.d.ts +2 -0
  10. package/dist/cli/commands/compile.js +50 -0
  11. package/dist/cli/commands/compile.js.map +1 -0
  12. package/dist/cli/commands/concept.d.ts +2 -0
  13. package/dist/cli/commands/concept.js +36 -0
  14. package/dist/cli/commands/concept.js.map +1 -0
  15. package/dist/cli/commands/decision.d.ts +2 -0
  16. package/dist/cli/commands/decision.js +38 -0
  17. package/dist/cli/commands/decision.js.map +1 -0
  18. package/dist/cli/commands/event.d.ts +2 -0
  19. package/dist/cli/commands/event.js +32 -0
  20. package/dist/cli/commands/event.js.map +1 -0
  21. package/dist/cli/commands/init.d.ts +2 -0
  22. package/dist/cli/commands/init.js +52 -0
  23. package/dist/cli/commands/init.js.map +1 -0
  24. package/dist/cli/commands/installAgent.d.ts +2 -0
  25. package/dist/cli/commands/installAgent.js +130 -0
  26. package/dist/cli/commands/installAgent.js.map +1 -0
  27. package/dist/cli/commands/link.d.ts +2 -0
  28. package/dist/cli/commands/link.js +36 -0
  29. package/dist/cli/commands/link.js.map +1 -0
  30. package/dist/cli/commands/note.d.ts +2 -0
  31. package/dist/cli/commands/note.js +34 -0
  32. package/dist/cli/commands/note.js.map +1 -0
  33. package/dist/cli/commands/pages.d.ts +2 -0
  34. package/dist/cli/commands/pages.js +46 -0
  35. package/dist/cli/commands/pages.js.map +1 -0
  36. package/dist/cli/commands/record.d.ts +2 -0
  37. package/dist/cli/commands/record.js +101 -0
  38. package/dist/cli/commands/record.js.map +1 -0
  39. package/dist/cli/commands/render.d.ts +2 -0
  40. package/dist/cli/commands/render.js +33 -0
  41. package/dist/cli/commands/render.js.map +1 -0
  42. package/dist/cli/commands/search.d.ts +2 -0
  43. package/dist/cli/commands/search.js +125 -0
  44. package/dist/cli/commands/search.js.map +1 -0
  45. package/dist/cli/commands/setup.d.ts +2 -0
  46. package/dist/cli/commands/setup.js +53 -0
  47. package/dist/cli/commands/setup.js.map +1 -0
  48. package/dist/cli/commands/site.d.ts +2 -0
  49. package/dist/cli/commands/site.js +47 -0
  50. package/dist/cli/commands/site.js.map +1 -0
  51. package/dist/cli/commands/spin.d.ts +2 -0
  52. package/dist/cli/commands/spin.js +43 -0
  53. package/dist/cli/commands/spin.js.map +1 -0
  54. package/dist/cli/commands/status.d.ts +2 -0
  55. package/dist/cli/commands/status.js +63 -0
  56. package/dist/cli/commands/status.js.map +1 -0
  57. package/dist/cli/commands/symbol.d.ts +2 -0
  58. package/dist/cli/commands/symbol.js +37 -0
  59. package/dist/cli/commands/symbol.js.map +1 -0
  60. package/dist/cli/commands/theme.d.ts +2 -0
  61. package/dist/cli/commands/theme.js +52 -0
  62. package/dist/cli/commands/theme.js.map +1 -0
  63. package/dist/cli/commands/validate.d.ts +2 -0
  64. package/dist/cli/commands/validate.js +44 -0
  65. package/dist/cli/commands/validate.js.map +1 -0
  66. package/dist/cli/helpers.d.ts +30 -0
  67. package/dist/cli/helpers.js +131 -0
  68. package/dist/cli/helpers.js.map +1 -0
  69. package/dist/core/articles.d.ts +5 -0
  70. package/dist/core/articles.js +25 -0
  71. package/dist/core/articles.js.map +1 -0
  72. package/dist/core/automation.d.ts +96 -0
  73. package/dist/core/automation.js +384 -0
  74. package/dist/core/automation.js.map +1 -0
  75. package/dist/core/beads.d.ts +43 -0
  76. package/dist/core/beads.js +358 -0
  77. package/dist/core/beads.js.map +1 -0
  78. package/dist/core/compiler.d.ts +45 -0
  79. package/dist/core/compiler.js +399 -0
  80. package/dist/core/compiler.js.map +1 -0
  81. package/dist/core/config.d.ts +59 -0
  82. package/dist/core/config.js +187 -0
  83. package/dist/core/config.js.map +1 -0
  84. package/dist/core/git.d.ts +21 -0
  85. package/dist/core/git.js +109 -0
  86. package/dist/core/git.js.map +1 -0
  87. package/dist/core/ids.d.ts +2 -0
  88. package/dist/core/ids.js +8 -0
  89. package/dist/core/ids.js.map +1 -0
  90. package/dist/core/pages.d.ts +34 -0
  91. package/dist/core/pages.js +233 -0
  92. package/dist/core/pages.js.map +1 -0
  93. package/dist/core/paths.d.ts +15 -0
  94. package/dist/core/paths.js +67 -0
  95. package/dist/core/paths.js.map +1 -0
  96. package/dist/core/profiles.d.ts +30 -0
  97. package/dist/core/profiles.js +243 -0
  98. package/dist/core/profiles.js.map +1 -0
  99. package/dist/core/renderer.d.ts +8 -0
  100. package/dist/core/renderer.js +106 -0
  101. package/dist/core/renderer.js.map +1 -0
  102. package/dist/core/schemas.d.ts +1039 -0
  103. package/dist/core/schemas.js +108 -0
  104. package/dist/core/schemas.js.map +1 -0
  105. package/dist/core/site.d.ts +20 -0
  106. package/dist/core/site.js +2684 -0
  107. package/dist/core/site.js.map +1 -0
  108. package/dist/core/spin.d.ts +42 -0
  109. package/dist/core/spin.js +265 -0
  110. package/dist/core/spin.js.map +1 -0
  111. package/dist/core/store.d.ts +29 -0
  112. package/dist/core/store.js +146 -0
  113. package/dist/core/store.js.map +1 -0
  114. package/dist/core/theme.d.ts +35 -0
  115. package/dist/core/theme.js +1086 -0
  116. package/dist/core/theme.js.map +1 -0
  117. package/dist/core/validator.d.ts +8 -0
  118. package/dist/core/validator.js +154 -0
  119. package/dist/core/validator.js.map +1 -0
  120. package/dist/index.d.ts +3 -0
  121. package/dist/index.js +61 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/templates/articlesPage.d.ts +3 -0
  124. package/dist/templates/articlesPage.js +46 -0
  125. package/dist/templates/articlesPage.js.map +1 -0
  126. package/dist/templates/conceptsPage.d.ts +2 -0
  127. package/dist/templates/conceptsPage.js +24 -0
  128. package/dist/templates/conceptsPage.js.map +1 -0
  129. package/dist/templates/decisionsPage.d.ts +2 -0
  130. package/dist/templates/decisionsPage.js +27 -0
  131. package/dist/templates/decisionsPage.js.map +1 -0
  132. package/dist/templates/devlogPage.d.ts +2 -0
  133. package/dist/templates/devlogPage.js +22 -0
  134. package/dist/templates/devlogPage.js.map +1 -0
  135. package/dist/templates/indexPage.d.ts +6 -0
  136. package/dist/templates/indexPage.js +33 -0
  137. package/dist/templates/indexPage.js.map +1 -0
  138. package/dist/templates/linksPage.d.ts +2 -0
  139. package/dist/templates/linksPage.js +26 -0
  140. package/dist/templates/linksPage.js.map +1 -0
  141. package/dist/templates/notesPage.d.ts +2 -0
  142. package/dist/templates/notesPage.js +26 -0
  143. package/dist/templates/notesPage.js.map +1 -0
  144. package/dist/templates/symbolsPage.d.ts +2 -0
  145. package/dist/templates/symbolsPage.js +25 -0
  146. package/dist/templates/symbolsPage.js.map +1 -0
  147. package/docs/concepts.md +249 -0
  148. package/docs/reference.md +356 -0
  149. package/docs/setup.md +331 -0
  150. package/docs/workflows.md +154 -0
  151. package/package.json +61 -0
  152. package/skills/wk/SKILL.md +251 -0
  153. package/skills/wk/agents/openai.yaml +4 -0
@@ -0,0 +1,2684 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.siteStaticPageFileNames = exports.siteGeneratedNotice = void 0;
7
+ exports.buildSiteFiles = buildSiteFiles;
8
+ exports.renderSite = renderSite;
9
+ exports.resolveSiteOptions = resolveSiteOptions;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const articles_1 = require("./articles");
13
+ const beads_1 = require("./beads");
14
+ const config_1 = require("./config");
15
+ const paths_1 = require("./paths");
16
+ const store_1 = require("./store");
17
+ const schemas_1 = require("./schemas");
18
+ const profiles_1 = require("./profiles");
19
+ exports.siteGeneratedNotice = "<!-- Generated by Wikiwiki from .wikiwiki/records. Edit structured records instead, then run `wk site`. -->";
20
+ exports.siteStaticPageFileNames = [
21
+ "index.html",
22
+ "articles.html",
23
+ "guides.html",
24
+ "concepts.html",
25
+ "decisions.html",
26
+ "devlog.html",
27
+ "notes.html",
28
+ "symbols.html",
29
+ "links.html",
30
+ "work.html",
31
+ "search.html"
32
+ ];
33
+ const categories = [
34
+ {
35
+ type: "article",
36
+ title: "Articles",
37
+ navLabel: "Articles",
38
+ fileName: "articles.html",
39
+ description: "Public wiki articles curated from durable project knowledge."
40
+ },
41
+ {
42
+ type: "concept",
43
+ title: "Concepts",
44
+ navLabel: "Concepts",
45
+ fileName: "concepts.html",
46
+ description: "Durable explanations, domain terms, product ideas, and system concepts."
47
+ },
48
+ {
49
+ type: "decision",
50
+ title: "Decisions",
51
+ navLabel: "Decisions",
52
+ fileName: "decisions.html",
53
+ description: "Product, architecture, workflow, and implementation decisions with rationale."
54
+ },
55
+ {
56
+ type: "event",
57
+ title: "Devlog",
58
+ navLabel: "Devlog",
59
+ fileName: "devlog.html",
60
+ description: "Meaningful changes and development milestones in chronological context."
61
+ },
62
+ {
63
+ type: "note",
64
+ title: "Notes",
65
+ navLabel: "Notes",
66
+ fileName: "notes.html",
67
+ description: "Lightweight facts, reminders, and working context."
68
+ },
69
+ {
70
+ type: "symbol",
71
+ title: "Symbols",
72
+ navLabel: "Symbols",
73
+ fileName: "symbols.html",
74
+ description: "Important code symbols and where maintainers should look first."
75
+ },
76
+ {
77
+ type: "link",
78
+ title: "Links",
79
+ navLabel: "Links",
80
+ fileName: "links.html",
81
+ description: "Relationships between records, files, generated docs, and source concepts."
82
+ }
83
+ ];
84
+ const categoryByType = new Map(categories.map((category) => [category.type, category]));
85
+ function recordCategories() {
86
+ return categories.filter((category) => category.type !== "article");
87
+ }
88
+ function buildSiteFiles(root, options = {}) {
89
+ const records = (0, store_1.readAllRecords)(root);
90
+ const model = createSiteModel(root, records, resolveSiteOptions(root, options));
91
+ const files = [];
92
+ files.push({
93
+ fileName: "index.html",
94
+ content: renderLayout({
95
+ model,
96
+ fileName: "index.html",
97
+ title: model.siteTitle,
98
+ description: model.siteDescription,
99
+ active: "home",
100
+ body: renderHomePage(model)
101
+ })
102
+ });
103
+ files.push({
104
+ fileName: "articles.html",
105
+ content: renderLayout({
106
+ model,
107
+ fileName: "articles.html",
108
+ title: "Articles",
109
+ description: "Browse public wiki articles for this project.",
110
+ active: "articles",
111
+ body: renderArticlesPage(model)
112
+ })
113
+ });
114
+ files.push({
115
+ fileName: "guides.html",
116
+ content: renderLayout({
117
+ model,
118
+ fileName: "guides.html",
119
+ title: "Guides",
120
+ description: `Curated paths through ${model.repoName} knowledge.`,
121
+ active: "guides",
122
+ body: renderGuidesPage(model)
123
+ })
124
+ });
125
+ for (const category of recordCategories()) {
126
+ files.push({
127
+ fileName: category.fileName,
128
+ content: renderLayout({
129
+ model,
130
+ fileName: category.fileName,
131
+ title: category.title,
132
+ description: category.description,
133
+ active: category.type,
134
+ body: renderCategoryPage(model, category)
135
+ })
136
+ });
137
+ }
138
+ if (showBeadsWork(model)) {
139
+ files.push({
140
+ fileName: "work.html",
141
+ content: renderLayout({
142
+ model,
143
+ fileName: "work.html",
144
+ title: "Project Work",
145
+ description: "Developer-only Beads task context for this project.",
146
+ active: "work",
147
+ body: renderWorkPage(model)
148
+ })
149
+ });
150
+ }
151
+ files.push({
152
+ fileName: "search.html",
153
+ content: renderLayout({
154
+ model,
155
+ fileName: "search.html",
156
+ title: "Search",
157
+ description: "Search this project wiki without a backend.",
158
+ active: "search",
159
+ body: renderSearchPage()
160
+ })
161
+ });
162
+ for (const article of model.records.article) {
163
+ const fileName = articleHref(article);
164
+ files.push({
165
+ fileName,
166
+ content: renderLayout({
167
+ model,
168
+ fileName,
169
+ title: article.title,
170
+ description: article.summary,
171
+ active: "articles",
172
+ body: renderArticlePage(article, model, fileName)
173
+ })
174
+ });
175
+ }
176
+ for (const record of model.sourceRecords) {
177
+ const fileName = recordHref(record);
178
+ files.push({
179
+ fileName,
180
+ content: renderLayout({
181
+ model,
182
+ fileName,
183
+ title: recordTitle(record),
184
+ description: recordSummary(record),
185
+ active: record.type,
186
+ body: renderRecordPage(record, model, fileName)
187
+ })
188
+ });
189
+ }
190
+ files.push({ fileName: "favicon.svg", content: faviconSvg(model) }, { fileName: "assets/wikiwiki.css", content: siteCss() }, { fileName: "assets/project-theme.css", content: projectThemeCss(model.options.theme) }, { fileName: "assets/search-index.js", content: searchIndexJs(model) }, { fileName: "assets/wikiwiki.js", content: siteJs() }, { fileName: ".nojekyll", content: "" }, { fileName: "site-manifest.json", content: `${JSON.stringify(siteManifest(model), null, 2)}\n` });
191
+ return files;
192
+ }
193
+ function renderSite(root, options = {}) {
194
+ const files = buildSiteFiles(root, options);
195
+ const outputPath = (0, paths_1.sitePath)(root);
196
+ fs_1.default.rmSync(outputPath, { recursive: true, force: true });
197
+ fs_1.default.mkdirSync(outputPath, { recursive: true });
198
+ return files.map((file) => {
199
+ const outputFile = path_1.default.join(outputPath, file.fileName);
200
+ fs_1.default.mkdirSync(path_1.default.dirname(outputFile), { recursive: true });
201
+ fs_1.default.writeFileSync(outputFile, file.content, "utf8");
202
+ return outputFile;
203
+ });
204
+ }
205
+ function resolveSiteOptions(root, options = {}) {
206
+ const config = (0, config_1.readWikiwikiConfig)(root);
207
+ return {
208
+ sourceBaseUrl: (0, config_1.normalizeSourceBaseUrl)(options.sourceBaseUrl ?? config.source_base_url),
209
+ theme: (0, config_1.readWikiwikiSiteTheme)(root),
210
+ audience: (0, profiles_1.parseSiteAudience)(options.audience ?? config.site_audience, "all")
211
+ };
212
+ }
213
+ function createSiteModel(root, records, options) {
214
+ const audienceRecords = filterRecordsByAudience(records, options.audience);
215
+ const integrations = (0, beads_1.readIntegrations)(root, (0, config_1.readWikiwikiConfig)(root));
216
+ const flatRecords = [];
217
+ for (const type of schemas_1.recordTypes) {
218
+ flatRecords.push(...audienceRecords[type]);
219
+ }
220
+ flatRecords.sort(compareRecords);
221
+ const sourceRecords = flatRecords.filter((record) => record.type !== "article");
222
+ const hrefById = new Map(flatRecords.map((record) => [record.id, record.type === "article" ? articleHref(record) : recordHref(record)]));
223
+ const articleHrefByLookup = articleLookupMap(audienceRecords.article);
224
+ const counts = Object.fromEntries(schemas_1.recordTypes.map((type) => [type, audienceRecords[type].length]));
225
+ const repoName = options.theme.project_name?.trim() || path_1.default.basename(root);
226
+ return {
227
+ root,
228
+ repoName,
229
+ siteTitle: `${repoName} Wiki`,
230
+ siteDescription: options.theme.project_description?.trim() || "A project wiki generated from durable repo knowledge.",
231
+ projectInitials: initialsForName(repoName),
232
+ records: audienceRecords,
233
+ flatRecords,
234
+ sourceRecords,
235
+ hrefById,
236
+ articleHrefByLookup,
237
+ counts,
238
+ visibleCategories: recordCategories().filter((category) => counts[category.type] > 0),
239
+ options,
240
+ audience: options.audience,
241
+ integrations
242
+ };
243
+ }
244
+ function articleLookupMap(articles) {
245
+ const lookup = new Map();
246
+ for (const article of articles) {
247
+ for (const key of [article.id, article.slug, article.title, ...article.aliases]) {
248
+ const normalized = normalizeArticleLookup(key);
249
+ if (normalized && !lookup.has(normalized)) {
250
+ lookup.set(normalized, articleHref(article));
251
+ }
252
+ }
253
+ }
254
+ return lookup;
255
+ }
256
+ function normalizeArticleLookup(value) {
257
+ return value.trim().toLowerCase();
258
+ }
259
+ function renderLayout(options) {
260
+ const prefix = relativePrefix(options.fileName);
261
+ const htmlTitle = options.title === options.model.siteTitle
262
+ ? options.title
263
+ : `${options.title} - ${options.model.siteTitle}`;
264
+ const defaultColorScheme = defaultThemeColorScheme(options.model.options.theme);
265
+ const defaultThemeAttribute = defaultColorScheme === "auto" ? "" : ` data-theme="${defaultColorScheme}"`;
266
+ return `${exports.siteGeneratedNotice}
267
+ <!doctype html>
268
+ <html lang="en" data-default-theme="${defaultColorScheme}"${defaultThemeAttribute}>
269
+ <head>
270
+ <meta charset="utf-8">
271
+ <meta name="viewport" content="width=device-width, initial-scale=1">
272
+ <meta name="generator" content="Wikiwiki">
273
+ <title>${escapeHtml(htmlTitle)}</title>
274
+ <script>${themeBootScript(defaultColorScheme)}</script>
275
+ <link rel="icon" href="${prefix}favicon.svg" type="image/svg+xml">
276
+ <link rel="stylesheet" href="${prefix}assets/wikiwiki.css">
277
+ <link rel="stylesheet" href="${prefix}assets/project-theme.css">
278
+ <script src="${prefix}assets/search-index.js" defer></script>
279
+ <script src="${prefix}assets/wikiwiki.js" defer></script>
280
+ </head>
281
+ <body>
282
+ <a class="skip-link" href="#content">Skip to content</a>
283
+ <div class="site-shell">
284
+ <aside class="sidebar" aria-label="Wiki navigation">
285
+ <div class="sidebar-top">
286
+ <a class="brand" href="${hrefFrom(options.fileName, "index.html")}" aria-label="Wiki home">
287
+ <span class="brand-mark">${escapeHtml(options.model.projectInitials)}</span>
288
+ <span>
289
+ <strong>${escapeHtml(options.model.repoName)}</strong>
290
+ <small>Project wiki</small>
291
+ </span>
292
+ </a>
293
+ <button class="nav-toggle" type="button" data-nav-toggle aria-expanded="false">Menu</button>
294
+ </div>
295
+ <div class="theme-control" role="group" aria-label="Color theme">
296
+ ${themeChoiceButton("auto", "Auto", "Use system theme", defaultColorScheme)}
297
+ ${themeChoiceButton("light", "Light", "Use light theme", defaultColorScheme)}
298
+ ${themeChoiceButton("dark", "Dark", "Use dark theme", defaultColorScheme)}
299
+ </div>
300
+ <div class="sidebar-nav" data-sidebar-nav>
301
+ ${renderNav(options.model, options.fileName, options.active)}
302
+ </div>
303
+ </aside>
304
+ <main id="content" class="content">
305
+ <header class="page-header">
306
+ <h1>${escapeHtml(options.title)}</h1>
307
+ <p>${escapeHtml(options.description)}</p>
308
+ </header>
309
+ ${options.body}
310
+ <footer class="site-footer">Created with <a href="https://github.com/Thjodann/Wikiwiki">Wikiwiki</a>.</footer>
311
+ </main>
312
+ </div>
313
+ </body>
314
+ </html>
315
+ `;
316
+ }
317
+ function defaultThemeColorScheme(theme) {
318
+ return theme.default_color_scheme ?? "auto";
319
+ }
320
+ function themeBootScript(defaultColorScheme) {
321
+ return `(function(){try{var m=localStorage.getItem("wikiwiki-theme-mode")||"${defaultColorScheme}";if(m==="light"||m==="dark"){document.documentElement.dataset.theme=m;}else{delete document.documentElement.dataset.theme;}}catch(e){}}());`;
322
+ }
323
+ function themeChoiceButton(mode, label, title, defaultColorScheme) {
324
+ const pressed = mode === defaultColorScheme ? "true" : "false";
325
+ return `<button type="button" data-theme-choice="${mode}" aria-pressed="${pressed}" title="${escapeAttribute(title)}">${escapeHtml(label)}</button>`;
326
+ }
327
+ function renderNav(model, currentFile, active) {
328
+ const categoryLinks = model.visibleCategories
329
+ .map((category) => navLink(currentFile, category.fileName, category.navLabel, active === category.type, model.counts[category.type]))
330
+ .join("\n");
331
+ const categorySection = categoryLinks
332
+ ? `<p class="nav-heading">Agent records</p>\n${categoryLinks}`
333
+ : "";
334
+ const links = [
335
+ `<p class="nav-heading">Start Here</p>`,
336
+ navLink(currentFile, "index.html", "Home", active === "home"),
337
+ navLink(currentFile, "articles.html", "Articles", active === "articles", model.counts.article),
338
+ navLink(currentFile, "guides.html", "Guides", active === "guides"),
339
+ navLink(currentFile, "search.html", "Search", active === "search"),
340
+ showBeadsWork(model) ? navLink(currentFile, "work.html", "Project Work", active === "work", beadsNavCount(model.integrations.beads)) : "",
341
+ categorySection
342
+ ].filter(Boolean).join("\n");
343
+ return `<nav class="nav">
344
+ ${links}
345
+ </nav>`;
346
+ }
347
+ function navLink(currentFile, targetFile, label, active, count) {
348
+ const countHtml = typeof count === "number" ? `<span>${count}</span>` : "";
349
+ const activeAttribute = active ? ` aria-current="page"` : "";
350
+ return `<a class="nav-link${active ? " active" : ""}" href="${hrefFrom(currentFile, targetFile)}"${activeAttribute}>${escapeHtml(label)}${countHtml}</a>`;
351
+ }
352
+ function renderHomePage(model) {
353
+ const coreCategories = recordCategories()
354
+ .filter((category) => ["concept", "decision"].includes(category.type) && model.counts[category.type] > 0);
355
+ const supportCategories = recordCategories()
356
+ .filter((category) => !["concept", "decision"].includes(category.type) && model.counts[category.type] > 0);
357
+ const guideCards = [
358
+ sectionCard("index.html", "articles.html", "Articles", "Public wiki pages curated from source records and project knowledge.", model.counts.article),
359
+ sectionCard("index.html", "guides.html", "Guides", "Curated paths through user orientation, project concepts, and maintainer context.")
360
+ ].join("\n");
361
+ const primaryCards = coreCategories
362
+ .map((category) => sectionCard("index.html", category.fileName, category.title, category.description, model.counts[category.type]))
363
+ .join("\n");
364
+ const supportCards = supportCategories
365
+ .map((category) => sectionCard("index.html", category.fileName, category.title, category.description, model.counts[category.type]))
366
+ .join("\n");
367
+ const workCards = showBeadsWork(model)
368
+ ? sectionCard("index.html", "work.html", "Project Work", "Developer-only Beads task context: ready work, in-progress issues, and recent closed work.", beadsNavCount(model.integrations.beads))
369
+ : "";
370
+ const featuredRecords = homepageRecords(model)
371
+ .map((record) => recordCard(record, "index.html", model))
372
+ .join("\n");
373
+ const stats = statsTypes(model)
374
+ .map((type) => `<div><dt>${escapeHtml(labelForType(type))}</dt><dd>${model.counts[type]}</dd></div>`)
375
+ .join("\n");
376
+ return `<section class="hero-panel">
377
+ <div>
378
+ <p class="eyebrow">Start here</p>
379
+ <h2>Understand ${escapeHtml(model.repoName)} without reading raw records.</h2>
380
+ <p>${escapeHtml(model.siteDescription)} ${escapeHtml(audienceIntro(model.audience))}</p>
381
+ </div>
382
+ ${stats ? `<dl class="stats-grid">${stats}</dl>` : ""}
383
+ </section>
384
+
385
+ <section class="section">
386
+ <div class="section-heading">
387
+ <p class="eyebrow">Field guide</p>
388
+ <h2>Start with the project story</h2>
389
+ </div>
390
+ <div class="card-grid">${guideCards}${primaryCards}</div>
391
+ </section>
392
+
393
+ ${workCards || supportCards ? `<section class="section">
394
+ <div class="section-heading">
395
+ <p class="eyebrow">Maintainer context</p>
396
+ <h2>Browse the source records</h2>
397
+ </div>
398
+ <div class="card-grid compact">${workCards}${supportCards}</div>
399
+ </section>` : ""}
400
+
401
+ <section class="section">
402
+ <div class="section-heading">
403
+ <p class="eyebrow">Featured</p>
404
+ <h2>High-signal knowledge</h2>
405
+ </div>
406
+ <div class="record-list">${featuredRecords || emptyState("No curated records yet. Add high-confidence concepts or decisions to shape this front page.")}</div>
407
+ </section>`;
408
+ }
409
+ function renderGuidesPage(model) {
410
+ const usedRecords = new Set();
411
+ const articleRecords = takeUnusedRecords(curatedRecords(model, ["article"], model.records.article.length), usedRecords, 8);
412
+ const userGuideRecords = takeUnusedRecords(userGuideRecordsFor(model), usedRecords, 8);
413
+ const startHereRecords = takeUnusedRecords(homepageRecords(model), usedRecords, 6);
414
+ const conceptRecords = takeUnusedRecords(curatedRecords(model, ["concept"], model.records.concept.length), usedRecords, 6);
415
+ const decisionRecords = takeUnusedRecords(curatedRecords(model, ["decision"], model.records.decision.length), usedRecords, 6);
416
+ const articleCards = articleRecords
417
+ .map((record) => recordCard(record, "guides.html", model))
418
+ .join("\n");
419
+ const userGuideCards = userGuideRecords
420
+ .map((record) => recordCard(record, "guides.html", model))
421
+ .join("\n");
422
+ const userGuideSection = userGuideCards ? `<section class="section">
423
+ <div class="section-heading">
424
+ <p class="eyebrow">User guide</p>
425
+ <h2>Start, learn, and troubleshoot</h2>
426
+ </div>
427
+ <div class="record-list">${userGuideCards}</div>
428
+ </section>
429
+
430
+ ` : "";
431
+ const startHere = startHereRecords
432
+ .map((record) => recordCard(record, "guides.html", model))
433
+ .join("\n");
434
+ const conceptCards = conceptRecords
435
+ .map((record) => recordCard(record, "guides.html", model))
436
+ .join("\n");
437
+ const decisionCards = decisionRecords
438
+ .map((record) => recordCard(record, "guides.html", model))
439
+ .join("\n");
440
+ const maintainerCards = recordCategories()
441
+ .filter((category) => model.counts[category.type] > 0)
442
+ .map((category) => sectionCard("guides.html", category.fileName, category.title, category.description, model.counts[category.type]))
443
+ .join("\n");
444
+ const workCard = showBeadsWork(model)
445
+ ? sectionCard("guides.html", "work.html", "Project Work", "Developer-only Beads task context for planning, blockers, and completed work.", beadsNavCount(model.integrations.beads))
446
+ : "";
447
+ const articlesSection = articleCards ? `<section class="section">
448
+ <div class="section-heading">
449
+ <p class="eyebrow">Articles</p>
450
+ <h2>Start with the public wiki</h2>
451
+ </div>
452
+ <div class="record-list">${articleCards}</div>
453
+ </section>
454
+
455
+ ` : "";
456
+ return `${articlesSection}${userGuideSection}<section class="section">
457
+ <div class="section-heading">
458
+ <p class="eyebrow">Start here</p>
459
+ <h2>Project essentials</h2>
460
+ </div>
461
+ <div class="record-list">${startHere || emptyState("No high-signal records captured yet.")}</div>
462
+ </section>
463
+
464
+ ${conceptCards ? `<section class="section">
465
+ <div class="section-heading">
466
+ <p class="eyebrow">Experience</p>
467
+ <h2>Concepts to understand first</h2>
468
+ </div>
469
+ <div class="record-list">${conceptCards}</div>
470
+ </section>` : ""}
471
+
472
+ ${decisionCards ? `<section class="section">
473
+ <div class="section-heading">
474
+ <p class="eyebrow">Rationale</p>
475
+ <h2>Decisions that shaped the project</h2>
476
+ </div>
477
+ <div class="record-list">${decisionCards}</div>
478
+ </section>` : ""}
479
+
480
+ ${workCard || maintainerCards ? `<section class="section">
481
+ <div class="section-heading">
482
+ <p class="eyebrow">Docs map</p>
483
+ <h2>Agent-maintained record indexes</h2>
484
+ </div>
485
+ <div class="card-grid compact">${workCard}${maintainerCards}</div>
486
+ </section>` : ""}`;
487
+ }
488
+ function renderWorkPage(model) {
489
+ const beads = model.integrations.beads;
490
+ if (!beads) {
491
+ return `<section class="section">${emptyState("No Beads workspace detected.")}</section>`;
492
+ }
493
+ if (!beads.enabled) {
494
+ return `<section class="section">${emptyState("Beads is detected but disabled in Wikiwiki config.")}</section>`;
495
+ }
496
+ if (!beads.available) {
497
+ const message = beads.error === "beads_auto_read_skipped"
498
+ ? "Detailed Beads reads were skipped to avoid dirtying .beads. Set integrations.beads.enabled to true only after checking that local bd reads are clean."
499
+ : `Wikiwiki could not read Beads with <code>bd --readonly --json</code>. ${escapeHtml(beads.error ?? "Install bd or check the Beads workspace, then rerun wk site.")}`;
500
+ return `<section class="section">
501
+ <div class="empty-state">
502
+ <strong>Beads workspace detected.</strong>
503
+ <p>${message}</p>
504
+ </div>
505
+ </section>`;
506
+ }
507
+ const stats = [
508
+ ["Ready", beads.counts.ready],
509
+ ["In progress", beads.counts.in_progress],
510
+ ["Recent closed", beads.counts.recent_closed],
511
+ ["Total", beads.counts.total]
512
+ ]
513
+ .filter(([, value]) => typeof value === "number")
514
+ .map(([label, value]) => `<div><dt>${escapeHtml(String(label))}</dt><dd>${value}</dd></div>`)
515
+ .join("\n");
516
+ return `<section class="hero-panel">
517
+ <div>
518
+ <p class="eyebrow">Developer work graph</p>
519
+ <h2>Beads tracks what to do; Wikiwiki records what becomes true.</h2>
520
+ <p>Use this page to orient on active task context without turning tasks into wiki records.</p>
521
+ </div>
522
+ ${stats ? `<dl class="stats-grid">${stats}</dl>` : ""}
523
+ </section>
524
+
525
+ ${beadsIssueSection("Ready work", "Open work with no active blockers.", beads.ready)}
526
+ ${beadsIssueSection("In progress", "Work currently claimed or underway.", beads.in_progress)}
527
+ ${beadsIssueSection("Recently closed", "Completed work that may have durable wiki outcomes.", beads.recent_closed)}`;
528
+ }
529
+ function beadsIssueSection(title, description, issues) {
530
+ const cards = issues.map(beadsIssueCard).join("\n");
531
+ return `<section class="section">
532
+ <div class="section-heading">
533
+ <p class="eyebrow">Beads</p>
534
+ <h2>${escapeHtml(title)}</h2>
535
+ <p>${escapeHtml(description)}</p>
536
+ </div>
537
+ <div class="record-list">${cards || emptyState(`No ${title.toLowerCase()} found.`)}</div>
538
+ </section>`;
539
+ }
540
+ function beadsIssueCard(issue) {
541
+ const details = [
542
+ issue.type,
543
+ issue.status,
544
+ issue.priority,
545
+ issue.assignee ? `Assigned to ${issue.assignee}` : ""
546
+ ].filter(Boolean).join(" · ");
547
+ const labels = issue.labels.length > 0 ? tagList(issue.labels.slice(0, 6)) : "";
548
+ return `<article class="record-card work-card">
549
+ <div class="record-card-top">
550
+ <span class="type-badge">Beads</span>
551
+ <code>${escapeHtml(issue.id)}</code>
552
+ </div>
553
+ <h3>${escapeHtml(issue.title)}</h3>
554
+ ${details ? `<p>${escapeHtml(details)}</p>` : ""}
555
+ ${labels ? `<footer>${labels}</footer>` : ""}
556
+ </article>`;
557
+ }
558
+ function renderArticlesPage(model) {
559
+ const articles = [...model.records.article].sort(compareRecords);
560
+ const cards = articles.map((article) => recordCard(article, "articles.html", model)).join("\n");
561
+ const categories = articleCategoryCards(model, "articles.html");
562
+ return `<section class="section">
563
+ <div class="section-heading">
564
+ <p class="eyebrow">Articles</p>
565
+ <h2>${articles.length} ${articles.length === 1 ? "article" : "articles"}</h2>
566
+ </div>
567
+ <div class="record-list">${cards || emptyState("No articles captured yet. Add article records to make the public wiki lead with curated pages.")}</div>
568
+ </section>
569
+
570
+ ${categories ? `<section class="section">
571
+ <div class="section-heading">
572
+ <p class="eyebrow">Categories</p>
573
+ <h2>Browse by topic</h2>
574
+ </div>
575
+ <div class="card-grid compact">${categories}</div>
576
+ </section>` : ""}`;
577
+ }
578
+ function renderArticlePage(article, model, currentFile) {
579
+ const context = {
580
+ root: model.root,
581
+ currentFile,
582
+ hrefById: model.hrefById,
583
+ articleHrefByLookup: model.articleHrefByLookup,
584
+ sourceBaseUrl: model.options.sourceBaseUrl
585
+ };
586
+ const tagsHtml = tagList(displayTags(article));
587
+ const tagsLine = tagsHtml ? `\n ${tagsHtml}` : "";
588
+ const categories = article.categories.length
589
+ ? `<h2>Categories</h2><p>${article.categories.map((category) => `<code>${escapeHtml(category)}</code>`).join(" ")}</p>`
590
+ : "";
591
+ const aliases = article.aliases.length
592
+ ? `<h2>Aliases</h2><p>${article.aliases.map((alias) => `<code>${escapeHtml(alias)}</code>`).join(" ")}</p>`
593
+ : "";
594
+ const sources = sourceRecordsForArticle(article, model);
595
+ const sourceList = sources.length
596
+ ? `<aside class="sources-panel">
597
+ <h2>Source records</h2>
598
+ <ul>${sources.map((record) => `<li><a href="${hrefFrom(currentFile, hrefForRecord(record))}">${escapeHtml(recordTitle(record))}</a> <code>${escapeHtml(record.id)}</code></li>`).join("\n")}</ul>
599
+ </aside>`
600
+ : "";
601
+ const filesBlock = renderSourcesBlock(article, context);
602
+ const body = article.body.trim() ? formatText(article.body, context) : "<p>No article body captured yet.</p>";
603
+ const audienceHtml = audienceLabel(article)
604
+ ? `<span class="audience-badge">${escapeHtml(audienceLabel(article))}</span>`
605
+ : "";
606
+ return `<article class="record-page">
607
+ <nav class="breadcrumbs" aria-label="Breadcrumb">
608
+ <a href="${hrefFrom(currentFile, "index.html")}">Home</a>
609
+ <span>/</span>
610
+ <a href="${hrefFrom(currentFile, "articles.html")}">Articles</a>
611
+ </nav>
612
+ <div class="record-title-row">
613
+ <span class="type-badge">Article</span>${audienceHtml}${tagsLine}
614
+ </div>
615
+ <section class="prose">
616
+ <h2>Summary</h2>
617
+ ${formatText(article.summary, context)}
618
+ <h2>Article</h2>
619
+ ${body}
620
+ ${categories}
621
+ ${aliases}
622
+ </section>${sourceList ? `\n ${sourceList}` : ""}${filesBlock ? `\n ${filesBlock}` : ""}
623
+ ${renderRecordMeta(article, context)}
624
+ </article>`;
625
+ }
626
+ function renderCategoryPage(model, category) {
627
+ const records = [...model.records[category.type]].sort(compareRecords);
628
+ const cards = records.map((record) => recordCard(record, category.fileName, model)).join("\n");
629
+ return `<section class="section">
630
+ <div class="section-heading">
631
+ <p class="eyebrow">${escapeHtml(labelForType(category.type))}</p>
632
+ <h2>${records.length} ${records.length === 1 ? "record" : "records"}</h2>
633
+ </div>
634
+ <div class="record-list">${cards || emptyState(`No ${category.title.toLowerCase()} captured yet.`)}</div>
635
+ </section>`;
636
+ }
637
+ function renderSearchPage() {
638
+ return `<section class="section">
639
+ <div class="search-panel">
640
+ <label>
641
+ <span>Search this project wiki</span>
642
+ <input type="search" placeholder="Type an article, concept, file, tag, or decision" data-search-page-input autofocus>
643
+ </label>
644
+ </div>
645
+ <div class="search-results" data-search-results></div>
646
+ </section>`;
647
+ }
648
+ function renderRecordPage(record, model, currentFile) {
649
+ const context = {
650
+ root: model.root,
651
+ currentFile,
652
+ hrefById: model.hrefById,
653
+ articleHrefByLookup: model.articleHrefByLookup,
654
+ sourceBaseUrl: model.options.sourceBaseUrl
655
+ };
656
+ const category = categoryByType.get(record.type);
657
+ const backHref = category ? hrefFrom(currentFile, category.fileName) : hrefFrom(currentFile, "index.html");
658
+ const tagsHtml = tagList(displayTags(record));
659
+ const tagsLine = tagsHtml ? `\n ${tagsHtml}` : "";
660
+ const sourcesBlock = renderSourcesBlock(record, context);
661
+ const audienceHtml = audienceLabel(record)
662
+ ? `<span class="audience-badge">${escapeHtml(audienceLabel(record))}</span>`
663
+ : "";
664
+ return `<article class="record-page">
665
+ <nav class="breadcrumbs" aria-label="Breadcrumb">
666
+ <a href="${hrefFrom(currentFile, "index.html")}">Home</a>
667
+ <span>/</span>
668
+ <a href="${backHref}">${escapeHtml(category?.title ?? labelForType(record.type))}</a>
669
+ </nav>
670
+ <div class="record-title-row">
671
+ <span class="type-badge">${escapeHtml(labelForType(record.type))}</span>${audienceHtml}${tagsLine}
672
+ </div>
673
+ ${renderRecordBody(record, context)}${sourcesBlock ? `\n ${sourcesBlock}` : ""}
674
+ ${renderRecordMeta(record, context)}
675
+ </article>`;
676
+ }
677
+ function renderRecordBody(record, context) {
678
+ if (record.type === "concept") {
679
+ const details = record.details ? `<h2>Details</h2>${formatText(record.details, context)}` : "";
680
+ return `<section class="prose">
681
+ <h2>Summary</h2>
682
+ ${formatText(record.summary, context)}
683
+ ${details}
684
+ </section>`;
685
+ }
686
+ if (record.type === "decision") {
687
+ return `<section class="prose">
688
+ <h2>Context</h2>
689
+ ${formatText(record.context || "Not recorded.", context)}
690
+ <h2>Decision</h2>
691
+ ${formatText(record.decision, context)}
692
+ <h2>Consequences</h2>
693
+ ${formatText(record.consequences || "Not recorded.", context)}
694
+ </section>`;
695
+ }
696
+ if (record.type === "event") {
697
+ return `<section class="prose">
698
+ <h2>What happened</h2>
699
+ ${formatText(record.details || "No details recorded.", context)}
700
+ </section>`;
701
+ }
702
+ if (record.type === "note") {
703
+ return `<section class="prose">
704
+ <h2>Note</h2>
705
+ ${formatText(record.body, context)}
706
+ </section>`;
707
+ }
708
+ if (record.type === "article") {
709
+ const categories = record.categories.length
710
+ ? `<h2>Categories</h2><p>${record.categories.map((category) => `<code>${escapeHtml(category)}</code>`).join(" ")}</p>`
711
+ : "";
712
+ const aliases = record.aliases.length
713
+ ? `<h2>Aliases</h2><p>${record.aliases.map((alias) => `<code>${escapeHtml(alias)}</code>`).join(" ")}</p>`
714
+ : "";
715
+ const sources = record.source_record_ids.length
716
+ ? `<h2>Source Records</h2><ul>${record.source_record_ids.map((id) => `<li>${endpointLink(id, context)}</li>`).join("")}</ul>`
717
+ : "";
718
+ const body = record.body.trim() ? formatText(record.body, context) : "<p>No article body captured yet.</p>";
719
+ return `<section class="prose">
720
+ <h2>Summary</h2>
721
+ ${formatText(record.summary, context)}
722
+ <h2>Article</h2>
723
+ ${body}
724
+ ${categories}
725
+ ${aliases}
726
+ ${sources}
727
+ </section>`;
728
+ }
729
+ if (record.type === "symbol") {
730
+ return `<section class="prose">
731
+ <h2>Purpose</h2>
732
+ ${formatText(record.summary || "No summary recorded.", context)}
733
+ <h2>Location</h2>
734
+ <p>${sourceFileLink(record.file, context)}</p>
735
+ </section>`;
736
+ }
737
+ return `<section class="prose">
738
+ <h2>Relationship</h2>
739
+ <p><strong>From:</strong> ${endpointLink(record.from, context)}</p>
740
+ <p><strong>To:</strong> ${endpointLink(record.to, context)}</p>
741
+ <p><strong>Relationship:</strong> ${escapeHtml(record.relationship)}</p>
742
+ </section>`;
743
+ }
744
+ function renderRecordMeta(record, context) {
745
+ const files = relatedFiles(record);
746
+ const fileList = files.length
747
+ ? files.map((file) => `<li>${sourceFileLink(file, context)}</li>`).join("\n")
748
+ : "<li>None</li>";
749
+ const tags = recordTags(record);
750
+ const tagText = tags.length ? tags.map((tag) => `<code>${escapeHtml(tag)}</code>`).join(" ") : "None";
751
+ const updatedAt = "updated_at" in record && typeof record.updated_at === "string"
752
+ ? `\n <div><dt>Updated</dt><dd><time datetime="${escapeHtml(record.updated_at)}">${escapeHtml(formatDate(record.updated_at))}</time></dd></div>`
753
+ : "";
754
+ return `<details class="record-meta">
755
+ <summary>Agent details</summary>
756
+ <dl>
757
+ <div><dt>ID</dt><dd><code>${escapeHtml(record.id)}</code></dd></div>
758
+ <div><dt>Source</dt><dd>${escapeHtml(record.source)}</dd></div>
759
+ <div><dt>Authority</dt><dd>${escapeHtml(record.authority)}</dd></div>
760
+ <div><dt>Confidence</dt><dd>${escapeHtml(record.confidence)}</dd></div>
761
+ <div><dt>Created</dt><dd><time datetime="${escapeHtml(record.created_at)}">${escapeHtml(formatDate(record.created_at))}</time></dd></div>${updatedAt}
762
+ <div><dt>Tags</dt><dd>${tagText}</dd></div>
763
+ <div><dt>Files</dt><dd><ul>${fileList}</ul></dd></div>
764
+ </dl>
765
+ </details>`;
766
+ }
767
+ function renderSourcesBlock(record, context) {
768
+ const files = relatedFiles(record);
769
+ if (files.length === 0) {
770
+ return "";
771
+ }
772
+ const fileList = files.map((file) => `<li>${sourceFileLink(file, context)}</li>`).join("\n");
773
+ return `<aside class="sources-panel">
774
+ <h2>Related files</h2>
775
+ <ul>${fileList}</ul>
776
+ </aside>`;
777
+ }
778
+ function sectionCard(currentFile, targetFile, title, description, count) {
779
+ const countHtml = typeof count === "number"
780
+ ? `<span>${count} ${count === 1 ? "record" : "records"}</span>`
781
+ : "<span>Guide</span>";
782
+ return `<a class="section-card" href="${hrefFrom(currentFile, targetFile)}">
783
+ ${countHtml}
784
+ <strong>${escapeHtml(title)}</strong>
785
+ <p>${escapeHtml(description)}</p>
786
+ </a>`;
787
+ }
788
+ function articleCategoryCards(model, currentFile) {
789
+ const categoriesByName = new Map();
790
+ for (const article of model.records.article) {
791
+ for (const category of article.categories) {
792
+ const articles = categoriesByName.get(category) ?? [];
793
+ articles.push(article);
794
+ categoriesByName.set(category, articles);
795
+ }
796
+ }
797
+ return [...categoriesByName.entries()]
798
+ .sort(([a], [b]) => a.localeCompare(b))
799
+ .map(([category, articles]) => sectionCard(currentFile, "articles.html", category, `${articles.length} ${articles.length === 1 ? "article" : "articles"} in this topic.`, articles.length))
800
+ .join("\n");
801
+ }
802
+ function sourceRecordsForArticle(article, model) {
803
+ const byId = new Map(model.flatRecords.map((record) => [record.id, record]));
804
+ return article.source_record_ids
805
+ .map((id) => byId.get(id))
806
+ .filter((record) => record !== undefined);
807
+ }
808
+ function recordCard(record, currentFile, _model) {
809
+ const summary = recordExcerpt(record);
810
+ const tags = displayTags(record).slice(0, 5);
811
+ const tagsHtml = tagList(tags);
812
+ const tagsLine = tagsHtml ? `\n ${tagsHtml}` : "";
813
+ const audienceHtml = audienceLabel(record)
814
+ ? `<span class="audience-badge">${escapeHtml(audienceLabel(record))}</span>`
815
+ : "";
816
+ return `<article class="record-card" data-search-card>
817
+ <div class="record-card-top">
818
+ <span class="type-badge">${escapeHtml(labelForType(record.type))}</span>${audienceHtml}
819
+ </div>
820
+ <h3><a href="${hrefFrom(currentFile, hrefForRecord(record))}">${escapeHtml(recordTitle(record))}</a></h3>
821
+ <p>${escapeHtml(summary)}</p>${tagsLine ? `\n <footer>${tagsLine}</footer>` : ""}
822
+ </article>`;
823
+ }
824
+ function formatText(text, context) {
825
+ const trimmed = text.trim();
826
+ if (!trimmed) {
827
+ return "<p>Not recorded.</p>";
828
+ }
829
+ const blocks = trimmed.split(/\n{2,}/);
830
+ return blocks.map((block) => formatBlock(block, context)).join("\n");
831
+ }
832
+ function formatBlock(block, context) {
833
+ const lines = block.split(/\r?\n/);
834
+ if (lines.every((line) => line.trim().startsWith("- "))) {
835
+ const items = lines
836
+ .map((line) => `<li>${formatInline(line.trim().slice(2), context)}</li>`)
837
+ .join("\n");
838
+ return `<ul>${items}</ul>`;
839
+ }
840
+ return `<p>${formatInline(lines.join(" "), context)}</p>`;
841
+ }
842
+ function formatInline(text, context) {
843
+ const parts = [];
844
+ const pattern = /(`[^`]+`|\[[^\]]+\]\([^)]+\))/g;
845
+ let lastIndex = 0;
846
+ let match;
847
+ while ((match = pattern.exec(text)) !== null) {
848
+ parts.push(escapeHtml(text.slice(lastIndex, match.index)));
849
+ const token = match[0];
850
+ if (token.startsWith("`")) {
851
+ parts.push(`<code>${escapeHtml(token.slice(1, -1))}</code>`);
852
+ }
853
+ else {
854
+ const linkMatch = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
855
+ if (linkMatch) {
856
+ const label = linkMatch[1];
857
+ const target = linkMatch[2];
858
+ parts.push(`<a href="${escapeAttribute(normalizeContentHref(target, context))}">${escapeHtml(label)}</a>`);
859
+ }
860
+ }
861
+ lastIndex = match.index + token.length;
862
+ }
863
+ parts.push(escapeHtml(text.slice(lastIndex)));
864
+ return parts.join("");
865
+ }
866
+ function endpointLink(endpoint, context) {
867
+ const recordHrefForEndpoint = context.hrefById.get(endpoint);
868
+ if (recordHrefForEndpoint) {
869
+ return `<a href="${hrefFrom(context.currentFile, recordHrefForEndpoint)}"><code>${escapeHtml(endpoint)}</code></a>`;
870
+ }
871
+ const articleHrefForEndpoint = context.articleHrefByLookup.get(normalizeArticleLookup(endpoint));
872
+ if (articleHrefForEndpoint) {
873
+ return `<a href="${hrefFrom(context.currentFile, articleHrefForEndpoint)}"><code>${escapeHtml(endpoint)}</code></a>`;
874
+ }
875
+ const siteHref = siteHrefForTarget(endpoint, context);
876
+ if (siteHref) {
877
+ return `<a href="${escapeAttribute(siteHref)}"><code>${escapeHtml(endpoint)}</code></a>`;
878
+ }
879
+ const sourceHref = sourceHrefForFile(endpoint, context);
880
+ if (sourceHref) {
881
+ return `<a class="source-link" href="${escapeAttribute(sourceHref)}"><code>${escapeHtml(endpoint)}</code></a>`;
882
+ }
883
+ return `<code>${escapeHtml(endpoint)}</code>`;
884
+ }
885
+ function sourceFileLink(file, context) {
886
+ const href = sourceHrefForFile(file, context);
887
+ if (!href) {
888
+ return `<code>${escapeHtml(file)}</code>`;
889
+ }
890
+ return `<a class="source-link" href="${escapeAttribute(href)}"><code>${escapeHtml(file)}</code></a>`;
891
+ }
892
+ function normalizeContentHref(target, context) {
893
+ const trimmed = target.trim();
894
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) || trimmed.startsWith("#")) {
895
+ return trimmed;
896
+ }
897
+ const recordHrefForTarget = context.hrefById.get(trimmed);
898
+ if (recordHrefForTarget) {
899
+ return hrefFrom(context.currentFile, recordHrefForTarget);
900
+ }
901
+ const articleHrefForTarget = context.articleHrefByLookup.get(normalizeArticleLookup(trimmed));
902
+ if (articleHrefForTarget) {
903
+ return hrefFrom(context.currentFile, articleHrefForTarget);
904
+ }
905
+ const siteHref = siteHrefForTarget(trimmed, context);
906
+ if (siteHref) {
907
+ return siteHref;
908
+ }
909
+ const sourceHref = sourceHrefForFile(trimmed.replace(/^\.\//, ""), context);
910
+ if (sourceHref) {
911
+ return sourceHref;
912
+ }
913
+ return trimmed;
914
+ }
915
+ function siteHrefForTarget(target, context) {
916
+ const trimmed = target.trim();
917
+ const [withoutHash, hash = ""] = trimmed.split("#", 2);
918
+ const hashSuffix = hash ? `#${hash}` : "";
919
+ const normalized = withoutHash
920
+ .replace(/\\/g, "/")
921
+ .replace(/^\.\//, "")
922
+ .replace(/^(\.\.\/)+/, "")
923
+ .replace(/^wiki\//, "");
924
+ const route = normalized.replace(/\/$/, "");
925
+ const targetFile = markdownRouteToHtml(route);
926
+ if (targetFile) {
927
+ return `${hrefFrom(context.currentFile, targetFile)}${hashSuffix}`;
928
+ }
929
+ return undefined;
930
+ }
931
+ function markdownRouteToHtml(route) {
932
+ const routes = new Map([
933
+ ["", "index.html"],
934
+ ["index", "index.html"],
935
+ ["index.md", "index.html"],
936
+ ["articles", "articles.html"],
937
+ ["articles.md", "articles.html"],
938
+ ["guides", "guides.html"],
939
+ ["guides.md", "guides.html"],
940
+ ["concepts", "concepts.html"],
941
+ ["concepts.md", "concepts.html"],
942
+ ["decisions", "decisions.html"],
943
+ ["decisions.md", "decisions.html"],
944
+ ["devlog", "devlog.html"],
945
+ ["devlog.md", "devlog.html"],
946
+ ["notes", "notes.html"],
947
+ ["notes.md", "notes.html"],
948
+ ["symbols", "symbols.html"],
949
+ ["symbols.md", "symbols.html"],
950
+ ["links", "links.html"],
951
+ ["links.md", "links.html"]
952
+ ]);
953
+ return routes.get(route);
954
+ }
955
+ function sourceHrefForFile(file, context) {
956
+ if (!file || path_1.default.isAbsolute(file) || file.startsWith("..") || file.includes("://") || file.startsWith("#")) {
957
+ return undefined;
958
+ }
959
+ if (context.sourceBaseUrl) {
960
+ return remoteSourceHref(file, context.sourceBaseUrl, context.root);
961
+ }
962
+ return `${"../".repeat(depthOf(context.currentFile) + 1)}${encodePath(toPosixPath(file))}`;
963
+ }
964
+ function remoteSourceHref(file, sourceBaseUrl, root) {
965
+ const normalizedFile = toPosixPath(file).replace(/^\.\//, "");
966
+ const isDirectory = isDirectoryPath(root, normalizedFile);
967
+ const baseUrl = isDirectory ? treeUrlForBase(sourceBaseUrl) : sourceBaseUrl;
968
+ return `${baseUrl}${encodePath(normalizedFile)}`;
969
+ }
970
+ function treeUrlForBase(sourceBaseUrl) {
971
+ return sourceBaseUrl.replace("/blob/", "/tree/");
972
+ }
973
+ function isDirectoryPath(root, file) {
974
+ try {
975
+ return fs_1.default.statSync(path_1.default.resolve(root, file)).isDirectory();
976
+ }
977
+ catch {
978
+ return false;
979
+ }
980
+ }
981
+ function searchIndexJs(model) {
982
+ const articleEntries = model.records.article.map((article) => ({
983
+ type: "article",
984
+ typeLabel: "Article",
985
+ id: article.id,
986
+ title: article.title,
987
+ summary: recordExcerpt(article),
988
+ tags: [...article.categories, ...article.aliases, ...displayTags(article)],
989
+ authority: article.authority,
990
+ confidence: article.confidence,
991
+ audienceLabel: audienceLabel(article),
992
+ url: articleHref(article),
993
+ text: recordSearchText(article)
994
+ }));
995
+ const recordEntries = model.sourceRecords.map((record) => ({
996
+ type: record.type,
997
+ typeLabel: labelForType(record.type),
998
+ id: record.id,
999
+ title: recordTitle(record),
1000
+ summary: recordExcerpt(record),
1001
+ tags: displayTags(record),
1002
+ authority: record.authority,
1003
+ confidence: record.confidence,
1004
+ audienceLabel: audienceLabel(record),
1005
+ url: recordHref(record),
1006
+ text: recordSearchText(record)
1007
+ }));
1008
+ const workEntries = showBeadsWork(model) && model.integrations.beads?.available
1009
+ ? [
1010
+ ...model.integrations.beads.ready,
1011
+ ...model.integrations.beads.in_progress,
1012
+ ...model.integrations.beads.recent_closed
1013
+ ].map((issue) => ({
1014
+ type: "work",
1015
+ typeLabel: "Project Work",
1016
+ id: `beads:${issue.id}`,
1017
+ title: issue.title,
1018
+ summary: beadsIssueSummary(issue),
1019
+ tags: issue.labels,
1020
+ authority: "system",
1021
+ confidence: "high",
1022
+ audienceLabel: "For developers",
1023
+ url: "work.html",
1024
+ text: [
1025
+ issue.id,
1026
+ issue.title,
1027
+ issue.status,
1028
+ issue.type,
1029
+ issue.priority,
1030
+ issue.assignee,
1031
+ issue.labels.join(" ")
1032
+ ].filter(Boolean).join(" ")
1033
+ }))
1034
+ : [];
1035
+ const entries = [...articleEntries, ...recordEntries, ...workEntries];
1036
+ return `/* Generated by Wikiwiki from .wikiwiki/records. Edit structured records instead, then run \`wk site\`. */
1037
+ window.WIKIWIKI_SEARCH_INDEX = ${JSON.stringify(entries, null, 2)};
1038
+ `;
1039
+ }
1040
+ function siteManifest(model) {
1041
+ const integrations = siteManifestIntegrations(model);
1042
+ return {
1043
+ generator: "Wikiwiki",
1044
+ generated_from: ".wikiwiki/records",
1045
+ command: "wk site",
1046
+ source_base_url: model.options.sourceBaseUrl ?? null,
1047
+ audience: model.audience,
1048
+ project_name: model.repoName,
1049
+ theme_file: ".wikiwiki/site-theme.json",
1050
+ pages: sitePages(model),
1051
+ ...(integrations ? { integrations } : {}),
1052
+ articles: model.records.article.map((article) => ({
1053
+ id: article.id,
1054
+ title: article.title,
1055
+ slug: article.slug,
1056
+ url: articleHref(article)
1057
+ })),
1058
+ records: model.sourceRecords.map((record) => ({
1059
+ type: record.type,
1060
+ id: record.id,
1061
+ title: recordTitle(record),
1062
+ url: recordHref(record)
1063
+ }))
1064
+ };
1065
+ }
1066
+ function siteManifestIntegrations(model) {
1067
+ const beads = model.integrations.beads;
1068
+ if (!beads || model.audience === "user") {
1069
+ return undefined;
1070
+ }
1071
+ if (showBeadsWork(model)) {
1072
+ return { beads };
1073
+ }
1074
+ if (!(0, beads_1.shouldReportIntegrations)(model.integrations)) {
1075
+ return undefined;
1076
+ }
1077
+ return {
1078
+ beads: {
1079
+ detected: beads.detected,
1080
+ enabled: beads.enabled,
1081
+ available: beads.available,
1082
+ configured: beads.configured,
1083
+ ...(beads.error ? { error: beads.error } : {}),
1084
+ warnings: beads.warnings,
1085
+ counts: beads.counts,
1086
+ issue_ids: [],
1087
+ ready: [],
1088
+ in_progress: [],
1089
+ recent_closed: []
1090
+ }
1091
+ };
1092
+ }
1093
+ function sitePages(model) {
1094
+ return [
1095
+ ...exports.siteStaticPageFileNames.filter((fileName) => fileName !== "work.html" || showBeadsWork(model)),
1096
+ ...model.records.article.map(articleHref),
1097
+ ...model.sourceRecords.map(recordHref)
1098
+ ];
1099
+ }
1100
+ function hrefForRecord(record) {
1101
+ return record.type === "article" ? articleHref(record) : recordHref(record);
1102
+ }
1103
+ function articleHref(record) {
1104
+ return `articles/${(0, articles_1.articleFileName)(record)}`;
1105
+ }
1106
+ function recordHref(record) {
1107
+ return `records/${record.type}/${safeFileName(record.id)}.html`;
1108
+ }
1109
+ function recordTitle(record) {
1110
+ if (record.type === "note") {
1111
+ return noteTitle(record);
1112
+ }
1113
+ if ("name" in record && typeof record.name === "string") {
1114
+ return record.name;
1115
+ }
1116
+ if ("title" in record && typeof record.title === "string") {
1117
+ return record.title;
1118
+ }
1119
+ if ("summary" in record && typeof record.summary === "string") {
1120
+ return record.summary;
1121
+ }
1122
+ if ("body" in record && typeof record.body === "string") {
1123
+ return conciseExcerpt(record.body, 80);
1124
+ }
1125
+ if ("relationship" in record && typeof record.relationship === "string") {
1126
+ return `${record.from} ${record.relationship} ${record.to}`;
1127
+ }
1128
+ return record.id;
1129
+ }
1130
+ function noteTitle(record) {
1131
+ return record.title ?? `Note from ${formatDate(record.created_at)}`;
1132
+ }
1133
+ function recordSummary(record) {
1134
+ if ("summary" in record && typeof record.summary === "string" && record.summary.trim()) {
1135
+ return record.summary.trim();
1136
+ }
1137
+ if ("decision" in record && typeof record.decision === "string" && record.decision.trim()) {
1138
+ return record.decision.trim();
1139
+ }
1140
+ if ("body" in record && typeof record.body === "string" && record.body.trim()) {
1141
+ return record.body.trim();
1142
+ }
1143
+ if ("relationship" in record && typeof record.relationship === "string") {
1144
+ return `${record.from} ${record.relationship} ${record.to}`;
1145
+ }
1146
+ return "No summary recorded.";
1147
+ }
1148
+ function recordExcerpt(record) {
1149
+ return conciseExcerpt(recordSummary(record));
1150
+ }
1151
+ function conciseExcerpt(text, maxLength = 150) {
1152
+ const normalized = text.replace(/\s+/g, " ").trim();
1153
+ if (normalized.length <= maxLength) {
1154
+ return normalized;
1155
+ }
1156
+ const window = normalized.slice(0, maxLength + 1);
1157
+ const sentenceEnds = [". ", "! ", "? "]
1158
+ .map((marker) => window.lastIndexOf(marker))
1159
+ .filter((index) => index >= 32);
1160
+ const sentenceEnd = sentenceEnds.length ? Math.max(...sentenceEnds) : -1;
1161
+ if (sentenceEnd >= 0) {
1162
+ return window.slice(0, sentenceEnd + 1).trim();
1163
+ }
1164
+ const wordBreak = window.lastIndexOf(" ");
1165
+ const end = wordBreak >= 72 ? wordBreak : maxLength;
1166
+ return `${normalized.slice(0, end).replace(/[\s,;:.-]+$/, "")}...`;
1167
+ }
1168
+ function recordSearchText(record) {
1169
+ return Object.values(record)
1170
+ .flatMap((value) => Array.isArray(value) ? value : [value])
1171
+ .filter((value) => typeof value === "string")
1172
+ .join(" ");
1173
+ }
1174
+ function recordTimestamp(record) {
1175
+ if ("updated_at" in record && typeof record.updated_at === "string") {
1176
+ return record.updated_at;
1177
+ }
1178
+ return record.created_at;
1179
+ }
1180
+ function recordTags(record) {
1181
+ if ("tags" in record && Array.isArray(record.tags)) {
1182
+ return record.tags;
1183
+ }
1184
+ return [];
1185
+ }
1186
+ function relatedFiles(record) {
1187
+ if ("files" in record && Array.isArray(record.files)) {
1188
+ return record.files.map(toPosixPath);
1189
+ }
1190
+ if ("file" in record && typeof record.file === "string") {
1191
+ return [toPosixPath(record.file)];
1192
+ }
1193
+ return [];
1194
+ }
1195
+ function filterRecordsByAudience(records, audience) {
1196
+ const filtered = {};
1197
+ for (const type of schemas_1.recordTypes) {
1198
+ filtered[type] = records[type].filter((record) => recordMatchesAudience(record, audience));
1199
+ }
1200
+ return filtered;
1201
+ }
1202
+ function recordMatchesAudience(record, audience) {
1203
+ if (audience === "all") {
1204
+ return true;
1205
+ }
1206
+ const recordAudienceValue = recordAudience(record);
1207
+ if (recordAudienceValue === "all" || recordAudienceValue === audience) {
1208
+ return true;
1209
+ }
1210
+ if (recordAudienceValue) {
1211
+ return false;
1212
+ }
1213
+ if (audience === "user" && (record.type === "symbol" || record.type === "link")) {
1214
+ return false;
1215
+ }
1216
+ return true;
1217
+ }
1218
+ function compareRecords(a, b) {
1219
+ return a.type.localeCompare(b.type) ||
1220
+ recordTitle(a).localeCompare(recordTitle(b)) ||
1221
+ a.id.localeCompare(b.id);
1222
+ }
1223
+ function homepageRecords(model) {
1224
+ const articles = curatedRecords(model, ["article"], 6);
1225
+ if (articles.length > 0) {
1226
+ return articles;
1227
+ }
1228
+ const curated = curatedRecords(model, ["concept", "decision", "note"], 6);
1229
+ if (curated.length > 0) {
1230
+ return curated;
1231
+ }
1232
+ return curatedRecords(model, schemas_1.recordTypes, 6);
1233
+ }
1234
+ function userGuideRecordsFor(model) {
1235
+ return model.flatRecords
1236
+ .filter((record) => ["concept", "note", "decision"].includes(record.type))
1237
+ .filter(isUserGuideRecord)
1238
+ .sort(compareRecordsForCuration)
1239
+ .slice(0, 8);
1240
+ }
1241
+ function curatedRecords(model, types, limit) {
1242
+ const allowedTypes = new Set(types);
1243
+ return model.flatRecords
1244
+ .filter((record) => allowedTypes.has(record.type))
1245
+ .sort(compareRecordsForCuration)
1246
+ .slice(0, limit);
1247
+ }
1248
+ function takeUnusedRecords(records, usedIds, limit) {
1249
+ const result = [];
1250
+ for (const record of records) {
1251
+ if (usedIds.has(record.id)) {
1252
+ continue;
1253
+ }
1254
+ usedIds.add(record.id);
1255
+ result.push(record);
1256
+ if (result.length >= limit) {
1257
+ break;
1258
+ }
1259
+ }
1260
+ return result;
1261
+ }
1262
+ function compareRecordsForCuration(a, b) {
1263
+ return curationScore(b) - curationScore(a) ||
1264
+ recordTitle(a).localeCompare(recordTitle(b)) ||
1265
+ a.id.localeCompare(b.id);
1266
+ }
1267
+ function curationScore(record) {
1268
+ let score = 0;
1269
+ const tags = recordTags(record).map((tag) => tag.toLowerCase());
1270
+ if (tags.includes(profiles_1.audienceTags.user) || tags.includes(profiles_1.audienceTags.all)) {
1271
+ score += 16;
1272
+ }
1273
+ if (tags.includes(profiles_1.audienceTags.developer)) {
1274
+ score -= 4;
1275
+ }
1276
+ if (isUserGuideRecord(record)) {
1277
+ score += 28;
1278
+ }
1279
+ if (tags.some((tag) => ["pinned", "homepage", "start-here", "overview", "guide"].includes(tag))) {
1280
+ score += 80;
1281
+ }
1282
+ if (record.authority === "user") {
1283
+ score += 35;
1284
+ }
1285
+ if (record.confidence === "high") {
1286
+ score += 18;
1287
+ }
1288
+ if (record.type === "concept") {
1289
+ score += 24;
1290
+ }
1291
+ if (record.type === "decision") {
1292
+ score += 20;
1293
+ }
1294
+ if (record.type === "note") {
1295
+ score += 6;
1296
+ }
1297
+ if (record.type === "event") {
1298
+ score -= tags.some((tag) => ["release", "milestone", "launch"].includes(tag)) ? 6 : 28;
1299
+ }
1300
+ if (tags.some((tag) => ["internal", "maintenance", "implementation", "package", "ci", "devlog"].includes(tag))) {
1301
+ score -= 12;
1302
+ }
1303
+ return score;
1304
+ }
1305
+ function isUserGuideRecord(record) {
1306
+ const tags = recordTags(record).map((tag) => tag.toLowerCase());
1307
+ return tags.some((tag) => profiles_1.userMaterialTags.includes(tag)) ||
1308
+ tags.includes(profiles_1.audienceTags.user);
1309
+ }
1310
+ function statsTypes(model) {
1311
+ const preferred = ["concept", "decision", "event", "note"];
1312
+ const visible = preferred.filter((type) => model.counts[type] > 0);
1313
+ if (visible.length > 0) {
1314
+ return visible;
1315
+ }
1316
+ return schemas_1.recordTypes.filter((type) => model.counts[type] > 0).slice(0, 4);
1317
+ }
1318
+ function showBeadsWork(model) {
1319
+ const beads = model.integrations.beads;
1320
+ return model.audience !== "user" &&
1321
+ Boolean(beads?.detected) &&
1322
+ Boolean(beads?.enabled) &&
1323
+ beads?.configured === "enabled";
1324
+ }
1325
+ function beadsNavCount(beads) {
1326
+ if (!beads?.available) {
1327
+ return undefined;
1328
+ }
1329
+ return beads.counts.ready + beads.counts.in_progress;
1330
+ }
1331
+ function beadsIssueSummary(issue) {
1332
+ return [
1333
+ issue.status,
1334
+ issue.type,
1335
+ issue.priority,
1336
+ issue.assignee ? `assigned to ${issue.assignee}` : ""
1337
+ ].filter(Boolean).join(" · ") || "Beads work item.";
1338
+ }
1339
+ function tagList(tags) {
1340
+ if (tags.length === 0) {
1341
+ return "";
1342
+ }
1343
+ return `<span class="tag-list">${tags.map((tag) => `<span>${escapeHtml(tag)}</span>`).join("")}</span>`;
1344
+ }
1345
+ function displayTags(record) {
1346
+ return recordTags(record).filter((tag) => !isAudienceTag(tag));
1347
+ }
1348
+ function isAudienceTag(tag) {
1349
+ const normalized = tag.toLowerCase();
1350
+ return normalized === profiles_1.audienceTags.user ||
1351
+ normalized === profiles_1.audienceTags.developer ||
1352
+ normalized === profiles_1.audienceTags.all;
1353
+ }
1354
+ function recordAudience(record) {
1355
+ const tags = recordTags(record).map((tag) => tag.toLowerCase());
1356
+ if (tags.includes(profiles_1.audienceTags.user)) {
1357
+ return "user";
1358
+ }
1359
+ if (tags.includes(profiles_1.audienceTags.developer)) {
1360
+ return "developer";
1361
+ }
1362
+ if (tags.includes(profiles_1.audienceTags.all)) {
1363
+ return "all";
1364
+ }
1365
+ return undefined;
1366
+ }
1367
+ function audienceLabel(record) {
1368
+ const audience = recordAudience(record);
1369
+ if (audience === "user") {
1370
+ return "For users";
1371
+ }
1372
+ if (audience === "developer") {
1373
+ return "For developers";
1374
+ }
1375
+ if (audience === "all") {
1376
+ return "For everyone";
1377
+ }
1378
+ return "";
1379
+ }
1380
+ function audienceIntro(audience) {
1381
+ if (audience === "user") {
1382
+ return "This view emphasizes getting started, product orientation, privacy, FAQ, and troubleshooting while keeping developer-only records out of the way.";
1383
+ }
1384
+ if (audience === "developer") {
1385
+ return "This view emphasizes architecture, decisions, source symbols, generated-file workflow, and maintainer context.";
1386
+ }
1387
+ return "Start with curated guides, then browse source-backed user and developer knowledge when you need more detail.";
1388
+ }
1389
+ function emptyState(message) {
1390
+ return `<div class="empty-state">${escapeHtml(message)}</div>`;
1391
+ }
1392
+ function labelForType(type) {
1393
+ if (type === "event") {
1394
+ return "Devlog";
1395
+ }
1396
+ return type[0].toUpperCase() + type.slice(1);
1397
+ }
1398
+ function initialsForName(value) {
1399
+ const words = value
1400
+ .replace(/[_-]+/g, " ")
1401
+ .split(/\s+/)
1402
+ .map((word) => word.trim())
1403
+ .filter(Boolean);
1404
+ const initials = words.length >= 2
1405
+ ? `${words[0][0]}${words[1][0]}`
1406
+ : (words[0] ?? "W").slice(0, 2);
1407
+ return initials.toUpperCase();
1408
+ }
1409
+ function formatDate(value) {
1410
+ const date = new Date(value);
1411
+ if (Number.isNaN(date.getTime())) {
1412
+ return value;
1413
+ }
1414
+ return date.toISOString().slice(0, 10);
1415
+ }
1416
+ function hrefFrom(currentFile, targetFile) {
1417
+ const currentDir = path_1.default.posix.dirname(currentFile);
1418
+ const relative = currentDir === "."
1419
+ ? targetFile
1420
+ : path_1.default.posix.relative(currentDir, targetFile);
1421
+ return escapeAttribute(relative || path_1.default.posix.basename(targetFile));
1422
+ }
1423
+ function relativePrefix(fileName) {
1424
+ return "../".repeat(depthOf(fileName));
1425
+ }
1426
+ function depthOf(fileName) {
1427
+ return fileName.split("/").length - 1;
1428
+ }
1429
+ function safeFileName(value) {
1430
+ return value.replace(/[^a-z0-9._-]+/gi, "-");
1431
+ }
1432
+ function toPosixPath(value) {
1433
+ return value.replace(/\\/g, "/");
1434
+ }
1435
+ function encodePath(value) {
1436
+ return value.split("/").map(encodeURIComponent).join("/");
1437
+ }
1438
+ function escapeHtml(value) {
1439
+ return value
1440
+ .replace(/&/g, "&amp;")
1441
+ .replace(/</g, "&lt;")
1442
+ .replace(/>/g, "&gt;")
1443
+ .replace(/"/g, "&quot;")
1444
+ .replace(/'/g, "&#39;");
1445
+ }
1446
+ function escapeAttribute(value) {
1447
+ return escapeHtml(value);
1448
+ }
1449
+ function projectThemeCss(theme) {
1450
+ const resolved = resolveThemePalettes(theme);
1451
+ if (!resolved.hasTheme) {
1452
+ return "/* Optional project theme. Add .wikiwiki/site-theme.json to override CSS custom properties. */\n";
1453
+ }
1454
+ const light = themeWithContrastGuardrails(resolved.light, "light");
1455
+ const dark = themeWithContrastGuardrails(resolved.dark, "dark");
1456
+ const comments = [...light.comments, ...dark.comments];
1457
+ const commentBlock = comments.length
1458
+ ? `\n${comments.map((comment) => `/* ${comment} */`).join("\n")}`
1459
+ : "";
1460
+ return `/* Generated from .wikiwiki/site-theme.json. Edit the theme file, then run \`wk site\`. */${commentBlock}
1461
+ ${themeCssBlock(":root,\n:root[data-theme=\"light\"]", light.theme, "light")}
1462
+
1463
+ @media (prefers-color-scheme: dark) {
1464
+ ${themeCssBlock(":root:not([data-theme=\"light\"])", dark.theme, "dark", " ")}
1465
+ }
1466
+
1467
+ ${themeCssBlock(":root[data-theme=\"dark\"]", dark.theme, "dark")}
1468
+ `;
1469
+ }
1470
+ function resolveThemePalettes(theme) {
1471
+ const flat = themePaletteFrom(theme);
1472
+ const modeLight = theme.modes?.light ?? {};
1473
+ const modeDark = theme.modes?.dark ?? {};
1474
+ const hasFlat = hasThemePaletteValues(flat);
1475
+ const hasLight = hasThemePaletteValues(modeLight);
1476
+ const hasDark = hasThemePaletteValues(modeDark);
1477
+ if (!hasFlat && !hasLight && !hasDark) {
1478
+ return { hasTheme: false, light: {}, dark: {} };
1479
+ }
1480
+ const common = commonPaletteValues(flat);
1481
+ const light = hasLight || hasFlat
1482
+ ? { ...flat, ...modeLight }
1483
+ : { ...deriveLightPalette(modeDark), ...common };
1484
+ const dark = hasDark
1485
+ ? { ...deriveDarkPalette(light), ...common, ...modeDark }
1486
+ : deriveDarkPalette(light);
1487
+ return { hasTheme: true, light, dark };
1488
+ }
1489
+ function themePaletteFrom(theme) {
1490
+ const palette = {};
1491
+ for (const key of config_1.siteThemePaletteKeys) {
1492
+ const value = theme[key];
1493
+ if (value) {
1494
+ palette[key] = value;
1495
+ }
1496
+ }
1497
+ return palette;
1498
+ }
1499
+ function hasThemePaletteValues(theme) {
1500
+ return config_1.siteThemePaletteKeys.some((key) => Boolean(theme[key]?.trim()));
1501
+ }
1502
+ function commonPaletteValues(theme) {
1503
+ return {
1504
+ ...(theme.radius ? { radius: theme.radius } : {}),
1505
+ ...(theme.font_family ? { font_family: theme.font_family } : {})
1506
+ };
1507
+ }
1508
+ function deriveLightPalette(theme) {
1509
+ const accent = parseHexColor(theme.accent) ?? parseHexColor(theme.accent_strong) ?? { r: 36, g: 107, b: 98 };
1510
+ const accentHex = theme.accent ?? colorToHex(accent);
1511
+ const accentStrong = theme.accent_strong ?? colorToHex(mixRgb(accent, { r: 29, g: 48, b: 44 }, 0.64));
1512
+ const secondary = theme.secondary ?? colorToHex(mixRgb(accent, { r: 196, g: 63, b: 125 }, 0.45));
1513
+ return {
1514
+ accent: accentHex,
1515
+ accent_strong: accentStrong,
1516
+ secondary,
1517
+ bg: colorToHex(mixRgb(accent, { r: 249, g: 248, b: 242 }, 0.08)),
1518
+ panel: colorToHex(mixRgb(accent, { r: 253, g: 252, b: 247 }, 0.04)),
1519
+ panel_soft: colorToHex(mixRgb(accent, { r: 238, g: 241, b: 232 }, 0.12)),
1520
+ text: "#242521",
1521
+ muted: "#63675e",
1522
+ border: colorToHex(mixRgb(accent, { r: 220, g: 223, b: 214 }, 0.18)),
1523
+ code_bg: colorToHex(mixRgb(accent, { r: 236, g: 238, b: 232 }, 0.12)),
1524
+ shadow: `0 18px 45px ${rgba({ r: 35, g: 38, b: 31 }, 0.08)}`,
1525
+ shadow_strong: `0 24px 70px ${rgba({ r: 35, g: 38, b: 31 }, 0.14)}`,
1526
+ radius: theme.radius ?? "8px",
1527
+ font_family: theme.font_family,
1528
+ sidebar_bg: `linear-gradient(180deg, ${rgba({ r: 253, g: 252, b: 247 }, 0.94)}, ${rgba(mixRgb(accent, { r: 238, g: 241, b: 232 }, 0.12), 0.88)})`,
1529
+ hero_gradient: `linear-gradient(135deg, ${colorToHex(mixRgb(accent, { r: 253, g: 252, b: 247 }, 0.06))} 0%, ${colorToHex(mixRgb(accent, { r: 244, g: 246, b: 238 }, 0.18))} 100%)`,
1530
+ card_gradient: `linear-gradient(180deg, ${colorToHex(mixRgb(accent, { r: 253, g: 252, b: 247 }, 0.04))} 0%, ${colorToHex(mixRgb(accent, { r: 248, g: 247, b: 242 }, 0.05))} 100%)`,
1531
+ brand_gradient: `linear-gradient(135deg, ${accentHex}, ${accentStrong})`,
1532
+ brand_mark_text: "#fffaf0",
1533
+ badge_bg: colorToHex(mixRgb(accent, { r: 229, g: 240, b: 237 }, 0.22)),
1534
+ badge_text: accentStrong,
1535
+ tag_bg: colorToHex(mixRgb(parseHexColor(secondary) ?? accent, { r: 248, g: 232, b: 241 }, 0.18)),
1536
+ tag_text: "#743050",
1537
+ success_bg: "#e7f3df",
1538
+ success_text: "#2d5c1f",
1539
+ warning_bg: "#f5e7df",
1540
+ warning_text: "#7a3c1d",
1541
+ focus_ring: rgba(accent, 0.18),
1542
+ gloss: rgba(accent, 0.14)
1543
+ };
1544
+ }
1545
+ function deriveDarkPalette(theme) {
1546
+ const accent = parseHexColor(theme.accent) ?? parseHexColor(theme.accent_strong) ?? { r: 56, g: 189, b: 248 };
1547
+ const accentHex = theme.accent ?? colorToHex(accent);
1548
+ const accentStrong = theme.accent_strong ?? colorToHex(mixRgb(accent, { r: 186, g: 230, b: 253 }, 0.72));
1549
+ const secondary = theme.secondary ?? colorToHex(mixRgb(accent, { r: 244, g: 114, b: 182 }, 0.36));
1550
+ const bg = mixRgb(accent, { r: 12, g: 18, b: 31 }, 0.13);
1551
+ const panel = mixRgb(accent, { r: 18, g: 25, b: 42 }, 0.11);
1552
+ const panelSoft = mixRgb(accent, { r: 28, g: 38, b: 58 }, 0.14);
1553
+ return {
1554
+ accent: accentHex,
1555
+ accent_strong: accentStrong,
1556
+ secondary,
1557
+ bg: colorToHex(bg),
1558
+ panel: colorToHex(panel),
1559
+ panel_soft: colorToHex(panelSoft),
1560
+ text: "#f7f2e8",
1561
+ muted: "#c9d0dc",
1562
+ border: colorToHex(mixRgb(accent, { r: 51, g: 65, b: 85 }, 0.16)),
1563
+ code_bg: colorToHex(mixRgb(accent, { r: 10, g: 16, b: 28 }, 0.1)),
1564
+ shadow: `0 22px 60px ${rgba({ r: 4, g: 8, b: 16 }, 0.34)}`,
1565
+ shadow_strong: `0 30px 90px ${rgba({ r: 4, g: 8, b: 16 }, 0.48)}`,
1566
+ radius: theme.radius ?? "8px",
1567
+ font_family: theme.font_family,
1568
+ sidebar_bg: `linear-gradient(180deg, ${rgba(mixRgb(accent, { r: 18, g: 25, b: 42 }, 0.14), 0.94)}, ${rgba(mixRgb(accent, { r: 12, g: 18, b: 31 }, 0.16), 0.9)})`,
1569
+ hero_gradient: `linear-gradient(135deg, ${colorToHex(mixRgb(accent, { r: 21, g: 28, b: 46 }, 0.16))} 0%, ${colorToHex(mixRgb(accent, { r: 9, g: 14, b: 25 }, 0.1))} 100%)`,
1570
+ card_gradient: `linear-gradient(180deg, ${colorToHex(mixRgb(accent, { r: 20, g: 28, b: 45 }, 0.12))} 0%, ${colorToHex(mixRgb(accent, { r: 14, g: 20, b: 34 }, 0.1))} 100%)`,
1571
+ brand_gradient: `linear-gradient(135deg, ${accentHex}, ${accentStrong})`,
1572
+ brand_mark_text: "#fffaf0",
1573
+ badge_bg: rgba(accent, 0.18),
1574
+ badge_text: "#f7f2e8",
1575
+ tag_bg: rgba(parseHexColor(secondary) ?? accent, 0.2),
1576
+ tag_text: "#f7d8e8",
1577
+ success_bg: "rgba(134, 239, 172, 0.16)",
1578
+ success_text: "#bbf7d0",
1579
+ warning_bg: "rgba(253, 186, 116, 0.16)",
1580
+ warning_text: "#fed7aa",
1581
+ focus_ring: rgba(accent, 0.28),
1582
+ gloss: rgba(accent, 0.18)
1583
+ };
1584
+ }
1585
+ function themeCssBlock(selector, theme, colorScheme, indent = "") {
1586
+ const lines = themeVariableLines(theme)
1587
+ .map((line) => `${indent}${line}`);
1588
+ return `${indent}${selector} {
1589
+ ${indent} color-scheme: ${colorScheme};
1590
+ ${lines.join("\n")}
1591
+ ${indent}}`;
1592
+ }
1593
+ function themeVariableLines(theme) {
1594
+ const variables = [
1595
+ ["accent", "--accent"],
1596
+ ["accent_strong", "--accent-strong"],
1597
+ ["secondary", "--secondary"],
1598
+ ["bg", "--bg"],
1599
+ ["panel", "--panel"],
1600
+ ["panel_soft", "--panel-soft"],
1601
+ ["text", "--text"],
1602
+ ["muted", "--muted"],
1603
+ ["border", "--border"],
1604
+ ["code_bg", "--code-bg"],
1605
+ ["shadow", "--shadow"],
1606
+ ["shadow_strong", "--shadow-strong"],
1607
+ ["radius", "--radius"],
1608
+ ["font_family", "--font-family"],
1609
+ ["sidebar_bg", "--sidebar-bg"],
1610
+ ["hero_gradient", "--hero-gradient"],
1611
+ ["card_gradient", "--card-gradient"],
1612
+ ["brand_gradient", "--brand-gradient"],
1613
+ ["brand_mark_text", "--brand-mark-text"],
1614
+ ["badge_bg", "--badge-bg"],
1615
+ ["badge_text", "--badge-text"],
1616
+ ["tag_bg", "--tag-bg"],
1617
+ ["tag_text", "--tag-text"],
1618
+ ["success_bg", "--success-bg"],
1619
+ ["success_text", "--success-text"],
1620
+ ["warning_bg", "--warning-bg"],
1621
+ ["warning_text", "--warning-text"],
1622
+ ["focus_ring", "--focus-ring"],
1623
+ ["gloss", "--gloss"]
1624
+ ];
1625
+ return variables.flatMap(([key, variable]) => {
1626
+ const value = theme[key]?.trim();
1627
+ return value ? [` ${variable}: ${cssValue(value)};`] : [];
1628
+ });
1629
+ }
1630
+ function themeWithContrastGuardrails(theme, mode) {
1631
+ const guarded = { ...theme };
1632
+ const comments = [];
1633
+ const bg = parseHexColor(guarded.bg);
1634
+ if (bg && !guarded.panel && relativeLuminance(bg) < 0.18) {
1635
+ guarded.panel = "#111827";
1636
+ guarded.panel_soft = guarded.panel_soft ?? "#1f2937";
1637
+ guarded.text = guarded.text ?? "#f8fafc";
1638
+ guarded.muted = guarded.muted ?? "#cbd5e1";
1639
+ guarded.border = guarded.border ?? "#334155";
1640
+ guarded.code_bg = guarded.code_bg ?? "#0f172a";
1641
+ comments.push(`${mode} mode: Applied dark-theme surface defaults so text remains readable.`);
1642
+ }
1643
+ const panel = parseHexColor(guarded.panel ?? (mode === "dark" ? "#111827" : "#fffdf7"));
1644
+ const text = parseHexColor(guarded.text ?? (mode === "dark" ? "#f8fafc" : "#242521"));
1645
+ if (panel && text && contrastRatio(panel, text) < 4.5) {
1646
+ guarded.text = readableTextFor(panel);
1647
+ comments.push(`${mode} mode: Adjusted --text for readable contrast against --panel.`);
1648
+ }
1649
+ const muted = parseHexColor(guarded.muted ?? (mode === "dark" ? "#cbd5e1" : "#63675e"));
1650
+ const guardedPanel = parseHexColor(guarded.panel ?? (mode === "dark" ? "#111827" : "#fffdf7"));
1651
+ if (guardedPanel && muted && contrastRatio(guardedPanel, muted) < 3) {
1652
+ guarded.muted = relativeLuminance(guardedPanel) > 0.5 ? "#4b5563" : "#cbd5e1";
1653
+ comments.push(`${mode} mode: Adjusted --muted for readable contrast against --panel.`);
1654
+ }
1655
+ const accent = parseHexColor(guarded.accent);
1656
+ if (guardedPanel && accent && contrastRatio(guardedPanel, accent) < 3) {
1657
+ guarded.accent = readableTextFor(guardedPanel);
1658
+ comments.push(`${mode} mode: Adjusted --accent for readable contrast against --panel.`);
1659
+ }
1660
+ const badgeBg = parseHexColor(guarded.badge_bg);
1661
+ const badgeText = parseHexColor(guarded.badge_text);
1662
+ if (badgeBg && badgeText && contrastRatio(badgeBg, badgeText) < 4.5) {
1663
+ guarded.badge_text = readableTextFor(badgeBg);
1664
+ comments.push(`${mode} mode: Adjusted --badge-text for readable contrast against --badge-bg.`);
1665
+ }
1666
+ const tagBg = parseHexColor(guarded.tag_bg);
1667
+ const tagText = parseHexColor(guarded.tag_text);
1668
+ if (tagBg && tagText && contrastRatio(tagBg, tagText) < 4.5) {
1669
+ guarded.tag_text = readableTextFor(tagBg);
1670
+ comments.push(`${mode} mode: Adjusted --tag-text for readable contrast against --tag-bg.`);
1671
+ }
1672
+ return { theme: guarded, comments };
1673
+ }
1674
+ function parseHexColor(value) {
1675
+ const trimmed = value?.trim();
1676
+ if (!trimmed) {
1677
+ return undefined;
1678
+ }
1679
+ const match = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(trimmed);
1680
+ if (!match) {
1681
+ return undefined;
1682
+ }
1683
+ const hex = match[1].length === 3
1684
+ ? match[1].split("").map((part) => `${part}${part}`).join("")
1685
+ : match[1];
1686
+ return {
1687
+ r: Number.parseInt(hex.slice(0, 2), 16),
1688
+ g: Number.parseInt(hex.slice(2, 4), 16),
1689
+ b: Number.parseInt(hex.slice(4, 6), 16)
1690
+ };
1691
+ }
1692
+ function contrastRatio(a, b) {
1693
+ const lighter = Math.max(relativeLuminance(a), relativeLuminance(b));
1694
+ const darker = Math.min(relativeLuminance(a), relativeLuminance(b));
1695
+ return (lighter + 0.05) / (darker + 0.05);
1696
+ }
1697
+ function relativeLuminance(color) {
1698
+ const [r, g, b] = [color.r, color.g, color.b].map((channel) => {
1699
+ const value = channel / 255;
1700
+ return value <= 0.03928
1701
+ ? value / 12.92
1702
+ : ((value + 0.055) / 1.055) ** 2.4;
1703
+ });
1704
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
1705
+ }
1706
+ function readableTextFor(background) {
1707
+ return relativeLuminance(background) > 0.5 ? "#111827" : "#f8fafc";
1708
+ }
1709
+ function mixRgb(a, b, aWeight) {
1710
+ const clampedWeight = Math.max(0, Math.min(1, aWeight));
1711
+ const bWeight = 1 - clampedWeight;
1712
+ return {
1713
+ r: Math.round(a.r * clampedWeight + b.r * bWeight),
1714
+ g: Math.round(a.g * clampedWeight + b.g * bWeight),
1715
+ b: Math.round(a.b * clampedWeight + b.b * bWeight)
1716
+ };
1717
+ }
1718
+ function colorToHex(color) {
1719
+ return `#${[color.r, color.g, color.b]
1720
+ .map((channel) => Math.max(0, Math.min(255, channel)).toString(16).padStart(2, "0"))
1721
+ .join("")}`;
1722
+ }
1723
+ function rgba(color, alpha) {
1724
+ const clampedAlpha = Math.max(0, Math.min(1, alpha));
1725
+ return `rgba(${color.r}, ${color.g}, ${color.b}, ${Number(clampedAlpha.toFixed(3))})`;
1726
+ }
1727
+ function cssValue(value) {
1728
+ return value.replace(/[;\n\r]/g, "").trim();
1729
+ }
1730
+ function faviconSvg(model) {
1731
+ const initials = escapeHtml(model.projectInitials.slice(0, 2));
1732
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
1733
+ <rect width="64" height="64" rx="14" fill="#111827"/>
1734
+ <circle cx="46" cy="18" r="8" fill="#38bdf8"/>
1735
+ <text x="32" y="41" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="22" font-weight="700" fill="#f8fafc">${initials}</text>
1736
+ </svg>
1737
+ `;
1738
+ }
1739
+ function siteCss() {
1740
+ return `/* Generated by Wikiwiki from .wikiwiki/records. Edit structured records instead, then run \`wk site\`. */
1741
+ :root {
1742
+ color-scheme: light;
1743
+ --bg: #f7f7f4;
1744
+ --panel: #fffdf7;
1745
+ --panel-soft: #f0f2ed;
1746
+ --text: #242521;
1747
+ --muted: #63675e;
1748
+ --border: #dcdfd6;
1749
+ --accent: #246b62;
1750
+ --accent-strong: #163f3a;
1751
+ --secondary: #c43f7d;
1752
+ --code-bg: #eceee8;
1753
+ --shadow: 0 18px 45px rgba(35, 38, 31, 0.08);
1754
+ --shadow-strong: 0 24px 70px rgba(35, 38, 31, 0.14);
1755
+ --radius: 8px;
1756
+ --font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1757
+ --sidebar-bg: linear-gradient(180deg, rgba(255, 253, 247, 0.94), rgba(240, 242, 237, 0.88));
1758
+ --hero-gradient: linear-gradient(135deg, #fffdf7 0%, #f4f6f0 100%);
1759
+ --card-gradient: linear-gradient(180deg, #fffdf7 0%, #faf9f3 100%);
1760
+ --brand-gradient: linear-gradient(135deg, var(--accent), var(--accent-strong));
1761
+ --brand-mark-text: #fffaf0;
1762
+ --badge-bg: #e5f0ed;
1763
+ --badge-text: var(--accent-strong);
1764
+ --tag-bg: #f8e8f1;
1765
+ --tag-text: #743050;
1766
+ --success-bg: #e7f3df;
1767
+ --success-text: #2d5c1f;
1768
+ --warning-bg: #f5e7df;
1769
+ --warning-text: #7a3c1d;
1770
+ --focus-ring: rgba(36, 107, 98, 0.18);
1771
+ --gloss: rgba(36, 107, 98, 0.13);
1772
+ }
1773
+
1774
+ @media (prefers-color-scheme: dark) {
1775
+ :root:not([data-theme="light"]) {
1776
+ color-scheme: dark;
1777
+ --bg: #111914;
1778
+ --panel: #18221d;
1779
+ --panel-soft: #223029;
1780
+ --text: #f7f2e8;
1781
+ --muted: #c7d0c9;
1782
+ --border: #34463d;
1783
+ --accent: #6ee7c7;
1784
+ --accent-strong: #a7f3d0;
1785
+ --secondary: #f0abfc;
1786
+ --code-bg: #0d1511;
1787
+ --shadow: 0 22px 60px rgba(4, 8, 16, 0.34);
1788
+ --shadow-strong: 0 30px 90px rgba(4, 8, 16, 0.48);
1789
+ --sidebar-bg: linear-gradient(180deg, rgba(24, 34, 29, 0.95), rgba(17, 25, 20, 0.9));
1790
+ --hero-gradient: linear-gradient(135deg, #1c2b24 0%, #0f1713 100%);
1791
+ --card-gradient: linear-gradient(180deg, #1a261f 0%, #141e18 100%);
1792
+ --brand-gradient: linear-gradient(135deg, var(--accent), #2dd4bf);
1793
+ --brand-mark-text: #101914;
1794
+ --badge-bg: rgba(110, 231, 199, 0.16);
1795
+ --badge-text: #d1fae5;
1796
+ --tag-bg: rgba(240, 171, 252, 0.18);
1797
+ --tag-text: #f5d0fe;
1798
+ --success-bg: rgba(134, 239, 172, 0.16);
1799
+ --success-text: #bbf7d0;
1800
+ --warning-bg: rgba(253, 186, 116, 0.16);
1801
+ --warning-text: #fed7aa;
1802
+ --focus-ring: rgba(110, 231, 199, 0.28);
1803
+ --gloss: rgba(110, 231, 199, 0.16);
1804
+ }
1805
+ }
1806
+
1807
+ :root[data-theme="dark"] {
1808
+ color-scheme: dark;
1809
+ --bg: #111914;
1810
+ --panel: #18221d;
1811
+ --panel-soft: #223029;
1812
+ --text: #f7f2e8;
1813
+ --muted: #c7d0c9;
1814
+ --border: #34463d;
1815
+ --accent: #6ee7c7;
1816
+ --accent-strong: #a7f3d0;
1817
+ --secondary: #f0abfc;
1818
+ --code-bg: #0d1511;
1819
+ --shadow: 0 22px 60px rgba(4, 8, 16, 0.34);
1820
+ --shadow-strong: 0 30px 90px rgba(4, 8, 16, 0.48);
1821
+ --sidebar-bg: linear-gradient(180deg, rgba(24, 34, 29, 0.95), rgba(17, 25, 20, 0.9));
1822
+ --hero-gradient: linear-gradient(135deg, #1c2b24 0%, #0f1713 100%);
1823
+ --card-gradient: linear-gradient(180deg, #1a261f 0%, #141e18 100%);
1824
+ --brand-gradient: linear-gradient(135deg, var(--accent), #2dd4bf);
1825
+ --brand-mark-text: #101914;
1826
+ --badge-bg: rgba(110, 231, 199, 0.16);
1827
+ --badge-text: #d1fae5;
1828
+ --tag-bg: rgba(240, 171, 252, 0.18);
1829
+ --tag-text: #f5d0fe;
1830
+ --success-bg: rgba(134, 239, 172, 0.16);
1831
+ --success-text: #bbf7d0;
1832
+ --warning-bg: rgba(253, 186, 116, 0.16);
1833
+ --warning-text: #fed7aa;
1834
+ --focus-ring: rgba(110, 231, 199, 0.28);
1835
+ --gloss: rgba(110, 231, 199, 0.16);
1836
+ }
1837
+
1838
+ :root[data-theme="light"] {
1839
+ color-scheme: light;
1840
+ }
1841
+
1842
+ * {
1843
+ box-sizing: border-box;
1844
+ }
1845
+
1846
+ html {
1847
+ scroll-behavior: smooth;
1848
+ }
1849
+
1850
+ body {
1851
+ margin: 0;
1852
+ background:
1853
+ radial-gradient(circle at 18% -10%, var(--gloss), transparent 34rem),
1854
+ radial-gradient(circle at 100% 0%, color-mix(in srgb, var(--secondary) 13%, transparent), transparent 28rem),
1855
+ var(--bg);
1856
+ color: var(--text);
1857
+ font-family: var(--font-family);
1858
+ line-height: 1.55;
1859
+ }
1860
+
1861
+ a {
1862
+ color: var(--accent);
1863
+ text-decoration-thickness: 0.08em;
1864
+ text-underline-offset: 0.18em;
1865
+ }
1866
+
1867
+ code {
1868
+ border-radius: 5px;
1869
+ background: var(--code-bg);
1870
+ padding: 0.12rem 0.32rem;
1871
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
1872
+ font-size: 0.9em;
1873
+ overflow-wrap: anywhere;
1874
+ }
1875
+
1876
+ .skip-link {
1877
+ position: absolute;
1878
+ left: 1rem;
1879
+ top: -4rem;
1880
+ z-index: 10;
1881
+ border-radius: 6px;
1882
+ background: var(--text);
1883
+ color: var(--panel);
1884
+ padding: 0.55rem 0.8rem;
1885
+ }
1886
+
1887
+ .skip-link:focus {
1888
+ top: 1rem;
1889
+ }
1890
+
1891
+ .site-shell {
1892
+ display: grid;
1893
+ grid-template-columns: 280px minmax(0, 1fr);
1894
+ min-height: 100vh;
1895
+ }
1896
+
1897
+ .sidebar {
1898
+ position: sticky;
1899
+ top: 0;
1900
+ height: 100vh;
1901
+ overflow: auto;
1902
+ border-right: 1px solid var(--border);
1903
+ background: var(--sidebar-bg);
1904
+ box-shadow: 12px 0 40px rgba(20, 24, 20, 0.04);
1905
+ padding: 1.1rem;
1906
+ backdrop-filter: blur(18px);
1907
+ }
1908
+
1909
+ .sidebar-top {
1910
+ display: flex;
1911
+ align-items: center;
1912
+ justify-content: space-between;
1913
+ gap: 0.8rem;
1914
+ }
1915
+
1916
+ .brand {
1917
+ display: flex;
1918
+ align-items: center;
1919
+ gap: 0.7rem;
1920
+ color: var(--text);
1921
+ text-decoration: none;
1922
+ }
1923
+
1924
+ .brand-mark {
1925
+ display: grid;
1926
+ place-items: center;
1927
+ width: 2.4rem;
1928
+ height: 2.4rem;
1929
+ border-radius: var(--radius);
1930
+ background: var(--brand-gradient);
1931
+ box-shadow: var(--shadow);
1932
+ color: var(--brand-mark-text);
1933
+ font-size: 0.85rem;
1934
+ font-weight: 800;
1935
+ letter-spacing: 0;
1936
+ }
1937
+
1938
+ .brand strong,
1939
+ .brand small {
1940
+ display: block;
1941
+ }
1942
+
1943
+ .brand small,
1944
+ .nav-heading,
1945
+ .eyebrow {
1946
+ color: var(--muted);
1947
+ font-size: 0.76rem;
1948
+ font-weight: 700;
1949
+ letter-spacing: 0;
1950
+ text-transform: uppercase;
1951
+ }
1952
+
1953
+ .nav-toggle {
1954
+ display: none;
1955
+ border: 1px solid var(--border);
1956
+ border-radius: var(--radius);
1957
+ background: var(--card-gradient);
1958
+ color: var(--text);
1959
+ padding: 0.45rem 0.65rem;
1960
+ font: inherit;
1961
+ font-weight: 700;
1962
+ }
1963
+
1964
+ .theme-control {
1965
+ display: grid;
1966
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1967
+ gap: 0.18rem;
1968
+ margin-top: 1rem;
1969
+ border: 1px solid var(--border);
1970
+ border-radius: var(--radius);
1971
+ background: var(--panel-soft);
1972
+ padding: 0.18rem;
1973
+ }
1974
+
1975
+ .theme-control button {
1976
+ min-width: 0;
1977
+ border: 0;
1978
+ border-radius: calc(var(--radius) - 2px);
1979
+ background: transparent;
1980
+ color: var(--muted);
1981
+ cursor: pointer;
1982
+ font: inherit;
1983
+ font-size: 0.76rem;
1984
+ font-weight: 800;
1985
+ letter-spacing: 0;
1986
+ padding: 0.34rem 0.2rem;
1987
+ }
1988
+
1989
+ .theme-control button[aria-pressed="true"] {
1990
+ background: var(--card-gradient);
1991
+ box-shadow: var(--shadow);
1992
+ color: var(--accent-strong);
1993
+ }
1994
+
1995
+ .theme-control button:focus-visible,
1996
+ .nav-toggle:focus-visible,
1997
+ .nav-link:focus-visible,
1998
+ a:focus-visible {
1999
+ outline: 3px solid var(--focus-ring);
2000
+ outline-offset: 2px;
2001
+ }
2002
+
2003
+ input[type="search"] {
2004
+ width: 100%;
2005
+ border: 1px solid var(--border);
2006
+ border-radius: var(--radius);
2007
+ background: var(--panel);
2008
+ color: var(--text);
2009
+ padding: 0.7rem 0.78rem;
2010
+ font: inherit;
2011
+ }
2012
+
2013
+ input[type="search"]:focus {
2014
+ border-color: var(--accent);
2015
+ outline: 3px solid var(--focus-ring);
2016
+ }
2017
+
2018
+ .nav {
2019
+ display: grid;
2020
+ gap: 0.2rem;
2021
+ margin-top: 1.2rem;
2022
+ }
2023
+
2024
+ .nav-heading {
2025
+ margin: 1rem 0 0.25rem;
2026
+ }
2027
+
2028
+ .nav-link {
2029
+ display: flex;
2030
+ align-items: center;
2031
+ justify-content: space-between;
2032
+ gap: 0.7rem;
2033
+ border-radius: var(--radius);
2034
+ color: var(--text);
2035
+ padding: 0.5rem 0.58rem;
2036
+ text-decoration: none;
2037
+ }
2038
+
2039
+ .nav-link:hover,
2040
+ .nav-link.active {
2041
+ background: var(--panel-soft);
2042
+ color: var(--accent-strong);
2043
+ }
2044
+
2045
+ .nav-link span {
2046
+ min-width: 1.7rem;
2047
+ border: 1px solid var(--border);
2048
+ border-radius: 999px;
2049
+ color: var(--muted);
2050
+ padding: 0.02rem 0.45rem;
2051
+ text-align: center;
2052
+ font-size: 0.76rem;
2053
+ }
2054
+
2055
+ .content {
2056
+ width: min(100%, 1120px);
2057
+ min-width: 0;
2058
+ padding: 2.3rem clamp(1rem, 4vw, 4rem) 4rem;
2059
+ }
2060
+
2061
+ .page-header {
2062
+ margin-bottom: 1.5rem;
2063
+ }
2064
+
2065
+ .page-header h1 {
2066
+ margin: 0.15rem 0 0.45rem;
2067
+ font-size: clamp(2rem, 4vw, 3.8rem);
2068
+ line-height: 1;
2069
+ }
2070
+
2071
+ .page-header p {
2072
+ max-width: 760px;
2073
+ color: var(--muted);
2074
+ font-size: 1.08rem;
2075
+ }
2076
+
2077
+ .hero-panel {
2078
+ display: grid;
2079
+ grid-template-columns: minmax(0, 1.5fr) minmax(260px, 0.9fr);
2080
+ gap: 1rem;
2081
+ align-items: stretch;
2082
+ border: 1px solid var(--border);
2083
+ border-radius: var(--radius);
2084
+ background: var(--hero-gradient);
2085
+ box-shadow: var(--shadow-strong);
2086
+ padding: clamp(1.1rem, 3vw, 2rem);
2087
+ }
2088
+
2089
+ .hero-panel h2,
2090
+ .section-heading h2 {
2091
+ margin: 0.2rem 0 0;
2092
+ font-size: clamp(1.35rem, 2vw, 2rem);
2093
+ line-height: 1.15;
2094
+ }
2095
+
2096
+ .hero-panel p {
2097
+ max-width: 680px;
2098
+ color: var(--muted);
2099
+ }
2100
+
2101
+ .stats-grid {
2102
+ display: grid;
2103
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2104
+ gap: 0.7rem;
2105
+ margin: 0;
2106
+ }
2107
+
2108
+ .stats-grid div {
2109
+ border: 1px solid var(--border);
2110
+ border-radius: var(--radius);
2111
+ background: color-mix(in srgb, var(--panel) 82%, transparent);
2112
+ padding: 0.9rem;
2113
+ }
2114
+
2115
+ .stats-grid dt {
2116
+ color: var(--muted);
2117
+ font-size: 0.8rem;
2118
+ }
2119
+
2120
+ .stats-grid dd {
2121
+ margin: 0.1rem 0 0;
2122
+ font-size: 1.7rem;
2123
+ font-weight: 800;
2124
+ }
2125
+
2126
+ .section {
2127
+ margin-top: 2rem;
2128
+ }
2129
+
2130
+ .section-heading {
2131
+ margin-bottom: 0.9rem;
2132
+ }
2133
+
2134
+ .card-grid {
2135
+ display: grid;
2136
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2137
+ gap: 0.85rem;
2138
+ }
2139
+
2140
+ .card-grid.compact {
2141
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2142
+ }
2143
+
2144
+ .section-card,
2145
+ .record-card,
2146
+ .search-result,
2147
+ .empty-state,
2148
+ .search-panel,
2149
+ .record-meta,
2150
+ .sources-panel {
2151
+ border: 1px solid var(--border);
2152
+ border-radius: var(--radius);
2153
+ background: var(--card-gradient);
2154
+ }
2155
+
2156
+ .section-card {
2157
+ display: block;
2158
+ min-width: 0;
2159
+ min-height: 9.5rem;
2160
+ color: var(--text);
2161
+ padding: 1rem;
2162
+ text-decoration: none;
2163
+ }
2164
+
2165
+ .section-card:hover,
2166
+ .record-card:hover,
2167
+ .search-result:hover {
2168
+ border-color: var(--focus-ring);
2169
+ box-shadow: var(--shadow-strong);
2170
+ }
2171
+
2172
+ .section-card span {
2173
+ color: var(--secondary);
2174
+ font-size: 0.8rem;
2175
+ font-weight: 700;
2176
+ }
2177
+
2178
+ .section-card strong {
2179
+ display: block;
2180
+ margin: 0.35rem 0;
2181
+ font-size: 1.25rem;
2182
+ }
2183
+
2184
+ .section-card p,
2185
+ .record-card p,
2186
+ .search-result p {
2187
+ margin-bottom: 0;
2188
+ color: var(--muted);
2189
+ }
2190
+
2191
+ .record-list {
2192
+ display: grid;
2193
+ gap: 0.75rem;
2194
+ }
2195
+
2196
+ .record-card,
2197
+ .search-result {
2198
+ min-width: 0;
2199
+ padding: 1rem;
2200
+ }
2201
+
2202
+ .record-card-top,
2203
+ .record-card footer,
2204
+ .record-title-row,
2205
+ .search-result footer {
2206
+ display: flex;
2207
+ flex-wrap: wrap;
2208
+ align-items: center;
2209
+ gap: 0.5rem;
2210
+ }
2211
+
2212
+ .record-card-top {
2213
+ justify-content: space-between;
2214
+ }
2215
+
2216
+ .record-card-top > *,
2217
+ .record-card footer > *,
2218
+ .search-result footer > *,
2219
+ .tag-list span {
2220
+ min-width: 0;
2221
+ }
2222
+
2223
+ .record-card h3,
2224
+ .search-result h3 {
2225
+ margin: 0.6rem 0 0.25rem;
2226
+ font-size: 1.15rem;
2227
+ }
2228
+
2229
+ .record-card h3,
2230
+ .record-card p,
2231
+ .search-result h3,
2232
+ .search-result p,
2233
+ .section-card strong,
2234
+ .section-card p,
2235
+ .prose,
2236
+ .record-meta,
2237
+ .sources-panel {
2238
+ overflow-wrap: anywhere;
2239
+ }
2240
+
2241
+ .work-card code {
2242
+ max-width: 100%;
2243
+ white-space: normal;
2244
+ }
2245
+
2246
+ .record-card h3 a,
2247
+ .search-result h3 a {
2248
+ color: var(--text);
2249
+ }
2250
+
2251
+ .record-card footer,
2252
+ .search-result footer {
2253
+ justify-content: space-between;
2254
+ margin-top: 0.85rem;
2255
+ }
2256
+
2257
+ .type-badge,
2258
+ .audience-badge,
2259
+ .confidence,
2260
+ .tag-list span {
2261
+ display: inline-flex;
2262
+ align-items: center;
2263
+ min-height: 1.45rem;
2264
+ border-radius: 999px;
2265
+ padding: 0.12rem 0.5rem;
2266
+ font-size: 0.76rem;
2267
+ font-weight: 700;
2268
+ }
2269
+
2270
+ .type-badge {
2271
+ background: var(--badge-bg);
2272
+ color: var(--badge-text);
2273
+ }
2274
+
2275
+ .audience-badge {
2276
+ background: var(--panel-soft);
2277
+ color: var(--muted);
2278
+ }
2279
+
2280
+ .confidence {
2281
+ background: var(--panel-soft);
2282
+ color: var(--muted);
2283
+ }
2284
+
2285
+ .confidence.high {
2286
+ background: var(--success-bg);
2287
+ color: var(--success-text);
2288
+ }
2289
+
2290
+ .confidence.low {
2291
+ background: var(--warning-bg);
2292
+ color: var(--warning-text);
2293
+ }
2294
+
2295
+ .tag-list {
2296
+ display: inline-flex;
2297
+ flex-wrap: wrap;
2298
+ gap: 0.3rem;
2299
+ }
2300
+
2301
+ .tag-list span {
2302
+ background: var(--tag-bg);
2303
+ color: var(--tag-text);
2304
+ }
2305
+
2306
+ .record-page {
2307
+ max-width: 860px;
2308
+ }
2309
+
2310
+ .breadcrumbs {
2311
+ display: flex;
2312
+ gap: 0.45rem;
2313
+ margin-bottom: 1rem;
2314
+ color: var(--muted);
2315
+ font-size: 0.9rem;
2316
+ }
2317
+
2318
+ .prose {
2319
+ border: 1px solid var(--border);
2320
+ border-radius: var(--radius);
2321
+ background: var(--panel);
2322
+ padding: clamp(1rem, 3vw, 1.6rem);
2323
+ }
2324
+
2325
+ .prose h2 {
2326
+ margin: 1.3rem 0 0.3rem;
2327
+ font-size: 1.25rem;
2328
+ }
2329
+
2330
+ .prose h2:first-child {
2331
+ margin-top: 0;
2332
+ }
2333
+
2334
+ .prose p,
2335
+ .prose ul {
2336
+ color: var(--text);
2337
+ }
2338
+
2339
+ .record-meta {
2340
+ margin-top: 1rem;
2341
+ padding: 0.75rem 1rem;
2342
+ }
2343
+
2344
+ .sources-panel {
2345
+ margin-top: 1rem;
2346
+ padding: 1rem;
2347
+ }
2348
+
2349
+ .sources-panel h2 {
2350
+ margin: 0 0 0.5rem;
2351
+ font-size: 1rem;
2352
+ }
2353
+
2354
+ .sources-panel ul {
2355
+ margin: 0;
2356
+ padding-left: 1.1rem;
2357
+ }
2358
+
2359
+ .record-meta summary {
2360
+ cursor: pointer;
2361
+ font-weight: 700;
2362
+ }
2363
+
2364
+ .record-meta dl {
2365
+ display: grid;
2366
+ gap: 0.55rem;
2367
+ margin: 0.8rem 0 0;
2368
+ }
2369
+
2370
+ .record-meta dl div {
2371
+ display: grid;
2372
+ grid-template-columns: 7rem minmax(0, 1fr);
2373
+ gap: 0.75rem;
2374
+ }
2375
+
2376
+ .record-meta dt {
2377
+ color: var(--muted);
2378
+ font-weight: 700;
2379
+ }
2380
+
2381
+ .record-meta dd {
2382
+ margin: 0;
2383
+ }
2384
+
2385
+ .record-meta ul {
2386
+ margin: 0;
2387
+ padding-left: 1.1rem;
2388
+ }
2389
+
2390
+ .search-panel {
2391
+ padding: 1rem;
2392
+ }
2393
+
2394
+ .search-panel label {
2395
+ display: grid;
2396
+ gap: 0.45rem;
2397
+ font-weight: 700;
2398
+ }
2399
+
2400
+ .search-results {
2401
+ display: grid;
2402
+ gap: 0.75rem;
2403
+ margin-top: 1rem;
2404
+ }
2405
+
2406
+ .empty-state {
2407
+ color: var(--muted);
2408
+ padding: 1rem;
2409
+ }
2410
+
2411
+ .empty-state strong {
2412
+ display: block;
2413
+ color: var(--text);
2414
+ margin-bottom: 0.2rem;
2415
+ }
2416
+
2417
+ .site-footer {
2418
+ margin-top: 3rem;
2419
+ border-top: 1px solid var(--border);
2420
+ color: var(--muted);
2421
+ padding-top: 1rem;
2422
+ font-size: 0.9rem;
2423
+ }
2424
+
2425
+ @media (max-width: 860px) {
2426
+ .site-shell {
2427
+ grid-template-columns: 1fr;
2428
+ }
2429
+
2430
+ .sidebar {
2431
+ position: static;
2432
+ height: auto;
2433
+ border-bottom: 1px solid var(--border);
2434
+ border-right: 0;
2435
+ padding: 0.85rem 1rem;
2436
+ }
2437
+
2438
+ .nav-toggle {
2439
+ display: inline-flex;
2440
+ }
2441
+
2442
+ .sidebar-nav {
2443
+ display: none;
2444
+ }
2445
+
2446
+ .sidebar-nav.open {
2447
+ display: block;
2448
+ }
2449
+
2450
+ .nav {
2451
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2452
+ }
2453
+
2454
+ .nav-heading {
2455
+ grid-column: 1 / -1;
2456
+ }
2457
+
2458
+ .hero-panel,
2459
+ .card-grid,
2460
+ .card-grid.compact {
2461
+ grid-template-columns: 1fr;
2462
+ }
2463
+ }
2464
+
2465
+ @media (max-width: 560px) {
2466
+ .content {
2467
+ padding: 1.4rem 1rem 3rem;
2468
+ }
2469
+
2470
+ .nav {
2471
+ grid-template-columns: 1fr;
2472
+ }
2473
+
2474
+ .stats-grid {
2475
+ grid-template-columns: 1fr;
2476
+ }
2477
+
2478
+ .record-meta dl div {
2479
+ grid-template-columns: 1fr;
2480
+ gap: 0.1rem;
2481
+ }
2482
+ }
2483
+
2484
+ @media print {
2485
+ :root {
2486
+ --bg: #ffffff;
2487
+ --panel: #ffffff;
2488
+ --shadow: none;
2489
+ }
2490
+
2491
+ body {
2492
+ background: #ffffff;
2493
+ }
2494
+
2495
+ .site-shell {
2496
+ display: block;
2497
+ }
2498
+
2499
+ .sidebar,
2500
+ .skip-link,
2501
+ .nav-toggle,
2502
+ script {
2503
+ display: none !important;
2504
+ }
2505
+
2506
+ .content {
2507
+ width: auto;
2508
+ padding: 0;
2509
+ }
2510
+
2511
+ .page-header,
2512
+ .hero-panel,
2513
+ .section,
2514
+ .section-card,
2515
+ .record-card,
2516
+ .search-result,
2517
+ .prose,
2518
+ .record-meta,
2519
+ .sources-panel,
2520
+ .site-footer {
2521
+ break-inside: avoid;
2522
+ page-break-inside: avoid;
2523
+ }
2524
+
2525
+ .hero-panel,
2526
+ .section-card,
2527
+ .record-card,
2528
+ .search-result,
2529
+ .prose,
2530
+ .record-meta,
2531
+ .sources-panel {
2532
+ box-shadow: none;
2533
+ }
2534
+
2535
+ a {
2536
+ color: inherit;
2537
+ }
2538
+ }
2539
+ `;
2540
+ }
2541
+ function siteJs() {
2542
+ return `/* Generated by Wikiwiki from .wikiwiki/records. Edit structured records instead, then run \`wk site\`. */
2543
+ (function () {
2544
+ const entries = Array.isArray(window.WIKIWIKI_SEARCH_INDEX) ? window.WIKIWIKI_SEARCH_INDEX : [];
2545
+ const themeStorageKey = "wikiwiki-theme-mode";
2546
+ const validThemeModes = ["auto", "light", "dark"];
2547
+
2548
+ function escapeHtml(value) {
2549
+ return String(value)
2550
+ .replace(/&/g, "&amp;")
2551
+ .replace(/</g, "&lt;")
2552
+ .replace(/>/g, "&gt;")
2553
+ .replace(/"/g, "&quot;")
2554
+ .replace(/'/g, "&#39;");
2555
+ }
2556
+
2557
+ function normalize(value) {
2558
+ return String(value || "").toLowerCase();
2559
+ }
2560
+
2561
+ function normalizeThemeMode(value) {
2562
+ return validThemeModes.includes(value) ? value : "auto";
2563
+ }
2564
+
2565
+ function applyThemeMode(mode, persist) {
2566
+ const normalizedMode = normalizeThemeMode(mode);
2567
+ if (normalizedMode === "light" || normalizedMode === "dark") {
2568
+ document.documentElement.dataset.theme = normalizedMode;
2569
+ } else {
2570
+ delete document.documentElement.dataset.theme;
2571
+ }
2572
+ document.documentElement.dataset.themeMode = normalizedMode;
2573
+ document.querySelectorAll("[data-theme-choice]").forEach((button) => {
2574
+ button.setAttribute("aria-pressed", button.getAttribute("data-theme-choice") === normalizedMode ? "true" : "false");
2575
+ });
2576
+ if (persist) {
2577
+ try {
2578
+ localStorage.setItem(themeStorageKey, normalizedMode);
2579
+ } catch (error) {
2580
+ // Ignore localStorage failures in file previews and locked-down browsers.
2581
+ }
2582
+ }
2583
+ }
2584
+
2585
+ function resultHtml(entry) {
2586
+ const tags = Array.isArray(entry.tags) && entry.tags.length
2587
+ ? '<span class="tag-list">' + entry.tags.slice(0, 6).map((tag) => '<span>' + escapeHtml(tag) + '</span>').join("") + '</span>'
2588
+ : "";
2589
+ const audience = entry.audienceLabel
2590
+ ? '<span class="audience-badge">' + escapeHtml(entry.audienceLabel) + '</span>'
2591
+ : "";
2592
+ return '<article class="search-result">' +
2593
+ '<div class="record-card-top"><span class="type-badge">' + escapeHtml(entry.typeLabel || entry.type) + '</span>' + audience + '</div>' +
2594
+ '<h3><a href="' + encodeURI(entry.url) + '">' + escapeHtml(entry.title) + '</a></h3>' +
2595
+ '<p>' + escapeHtml(entry.summary) + '</p>' +
2596
+ (tags ? '<footer>' + tags + '</footer>' : '') +
2597
+ '</article>';
2598
+ }
2599
+
2600
+ function search(query) {
2601
+ const normalizedQuery = normalize(query).trim();
2602
+ if (!normalizedQuery) {
2603
+ return [];
2604
+ }
2605
+
2606
+ const terms = normalizedQuery.split(/\\s+/).filter(Boolean);
2607
+ return entries
2608
+ .map((entry) => {
2609
+ const text = normalize([entry.title, entry.summary, entry.text, entry.id, entry.type, (entry.tags || []).join(" ")].join(" "));
2610
+ const matchedTerms = terms.filter((term) => text.includes(term)).length;
2611
+ return { entry, matchedTerms };
2612
+ })
2613
+ .filter((result) => result.matchedTerms > 0)
2614
+ .sort((a, b) => b.matchedTerms - a.matchedTerms || a.entry.title.localeCompare(b.entry.title))
2615
+ .slice(0, 50)
2616
+ .map((result) => result.entry);
2617
+ }
2618
+
2619
+ function updateSearchPage(query) {
2620
+ const resultsRoot = document.querySelector("[data-search-results]");
2621
+ if (!resultsRoot) {
2622
+ return;
2623
+ }
2624
+
2625
+ if (!normalize(query).trim()) {
2626
+ resultsRoot.innerHTML = '<div class="empty-state"><strong>Search is ready.</strong><p>Try a concept, decision, tag, or source file. Results stay local to this generated site.</p></div>';
2627
+ return;
2628
+ }
2629
+
2630
+ const matches = search(query);
2631
+ resultsRoot.innerHTML = matches.length
2632
+ ? matches.map(resultHtml).join("")
2633
+ : '<div class="empty-state">No matching records.</div>';
2634
+ }
2635
+
2636
+ const params = new URLSearchParams(window.location.search);
2637
+ const initialQuery = params.get("q") || "";
2638
+ const pageInput = document.querySelector("[data-search-page-input]");
2639
+ if (pageInput) {
2640
+ pageInput.value = initialQuery;
2641
+ pageInput.addEventListener("input", () => updateSearchPage(pageInput.value));
2642
+ updateSearchPage(initialQuery);
2643
+ }
2644
+
2645
+ document.querySelectorAll("[data-site-search]").forEach((input) => {
2646
+ input.addEventListener("keydown", (event) => {
2647
+ if (event.key !== "Enter") {
2648
+ return;
2649
+ }
2650
+ event.preventDefault();
2651
+ const target = input.getAttribute("data-search-target") || "search.html";
2652
+ const query = input.value ? "?q=" + encodeURIComponent(input.value) : "";
2653
+ window.location.href = target + query;
2654
+ });
2655
+ });
2656
+
2657
+ document.querySelectorAll("[data-nav-toggle]").forEach((button) => {
2658
+ button.addEventListener("click", () => {
2659
+ const nav = document.querySelector("[data-sidebar-nav]");
2660
+ if (!nav) {
2661
+ return;
2662
+ }
2663
+ const isOpen = nav.classList.toggle("open");
2664
+ button.setAttribute("aria-expanded", isOpen ? "true" : "false");
2665
+ });
2666
+ });
2667
+
2668
+ const defaultThemeMode = normalizeThemeMode(document.documentElement.getAttribute("data-default-theme") || "auto");
2669
+ let savedThemeMode = "";
2670
+ try {
2671
+ savedThemeMode = localStorage.getItem(themeStorageKey) || "";
2672
+ } catch (error) {
2673
+ savedThemeMode = "";
2674
+ }
2675
+ applyThemeMode(savedThemeMode || defaultThemeMode, false);
2676
+ document.querySelectorAll("[data-theme-choice]").forEach((button) => {
2677
+ button.addEventListener("click", () => {
2678
+ applyThemeMode(button.getAttribute("data-theme-choice") || "auto", true);
2679
+ });
2680
+ });
2681
+ }());
2682
+ `;
2683
+ }
2684
+ //# sourceMappingURL=site.js.map