@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
@@ -90,6 +90,11 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
90
90
 
91
91
  let { data, registry, render, icons, form }: Props = $props();
92
92
 
93
+ /** One action row in an advisory notice: an `href` row renders a link, an `onAct` row a button. */
94
+ type AdvisoryRow = { rowLabel?: string; rowCode?: boolean; label: string; href?: string; onAct?: () => void };
95
+ /** A notice ready to render: the server advisory and the client needs-alt notice both map to this. */
96
+ type RenderNotice = { kind: string; message: string; detail?: string; rows: AdvisoryRow[] };
97
+
93
98
  // The client-side tidy deadline (spec 2.1, Task 14): a slow call becomes a cancel/retry rather than a
94
99
  // hung review. Set above the action's own 30s Worker deadline so the server's retryable fail lands
95
100
  // first when the model is merely slow; this catches a stalled connection past that.
@@ -150,7 +155,6 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
150
155
  const bodyDirty = $derived(body !== (form?.body ?? data.body));
151
156
  let fieldsDirty = $state(false);
152
157
  const dirty = $derived(bodyDirty || fieldsDirty);
153
- // What the header's save-state indicator says.
154
158
  const saveState = $derived(dirty ? 'Unsaved changes' : data.saved ? 'Saved' : '');
155
159
  function onFormInput(e: Event) {
156
160
  const target = e.target as Element | null;
@@ -976,6 +980,41 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
976
980
  const heroRows = $derived(imageFields.filter((f) => heroNeedsAlt[f.name]));
977
981
  const needsAltCount = $derived(needsAlt.length + heroRows.length);
978
982
 
983
+ // The advisory region renders two notice sources through one shape: the server's data-only
984
+ // advisories (an action carries an href) and the client-derived needs-alt notice (its rows carry
985
+ // callbacks the editor must run, so they cannot ride the serializable server shape). Both map into
986
+ // this local render type, where the snippet draws an href row as a link and an onAct row as a button.
987
+ const renderNotices = $derived<RenderNotice[]>([
988
+ ...data.advisories.map((notice) => ({
989
+ kind: notice.kind,
990
+ message: notice.message,
991
+ rows: (notice.actions ?? []).map((action) => ({ label: action.label, href: action.href })),
992
+ })),
993
+ ...(needsAltCount
994
+ ? [
995
+ {
996
+ kind: 'needs-alt',
997
+ message: `${needsAltCount} ${needsAltCount === 1 ? 'image needs' : 'images need'} alt text`,
998
+ detail:
999
+ 'Alt text describes an image for readers who cannot see it. Add it now, or save and come back to it.',
1000
+ rows: [
1001
+ ...needsAlt.map((item) => ({
1002
+ rowLabel: item.ref,
1003
+ rowCode: true,
1004
+ label: 'Add alt text',
1005
+ onAct: () => selectRange(item.from, item.to),
1006
+ })),
1007
+ ...heroRows.map((hero) => ({
1008
+ rowLabel: hero.label,
1009
+ label: 'Add alt text',
1010
+ onAct: () => heroFieldRefs[hero.name]?.focusAlt(),
1011
+ })),
1012
+ ],
1013
+ },
1014
+ ]
1015
+ : []),
1016
+ ]);
1017
+
979
1018
  // The delete guard's inbound linkers, from a refused delete (fail 409). Empty when the delete was
980
1019
  // not refused. When set, a delete was blocked by a link that appeared since the page loaded.
981
1020
  const deleteRefusedLinks = $derived(form?.inboundLinks ?? []);
@@ -1201,7 +1240,6 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
1201
1240
  };
1202
1241
  });
1203
1242
 
1204
- // Coerce a frontmatter value to a string for text/date/textarea inputs.
1205
1243
  function str(v: unknown): string {
1206
1244
  return v == null ? '' : String(v);
1207
1245
  }
@@ -1216,6 +1254,11 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
1216
1254
  const titleField = $derived(data.fields.find((f) => f.name === 'title'));
1217
1255
  const draftField = $derived(data.fields.find((f) => f.type === 'boolean' && f.name === 'draft'));
1218
1256
  const detailFields = $derived(data.fields.filter((f) => f !== titleField && f !== draftField));
1257
+
1258
+ // The built-in hint a date field carries when its adapter sets no description. The control reads as
1259
+ // if it might schedule publishing, so this reassures the editor that the date is metadata and that
1260
+ // publishing is the separate, deliberate step. A field-level description overrides it.
1261
+ const DATE_PUBLISH_HINT = 'Sets the date for this post. Publishing is a separate step you choose.';
1219
1262
  </script>
1220
1263
 
1221
1264
  <!-- The desk controls live in the one header band: AdminLayout renders this snippet through the
@@ -1342,6 +1385,17 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
1342
1385
  </div>
1343
1386
  {/snippet}
1344
1387
 
