@glw907/cairn-cms 0.60.0 → 0.62.1

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 (281) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/components/AdminLayout.svelte +152 -229
  3. package/dist/components/CairnAdmin.svelte +13 -42
  4. package/dist/components/CairnLogo.svelte +1 -6
  5. package/dist/components/CairnMediaLibrary.svelte +821 -1210
  6. package/dist/components/CairnTidySettings.svelte +194 -261
  7. package/dist/components/CairnTidySettings.svelte.d.ts +1 -1
  8. package/dist/components/ComponentForm.svelte +110 -185
  9. package/dist/components/ComponentInsertDialog.svelte +163 -283
  10. package/dist/components/ConceptList.svelte +111 -191
  11. package/dist/components/ConfirmPage.svelte +5 -12
  12. package/dist/components/CsrfField.svelte +5 -11
  13. package/dist/components/DeleteDialog.svelte +15 -42
  14. package/dist/components/EditPage.svelte +781 -1205
  15. package/dist/components/EditorToolbar.svelte +108 -170
  16. package/dist/components/HelpHome.svelte +824 -0
  17. package/dist/components/HelpHome.svelte.d.ts +22 -0
  18. package/dist/components/IconPicker.svelte +23 -53
  19. package/dist/components/LinkPicker.svelte +34 -58
  20. package/dist/components/LoginPage.svelte +14 -27
  21. package/dist/components/ManageEditors.svelte +3 -15
  22. package/dist/components/MarkdownEditor.svelte +689 -957
  23. package/dist/components/MarkdownHelpDialog.svelte +12 -27
  24. package/dist/components/MediaCaptureCard.svelte +18 -57
  25. package/dist/components/MediaFigureControl.svelte +32 -71
  26. package/dist/components/MediaHeroField.svelte +210 -329
  27. package/dist/components/MediaInsertPopover.svelte +156 -283
  28. package/dist/components/MediaPicker.svelte +67 -131
  29. package/dist/components/NavTree.svelte +46 -78
  30. package/dist/components/RenameDialog.svelte +16 -43
  31. package/dist/components/ShortcutsDialog.svelte +9 -13
  32. package/dist/components/ShortcutsGrid.svelte +1 -2
  33. package/dist/components/TidyReview.svelte +140 -248
  34. package/dist/components/WebLinkDialog.svelte +19 -40
  35. package/dist/components/cairn-admin.css +4 -0
  36. package/dist/components/client-ingest.d.ts +16 -8
  37. package/dist/components/client-ingest.js +12 -6
  38. package/dist/components/editor-media.js +16 -8
  39. package/dist/components/editor-placeholder.d.ts +4 -2
  40. package/dist/components/editor-tidy.d.ts +24 -12
  41. package/dist/components/editor-tidy.js +8 -4
  42. package/dist/components/index.d.ts +1 -0
  43. package/dist/components/index.js +1 -0
  44. package/dist/components/link-completion.d.ts +12 -6
  45. package/dist/components/link-completion.js +12 -6
  46. package/dist/components/markdown-directives.d.ts +9 -6
  47. package/dist/components/markdown-directives.js +9 -6
  48. package/dist/components/markdown-format.d.ts +7 -2
  49. package/dist/components/markdown-format.js +59 -28
  50. package/dist/components/markdown-reference.d.ts +8 -0
  51. package/dist/components/markdown-reference.js +22 -0
  52. package/dist/components/media-upload-outcome.d.ts +12 -6
  53. package/dist/components/objective-errors.d.ts +8 -4
  54. package/dist/components/objective-errors.js +8 -4
  55. package/dist/components/preview-doc.d.ts +4 -2
  56. package/dist/components/preview-doc.js +4 -2
  57. package/dist/components/spellcheck.d.ts +57 -29
  58. package/dist/components/spellcheck.js +50 -20
  59. package/dist/components/tidy-categorize.d.ts +20 -10
  60. package/dist/components/tidy-categorize.js +16 -8
  61. package/dist/components/tidy-validate.d.ts +12 -6
  62. package/dist/components/tidy-validate.js +20 -10
  63. package/dist/components/topbar-context.d.ts +4 -2
  64. package/dist/content/advisories.d.ts +51 -0
  65. package/dist/content/advisories.js +79 -0
  66. package/dist/content/compose.d.ts +4 -2
  67. package/dist/content/compose.js +1 -0
  68. package/dist/content/excerpt.js +4 -2
  69. package/dist/content/getting-started.d.ts +18 -0
  70. package/dist/content/getting-started.js +12 -0
  71. package/dist/content/links.d.ts +16 -8
  72. package/dist/content/links.js +12 -6
  73. package/dist/content/manifest.d.ts +36 -18
  74. package/dist/content/manifest.js +32 -16
  75. package/dist/content/media-refs.d.ts +4 -2
  76. package/dist/content/media-refs.js +4 -2
  77. package/dist/content/media-rewrite.d.ts +8 -4
  78. package/dist/content/media-rewrite.js +76 -38
  79. package/dist/content/schema.d.ts +20 -10
  80. package/dist/content/site-dictionary.d.ts +4 -2
  81. package/dist/content/site-dictionary.js +8 -4
  82. package/dist/content/types.d.ts +97 -42
  83. package/dist/delivery/CairnHead.svelte +8 -11
  84. package/dist/delivery/content-index.d.ts +16 -8
  85. package/dist/delivery/feeds.js +4 -2
  86. package/dist/delivery/json-ld.d.ts +3 -0
  87. package/dist/delivery/json-ld.js +3 -0
  88. package/dist/delivery/manifest.d.ts +4 -2
  89. package/dist/delivery/manifest.js +4 -2
  90. package/dist/delivery/public-routes.d.ts +12 -6
  91. package/dist/delivery/public-routes.js +4 -2
  92. package/dist/delivery/seo-fields.d.ts +12 -6
  93. package/dist/delivery/seo-fields.js +8 -4
  94. package/dist/delivery/site-indexes.d.ts +4 -2
  95. package/dist/delivery/site-resolver.d.ts +4 -2
  96. package/dist/delivery/site-resolver.js +4 -2
  97. package/dist/doctor/cloudflare-api.d.ts +6 -0
  98. package/dist/doctor/cloudflare-api.js +6 -0
  99. package/dist/doctor/index.d.ts +12 -6
  100. package/dist/doctor/report.d.ts +3 -0
  101. package/dist/doctor/report.js +3 -0
  102. package/dist/doctor/run.d.ts +3 -0
  103. package/dist/doctor/run.js +3 -0
  104. package/dist/doctor/types.d.ts +10 -2
  105. package/dist/doctor/types.js +6 -0
  106. package/dist/doctor/wrangler-config.d.ts +7 -2
  107. package/dist/doctor/wrangler-config.js +3 -0
  108. package/dist/email.d.ts +4 -2
  109. package/dist/env.d.ts +0 -3
  110. package/dist/env.js +0 -3
  111. package/dist/github/branches.d.ts +4 -2
  112. package/dist/github/branches.js +4 -2
  113. package/dist/github/signing.d.ts +1 -1
  114. package/dist/github/signing.js +2 -2
  115. package/dist/log/events.d.ts +1 -1
  116. package/dist/media/bulk-delete-plan.d.ts +8 -4
  117. package/dist/media/config.d.ts +12 -6
  118. package/dist/media/config.js +16 -8
  119. package/dist/media/delivery-bucket.d.ts +4 -2
  120. package/dist/media/library-entry.d.ts +4 -2
  121. package/dist/media/library-entry.js +4 -2
  122. package/dist/media/manifest.d.ts +29 -15
  123. package/dist/media/manifest.js +29 -16
  124. package/dist/media/naming.d.ts +12 -6
  125. package/dist/media/naming.js +24 -12
  126. package/dist/media/orphan-scan.d.ts +4 -2
  127. package/dist/media/reconcile.d.ts +21 -11
  128. package/dist/media/reconcile.js +12 -6
  129. package/dist/media/reference.d.ts +8 -4
  130. package/dist/media/reference.js +12 -6
  131. package/dist/media/rewrite-plan.d.ts +12 -6
  132. package/dist/media/sniff.d.ts +4 -2
  133. package/dist/media/sniff.js +28 -14
  134. package/dist/media/store.d.ts +16 -8
  135. package/dist/media/store.js +4 -2
  136. package/dist/media/transform-url.d.ts +12 -6
  137. package/dist/media/transform-url.js +8 -4
  138. package/dist/media/usage.d.ts +8 -4
  139. package/dist/nav/site-config.d.ts +16 -8
  140. package/dist/render/component-grammar.d.ts +23 -10
  141. package/dist/render/component-grammar.js +19 -8
  142. package/dist/render/component-insert.d.ts +8 -4
  143. package/dist/render/component-insert.js +4 -2
  144. package/dist/render/component-reference.d.ts +4 -2
  145. package/dist/render/component-reference.js +4 -2
  146. package/dist/render/component-validate.d.ts +3 -0
  147. package/dist/render/component-validate.js +3 -0
  148. package/dist/render/glyph.d.ts +4 -2
  149. package/dist/render/glyph.js +4 -2
  150. package/dist/render/pipeline.d.ts +20 -10
  151. package/dist/render/pipeline.js +4 -2
  152. package/dist/render/registry.d.ts +40 -20
  153. package/dist/render/registry.js +16 -8
  154. package/dist/render/rehype-dispatch.d.ts +22 -8
  155. package/dist/render/rehype-dispatch.js +22 -8
  156. package/dist/render/remark-directives.d.ts +3 -0
  157. package/dist/render/remark-directives.js +3 -0
  158. package/dist/render/remark-figure.d.ts +4 -2
  159. package/dist/render/remark-figure.js +4 -2
  160. package/dist/render/resolve-links.d.ts +4 -2
  161. package/dist/render/resolve-links.js +4 -2
  162. package/dist/render/resolve-media.d.ts +16 -8
  163. package/dist/render/resolve-media.js +12 -6
  164. package/dist/sveltekit/admin-dispatch.d.ts +2 -0
  165. package/dist/sveltekit/admin-dispatch.js +9 -3
  166. package/dist/sveltekit/auth-routes.d.ts +3 -0
  167. package/dist/sveltekit/auth-routes.js +3 -0
  168. package/dist/sveltekit/cairn-admin.d.ts +16 -5
  169. package/dist/sveltekit/cairn-admin.js +26 -10
  170. package/dist/sveltekit/content-routes.d.ts +191 -86
  171. package/dist/sveltekit/content-routes.js +295 -107
  172. package/dist/sveltekit/editors-routes.d.ts +3 -0
  173. package/dist/sveltekit/editors-routes.js +3 -0
  174. package/dist/sveltekit/guard.d.ts +4 -2
  175. package/dist/sveltekit/guard.js +4 -2
  176. package/dist/sveltekit/https-required-page.d.ts +1 -1
  177. package/dist/sveltekit/https-required-page.js +1 -1
  178. package/dist/sveltekit/index.d.ts +1 -1
  179. package/dist/sveltekit/media-route.d.ts +1 -2
  180. package/dist/sveltekit/media-route.js +13 -8
  181. package/dist/sveltekit/nav-routes.d.ts +7 -2
  182. package/dist/sveltekit/nav-routes.js +3 -0
  183. package/dist/sveltekit/types.d.ts +4 -2
  184. package/dist/vite/index.d.ts +32 -16
  185. package/dist/vite/index.js +52 -26
  186. package/dist/vite/resolve-root.d.ts +8 -4
  187. package/dist/vite/resolve-root.js +4 -2
  188. package/package.json +8 -2
  189. package/src/lib/components/AdminLayout.svelte +22 -0
  190. package/src/lib/components/CairnAdmin.svelte +3 -0
  191. package/src/lib/components/CairnTidySettings.svelte +2 -2
  192. package/src/lib/components/ComponentForm.svelte +0 -1
  193. package/src/lib/components/EditPage.svelte +133 -41
  194. package/src/lib/components/HelpHome.svelte +850 -0
  195. package/src/lib/components/MarkdownHelpDialog.svelte +4 -15
  196. package/src/lib/components/client-ingest.ts +20 -10
  197. package/src/lib/components/editor-media.ts +20 -10
  198. package/src/lib/components/editor-placeholder.ts +12 -6
  199. package/src/lib/components/editor-tidy.ts +28 -14
  200. package/src/lib/components/index.ts +1 -0
  201. package/src/lib/components/link-completion.ts +12 -6
  202. package/src/lib/components/markdown-directives.ts +13 -8
  203. package/src/lib/components/markdown-format.ts +63 -30
  204. package/src/lib/components/markdown-reference.ts +30 -0
  205. package/src/lib/components/media-upload-outcome.ts +12 -6
  206. package/src/lib/components/objective-errors.ts +16 -8
  207. package/src/lib/components/preview-doc.ts +4 -2
  208. package/src/lib/components/spellcheck.ts +92 -40
  209. package/src/lib/components/tidy-categorize.ts +28 -14
  210. package/src/lib/components/tidy-validate.ts +28 -14
  211. package/src/lib/components/topbar-context.ts +4 -2
  212. package/src/lib/content/advisories.ts +141 -0
  213. package/src/lib/content/compose.ts +5 -2
  214. package/src/lib/content/excerpt.ts +4 -2
  215. package/src/lib/content/getting-started.ts +31 -0
  216. package/src/lib/content/links.ts +16 -8
  217. package/src/lib/content/manifest.ts +36 -18
  218. package/src/lib/content/media-refs.ts +4 -2
  219. package/src/lib/content/media-rewrite.ts +100 -50
  220. package/src/lib/content/schema.ts +20 -10
  221. package/src/lib/content/site-dictionary.ts +8 -4
  222. package/src/lib/content/types.ts +97 -42
  223. package/src/lib/delivery/content-index.ts +16 -8
  224. package/src/lib/delivery/feeds.ts +4 -2
  225. package/src/lib/delivery/json-ld.ts +3 -0
  226. package/src/lib/delivery/manifest.ts +4 -2
  227. package/src/lib/delivery/public-routes.ts +16 -8
  228. package/src/lib/delivery/seo-fields.ts +12 -6
  229. package/src/lib/delivery/site-indexes.ts +4 -2
  230. package/src/lib/delivery/site-resolver.ts +4 -2
  231. package/src/lib/doctor/cloudflare-api.ts +6 -0
  232. package/src/lib/doctor/index.ts +12 -6
  233. package/src/lib/doctor/report.ts +3 -0
  234. package/src/lib/doctor/run.ts +3 -0
  235. package/src/lib/doctor/types.ts +10 -2
  236. package/src/lib/doctor/wrangler-config.ts +7 -2
  237. package/src/lib/email.ts +4 -2
  238. package/src/lib/env.ts +0 -3
  239. package/src/lib/github/branches.ts +4 -2
  240. package/src/lib/github/signing.ts +2 -2
  241. package/src/lib/log/events.ts +1 -0
  242. package/src/lib/media/bulk-delete-plan.ts +8 -4
  243. package/src/lib/media/config.ts +24 -12
  244. package/src/lib/media/delivery-bucket.ts +4 -2
  245. package/src/lib/media/library-entry.ts +4 -2
  246. package/src/lib/media/manifest.ts +33 -18
  247. package/src/lib/media/naming.ts +24 -12
  248. package/src/lib/media/orphan-scan.ts +4 -2
  249. package/src/lib/media/reconcile.ts +21 -11
  250. package/src/lib/media/reference.ts +12 -6
  251. package/src/lib/media/rewrite-plan.ts +12 -6
  252. package/src/lib/media/sniff.ts +28 -14
  253. package/src/lib/media/store.ts +16 -8
  254. package/src/lib/media/transform-url.ts +12 -6
  255. package/src/lib/media/usage.ts +8 -4
  256. package/src/lib/nav/site-config.ts +16 -8
  257. package/src/lib/render/component-grammar.ts +23 -10
  258. package/src/lib/render/component-insert.ts +8 -4
  259. package/src/lib/render/component-reference.ts +4 -2
  260. package/src/lib/render/component-validate.ts +3 -0
  261. package/src/lib/render/glyph.ts +4 -2
  262. package/src/lib/render/pipeline.ts +20 -10
  263. package/src/lib/render/registry.ts +44 -22
  264. package/src/lib/render/rehype-dispatch.ts +22 -8
  265. package/src/lib/render/remark-directives.ts +3 -0
  266. package/src/lib/render/remark-figure.ts +4 -2
  267. package/src/lib/render/resolve-links.ts +4 -2
  268. package/src/lib/render/resolve-media.ts +16 -8
  269. package/src/lib/sveltekit/admin-dispatch.ts +10 -4
  270. package/src/lib/sveltekit/auth-routes.ts +3 -0
  271. package/src/lib/sveltekit/cairn-admin.ts +37 -15
  272. package/src/lib/sveltekit/content-routes.ts +492 -197
  273. package/src/lib/sveltekit/editors-routes.ts +3 -0
  274. package/src/lib/sveltekit/guard.ts +4 -2
  275. package/src/lib/sveltekit/https-required-page.ts +1 -1
  276. package/src/lib/sveltekit/index.ts +3 -0
  277. package/src/lib/sveltekit/media-route.ts +13 -8
  278. package/src/lib/sveltekit/nav-routes.ts +7 -2
  279. package/src/lib/sveltekit/types.ts +4 -2
  280. package/src/lib/vite/index.ts +60 -30
  281. package/src/lib/vite/resolve-root.ts +8 -4
