@glw907/cairn-cms 0.60.1 → 0.62.2

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 (254) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/dist/components/AdminLayout.svelte +22 -0
  3. package/dist/components/CairnAdmin.svelte +3 -0
  4. package/dist/components/CairnTidySettings.svelte +2 -2
  5. package/dist/components/CairnTidySettings.svelte.d.ts +1 -1
  6. package/dist/components/EditPage.svelte +116 -39
  7. package/dist/components/HelpHome.svelte +824 -0
  8. package/dist/components/HelpHome.svelte.d.ts +22 -0
  9. package/dist/components/MarkdownHelpDialog.svelte +4 -15
  10. package/dist/components/client-ingest.d.ts +16 -8
  11. package/dist/components/client-ingest.js +12 -6
  12. package/dist/components/editor-media.js +16 -8
  13. package/dist/components/editor-placeholder.d.ts +4 -2
  14. package/dist/components/editor-tidy.d.ts +24 -12
  15. package/dist/components/editor-tidy.js +8 -4
  16. package/dist/components/index.d.ts +1 -0
  17. package/dist/components/index.js +1 -0
  18. package/dist/components/link-completion.d.ts +12 -6
  19. package/dist/components/link-completion.js +12 -6
  20. package/dist/components/markdown-directives.d.ts +9 -6
  21. package/dist/components/markdown-directives.js +9 -6
  22. package/dist/components/markdown-format.d.ts +7 -2
  23. package/dist/components/markdown-format.js +59 -28
  24. package/dist/components/markdown-reference.d.ts +8 -0
  25. package/dist/components/markdown-reference.js +22 -0
  26. package/dist/components/media-upload-outcome.d.ts +12 -6
  27. package/dist/components/objective-errors.d.ts +8 -4
  28. package/dist/components/objective-errors.js +8 -4
  29. package/dist/components/preview-doc.d.ts +4 -2
  30. package/dist/components/preview-doc.js +4 -2
  31. package/dist/components/spellcheck.d.ts +55 -29
  32. package/dist/components/spellcheck.js +39 -21
  33. package/dist/components/tidy-categorize.d.ts +20 -10
  34. package/dist/components/tidy-categorize.js +16 -8
  35. package/dist/components/tidy-validate.d.ts +12 -6
  36. package/dist/components/tidy-validate.js +20 -10
  37. package/dist/components/topbar-context.d.ts +4 -2
  38. package/dist/content/advisories.d.ts +56 -0
  39. package/dist/content/advisories.js +87 -0
  40. package/dist/content/compose.d.ts +4 -2
  41. package/dist/content/compose.js +1 -0
  42. package/dist/content/excerpt.js +4 -2
  43. package/dist/content/getting-started.d.ts +18 -0
  44. package/dist/content/getting-started.js +12 -0
  45. package/dist/content/links.d.ts +16 -8
  46. package/dist/content/links.js +12 -6
  47. package/dist/content/manifest.d.ts +36 -18
  48. package/dist/content/manifest.js +32 -16
  49. package/dist/content/media-refs.d.ts +4 -2
  50. package/dist/content/media-refs.js +4 -2
  51. package/dist/content/media-rewrite.d.ts +8 -4
  52. package/dist/content/media-rewrite.js +76 -38
  53. package/dist/content/schema.d.ts +20 -10
  54. package/dist/content/site-dictionary.d.ts +4 -2
  55. package/dist/content/site-dictionary.js +8 -4
  56. package/dist/content/types.d.ts +97 -42
  57. package/dist/delivery/content-index.d.ts +16 -8
  58. package/dist/delivery/feeds.js +4 -2
  59. package/dist/delivery/json-ld.d.ts +3 -0
  60. package/dist/delivery/json-ld.js +3 -0
  61. package/dist/delivery/manifest.d.ts +4 -2
  62. package/dist/delivery/manifest.js +4 -2
  63. package/dist/delivery/public-routes.d.ts +12 -6
  64. package/dist/delivery/public-routes.js +4 -2
  65. package/dist/delivery/seo-fields.d.ts +12 -6
  66. package/dist/delivery/seo-fields.js +8 -4
  67. package/dist/delivery/site-indexes.d.ts +4 -2
  68. package/dist/delivery/site-resolver.d.ts +4 -2
  69. package/dist/delivery/site-resolver.js +4 -2
  70. package/dist/doctor/cloudflare-api.d.ts +6 -0
  71. package/dist/doctor/cloudflare-api.js +6 -0
  72. package/dist/doctor/index.d.ts +12 -6
  73. package/dist/doctor/report.d.ts +3 -0
  74. package/dist/doctor/report.js +3 -0
  75. package/dist/doctor/run.d.ts +3 -0
  76. package/dist/doctor/run.js +3 -0
  77. package/dist/doctor/types.d.ts +10 -2
  78. package/dist/doctor/types.js +6 -0
  79. package/dist/doctor/wrangler-config.d.ts +7 -2
  80. package/dist/doctor/wrangler-config.js +3 -0
  81. package/dist/email.d.ts +4 -2
  82. package/dist/env.d.ts +0 -3
  83. package/dist/env.js +0 -3
  84. package/dist/github/branches.d.ts +4 -2
  85. package/dist/github/branches.js +4 -2
  86. package/dist/github/signing.d.ts +1 -1
  87. package/dist/github/signing.js +2 -2
  88. package/dist/log/events.d.ts +1 -1
  89. package/dist/media/bulk-delete-plan.d.ts +8 -4
  90. package/dist/media/config.d.ts +12 -6
  91. package/dist/media/config.js +16 -8
  92. package/dist/media/delivery-bucket.d.ts +4 -2
  93. package/dist/media/library-entry.d.ts +4 -2
  94. package/dist/media/library-entry.js +4 -2
  95. package/dist/media/manifest.d.ts +29 -15
  96. package/dist/media/manifest.js +29 -16
  97. package/dist/media/naming.d.ts +12 -6
  98. package/dist/media/naming.js +24 -12
  99. package/dist/media/orphan-scan.d.ts +4 -2
  100. package/dist/media/reconcile.d.ts +21 -11
  101. package/dist/media/reconcile.js +12 -6
  102. package/dist/media/reference.d.ts +8 -4
  103. package/dist/media/reference.js +12 -6
  104. package/dist/media/rewrite-plan.d.ts +12 -6
  105. package/dist/media/sniff.d.ts +4 -2
  106. package/dist/media/sniff.js +28 -14
  107. package/dist/media/store.d.ts +16 -8
  108. package/dist/media/store.js +4 -2
  109. package/dist/media/transform-url.d.ts +12 -6
  110. package/dist/media/transform-url.js +8 -4
  111. package/dist/media/usage.d.ts +8 -4
  112. package/dist/nav/site-config.d.ts +16 -8
  113. package/dist/render/component-grammar.d.ts +23 -10
  114. package/dist/render/component-grammar.js +19 -8
  115. package/dist/render/component-insert.d.ts +8 -4
  116. package/dist/render/component-insert.js +4 -2
  117. package/dist/render/component-reference.d.ts +4 -2
  118. package/dist/render/component-reference.js +4 -2
  119. package/dist/render/component-validate.d.ts +3 -0
  120. package/dist/render/component-validate.js +3 -0
  121. package/dist/render/glyph.d.ts +4 -2
  122. package/dist/render/glyph.js +4 -2
  123. package/dist/render/pipeline.d.ts +20 -10
  124. package/dist/render/pipeline.js +4 -2
  125. package/dist/render/registry.d.ts +40 -20
  126. package/dist/render/registry.js +16 -8
  127. package/dist/render/rehype-dispatch.d.ts +22 -8
  128. package/dist/render/rehype-dispatch.js +22 -8
  129. package/dist/render/remark-directives.d.ts +3 -0
  130. package/dist/render/remark-directives.js +3 -0
  131. package/dist/render/remark-figure.d.ts +4 -2
  132. package/dist/render/remark-figure.js +4 -2
  133. package/dist/render/resolve-links.d.ts +4 -2
  134. package/dist/render/resolve-links.js +4 -2
  135. package/dist/render/resolve-media.d.ts +16 -8
  136. package/dist/render/resolve-media.js +12 -6
  137. package/dist/sveltekit/admin-dispatch.d.ts +2 -0
  138. package/dist/sveltekit/admin-dispatch.js +9 -3
  139. package/dist/sveltekit/auth-routes.d.ts +3 -0
  140. package/dist/sveltekit/auth-routes.js +3 -0
  141. package/dist/sveltekit/cairn-admin.d.ts +16 -5
  142. package/dist/sveltekit/cairn-admin.js +26 -10
  143. package/dist/sveltekit/content-routes.d.ts +191 -86
  144. package/dist/sveltekit/content-routes.js +297 -107
  145. package/dist/sveltekit/editors-routes.d.ts +3 -0
  146. package/dist/sveltekit/editors-routes.js +3 -0
  147. package/dist/sveltekit/guard.d.ts +4 -2
  148. package/dist/sveltekit/guard.js +4 -2
  149. package/dist/sveltekit/https-required-page.d.ts +1 -1
  150. package/dist/sveltekit/https-required-page.js +1 -1
  151. package/dist/sveltekit/index.d.ts +1 -1
  152. package/dist/sveltekit/media-route.d.ts +1 -2
  153. package/dist/sveltekit/media-route.js +13 -8
  154. package/dist/sveltekit/nav-routes.d.ts +7 -2
  155. package/dist/sveltekit/nav-routes.js +3 -0
  156. package/dist/sveltekit/types.d.ts +4 -2
  157. package/dist/vite/index.d.ts +32 -16
  158. package/dist/vite/index.js +52 -26
  159. package/dist/vite/resolve-root.d.ts +8 -4
  160. package/dist/vite/resolve-root.js +4 -2
  161. package/package.json +7 -1
  162. package/src/lib/components/AdminLayout.svelte +22 -0
  163. package/src/lib/components/CairnAdmin.svelte +3 -0
  164. package/src/lib/components/CairnTidySettings.svelte +2 -2
  165. package/src/lib/components/ComponentForm.svelte +0 -1
  166. package/src/lib/components/EditPage.svelte +133 -41
  167. package/src/lib/components/HelpHome.svelte +850 -0
  168. package/src/lib/components/MarkdownHelpDialog.svelte +4 -15
  169. package/src/lib/components/client-ingest.ts +20 -10
  170. package/src/lib/components/editor-media.ts +20 -10
  171. package/src/lib/components/editor-placeholder.ts +12 -6
  172. package/src/lib/components/editor-tidy.ts +28 -14
  173. package/src/lib/components/index.ts +1 -0
  174. package/src/lib/components/link-completion.ts +12 -6
  175. package/src/lib/components/markdown-directives.ts +13 -8
  176. package/src/lib/components/markdown-format.ts +63 -30
  177. package/src/lib/components/markdown-reference.ts +30 -0
  178. package/src/lib/components/media-upload-outcome.ts +12 -6
  179. package/src/lib/components/objective-errors.ts +16 -8
  180. package/src/lib/components/preview-doc.ts +4 -2
  181. package/src/lib/components/spellcheck.ts +79 -41
  182. package/src/lib/components/tidy-categorize.ts +28 -14
  183. package/src/lib/components/tidy-validate.ts +28 -14
  184. package/src/lib/components/topbar-context.ts +4 -2
  185. package/src/lib/content/advisories.ts +150 -0
  186. package/src/lib/content/compose.ts +5 -2
  187. package/src/lib/content/excerpt.ts +4 -2
  188. package/src/lib/content/getting-started.ts +31 -0
  189. package/src/lib/content/links.ts +16 -8
  190. package/src/lib/content/manifest.ts +36 -18
  191. package/src/lib/content/media-refs.ts +4 -2
  192. package/src/lib/content/media-rewrite.ts +100 -50
  193. package/src/lib/content/schema.ts +20 -10
  194. package/src/lib/content/site-dictionary.ts +8 -4
  195. package/src/lib/content/types.ts +97 -42
  196. package/src/lib/delivery/content-index.ts +16 -8
  197. package/src/lib/delivery/feeds.ts +4 -2
  198. package/src/lib/delivery/json-ld.ts +3 -0
  199. package/src/lib/delivery/manifest.ts +4 -2
  200. package/src/lib/delivery/public-routes.ts +16 -8
  201. package/src/lib/delivery/seo-fields.ts +12 -6
  202. package/src/lib/delivery/site-indexes.ts +4 -2
  203. package/src/lib/delivery/site-resolver.ts +4 -2
  204. package/src/lib/doctor/cloudflare-api.ts +6 -0
  205. package/src/lib/doctor/index.ts +12 -6
  206. package/src/lib/doctor/report.ts +3 -0
  207. package/src/lib/doctor/run.ts +3 -0
  208. package/src/lib/doctor/types.ts +10 -2
  209. package/src/lib/doctor/wrangler-config.ts +7 -2
  210. package/src/lib/email.ts +4 -2
  211. package/src/lib/env.ts +0 -3
  212. package/src/lib/github/branches.ts +4 -2
  213. package/src/lib/github/signing.ts +2 -2
  214. package/src/lib/log/events.ts +1 -0
  215. package/src/lib/media/bulk-delete-plan.ts +8 -4
  216. package/src/lib/media/config.ts +24 -12
  217. package/src/lib/media/delivery-bucket.ts +4 -2
  218. package/src/lib/media/library-entry.ts +4 -2
  219. package/src/lib/media/manifest.ts +33 -18
  220. package/src/lib/media/naming.ts +24 -12
  221. package/src/lib/media/orphan-scan.ts +4 -2
  222. package/src/lib/media/reconcile.ts +21 -11
  223. package/src/lib/media/reference.ts +12 -6
  224. package/src/lib/media/rewrite-plan.ts +12 -6
  225. package/src/lib/media/sniff.ts +28 -14
  226. package/src/lib/media/store.ts +16 -8
  227. package/src/lib/media/transform-url.ts +12 -6
  228. package/src/lib/media/usage.ts +8 -4
  229. package/src/lib/nav/site-config.ts +16 -8
  230. package/src/lib/render/component-grammar.ts +23 -10
  231. package/src/lib/render/component-insert.ts +8 -4
  232. package/src/lib/render/component-reference.ts +4 -2
  233. package/src/lib/render/component-validate.ts +3 -0
  234. package/src/lib/render/glyph.ts +4 -2
  235. package/src/lib/render/pipeline.ts +20 -10
  236. package/src/lib/render/registry.ts +44 -22
  237. package/src/lib/render/rehype-dispatch.ts +22 -8
  238. package/src/lib/render/remark-directives.ts +3 -0
  239. package/src/lib/render/remark-figure.ts +4 -2
  240. package/src/lib/render/resolve-links.ts +4 -2
  241. package/src/lib/render/resolve-media.ts +16 -8
  242. package/src/lib/sveltekit/admin-dispatch.ts +10 -4
  243. package/src/lib/sveltekit/auth-routes.ts +3 -0
  244. package/src/lib/sveltekit/cairn-admin.ts +37 -15
  245. package/src/lib/sveltekit/content-routes.ts +494 -197
  246. package/src/lib/sveltekit/editors-routes.ts +3 -0
  247. package/src/lib/sveltekit/guard.ts +4 -2
  248. package/src/lib/sveltekit/https-required-page.ts +1 -1
  249. package/src/lib/sveltekit/index.ts +3 -0
  250. package/src/lib/sveltekit/media-route.ts +13 -8
  251. package/src/lib/sveltekit/nav-routes.ts +7 -2
  252. package/src/lib/sveltekit/types.ts +4 -2
  253. package/src/lib/vite/index.ts +60 -30
  254. package/src/lib/vite/resolve-root.ts +8 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,84 @@