1388
+ <!-- The author-facing hint under a Details field. The id pairs with the input's aria-describedby
1389
+ (`<name>-hint`); its uniqueness rests on schema field names being unique within a concept, which
1390
+ is also the loop key. So assistive tech announces the sentence without bloating the accessible
1391
+ name. Each field branch decides whether and where to render it; this snippet holds the one shape.
1392
+ The `fld-hint` class is a styling hook with no rule today; the Tailwind utilities do the work. -->
1393
+ {#snippet fieldHint(name: string, text: string)}
1394
+ <p id={`${name}-hint`} class="fld-hint mt-1 text-sm text-[var(--color-muted)]">
1395
+ {text}
1396
+ </p>
1397
+ {/snippet}
1398
+
1345
1399
  <!-- The whole edit surface remounts when navigation lands on another entry (see the entryKey
1346
1400
  reset above); script-level state and the beforeNavigate registration sit outside the block,
1347
1401
  so only the template rebuilds. -->
@@ -1388,44 +1442,61 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
1388
1442
  </ul>
1389
1443
  </div>
1390
1444
  {/if}
1391
- <!-- The publish-time needs-alt notice: a non-blocking warning, never a block. Alt text is
1392
- accessibility debt, so the author can add it now or save without it; the count drops live as
1393
- each alt is filled and the notice clears at zero. The leading glyph carries the state alongside
1394
- the count, so the caution reads without relying on hue. Each row's jump control selects the
1395
- image in the source through the editor's select-range seam, landing the author on it to type
1396
- the alt. The role="status" live region renders unconditionally (present and empty at load), so
1397
- when the first debt appears its count change announces; a region conditionally mounted with its
1398
- first content may not be observed by assistive tech (WCAG 4.1.3). The visible alert chrome and
1399
- content gate on the count, so an empty region shows nothing. A plain wrapper (not display:contents)
1400
- carries the role, since some assistive tech drops a role off a display:contents box. -->
1401
- <div role="status">
1402
- {#if needsAltCount}
1445
+ <!-- The shared advisory notices: one live-region surface for every non-blocking editor warning. It
1446
+ carries the server's address-collision advisory and the client-derived needs-alt notice through
1447
+ one snippet. Each renders as one alert-warning row: the caution glyph, the message, an optional
1448
+ detail sentence, and a list of action rows. Each is a warning, never a block: the author can act
1449
+ on it or save without it. The leading glyph carries the state alongside the message, so the
1450
+ caution reads without relying on hue. A row with an href is a server advisory's link; a row with
1451
+ onAct is the needs-alt jump that runs an editor callback (selecting the image source, or focusing
1452
+ a hero alt input). -->
1453
+ <!-- Keyed by index, not by notice.kind: the kind is a free string with no uniqueness constraint, so
1454
+ two notices of one kind would otherwise throw each_key_duplicate. The list is append-only and
1455
+ never reordered, so the index is a stable key here. -->
1456
+ {#snippet advisoryNotices(notices: RenderNotice[])}
1457
+ {#each notices as notice, i (i)}
1403
1458
  <div class="alert alert-warning mb-4 flex-col items-start text-sm">
1404
1459
  <p class="flex items-center gap-2 font-medium">
1405
1460
  <svg class="h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
1406
1461
  <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" />
1407
1462
  </svg>
1408
- <span>{needsAltCount} {needsAltCount === 1 ? 'image needs' : 'images need'} alt text</span>
1463
+ <span>{notice.message}</span>
1409
1464
  </p>
1410
- <p>Alt text describes an image for readers who cannot see it. Add it now, or save and come back to it.</p>
1411
- <ul class="mt-1 w-full">
1412
- {#each needsAlt as item (item.from)}
1413
- <li class="flex items-center justify-between gap-2">
1414
- <code class="text-xs">{item.ref}</code>
1415
- <button type="button" class="btn btn-xs" onclick={() => selectRange(item.from, item.to)}>Add alt text</button>
1416
- </li>
1417
- {/each}
1418
- <!-- The frontmatter-hero rows: a hero has no body offset, so its action focuses the field's
1419
- own alt input rather than a source range. -->
1420
- {#each heroRows as hero (hero.name)}
1421
- <li class="flex items-center justify-between gap-2">
1422
- <span class="text-xs font-medium">{hero.label}</span>
1423
- <button type="button" class="btn btn-xs" onclick={() => heroFieldRefs[hero.name]?.focusAlt()}>Add alt text</button>
1424
- </li>
1425
- {/each}
1426
- </ul>
1465
+ {#if notice.detail}
1466
+ <p>{notice.detail}</p>
1467
+ {/if}
1468
+ {#if notice.rows.length}
1469
+ <ul class="mt-1 w-full">
1470
+ {#each notice.rows as row, i (i)}
1471
+ <li class="flex items-center justify-between gap-2">
1472
+ {#if row.rowLabel}
1473
+ <!-- A body needs-alt row labels with its source reference in a code span; a hero row
1474
+ and any future labelled row use a plain label. -->
1475
+ {#if row.rowCode}
1476
+ <code class="text-xs">{row.rowLabel}</code>
1477
+ {:else}
1478
+ <span class="text-xs font-medium">{row.rowLabel}</span>
1479
+ {/if}
1480
+ {/if}
1481
+ {#if row.href}
1482
+ <a class="btn btn-xs" href={row.href}>{row.label}</a>
1483
+ {:else}
1484
+ <button type="button" class="btn btn-xs" onclick={row.onAct}>{row.label}</button>
1485
+ {/if}
1486
+ </li>
1487
+ {/each}
1488
+ </ul>
1489
+ {/if}
1427
1490
  </div>
1428
- {/if}
1491
+ {/each}
1492
+ {/snippet}
1493
+ <!-- The role="status" live region renders unconditionally (present and empty at load), so when the
1494
+ first notice appears it announces; a region conditionally mounted with its first content may not
1495
+ be observed by assistive tech (WCAG 4.1.3). The notices gate on their own presence, so an empty
1496
+ region shows nothing. A plain wrapper (not display:contents) carries the role, since some
1497
+ assistive tech drops a role off a display:contents box. -->
1498
+ <div role="status">
1499
+ {@render advisoryNotices(renderNotices)}
1429
1500
  </div>
1430
1501
  {#if draftWarning}
1431
1502
  <div class="alert alert-warning mb-4 text-sm">
@@ -1817,23 +1888,37 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
1817
1888
  {@const f = field as TextareaField}
1818
1889
  <label class="flex flex-col gap-1">
1819
1890
  <span class="text-sm font-medium">{f.label}</span>
1820
- <textarea class="textarea textarea-sm" name={f.name} aria-label={f.label} rows={f.rows ?? 3}>{str(data.frontmatter[f.name])}</textarea>
1891
+ <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>
1892
+ {#if f.description}
1893
+ {@render fieldHint(f.name, f.description)}
1894
+ {/if}
1821
1895
  </label>
1822
1896
  {:else if field.type === 'date'}
1823
1897
  <label class="flex flex-col gap-1">
1824
1898
  <span class="text-sm font-medium">{field.label}</span>
1825
- <input class="input input-sm" type="date" name={field.name} aria-label={field.label} value={str(data.frontmatter[field.name])} />
1899
+ <!-- A date field always carries a hint: the adapter's description when set, else the
1900
+ built-in publish-clarity default. So aria-describedby always points at the paragraph. -->
1901
+ <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])} />
1902
+ {@render fieldHint(field.name, field.description || DATE_PUBLISH_HINT)}
1826
1903
  </label>
1827
1904
  {:else if field.type === 'boolean'}
1828
- <label class="label cursor-pointer justify-start gap-2">
1829
- <input class="checkbox checkbox-sm" type="checkbox" name={field.name} aria-label={field.label} checked={data.frontmatter[field.name] === true} />
1830
- <span class="text-sm">{field.label}</span>
1831
- </label>
1905
+ <div class="flex flex-col gap-1">
1906
+ <label class="label cursor-pointer justify-start gap-2">
1907
+ <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} />
1908
+ <span class="text-sm">{field.label}</span>
1909
+ </label>
1910
+ {#if field.description}
1911
+ {@render fieldHint(field.name, field.description)}
1912
+ {/if}
1913
+ </div>
1832
1914
  {:else if field.type === 'tags'}
1833
1915
  {@const f = field as TagsField}
1834
1916
  {@const selected = (data.frontmatter[f.name] ?? []) as string[]}
1835
- <fieldset class="fieldset">
1917
+ <fieldset class="fieldset" aria-describedby={f.description ? `${f.name}-hint` : undefined}>
1836
1918
  <legend class="fieldset-legend">{f.label}</legend>
1919
+ {#if f.description}
1920
+ {@render fieldHint(f.name, f.description)}
1921
+ {/if}
1837
1922
  <div class="flex flex-wrap gap-2">
1838
1923
  {#each f.options as option (option)}
1839
1924
  <label class="label cursor-pointer justify-start gap-2">
@@ -1858,9 +1943,13 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
1858
1943
  class="input input-sm"
1859
1944
  name={f.name}
1860
1945
  aria-label={f.label}
1946
+ aria-describedby={f.description ? `${f.name}-hint` : undefined}
1861
1947
  placeholder={f.placeholder}
1862
1948
  value={tagValue}
1863
1949
  />
1950
+ {#if f.description}
1951
+ {@render fieldHint(f.name, f.description)}
1952
+ {/if}
1864
1953
  </label>
1865
1954
  {:else if field.type === 'image'}
1866
1955
  {@const heroValue = data.frontmatter[field.name] as ImageValue | undefined}
@@ -1879,7 +1968,10 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
1879
1968
  {:else}
1880
1969
  <label class="flex flex-col gap-1">
1881
1970
  <span class="text-sm font-medium">{field.label}</span>
1882
- <input class="input input-sm" name={field.name} aria-label={field.label} value={str(data.frontmatter[field.name])} required={field.required} />
1971
+ <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} />
1972
+ {#if field.description}
1973
+ {@render fieldHint(field.name, field.description)}
1974
+ {/if}
1883
1975
  </label>
1884
1976
  {/if}
1885
1977
  {/each}