@@ -11,22 +11,28 @@ import type { UploadResult } from '../sveltekit/content-routes.js';
11
11
  import type { IngestFailureKind } from './client-ingest.js';
12
12
  import { mediaToken } from '../media/reference.js';
13
13
 
14
- /** A failure the card surfaces. The ingest taxonomy plus a `generic` catch-all for a refuse reason
15
- * with no specific author-facing card (a binding-missing, a length-required, a parse miss). */
14
+ /**
15
+ * A failure the card surfaces. The ingest taxonomy plus a `generic` catch-all for a refuse reason
16
+ * with no specific author-facing card (a binding-missing, a length-required, a parse miss).
17
+ */
16
18
  export type UploadFailureKind = IngestFailureKind | 'generic';
17
19
 
18
- /** The outcome the popover acts on. `inserted` swaps the placeholder for the reference and records
20
+ /**
21
+ * The outcome the popover acts on. `inserted` swaps the placeholder for the reference and records
19
22
  * the entry; `failed` cancels the placeholder and shows the typed card; `session-expired` cancels
20
- * the placeholder and tells the author to sign in again. */
23
+ * the placeholder and tells the author to sign in again.
24
+ */
21
25
  export type UploadOutcome =
22
26
  | { kind: 'inserted'; reference: string; record: MediaEntry; reused: boolean }