2
2
 
3
3
  All notable changes to this project are recorded here, most recent first.
4
4
 
5
+ ## 0.62.2
6
+
7
+ The edit-load address-collision advisory now checks the published corpus only. It fires when an entry
8
+ you are editing collides with an entry already published on `main`, and it no longer reads sibling
9
+ `cairn/<concept>/<id>` branches when an editor opens an entry, so opening the editor adds no GitHub
10
+ reads. The publish-time re-check is unchanged: it stays full cross-branch and still emits the
11
+ `publish.address_collision` log event when a publish overrides another entry's address. No consumer
12
+ action is required.
13
+
14
+ ## 0.62.1
15
+
16
+ The entry editor gains an advisory channel and its first notice: a cross-branch address-collision
17
+ warning. When another entry already resolves to the same public address, the editor shows a
18
+ non-blocking warning that names that entry and links to it. The warning never blocks Publish. It makes
19
+ the last-write-wins outcome visible instead of silent, since publishing replaces whatever currently
20
+ lives at that address.
21
+
22
+ The check runs at edit-load across `main` and every open `cairn/<concept>/<id>` branch. A publish
23
+ re-checks the address and emits a `publish.address_collision` log event (level `warn`, fields `editor`,
24
+ `address`, `displacedConcept`, `displacedId`) when it overrides one. The existing needs-alt notice now
25
+ renders through the same advisory region, with its live count and per-row actions unchanged.
26
+
27
+ This adds two exported types on `/sveltekit`, `AdvisoryNotice` and `AdvisoryAction`, the shape
28
+ `EditData.advisories` carries. No consumer action is required.
29
+
30
+ ## 0.62.0
31
+
32
+ <!-- release-size: minor -->
33
+
34
+ The admin gains a Help home, the pull half of the in-admin editor help. It is a standing screen at
35
+ `/admin/help`, reached from a labeled Help home pinned at the foot of the office sidebar (and from the
36
+ Ctrl+K command palette).
37
+
38
+ The screen carries three sections. A getting-started checklist derives its progress from what is
39
+ really on the site: writing a post, publishing one, and creating a page. The count is never stored, so
40
+ it always reflects the corpus, and the whole section drops away once all three steps are done. A hide
41
+ control tucks it away per device. A formatting reference promotes the editor's Ctrl+/ cheat sheet to a
42
+ standing two-column table. A support hand-off points a stuck author at the site's `supportContact`,
43
+ shaped to the contact (an email opens a `mailto`, a URL opens a link, anything else shows as a note),
44
+ and it renders only when the adapter sets one.
45
+
46
+ This adds two exports: the `HelpHome` component on the `/components` subpath and the `HelpData` type on
47
+ `/sveltekit`. The new `/admin/help` route is additive.
48
+
49
+ No consumer action is required. A site that sets no `supportContact` sees the Help home with a
50
+ self-serve line in place of the contact hand-off.
51
+
52
+ This release also fixes the admin-copy prose gate (`check:prose`): a component whose `@component` doc
53
+ comment wrote the literal `<style>` tag had its whole markup silently skipped, so its copy was never
54
+ scanned. The gate now strips comments before the script and style blocks.
55
+
56
+ ## 0.61.0
57
+
58
+ <!-- release-size: minor -->
59
+
60
+ The editor gains the groundwork for in-admin help. This pass adds the engine seams and one built-in
61
+ clarity default the help layer will build on.
62
+
63
+ A frontmatter field can now carry a `description`: one author-facing sentence shown under the field in
64
+ the editor's Details panel and tied to the input with `aria-describedby`. Set it on any field in a
65
+ concept's `defineFields` schema.
66
+
67
+ The `date` field ships a built-in publish-clarity hint ("Sets the date for this post. Publishing is a
68
+ separate step you choose.") when the field sets no `description`, so a new site gets the reassurance
69
+ without writing per-field copy. A field-level `description` overrides it; the hint cannot be turned
70
+ off, only replaced.
71
+
72
+ The adapter gains an optional `supportContact`: an email, a URL, or a name and instruction the
73
+ in-admin help points a stuck editor to. It passes through to the runtime untouched, and the help
74
+ renders the hand-off only when it is set, so there is never a button to a blank contact.
75
+
76
+ The admin design system documents the recipes the help shell will follow, including the non-modal help
77
+ region, the single right-slide-over slot, the disclosure-button ARIA contract, the getting-started
78
+ progress checklist, and the empty-state starter slot.
79
+
80
+ No consumer action is required. Every change is additive: the new field and adapter members are
81
+ optional, and a site that sets neither sees only the date field's new default hint.
82
+
5
83
  ## 0.60.1
6
84
 
7
85
  A packaging fix so the library bundles cleanly in a Vite 8 consumer. It supersedes `0.60.0`, whose
@@ -21,6 +21,7 @@ import UsersIcon from "@lucide/svelte/icons/users";
21
21
  import ImageIcon from "@lucide/svelte/icons/image";
22
22
  import BlocksIcon from "@lucide/svelte/icons/blocks";
23
23
  import ExternalLinkIcon from "@lucide/svelte/icons/external-link";
24
+ import HelpCircleIcon from "@lucide/svelte/icons/circle-help";
24
25
  import "./cairn-admin.css";
25
26
  let { data, children } = $props();
26
27
  setContext(CSRF_CONTEXT_KEY, () => data.csrf);
@@ -109,6 +110,7 @@ onMount(() => {
109
110
  });
110
111
  const paletteCommands = $derived([
111
112
  ...coreItems.map((item) => ({ label: item.label, icon: item.icon, href: item.href })),
113
+ { label: "Help", icon: HelpCircleIcon, href: "/admin/help" },
112
114
  { label: "View the live site", icon: ExternalLinkIcon, href: "/", external: true },
113
115
  theme === "cairn-admin" ? { label: "Switch to dark mode", icon: MoonIcon, action: toggleTheme } : { label: "Switch to light mode", icon: SunIcon, action: toggleTheme }
114
116
  ]);
@@ -379,6 +381,26 @@ provideTopbar(topbar);
379
381
  {/each}
380
382
  </div>
381
383
 
384
+ <!-- Help is a standing utility destination, pinned at the foot of the nav and set apart from
385
+ the content concepts by a top hairline. It is always present, labeled in plain text, and
386
+ styled as a peer of the nav items above it. -->
387
+ <div class="flex-none border-t border-[var(--cairn-card-border)] px-2 py-2">
388
+ <ul class="menu menu-sm w-full gap-0.5 p-0">
389
+ <li>
390
+ <a
391
+ href="/admin/help"
392
+ class={isActive('/admin/help')
393
+ ? 'bg-primary/10 font-semibold text-primary'
394
+ : 'font-medium text-[var(--color-subtle)]'}
395
+ aria-current={isActive('/admin/help') ? 'page' : undefined}
396
+ >
397
+ <HelpCircleIcon class="h-4 w-4" aria-hidden="true" />
398
+ Help
399
+ </a>
400
+ </li>
401
+ </ul>
402
+ </div>
403
+
382
404
  <div class="flex-none border-t border-[var(--cairn-card-border)] px-5 py-4">
383
405
  <div class="flex items-center gap-3">
384
406
  <div class="avatar avatar-placeholder">
@@ -14,6 +14,7 @@ import ManageEditors from "./ManageEditors.svelte";
14
14
  import NavTree from "./NavTree.svelte";
15
15
  import CairnMediaLibrary from "./CairnMediaLibrary.svelte";
16
16
  import CairnTidySettings from "./CairnTidySettings.svelte";
17
+ import HelpHome from "./HelpHome.svelte";
17
18
  let { data, form = null, render, registry, icons } = $props();
18
19
  </script>
19
20
 
@@ -40,6 +41,8 @@ let { data, form = null, render, registry, icons } = $props();
40
41
  <CairnMediaLibrary data={data.page} {form} />
41
42
  {:else if data.view === 'settings'}
42
43
  <CairnTidySettings data={data.page} />
44
+ {:else if data.view === 'help'}
45
+ <HelpHome data={data.page} />
43
46
  {/if}
44
47
  </AdminLayout>
45
48
  {/if}
@@ -8,7 +8,7 @@ Two tiers with a truthful visibility gate:
8
8
  that tidy is enabled, a key is configured, and which model runs, but cannot edit any of it. The
9
9
  literal deploy-time tokens sit in a marked "For your developer" sub-block.
10
10
  - The EDITOR tier (the per-convention config) renders ONLY when tidy is enabled AND the key is
11
- present (`data.enabled`). When tidy is not enabled, the editor tier is genuinely ABSENT, replaced
11
+ present (`data.enabled`). When tidy is not enabled, the editor tier is ABSENT, replaced
12
12
  by an honest labelled gate region with a read-only "what your developer needs to do" checklist and
13
13
  a "spellcheck still works" reassurance. No teasing disabled controls sit in the tab order.
14
14
 
@@ -448,7 +448,7 @@ function segClass(on) {
448
448
  </div>
449
449
  </form>
450
450
  {:else}
451
- <!-- THE VISIBILITY GATE: tidy NOT enabled by the developer. The convention list is genuinely
451
+ <!-- THE VISIBILITY GATE: tidy NOT enabled by the developer. The convention list is
452
452
  absent, not disabled. One honest labelled region names the deploy-time task and who does it,
453
453
  with no disabled controls in the tab order. -->
454
454
  <div role="region" aria-label="Tidy is not set up" class="mt-6 flex flex-col items-center gap-3 rounded-2xl border border-[var(--cairn-card-border)] bg-base-100 p-10 text-center shadow-[var(--cairn-shadow)]">
@@ -13,7 +13,7 @@ interface Props {
13
13
  * that tidy is enabled, a key is configured, and which model runs, but cannot edit any of it. The
14
14
  * literal deploy-time tokens sit in a marked "For your developer" sub-block.
15
15
  * - The EDITOR tier (the per-convention config) renders ONLY when tidy is enabled AND the key is
16
- * present (`data.enabled`). When tidy is not enabled, the editor tier is genuinely ABSENT, replaced
16
+ * present (`data.enabled`). When tidy is not enabled, the editor tier is ABSENT, replaced
17
17
  * by an honest labelled gate region with a read-only "what your developer needs to do" checklist and
18
18
  * a "spellcheck still works" reassurance. No teasing disabled controls sit in the tab order.
19
19
  *
@@ -560,6 +560,33 @@ const imageFields = $derived(
560
560
  );
561
561
  const heroRows = $derived(imageFields.filter((f) => heroNeedsAlt[f.name]));
562
562
  const needsAltCount = $derived(needsAlt.length + heroRows.length);
563
+ const renderNotices = $derived([
564
+ ...data.advisories.map((notice) => ({
565
+ kind: notice.kind,
566
+ message: notice.message,
567
+ rows: (notice.actions ?? []).map((action) => ({ label: action.label, href: action.href }))
568
+ })),
569
+ ...needsAltCount ? [
570
+ {
571
+ kind: "needs-alt",
572
+ message: `${needsAltCount} ${needsAltCount === 1 ? "image needs" : "images need"} alt text`,
573
+ detail: "Alt text describes an image for readers who cannot see it. Add it now, or save and come back to it.",
574
+ rows: [
575
+ ...needsAlt.map((item) => ({
576
+ rowLabel: item.ref,
577
+ rowCode: true,
578
+ label: "Add alt text",
579
+ onAct: () => selectRange(item.from, item.to)
580
+ })),
581
+ ...heroRows.map((hero) => ({
582
+ rowLabel: hero.label,
583
+ label: "Add alt text",
584
+ onAct: () => heroFieldRefs[hero.name]?.focusAlt()
585
+ }))
586
+ ]
587
+ }
588
+ ] : []
589
+ ]);
563
590
  const deleteRefusedLinks = $derived(form?.inboundLinks ?? []);
564
591
  const formError = $derived(
565
592
  form?.error && !form.brokenLinks?.length && !form.inboundLinks?.length ? form.error : ""
@@ -715,6 +742,7 @@ const eyebrowClass = "mb-2 text-[0.6875rem] font-semibold uppercase tracking-[0.
715
742
  const titleField = $derived(data.fields.find((f) => f.name === "title"));
716
743
  const draftField = $derived(data.fields.find((f) => f.type === "boolean" && f.name === "draft"));
717
744
  const detailFields = $derived(data.fields.filter((f) => f !== titleField && f !== draftField));
745
+ const DATE_PUBLISH_HINT = "Sets the date for this post. Publishing is a separate step you choose.";
718
746
  </script>
719
747
 
720
748
  <!-- The desk controls live in the one header band: AdminLayout renders this snippet through the
@@ -841,6 +869,17 @@ const detailFields = $derived(data.fields.filter((f) => f !== titleField && f !=
841
869
  </div>
842
870
  {/snippet}
843
871
 
872
+ <!-- The author-facing hint under a Details field. The id pairs with the input's aria-describedby
873
+ (`<name>-hint`); its uniqueness rests on schema field names being unique within a concept, which
874
+ is also the loop key. So assistive tech announces the sentence without bloating the accessible
875
+ name. Each field branch decides whether and where to render it; this snippet holds the one shape.
876
+ The `fld-hint` class is a styling hook with no rule today; the Tailwind utilities do the work. -->
877
+ {#snippet fieldHint(name: string, text: string)}
878
+ <p id={`${name}-hint`} class="fld-hint mt-1 text-sm text-[var(--color-muted)]">
879
+ {text}
880
+ </p>
881
+ {/snippet}
882
+
844
883
  <!-- The whole edit surface remounts when navigation lands on another entry (see the entryKey
845
884
  reset above); script-level state and the beforeNavigate registration sit outside the block,
846
885
  so only the template rebuilds. -->
@@ -887,44 +926,61 @@ const detailFields = $derived(data.fields.filter((f) => f !== titleField && f !=
887
926
  </ul>
888
927
  </div>
889
928
  {/if}
890
- <!-- The publish-time needs-alt notice: a non-blocking warning, never a block. Alt text is
891
- accessibility debt, so the author can add it now or save without it; the count drops live as
892
- each alt is filled and the notice clears at zero. The leading glyph carries the state alongside
893
- the count, so the caution reads without relying on hue. Each row's jump control selects the
894
- image in the source through the editor's select-range seam, landing the author on it to type
895
- the alt. The role="status" live region renders unconditionally (present and empty at load), so
896
- when the first debt appears its count change announces; a region conditionally mounted with its
897
- first content may not be observed by assistive tech (WCAG 4.1.3). The visible alert chrome and
898
- content gate on the count, so an empty region shows nothing. A plain wrapper (not display:contents)
899
- carries the role, since some assistive tech drops a role off a display:contents box. -->
900
- <div role="status">
901
- {#if needsAltCount}
929
+ <!-- The shared advisory notices: one live-region surface for every non-blocking editor warning. It
930
+ carries the server's address-collision advisory and the client-derived needs-alt notice through
931
+ one snippet. Each renders as one alert-warning row: the caution glyph, the message, an optional
932
+ detail sentence, and a list of action rows. Each is a warning, never a block: the author can act
933
+ on it or save without it. The leading glyph carries the state alongside the message, so the
934
+ caution reads without relying on hue. A row with an href is a server advisory's link; a row with
935
+ onAct is the needs-alt jump that runs an editor callback (selecting the image source, or focusing
936
+ a hero alt input). -->
937
+ <!-- Keyed by index, not by notice.kind: the kind is a free string with no uniqueness constraint, so
938
+ two notices of one kind would otherwise throw each_key_duplicate. The list is append-only and
939
+ never reordered, so the index is a stable key here. -->
940
+ {#snippet advisoryNotices(notices: RenderNotice[])}
941
+ {#each notices as notice, i (i)}
902
942
  <div class="alert alert-warning mb-4 flex-col items-start text-sm">
903
943
  <p class="flex items-center gap-2 font-medium">
904
944
  <svg class="h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
905
945
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v4m0 4h.01M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
906
946
  </svg>
907
- <span>{needsAltCount} {needsAltCount === 1 ? 'image needs' : 'images need'} alt text</span>
947
+ <span>{notice.message}</span>
908
948
  </p>
909
- <p>Alt text describes an image for readers who cannot see it. Add it now, or save and come back to it.</p>
910
- <ul class="mt-1 w-full">
911
- {#each needsAlt as item (item.from)}
912
- <li class="flex items-center justify-between gap-2">
913
- <code class="text-xs">{item.ref}</code>
914
- <button type="button" class="btn btn-xs" onclick={() => selectRange(item.from, item.to)}>Add alt text</button>
915
- </li>
916
- {/each}
917
- <!-- The frontmatter-hero rows: a hero has no body offset, so its action focuses the field's
918
- own alt input rather than a source range. -->
919
- {#each heroRows as hero (hero.name)}
920
- <li class="flex items-center justify-between gap-2">
921
- <span class="text-xs font-medium">{hero.label}</span>
922
- <button type="button" class="btn btn-xs" onclick={() => heroFieldRefs[hero.name]?.focusAlt()}>Add alt text</button>
923
- </li>
924
- {/each}
925
- </ul>
949
+ {#if notice.detail}
950
+ <p>{notice.detail}</p>
951
+ {/if}
952
+ {#if notice.rows.length}
953
+ <ul class="mt-1 w-full">
954
+ {#each notice.rows as row, i (i)}
955
+ <li class="flex items-center justify-between gap-2">
956
+ {#if row.rowLabel}
957
+ <!-- A body needs-alt row labels with its source reference in a code span; a hero row
958
+ and any future labelled row use a plain label. -->
959
+ {#if row.rowCode}
960
+ <code class="text-xs">{row.rowLabel}</code>
961
+ {:else}
962
+ <span class="text-xs font-medium">{row.rowLabel}</span>
963
+ {/if}
964
+ {/if}
965
+ {#if row.href}
966
+ <a class="btn btn-xs" href={row.href}>{row.label}</a>
967
+ {:else}
968
+ <button type="button" class="btn btn-xs" onclick={row.onAct}>{row.label}</button>
969
+ {/if}
970
+ </li>
971
+ {/each}
972
+ </ul>
973
+ {/if}
926
974
  </div>
927
- {/if}
975
+ {/each}
976
+ {/snippet}
977
+ <!-- The role="status" live region renders unconditionally (present and empty at load), so when the
978
+ first notice appears it announces; a region conditionally mounted with its first content may not
979
+ be observed by assistive tech (WCAG 4.1.3). The notices gate on their own presence, so an empty
980
+ region shows nothing. A plain wrapper (not display:contents) carries the role, since some
981
+ assistive tech drops a role off a display:contents box. -->
982
+ <div role="status">
983
+ {@render advisoryNotices(renderNotices)}
928
984
  </div>
929
985
  {#if draftWarning}
930
986
  <div class="alert alert-warning mb-4 text-sm">
@@ -1316,23 +1372,37 @@ const detailFields = $derived(data.fields.filter((f) => f !== titleField && f !=
1316
1372
  {@const f = field as TextareaField}
1317
1373
  <label class="flex flex-col gap-1">
1318
1374
  <span class="text-sm font-medium">{f.label}</span>
1319
- <textarea class="textarea textarea-sm" name={f.name} aria-label={f.label} rows={f.rows ?? 3}>{str(data.frontmatter[f.name])}</textarea>
1375
+ <textarea class="textarea textarea-sm" name={f.name} aria-label={f.label} aria-describedby={f.description ? `${f.name}-hint` : undefined} rows={f.rows ?? 3}>{str(data.frontmatter[f.name])}</textarea>
1376
+ {#if f.description}
1377
+ {@render fieldHint(f.name, f.description)}
1378
+ {/if}
1320
1379
  </label>
1321
1380
  {:else if field.type === 'date'}
1322
1381
  <label class="flex flex-col gap-1">
1323
1382
  <span class="text-sm font-medium">{field.label}</span>
1324
- <input class="input input-sm" type="date" name={field.name} aria-label={field.label} value={str(data.frontmatter[field.name])} />
1383
+ <!-- A date field always carries a hint: the adapter's description when set, else the
1384
+ built-in publish-clarity default. So aria-describedby always points at the paragraph. -->
1385
+ <input class="input input-sm" type="date" name={field.name} aria-label={field.label} aria-describedby={`${field.name}-hint`} value={str(data.frontmatter[field.name])} />
1386
+ {@render fieldHint(field.name, field.description || DATE_PUBLISH_HINT)}
1325
1387
  </label>
1326
1388
  {:else if field.type === 'boolean'}
1327
- <label class="label cursor-pointer justify-start gap-2">
1328
- <input class="checkbox checkbox-sm" type="checkbox" name={field.name} aria-label={field.label} checked={data.frontmatter[field.name] === true} />
1329
- <span class="text-sm">{field.label}</span>
1330
- </label>
1389
+ <div class="flex flex-col gap-1">
1390
+ <label class="label cursor-pointer justify-start gap-2">
1391
+ <input class="checkbox checkbox-sm" type="checkbox" name={field.name} aria-label={field.label} aria-describedby={field.description ? `${field.name}-hint` : undefined} checked={data.frontmatter[field.name] === true} />
1392
+ <span class="text-sm">{field.label}</span>
1393
+ </label>
1394
+ {#if field.description}
1395
+ {@render fieldHint(field.name, field.description)}
1396
+ {/if}
1397
+ </div>
1331
1398
  {:else if field.type === 'tags'}
1332
1399
  {@const f = field as TagsField}
1333
1400
  {@const selected = (data.frontmatter[f.name] ?? []) as string[]}
1334
- <fieldset class="fieldset">
1401
+ <fieldset class="fieldset" aria-describedby={f.description ? `${f.name}-hint` : undefined}>
1335
1402
  <legend class="fieldset-legend">{f.label}</legend>
1403
+ {#if f.description}
1404
+ {@render fieldHint(f.name, f.description)}
1405
+ {/if}
1336
1406
  <div class="flex flex-wrap gap-2">
1337
1407
  {#each f.options as option (option)}
1338
1408
  <label class="label cursor-pointer justify-start gap-2">
@@ -1357,9 +1427,13 @@ const detailFields = $derived(data.fields.filter((f) => f !== titleField && f !=
1357
1427
  class="input input-sm"
1358
1428
  name={f.name}
1359
1429
  aria-label={f.label}
1430
+ aria-describedby={f.description ? `${f.name}-hint` : undefined}
1360
1431
  placeholder={f.placeholder}
1361
1432
  value={tagValue}
1362
1433
  />
1434
+ {#if f.description}
1435
+ {@render fieldHint(f.name, f.description)}
1436
+ {/if}
1363
1437
  </label>
1364
1438
  {:else if field.type === 'image'}
1365
1439
  {@const heroValue = data.frontmatter[field.name] as ImageValue | undefined}
@@ -1378,7 +1452,10 @@ const detailFields = $derived(data.fields.filter((f) => f !== titleField && f !=
1378
1452
  {:else}
1379
1453
  <label class="flex flex-col gap-1">
1380
1454
  <span class="text-sm font-medium">{field.label}</span>
1381
- <input class="input input-sm" name={field.name} aria-label={field.label} value={str(data.frontmatter[field.name])} required={field.required} />
1455
+ <input class="input input-sm" name={field.name} aria-label={field.label} aria-describedby={field.description ? `${field.name}-hint` : undefined} value={str(data.frontmatter[field.name])} required={field.required} />
1456
+ {#if field.description}
1457
+ {@render fieldHint(field.name, field.description)}
1458
+ {/if}
1382
1459
  </label>
1383
1460
  {/if}
1384
1461
  {/each}