23
27
  | { kind: 'failed'; failure: UploadFailureKind }
24
28
  | { kind: 'session-expired' };
25
29
 
26
- /** The shape the popover hands in: either a parsed SvelteKit action result (success or failure) or a
30
+ /**
31
+ * The shape the popover hands in: either a parsed SvelteKit action result (success or failure) or a
27
32
  * bare response signal for the redirect and network-error cases. The popover deserializes the body
28
33
  * for the success and failure cases and passes the raw `response.type`/`response.status` for the
29
- * redirect case, so this one mapper covers every branch. */
34
+ * redirect case, so this one mapper covers every branch.
35
+ */
30
36
  export type UploadEnvelope =
31
37
  | { type: 'success'; status?: number; data: UploadResult }
32
38
  | { type: 'failure'; status?: number; data?: { error?: string } }
@@ -12,16 +12,20 @@ import type { Range } from './spellcheck.js';
12
12
  /** The three objective-error kinds, each its own check. */
13
13
  export type ObjectiveErrorKind = 'doubled-word' | 'double-space' | 'repeated-punct';
14
14
 
15
- /** A single deterministic edit that resolves one finding: replace [from, to) with `insert`. The lint
16
- * source turns this into the diagnostic's quick-fix action. */
15
+ /**
16
+ * A single deterministic edit that resolves one finding: replace [from, to) with `insert`. The lint
17
+ * source turns this into the diagnostic's quick-fix action.
18
+ */
17
19
  export interface ObjectiveFix {
18
20
  from: number;
19
21
  to: number;
20
22
  insert: string;
21
23
  }
22
24
 
23
- /** One objective-error finding: the flagged range a reader sees underlined, the error kind, a plain
24
- * message, and the one-edit fix. */
25
+ /**
26
+ * One objective-error finding: the flagged range a reader sees underlined, the error kind, a plain
27
+ * message, and the one-edit fix.
28
+ */
25
29
  export interface ObjectiveError {
26
30
  kind: ObjectiveErrorKind;
27
31
  /** The flagged range (absolute document offsets), the span the underline covers. */
@@ -59,15 +63,19 @@ const DOUBLE_SPACE = /[^\s] ( +)/g;
59
63
  // flagged because it is a legitimate construction; only a run of one identical mark counts.
60
64
  const REPEATED_PUNCT = /([!?,])\1+/g;
61
65
 
62
- /** Whether two matched word strings are the same word, case-insensitively. Both are already plain
63
- * word runs from the same WORD pattern, so a locale-insensitive lowercase compare is enough. */
66
+ /**
67
+ * Whether two matched word strings are the same word, case-insensitively. Both are already plain
68
+ * word runs from the same WORD pattern, so a locale-insensitive lowercase compare is enough.
69
+ */
64
70
  function sameWord(a: string, b: string): boolean {
65
71
  return a.toLowerCase() === b.toLowerCase();
66
72
  }
67
73
 
68
- /** Run the three objective checks over one prose span [from, to), returning every finding with an
74
+ /**
75
+ * Run the three objective checks over one prose span [from, to), returning every finding with an
69
76
  * absolute range and fix. The doubled-word check is bounded to this span so a repeat that straddles
70
- * a skipped region is never matched. */
77
+ * a skipped region is never matched.
78
+ */
71
79
  function checkSpan(text: string, from: number, to: number): ObjectiveError[] {
72
80
  const out: ObjectiveError[] = [];
73
81
  const slice = text.slice(from, to);
@@ -32,8 +32,10 @@ export function previewDevice(id: PreviewDeviceId): PreviewDevice {
32
32
  return previewDevices.find((d) => d.id === id) ?? previewDevices[0];
33
33
  }
34
34
 
35
- /** A device's user-facing text, shared by the toolbar's menu items and the frame caption: the
36
- * label with its width when one is fixed, so the value reaches assistive tech at pick time. */
35
+ /**
36
+ * A device's user-facing text, shared by the toolbar's menu items and the frame caption: the
37
+ * label with its width when one is fixed, so the value reaches assistive tech at pick time.
38
+ */
37
39
  export function deviceLabel(d: PreviewDevice): string {
38
40
  return d.width === null ? d.label : `${d.label} · ${d.width} px`;
39
41
  }
@@ -23,8 +23,10 @@ export interface Range {
23
23
  to: number;
24
24
  }
25
25
 
26
- /** A word extracted for lookup: the lowercased form the Worker checks, and its absolute range so a
27
- * verdict maps straight back to an underline. */
26
+ /**
27
+ * A word extracted for lookup: the lowercased form the Worker checks, and its absolute range so a
28
+ * verdict maps straight back to an underline.
29
+ */
28
30
  export interface ExtractedWord {
29
31
  /** The lowercased word, as the engine's case-insensitive lookup expects. */
30
32
  text: string;
@@ -73,8 +75,10 @@ const SKIP_NODES = new Set<string>([
73
75
  // the form authors type directly in prose, so it is never split into "media" plus a flagged hash.
74
76
  const MEDIA_TOKEN = /media:[\w.-]+/g;
75
77
 
76
- /** Merge overlapping or touching ranges into a sorted, disjoint set, so the keep-span computation
77
- * subtracts one clean list of skip regions. */
78
+ /**
79
+ * Merge overlapping or touching ranges into a sorted, disjoint set, so the keep-span computation
80
+ * subtracts one clean list of skip regions.
81
+ */
78
82
  function mergeRanges(ranges: Range[]): Range[] {
79
83
  if (ranges.length === 0) return [];
80
84
  const sorted = [...ranges].sort((a, b) => a.from - b.from || a.to - b.to);
@@ -88,9 +92,11 @@ function mergeRanges(ranges: Range[]): Range[] {
88
92
  return out;
89
93
  }
90
94
 
91
- /** Every absolute skip range in the document, from all three mechanisms, merged. This is the single
95
+ /**
96
+ * Every absolute skip range in the document, from all three mechanisms, merged. This is the single
92
97
  * skip authority the spec calls for: the tree decides node kind, frontmatterSpan covers the `---`
93
- * region, and fenceTokens covers the directive machinery the tree parses as plain text. */
98
+ * region, and fenceTokens covers the directive machinery the tree parses as plain text.
99
+ */
94
100
  function skipRanges(text: string, tree: Tree): Range[] {
95
101
  const skips: Range[] = [];
96
102
 
@@ -163,8 +169,10 @@ export function classifyProse(text: string, tree: Tree, from: number, to: number
163
169
  return out;
164
170
  }
165
171
 
166
- /** The prose ranges worth checking across the whole document. The lint source narrows this to the
167
- * visible window; the unit test reads the whole-document set. */
172
+ /**
173
+ * The prose ranges worth checking across the whole document. The lint source narrows this to the
174
+ * visible window; the unit test reads the whole-document set.
175
+ */
168
176
  export function spellcheckRanges(text: string, tree: Tree): Range[] {
169
177
  return classifyProse(text, tree, 0, text.length);
170
178
  }
@@ -175,8 +183,10 @@ export function spellcheckRanges(text: string, tree: Tree): Range[] {
175
183
  const WORD = /[\p{L}\p{N}]+(?:[-'’][\p{L}\p{N}]+)*/gu;
176
184
  const ALL_DIGITS = /^\p{N}+$/u;
177
185
 
178
- /** Whether a word is worth a lookup. Words under three characters, pure numbers, and all-caps tokens
179
- * are skipped to cut false positives (the conservative posture VSCode's spell checker takes). */
186
+ /**
187
+ * Whether a word is worth a lookup. Words under three characters, pure numbers, and all-caps tokens
188
+ * are skipped to cut false positives (the conservative posture VSCode's spell checker takes).
189
+ */
180
190
  function isCheckable(word: string): boolean {
181
191
  if (word.length < 3) return false;
182
192
  if (ALL_DIGITS.test(word)) return false;
@@ -207,8 +217,10 @@ export function extractWords(text: string, from: number, to: number): ExtractedW
207
217
  // wall of near-ties.
208
218
  const MAX_SUGGESTIONS = 5;
209
219
 
210
- /** The callbacks the management actions invoke. The lint source supplies these so the pure builder
211
- * never touches the Worker or the re-lint mechanism: it only wires the buttons to these handlers. */
220
+ /**
221
+ * The callbacks the management actions invoke. The lint source supplies these so the pure builder
222
+ * never touches the Worker or the re-lint mechanism: it only wires the buttons to these handlers.
223
+ */
212
224
  export interface SpellDiagnosticActions {
213
225
  /** Add the word to the personal dictionary (posts addWord, records the pending addition, re-lints). */
214
226
  onAddWord(word: string): void;
@@ -217,7 +229,7 @@ export interface SpellDiagnosticActions {
217
229
  }
218
230
 
219
231
  /**
220
- * Build the correction popover for one misspelled word, as a @codemirror/lint Diagnostic whose
232
+ * Build the correction popover for one misspelled word, as a `@codemirror/lint` Diagnostic whose
221
233
  * `actions` CodeMirror renders as tooltip buttons (no custom popover code). The actions, in order:
222
234
  * up to five ranked suggestions (each replaces the word's range with one transaction), then "Add to
223
235
  * dictionary", then "Ignore". The severity is `info` so the underline is quiet, and the message names
@@ -290,7 +302,7 @@ export function arbitrateChecked(): SeqArbiter {
290
302
  }
291
303
 
292
304
  /**
293
- * Build the quick-fix popover for one objective-error finding, as a @codemirror/lint Diagnostic whose
305
+ * Build the quick-fix popover for one objective-error finding, as a `@codemirror/lint` Diagnostic whose
294
306
  * one `actions` entry applies the finding's deterministic fix. The severity is `info` so the underline
295
307
  * shares the spellcheck surface and the locked amber color (an editor reads spelling and these
296
308
  * mechanical errors as one "spellcheck" layer). The fix range is recomputed from the live diagnostic
@@ -337,17 +349,21 @@ let langMod: typeof import('@codemirror/language') | null = null;
337
349
  let viewMod: typeof import('@codemirror/view') | null = null;
338
350
  let stateMod: typeof import('@codemirror/state') | null = null;
339
351
 
340
- /** The narrow Worker surface the lint source drives: it posts check, suggest, addWord, and ignoreWord
352
+ /**
353
+ * The narrow Worker surface the lint source drives: it posts check, suggest, addWord, and ignoreWord
341
354
  * messages and listens for the answers. A `suggest` answer is a one-shot, so the source removes its
342
- * own listener once it lands. A test injects a fake; production injects a real Worker. */
355
+ * own listener once it lands. A test injects a fake; production injects a real Worker.
356
+ */
343
357
  export interface SpellWorker {
344
358
  postMessage(message: unknown): void;
345
359
  addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
346
360
  removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
347
361
  }
348
362
 
349
- /** Construct the real spellcheck Worker, the spike's delivery shape. Kept behind the seam so the
350
- * lint source never references `Worker` at module scope and a test can swap it. */
363
+ /**
364
+ * Construct the real spellcheck Worker, the spike's delivery shape. Kept behind the seam so the
365
+ * lint source never references `Worker` at module scope and a test can swap it.
366
+ */
351
367
  export function createSpellWorker(): SpellWorker {
352
368
  return new Worker(new URL('./spellcheck-worker.js', import.meta.url), {
353
369
  type: 'module',
@@ -368,48 +384,80 @@ export function resolveWasmUrl(): string {
368
384
  return new URL('./spellcheck-assets/spellchecker-wasm.wasm', import.meta.url).href;
369
385
  }
370
386
 
371
- /** The real dictionary asset URL for a dictionary filename, resolved module-relative. The caller
372
- * passes the dialect-resolved filename (default `dictionary-en-us.txt`). */
387
+ /**
388
+ * Each shipped dictionary, mapped to a resolver that builds its asset URL with a LITERAL
389
+ * `new URL(..., import.meta.url)`. The literal path is load-bearing. A templated `new URL` makes Vite
390
+ * and rolldown treat the directory as a glob and parse every sibling module to build it, including the
391
+ * `.svelte` components that still carry `lang="ts"` in `dist`, and the glob parser chokes on the TS
392
+ * syntax and breaks the consumer build. This set mirrors the dialect map in `nav/site-config.ts`; add
393
+ * one line per new shipped dialect dictionary.
394
+ */
395
+ const DICTIONARY_URLS: Record<string, () => string> = {
396
+ 'dictionary-en-us.txt': () =>
397
+ new URL('./spellcheck-assets/dictionary-en-us.txt', import.meta.url).href,
398
+ };
399
+
400
+ /**
401
+ * The real dictionary asset URL for a dictionary filename, resolved module-relative. The caller
402
+ * passes the dialect-resolved filename (default `dictionary-en-us.txt`). `dictionaryFileForDialect`
403
+ * already collapses an unknown dialect to the default, so an unmapped name falls back the same way
404
+ * rather than pointing at an asset that does not ship.
405
+ */
373
406
  export function resolveDictionaryUrl(dictionaryFile: string): string {
374
- return new URL(`./spellcheck-assets/${dictionaryFile}`, import.meta.url).href;
407
+ const resolve = DICTIONARY_URLS[dictionaryFile] ?? DICTIONARY_URLS['dictionary-en-us.txt'];
408
+ return resolve();
375
409
  }
376
410
 
377
411
  /** How far past the visible viewport to lint, so a small scroll does not re-lint from scratch. */
378
412
  const VIEWPORT_MARGIN = 1000;
379
413
 
380
- /** Options for {@link cairnSpellcheck}, so the unit and component layers can inject a fake Worker
381
- * factory in place of the real `new Worker(...)`. */
414
+ /**
415
+ * Options for {@link cairnSpellcheck}, so the unit and component layers can inject a fake Worker
416
+ * factory in place of the real `new Worker(...)`.
417
+ */
382
418
  export interface SpellcheckOptions {
383
419
  /** The Worker factory; defaults to {@link createSpellWorker}. Created lazily on the first lint. */
384
420
  createWorker?: () => SpellWorker;
385
- /** The pending personal-dictionary additions, owned by the caller. When an author chooses "Add to
421
+ /**
422
+ * The pending personal-dictionary additions, owned by the caller. When an author chooses "Add to
386
423
  * dictionary" the source posts addWord to the Worker (the underline clears at once) and records the
387
424
  * word here. The set is the seam Task 9 commits to the git-backed dictionary file; this source only
388
- * fills it and never persists. A caller that does not pass one gets a fresh internal set. */
425
+ * fills it and never persists. A caller that does not pass one gets a fresh internal set.
426
+ */
389
427
  pendingAdditions?: Set<string>;
390
- /** The committed personal-dictionary words (spec 1.6) the source seeds the Worker's personal layer
428
+ /**
429
+ * The committed personal-dictionary words (spec 1.6) the source seeds the Worker's personal layer
391
430
  * with, posted as one batch `addWord` right after `init`. The git-backed site dictionary is the
392
431
  * durable layer; the editor reads it at load (EditData.siteDictionary) and hands it here, so a word
393
- * another editor committed answers correct from the first lint. Empty by default (dialect-only). */
432
+ * another editor committed answers correct from the first lint. Empty by default (dialect-only).
433
+ */
394
434
  siteWords?: ReadonlyArray<string>;
395
- /** The dialect-resolved dictionary filename, e.g. "dictionary-en-us.txt". The source resolves it to
396
- * a real asset URL and posts it in the Worker's `init`. Defaults to US English. */
435
+ /**
436
+ * The dialect-resolved dictionary filename, e.g. "dictionary-en-us.txt". The source resolves it to
437
+ * a real asset URL and posts it in the Worker's `init`. Defaults to US English.
438
+ */
397
439
  dictionaryFile?: string;
398
- /** Override the resolved wasm and dictionary URLs the source posts in `init`. The real resolution
440
+ /**
441
+ * Override the resolved wasm and dictionary URLs the source posts in `init`. The real resolution
399
442
  * uses {@link resolveWasmUrl}/{@link resolveDictionaryUrl} (module-relative `import.meta.url`); a
400
443
  * component test that injects a fake Worker can pass canned URLs so it never touches the asset
401
- * resolver. */
444
+ * resolver.
445
+ */
402
446
  assetUrls?: { wasmUrl: string; dictionaryUrl: string };
403
- /** Treat the Worker as ready without waiting for a `ready` message. The production path is strict
447
+ /**
448
+ * Treat the Worker as ready without waiting for a `ready` message. The production path is strict
404
449
  * (it posts `init` and waits for `ready` before painting); a fake Worker in a test that does not
405
- * answer `ready` can set this so a lint run is not held back. Defaults to false. */
450
+ * answer `ready` can set this so a lint run is not held back. Defaults to false.
451
+ */
406
452
  assumeReady?: boolean;
407
- /** The already-loaded CodeMirror modules to reuse instead of importing them again. The editor
453
+ /**
454
+ * The already-loaded CodeMirror modules to reuse instead of importing them again. The editor
408
455
  * component loads `@codemirror/view`/`@codemirror/language` for its own extensions, so passing them
409
456
  * here keeps the lint source on the SAME module instances; a second dynamic import can resolve to a
410
457
  * separate copy (the test bundler's dedup quirk), and CodeMirror's instanceof checks then reject the
411
458
  * extension. When omitted, the source imports them itself (the standalone path). `@codemirror/lint`
412
- * is loaded here when not supplied, since the editor does not otherwise need it. */
459
+ * is loaded here when not supplied, since the editor does not otherwise need it.
460
+ */
413
461
  modules?: {
414
462
  lint?: typeof import('@codemirror/lint');
415
463
  language?: typeof import('@codemirror/language');
@@ -455,7 +503,7 @@ function lockedUnderlineTheme(EditorViewMod: typeof import('@codemirror/view').E
455
503
  }
456
504
 
457
505
  /**
458
- * The @codemirror/lint linter() source, made markdown-aware by the Lezer tree. It runs over the
506
+ * The `@codemirror/lint` linter() source, made markdown-aware by the Lezer tree. It runs over the
459
507
  * visible viewport plus a margin (not the whole document), extracts the checkable words via the pure
460
508
  * classifier, posts them to the Worker keyed by a monotonic latest-wins seq, and maps the
461
509
  * `correct: false` answers back to ranges. Each wrong word becomes a correction popover: the source
@@ -576,10 +624,12 @@ export async function cairnSpellcheck(options: SpellcheckOptions = {}): Promise<
576
624
  return worker;
577
625
  }
578
626
 
579
- /** Fetch a single word's ranked suggestions over the Worker, a one-shot listener removed on the
627
+ /**
628
+ * Fetch a single word's ranked suggestions over the Worker, a one-shot listener removed on the
580
629
  * answer. The suggest path is independent of the check seq, so a slow suggest never blocks a fresh
581
630
  * check; an empty list (the engine returned nothing) still yields a popover with the two
582
- * management actions. */
631
+ * management actions.
632
+ */
583
633
  function fetchSuggestions(w: SpellWorker, word: string): Promise<string[]> {
584
634
  suggestSeq += 1;
585
635
  const seq = suggestSeq;
@@ -595,8 +645,10 @@ export async function cairnSpellcheck(options: SpellcheckOptions = {}): Promise<
595
645
  });
596
646
  }
597
647
 
598
- /** Turn the wrong words into correction popovers, each carrying its ranked suggestions and the two
599
- * management actions. */
648
+ /**
649
+ * Turn the wrong words into correction popovers, each carrying its ranked suggestions and the two
650
+ * management actions.
651
+ */
600
652
  async function buildDiagnostics(wrong: ExtractedWord[]): Promise<Diagnostic[]> {
601
653
  const w = ensureWorker();
602
654
  const callbacks: SpellDiagnosticActions = {
@@ -13,9 +13,11 @@
13
13
  import type { Change } from './tidy-diff.js';
14
14
  import type { TidyConventions } from '../nav/site-config.js';
15
15
 
16
- /** A change's locally-inferred category. The first four are objective (safe to sweep); `normalization`
16
+ /**
17
+ * A change's locally-inferred category. The first four are objective (safe to sweep); `normalization`
17
18
  * and `grammar` are judgment (held undecided, never swept by Accept-fixes). `normalization` carries
18
- * the convention key that authorized it, so the surface can name the setting and label the badge. */
19
+ * the convention key that authorized it, so the surface can name the setting and label the badge.
20
+ */
19
21
  export type TidyCategory =
20
22
  | { kind: 'spelling' }
21
23
  | { kind: 'typo' }
@@ -24,9 +26,11 @@ export type TidyCategory =
24
26
  | { kind: 'normalization'; convention: NormalizationKey }
25
27
  | { kind: 'grammar' };
26
28
 
27
- /** True for the objective categories: the safe, pre-kept, Accept-fixes-swept rank. A judgment
29
+ /**
30
+ * True for the objective categories: the safe, pre-kept, Accept-fixes-swept rank. A judgment
28
31
  * category (`normalization` or `grammar`) returns false. The bulk action and the surface both read
29
- * this, so the safety rank is one source of truth. */
32
+ * this, so the safety rank is one source of truth.
33
+ */
30
34
  export function isObjective(category: TidyCategory): boolean {
31
35
  return (
32
36
  category.kind === 'spelling' ||
@@ -36,9 +40,11 @@ export function isObjective(category: TidyCategory): boolean {
36
40
  );
37
41
  }
38
42
 
39
- /** The enabled-convention keys a normalization can be attributed to. Each maps to one config field on
43
+ /**
44
+ * The enabled-convention keys a normalization can be attributed to. Each maps to one config field on
40
45
  * TidyConventions and to a because-line. A change is only ever labelled a normalization when it matches
41
- * one of these AND the config has the matching variant enabled; otherwise it is never a normalization. */
46
+ * one of these AND the config has the matching variant enabled; otherwise it is never a normalization.
47
+ */
42
48
  export type NormalizationKey =
43
49
  | 'oxfordComma'
44
50
  | 'numberStyle'
@@ -133,9 +139,11 @@ function isPunctuationOnly(text: string): boolean {
133
139
  return text.length > 0 && /^[^A-Za-z0-9_\s]+$/.test(text);
134
140
  }
135
141
 
136
- /** The word ending immediately before `offset` in `text`, skipping any whitespace just before the
142
+ /**
143
+ * The word ending immediately before `offset` in `text`, skipping any whitespace just before the
137
144
  * offset, or null when none. The doubled-word rule reads it to confirm the deleted word repeats the
138
- * one before it. Pure text inspection, never a count. */
145
+ * one before it. Pure text inspection, never a count.
146
+ */
139
147
  function precedingWord(text: string, offset: number): string | null {
140
148
  let i = offset;
141
149
  while (i > 0 && /\s/.test(text[i - 1])) i--;
@@ -144,8 +152,10 @@ function precedingWord(text: string, offset: number): string | null {
144
152
  return j < i ? text.slice(j, i) : null;
145
153
  }
146
154
 
147
- /** The word starting immediately after `offset` in `text`, skipping any whitespace just after the
148
- * offset, or null when none. The doubled-word rule reads it as the other half of the look-around. */
155
+ /**
156
+ * The word starting immediately after `offset` in `text`, skipping any whitespace just after the
157
+ * offset, or null when none. The doubled-word rule reads it as the other half of the look-around.
158
+ */
149
159
  function followingWord(text: string, offset: number): string | null {
150
160
  let i = offset;
151
161
  while (i < text.length && /\s/.test(text[i])) i++;
@@ -335,11 +345,13 @@ function matchNormalization(
335
345
  return null;
336
346
  }
337
347
 
338
- /** The because-line data for a hunk: the convention's display name and the variant phrasing, both pure
348
+ /**
349
+ * The because-line data for a hunk: the convention's display name and the variant phrasing, both pure
339
350
  * strings derived from the config. The surface renders "Your <label> setting is <variant>, ..." from
340
351
  * these. Only a normalization carries a because-line; an objective or grammar hunk returns null (a
341
352
  * grammar hunk's rationale, when shown, is the local subject-verb note the surface composes, not a
342
- * config citation). */
353
+ * config citation).
354
+ */
343
355
  export interface BecauseLine {
344
356
  /** The convention's display label, e.g. "Oxford-comma". */
345
357
  label: string;
@@ -416,9 +428,11 @@ export function buildBecause(key: NormalizationKey, conventions: TidyConventions
416
428
  }
417
429
  }
418
430
 
419
- /** The human badge label for a category, the word shown in the hunk's category pill. A normalization's
431
+ /**
432
+ * The human badge label for a category, the word shown in the hunk's category pill. A normalization's
420
433
  * label is the convention's display name (its comma style, its time format), never "consistency" and
421
- * never a count. */
434
+ * never a count.
435
+ */
422
436
  export function categoryLabel(category: TidyCategory): string {
423
437
  switch (category.kind) {
424
438
  case 'spelling':
@@ -19,24 +19,30 @@ import { parseMediaToken } from '../media/reference.js';
19
19
  import { diffTokens, diffChanges } from './tidy-diff.js';
20
20
  import type { Change } from './tidy-diff.js';
21
21
 
22
- /** The reason a tidy result was rejected. Task 14 branches on this; every value maps to the one
22
+ /**
23
+ * The reason a tidy result was rejected. Task 14 branches on this; every value maps to the one
23
24
  * honest author-facing message, so the reason is for logging and tests, not the user surface.
24
25
  * - `structure`: a directive opener/closer sequence, a heading count or level, or a fenced-code
25
26
  * count diverged (the result restructured the document).
26
27
  * - `frontmatter`: the frontmatter block is not byte-for-byte equal.
27
28
  * - `media`: the multiset of `media:` hashes differs (a hash was altered, dropped, or invented).
28
29
  * - `code`: a code span or fenced code block was edited.
29
- * - `divergence`: the changed-token amount exceeds the length-aware bound (a wholesale rewrite). */
30
+ * - `divergence`: the changed-token amount exceeds the length-aware bound (a wholesale rewrite).
31
+ */
30
32
  export type TidyRejectionReason = 'structure' | 'frontmatter' | 'media' | 'code' | 'divergence';
31
33
 
32
- /** The honest author-facing message a rejection maps to. The same message for every reason, by
34
+ /**
35
+ * The honest author-facing message a rejection maps to. The same message for every reason, by
33
36
  * design: an author does not need the validator's internal taxonomy, only that the result was
34
- * discarded and their text is safe. */
37
+ * discarded and their text is safe.
38
+ */
35
39
  export const TIDY_REJECTION_MESSAGE =
36
40
  'Tidy returned a result that changed more than the wording, so it was discarded. Your text is unchanged.';
37
41
 
38
- /** The outcome of validating a tidy result. On success it carries the Task 12 change set the review
39
- * surface accepts and rejects against; on failure it carries the typed reason and the message. */
42
+ /**
43
+ * The outcome of validating a tidy result. On success it carries the Task 12 change set the review
44
+ * surface accepts and rejects against; on failure it carries the typed reason and the message.
45
+ */
40
46
  export type TidyValidation =
41
47
  | { ok: true; changes: Change[] }
42
48
  | { ok: false; reason: TidyRejectionReason; message: string };
@@ -61,10 +67,12 @@ const DIVERGENCE_FRACTION = 0.5;
61
67
  // caught here too, redundantly with the code check, which is the right posture for a backstop.
62
68
  const MEDIA_TOKEN = /media:[A-Za-z0-9.-]+/g;
63
69
 
64
- /** The sorted multiset of valid media hashes in the text. Each `media:` occurrence is parsed; a
70
+ /**
71
+ * The sorted multiset of valid media hashes in the text. Each `media:` occurrence is parsed; a
65
72
  * malformed token (a broken hash, an illegal slug) parses to null and is dropped, so a tidy that
66
73
  * CORRUPTED a hash drops it from the multiset and the comparison fails. Sorted so two multisets
67
- * compare by value, order-independent. */
74
+ * compare by value, order-independent.
75
+ */
68
76
  function mediaHashes(text: string): string[] {
69
77
  const hashes: string[] = [];
70
78
  for (const m of text.matchAll(MEDIA_TOKEN)) {
@@ -74,11 +82,13 @@ function mediaHashes(text: string): string[] {
74
82
  return hashes.sort();
75
83
  }
76
84
 
77
- /** The directive structure signature: each opener or closer in document order, paired with the depth
85
+ /**
86
+ * The directive structure signature: each opener or closer in document order, paired with the depth
78
87
  * the fence scan assigned it. Two texts share a directive structure when these signatures are equal,
79
88
  * so an added, removed, or relevelled container fails the comparison. A fence-shaped line inside a
80
89
  * code block is already disowned by the scan (its role is null), so a documented `:::` example does
81
- * not enter the signature. */
90
+ * not enter the signature.
91
+ */
82
92
  function directiveSignature(text: string): string {
83
93
  const { depths, roles } = fenceScan(text.split('\n'));
84
94
  const parts: string[] = [];
@@ -88,10 +98,12 @@ function directiveSignature(text: string): string {
88
98
  return parts.join(',');
89
99
  }
90
100
 
91
- /** The heading signature: every ATX heading's level in document order. Parsed as mdast so a `#`
101
+ /**
102
+ * The heading signature: every ATX heading's level in document order. Parsed as mdast so a `#`
92
103
  * inside a code block or an escaped one is never counted, and the level is the parser's own depth.
93
104
  * Two texts share a heading structure when these are equal, so an added, removed, or relevelled
94
- * heading fails the comparison. */
105
+ * heading fails the comparison.
106
+ */
95
107
  function headingSignature(text: string): string {
96
108
  const tree = unified().use(remarkParse).use(remarkGfm).parse(text);
97
109
  const levels: number[] = [];
@@ -101,11 +113,13 @@ function headingSignature(text: string): string {
101
113
  return levels.join(',');
102
114
  }
103
115
 
104
- /** Every code span and fenced or indented code block in the text, as a sorted multiset of values.
116
+ /**
117
+ * Every code span and fenced or indented code block in the text, as a sorted multiset of values.
105
118
  * Parsed as mdast so the comparison sees exactly what the parser treats as code, the same authority
106
119
  * the media body scan uses. Sorted so the comparison is order-independent: the divergence and
107
120
  * structure checks own ordering, this check owns the contents. A `code` node is a block, an
108
- * `inlineCode` node is a span. */
121
+ * `inlineCode` node is a span.
122
+ */
109
123
  function codeContents(text: string): string[] {
110
124
  const tree = unified().use(remarkParse).use(remarkGfm).parse(text);
111
125
  const values: string[] = [];
@@ -12,9 +12,11 @@ const TOPBAR_CONTEXT_KEY = Symbol('cairn-topbar');
12
12
  /** The shared holder: the desk snippet a document registers, or null on the office routes. */
13
13
  export interface TopbarHolder {
14
14
  desk: Snippet | null;
15
- /** True while the document is in zen: AdminLayout drops the whole topbar element so the band
15
+ /**
16
+ * True while the document is in zen: AdminLayout drops the whole topbar element so the band
16
17
  * slides away (the desk's three clusters include AdminLayout-owned chrome, the drawer toggle and
17
- * breadcrumb, that must vanish with it). EditPage sets this; the office routes leave it false. */
18
+ * breadcrumb, that must vanish with it). EditPage sets this; the office routes leave it false.
19
+ */
18
20
  zen: boolean;
19
21
  }
20
22