@glw907/cairn-cms 0.62.2 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/dist/ambient.d.ts +2 -0
  3. package/dist/auth/types.d.ts +7 -0
  4. package/dist/components/CairnAdmin.svelte.d.ts +2 -7
  5. package/dist/components/ComponentForm.svelte +44 -27
  6. package/dist/components/ComponentInsertDialog.svelte +22 -11
  7. package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
  8. package/dist/components/ConceptList.svelte +25 -4
  9. package/dist/components/EditPage.svelte +29 -107
  10. package/dist/components/EditPage.svelte.d.ts +2 -7
  11. package/dist/components/EntryPicker.svelte +117 -0
  12. package/dist/components/EntryPicker.svelte.d.ts +35 -0
  13. package/dist/components/FieldInput.svelte +218 -0
  14. package/dist/components/FieldInput.svelte.d.ts +51 -0
  15. package/dist/components/IconPicker.svelte +2 -2
  16. package/dist/components/IconPicker.svelte.d.ts +2 -0
  17. package/dist/components/LinkPicker.svelte +8 -75
  18. package/dist/components/LinkPicker.svelte.d.ts +4 -5
  19. package/dist/components/MediaHeroField.svelte +8 -5
  20. package/dist/components/MediaHeroField.svelte.d.ts +4 -0
  21. package/dist/components/ObjectGroupField.svelte +54 -0
  22. package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
  23. package/dist/components/ReferenceField.svelte +94 -0
  24. package/dist/components/ReferenceField.svelte.d.ts +27 -0
  25. package/dist/components/RepeatableField.svelte +221 -0
  26. package/dist/components/RepeatableField.svelte.d.ts +53 -0
  27. package/dist/components/cairn-admin.css +179 -2
  28. package/dist/components/preview-doc.js +5 -1
  29. package/dist/components/tidy-validate.js +1 -1
  30. package/dist/content/adapter.js +18 -0
  31. package/dist/content/advisories.d.ts +2 -2
  32. package/dist/content/advisories.js +3 -5
  33. package/dist/content/compose.d.ts +7 -6
  34. package/dist/content/compose.js +26 -20
  35. package/dist/content/concepts.d.ts +21 -15
  36. package/dist/content/concepts.js +55 -32
  37. package/dist/content/field-rules.d.ts +15 -0
  38. package/dist/content/field-rules.js +38 -0
  39. package/dist/content/fields.d.ts +169 -0
  40. package/dist/content/fields.js +41 -0
  41. package/dist/content/fieldset.d.ts +107 -0
  42. package/dist/content/fieldset.js +386 -0
  43. package/dist/content/frontmatter-region.d.ts +38 -0
  44. package/dist/content/frontmatter-region.js +75 -0
  45. package/dist/content/frontmatter.d.ts +35 -2
  46. package/dist/content/frontmatter.js +232 -11
  47. package/dist/content/manifest.d.ts +34 -0
  48. package/dist/content/manifest.js +80 -4
  49. package/dist/content/media-refs.d.ts +2 -2
  50. package/dist/content/media-rewrite.js +1 -69
  51. package/dist/content/reference-index.d.ts +56 -0
  52. package/dist/content/reference-index.js +95 -0
  53. package/dist/content/references.d.ts +40 -0
  54. package/dist/content/references.js +0 -0
  55. package/dist/content/standard-schema.d.ts +30 -0
  56. package/dist/content/standard-schema.js +4 -0
  57. package/dist/content/types.d.ts +127 -178
  58. package/dist/delivery/data.d.ts +2 -2
  59. package/dist/delivery/data.js +1 -1
  60. package/dist/delivery/public-routes.d.ts +10 -5
  61. package/dist/delivery/public-routes.js +25 -2
  62. package/dist/delivery/site-descriptors.d.ts +5 -1
  63. package/dist/delivery/site-descriptors.js +8 -3
  64. package/dist/delivery/site-indexes.d.ts +2 -2
  65. package/dist/delivery/site-resolver.d.ts +25 -0
  66. package/dist/delivery/site-resolver.js +49 -0
  67. package/dist/doctor/checks-local.js +6 -11
  68. package/dist/github/backend.d.ts +83 -0
  69. package/dist/github/backend.js +76 -0
  70. package/dist/github/credentials.d.ts +11 -5
  71. package/dist/github/credentials.js +3 -3
  72. package/dist/github/repo.d.ts +8 -19
  73. package/dist/github/repo.js +69 -80
  74. package/dist/github/types.d.ts +1 -1
  75. package/dist/github/types.js +4 -4
  76. package/dist/index.d.ts +18 -10
  77. package/dist/index.js +9 -5
  78. package/dist/islands/index.d.ts +12 -0
  79. package/dist/islands/index.js +83 -0
  80. package/dist/islands/types.d.ts +7 -0
  81. package/dist/islands/types.js +1 -0
  82. package/dist/log/events.d.ts +1 -1
  83. package/dist/media/index.d.ts +1 -1
  84. package/dist/media/index.js +1 -1
  85. package/dist/media/manifest.d.ts +11 -0
  86. package/dist/media/manifest.js +13 -0
  87. package/dist/media/rewrite-plan.d.ts +2 -3
  88. package/dist/media/rewrite-plan.js +2 -3
  89. package/dist/media/usage.d.ts +2 -2
  90. package/dist/media/usage.js +3 -5
  91. package/dist/nav/site-config.d.ts +0 -6
  92. package/dist/nav/site-config.js +6 -4
  93. package/dist/render/component-grammar.js +11 -11
  94. package/dist/render/component-reference.js +5 -3
  95. package/dist/render/component-validate.d.ts +4 -1
  96. package/dist/render/component-validate.js +10 -35
  97. package/dist/render/highlight.d.ts +9 -0
  98. package/dist/render/highlight.js +206 -0
  99. package/dist/render/pipeline.d.ts +0 -6
  100. package/dist/render/pipeline.js +13 -2
  101. package/dist/render/registry.d.ts +44 -36
  102. package/dist/render/registry.js +47 -6
  103. package/dist/render/rehype-dispatch.d.ts +6 -10
  104. package/dist/render/rehype-dispatch.js +38 -17
  105. package/dist/render/remark-directives.js +4 -5
  106. package/dist/render/sanitize-schema.d.ts +10 -0
  107. package/dist/render/sanitize-schema.js +30 -1
  108. package/dist/sveltekit/cairn-admin.d.ts +5 -5
  109. package/dist/sveltekit/cairn-admin.js +3 -4
  110. package/dist/sveltekit/content-routes.d.ts +10 -8
  111. package/dist/sveltekit/content-routes.js +269 -181
  112. package/dist/sveltekit/guard.js +10 -0
  113. package/dist/sveltekit/health.d.ts +7 -3
  114. package/dist/sveltekit/health.js +9 -3
  115. package/dist/sveltekit/index.d.ts +1 -1
  116. package/dist/sveltekit/nav-routes.d.ts +6 -5
  117. package/dist/sveltekit/nav-routes.js +22 -20
  118. package/dist/sveltekit/types.d.ts +2 -0
  119. package/dist/vite/index.d.ts +3 -3
  120. package/dist/vite/index.js +17 -8
  121. package/package.json +17 -2
  122. package/src/lib/ambient.ts +7 -0
  123. package/src/lib/auth/types.ts +7 -0
  124. package/src/lib/components/CairnAdmin.svelte +2 -6
  125. package/src/lib/components/ComponentForm.svelte +48 -27
  126. package/src/lib/components/ComponentInsertDialog.svelte +26 -14
  127. package/src/lib/components/ConceptList.svelte +41 -4
  128. package/src/lib/components/EditPage.svelte +43 -119
  129. package/src/lib/components/EntryPicker.svelte +154 -0
  130. package/src/lib/components/FieldInput.svelte +262 -0
  131. package/src/lib/components/IconPicker.svelte +4 -2
  132. package/src/lib/components/LinkPicker.svelte +10 -81
  133. package/src/lib/components/MediaHeroField.svelte +12 -5
  134. package/src/lib/components/ObjectGroupField.svelte +97 -0
  135. package/src/lib/components/ReferenceField.svelte +126 -0
  136. package/src/lib/components/RepeatableField.svelte +310 -0
  137. package/src/lib/components/preview-doc.ts +5 -1
  138. package/src/lib/components/tidy-validate.ts +1 -1
  139. package/src/lib/content/adapter.ts +21 -0
  140. package/src/lib/content/advisories.ts +4 -7
  141. package/src/lib/content/compose.ts +30 -23
  142. package/src/lib/content/concepts.ts +68 -40
  143. package/src/lib/content/field-rules.ts +39 -0
  144. package/src/lib/content/fields.ts +178 -0
  145. package/src/lib/content/fieldset.ts +470 -0
  146. package/src/lib/content/frontmatter-region.ts +90 -0
  147. package/src/lib/content/frontmatter.ts +231 -15
  148. package/src/lib/content/manifest.ts +101 -4
  149. package/src/lib/content/media-refs.ts +2 -2
  150. package/src/lib/content/media-rewrite.ts +7 -80
  151. package/src/lib/content/reference-index.ts +159 -0
  152. package/src/lib/content/references.ts +0 -0
  153. package/src/lib/content/standard-schema.ts +25 -0
  154. package/src/lib/content/types.ts +128 -195
  155. package/src/lib/delivery/data.ts +2 -2
  156. package/src/lib/delivery/public-routes.ts +36 -4
  157. package/src/lib/delivery/site-descriptors.ts +8 -3
  158. package/src/lib/delivery/site-indexes.ts +2 -2
  159. package/src/lib/delivery/site-resolver.ts +64 -0
  160. package/src/lib/doctor/checks-local.ts +6 -14
  161. package/src/lib/github/backend.ts +161 -0
  162. package/src/lib/github/credentials.ts +10 -7
  163. package/src/lib/github/repo.ts +79 -83
  164. package/src/lib/github/types.ts +5 -5
  165. package/src/lib/index.ts +40 -18
  166. package/src/lib/islands/index.ts +84 -0
  167. package/src/lib/islands/types.ts +11 -0
  168. package/src/lib/log/events.ts +1 -0
  169. package/src/lib/media/index.ts +1 -0
  170. package/src/lib/media/manifest.ts +14 -0
  171. package/src/lib/media/rewrite-plan.ts +4 -6
  172. package/src/lib/media/usage.ts +4 -7
  173. package/src/lib/nav/site-config.ts +8 -9
  174. package/src/lib/render/component-grammar.ts +10 -10
  175. package/src/lib/render/component-reference.ts +4 -3
  176. package/src/lib/render/component-validate.ts +10 -35
  177. package/src/lib/render/highlight.ts +259 -0
  178. package/src/lib/render/pipeline.ts +13 -8
  179. package/src/lib/render/registry.ts +88 -42
  180. package/src/lib/render/rehype-dispatch.ts +47 -16
  181. package/src/lib/render/remark-directives.ts +4 -5
  182. package/src/lib/render/sanitize-schema.ts +32 -1
  183. package/src/lib/sveltekit/cairn-admin.ts +8 -9
  184. package/src/lib/sveltekit/content-routes.ts +330 -221
  185. package/src/lib/sveltekit/guard.ts +15 -0
  186. package/src/lib/sveltekit/health.ts +13 -6
  187. package/src/lib/sveltekit/index.ts +2 -2
  188. package/src/lib/sveltekit/nav-routes.ts +33 -29
  189. package/src/lib/sveltekit/types.ts +5 -1
  190. package/src/lib/vite/index.ts +20 -11
  191. package/dist/content/schema.d.ts +0 -87
  192. package/dist/content/schema.js +0 -89
  193. package/dist/content/validate.d.ts +0 -17
  194. package/dist/content/validate.js +0 -93
  195. package/src/lib/content/schema.ts +0 -167
  196. package/src/lib/content/validate.ts +0 -90
package/CHANGELOG.md CHANGED
@@ -2,6 +2,222 @@
2
2
 
3
3
  All notable changes to this project are recorded here, most recent first.
4
4
 
5
+ ## 0.76.0
6
+
7
+ <!-- release-size: minor -->
8
+
9
+ The Contract v2 rollup, plus content islands, published as one release. cairn is still `0.x` and the
10
+ contract may change again before a stable 1.0. This consolidates the unpublished `0.69.0`–`0.75.0`
11
+ development minors plus islands into one published `0.76.0` release. The last published release was
12
+ `0.68.0`, so a consumer crosses the whole window in a single jump and applies the "Consumers must" steps
13
+ below; the granular per-phase history lives in `docs/STATUS.md` and the plan post-mortems.
14
+
15
+ What changed. The field system unifies on the `fieldset({...})` record built from the `fields.*`
16
+ constructors, the one live field system for concepts and directive components alike, with the leaf
17
+ vocabulary (`text`, `textarea`, `number`, `select`, `multiselect`, `url`, `email`, `date`, `datetime`,
18
+ `boolean`, `image`, `icon`, `reference`) plus the `object` and `array` containers. The adapter moves from
19
+ flat keys into six subsystem groups (`content`, `backend`, `email`, `rendering`, `media`, `editor`), and a
20
+ concept owns its own URL policy through `defineConcept`. The `backend` becomes a `Backend` interface behind
21
+ a `githubApp(...)` provider, so content stays build-time over the committed manifest and no runtime database
22
+ slips in. The `render` seam becomes the entry-aware `render({ body, concept?, frontmatter?, resolve?,
23
+ resolveMedia? }) => Promise<string>`. Content islands add opt-in client interactivity over a static, no-JS
24
+ fallback. References and structured fields arrive additively.
25
+
26
+ This is breaking. Consumers must, in order:
27
+
28
+ The field system (replaces the v1 `defineFields`):
29
+
30
+ 1. Move each concept's `schema` from `defineFields([...])` (an array) to `fieldset({...})` (a record).
31
+ 2. Drop the per-field `name`; the record key is now the frontmatter key.
32
+ 3. Rename field help from `description` to `help`.
33
+ 4. Move a closed `tags` field to `fields.multiselect({ options: [...] })`, and an open `freetags` field to
34
+ `fields.multiselect({ creatable: true })` (its `placeholder` is preserved).
35
+ 5. Preserve each field's frontmatter key, especially `tags`, or tag pages and feeds read empty.
36
+ 6. Extract a frontmatter type with `InferFieldset`, and drop imports of the removed `defineFields`,
37
+ `ConceptSchema`, `Infer`, `InferFields`, `DefineFieldsOptions`, `FrontmatterField`, `TagsField`, and
38
+ `FreeTagsField`.
39
+
40
+ The adapter and concepts:
41
+
42
+ 7. Regroup the adapter into `content`/`backend`/`email`/`rendering`/`media`/`editor` (`sender` to `email`,
43
+ `render`/`registry`/`icons` to `rendering.{render,components,icons}`, `assets` to `media`,
44
+ `navMenu`/`preview`/`supportContact` to `editor.{nav,preview,supportContact}`).
45
+ 8. Rename each concept's `schema:` to `fields:` and declare it through `defineConcept`.
46
+ 9. Move `permalink` and `datePrefix` from the YAML `content:` block onto the concept via `defineConcept`,
47
+ and declare each concept's routing with the routing shorthand. A leftover YAML `content:` block now
48
+ throws at `parseSiteConfig`.
49
+ 10. Move `siteName` out of the adapter into the YAML site-config.
50
+
51
+ Directive components:
52
+
53
+ 11. Declare each component's `attributes` as a `fields.*` record (was an `AttributeField[]` array), a
54
+ repeatable slot's `itemFields` the same way, and wrap each component in `defineComponent({ ... })`.
55
+ 12. Move any cross-field attribute `validate` into the component's `behavior` table with the
56
+ `validate(value, siblings)` signature, reading `siblings.min` rather than `all.attributes.min`.
57
+ 13. Replace a `pattern: { source, message }` attribute with `fields.text({ pattern })` plus a
58
+ `behavior.validate` for a custom message, and drop imports of `AttributeField` and `FieldType`.
59
+ Attribute validation now format-checks every value, so a directive that previously saved a malformed
60
+ value now fails `validateComponent`.
61
+
62
+ The backend:
63
+
64
+ 14. Change the adapter's `backend` from a `{ owner, repo, branch, appId, installationId }` object literal to
65
+ `backend: githubApp({ ... })`, importing `githubApp` from `@glw907/cairn-cms`. Drop imports of the
66
+ removed `BackendConfig`, `RepoRef`, and `AppCredentials`, and replace `GithubKeyEnv` (from the
67
+ `/sveltekit` subpath) with `BackendEnv`.
68
+
69
+ The render seam:
70
+
71
+ 15. Change the adapter `render` from `(md, opts) => ...` to
72
+ `({ body, resolve, resolveMedia }) => ...`, read the markdown from `body`, and return a
73
+ `Promise<string>` (a typical body is `renderMarkdown(body, { resolve, resolveMedia })`). Drop any
74
+ `stagger` option; `data-rise` is now always emitted and is inert without `[data-rise]` CSS. The
75
+ attribute now appears in all rendered output, including the syndication feeds and prerendered pages,
76
+ so a consumer that snapshots rendered HTML sees it.
77
+
78
+ Additive in this window, with no action required to keep working: reference fields (`fields.reference` and
79
+ `fields.array(fields.reference(...))`), structured fields (`fields.object` and the generalized
80
+ `fields.array`), and content islands (`hydrate` on a component, `rendering.islands`, and the `./islands`
81
+ runtime). Adopt them through their guides: [references](docs/guides/link-content-with-references.md),
82
+ [structured fields](docs/guides/structured-fields.md), and [islands](docs/guides/add-an-island.md).
83
+
84
+ ecxc-ski and 907-life stay pinned to the prior version range until they cut over. See [Upgrading
85
+ cairn](docs/guides/upgrade-cairn.md) for the per-change actions.
86
+
87
+ ## 0.68.0
88
+
89
+ <!-- release-size: minor -->
90
+
91
+ The second pre-cutover engine-hardening pass clears eight engine-misc items: two admin accessibility
92
+ fixes, an engine default-icon fallback, and gate, doc, and tooling hygiene.
93
+
94
+ The component picker dialog now caps its height at 85vh and scrolls its catalog within a held header
95
+ and footer, so a long catalog no longer takes the page over. A repeated content-lifecycle error in the
96
+ concept list now re-announces to a screen reader: the errors route through one polite live region that
97
+ re-speaks an identical message through an invisible nonce, and the visible alerts drop their redundant
98
+ `role` so the message announces once.
99
+
100
+ The component registry ships a default role-to-glyph fallback for the conventional admonition roles
101
+ (`note`, `tip`, `important`, `warning`, `caution`, `info`, `danger`). A component that declares an icon
102
+ field but no `defaultIconByRole` entry for a role now resolves the engine default, which a site's icon
103
+ set styles; a component's own `defaultIconByRole` still wins. The `ComponentDef.icon` and
104
+ `defaultIconByRole` guidance now states the "logically representative, prefer distinct" rule.
105
+
106
+ Three gates and one doc tightened: the admin-prose gate now scans the `.ts` copy modules it skipped, a
107
+ new `check:dev-package` gate type-checks and comment-lints `packages/**` in CI, the two
108
+ `rehype-dispatch` helpers gained real doc contracts, and the friction log marks its killed and shipped
109
+ items resolved so it stops resurfacing dead work.
110
+
111
+ No consumer action is required. The accessibility fixes and the icon fallback are additive; a site using
112
+ the registry's `defaultIcon` may now see an engine default glyph where it previously saw none.
113
+
114
+ ## 0.67.0
115
+
116
+ <!-- release-size: minor -->
117
+
118
+ The Contract v2 `fieldset` validator reaches constraint parity with `defineFields`, the first of two
119
+ pre-cutover engine-hardening passes. Both validators now call one shared constraint module, so they
120
+ cannot drift, and a v1-vs-v2 parity matrix proves they agree on the overlapping field types.
121
+
122
+ The `fieldset` validator gains the checks it lacked. A `text` or `textarea` field now enforces its
123
+ `min`, `max`, `length`, and `pattern`, and a `date` field enforces its `min` and `max`, with the same
124
+ messages `defineFields` produces. A malformed `pattern` now fails at `fieldset()` call time, not on
125
+ every save, the way `defineFields` already compiled patterns at declaration. The validator also reads a
126
+ parsed value, not only a form string: a numeric `number` (a finite `0` included), a `Date` on a
127
+ `datetime` field, the way the `date` field already coerced a parsed `Date`. A `multiselect` given a lone
128
+ scalar (a single hand-edited `tags: news`) coerces it to a single-element list rather than dropping it
129
+ or reporting a misleading "required".
130
+
131
+ No consumer action is required. The `fieldset` surface is still additive and not yet wired into the
132
+ adapter or editor, and the new behavior brings it in line with the long-standing `defineFields` checks.
133
+
134
+ ## 0.66.0
135
+
136
+ <!-- release-size: minor -->
137
+
138
+ Contract v2 begins with an additive `fields.*` field vocabulary, exported beside the existing
139
+ `defineFields` model. The new surface is opt-in and does not yet wire into the adapter or editor, so a
140
+ site on the current field model is unaffected.
141
+
142
+ A concept can declare its fields as a record of `fields.*` constructors, each returning a plain-data
143
+ descriptor. The scalars are `text`, `textarea`, `number`, `select`, `multiselect`, `url`, `email`,
144
+ `date`, `datetime`, and `boolean`, with `image` as the rich leaf. `fieldset(record)` derives a
145
+ server-side validator from those descriptors, returning field-keyed errors or normalized data, and
146
+ exposes Standard Schema v1 at its boundary. `InferFieldset` reads the inferred frontmatter type from a
147
+ fieldset, and `initialValues` resolves each field's `default` for the editor form, including the
148
+ `'today'` sentinel on a date field through an injected clock. The new root-barrel exports are `fields`,
149
+ `fieldset`, `initialValues`, and the types `FieldDescriptor`, `Fieldset`, `InferFieldset`,
150
+ `FieldsetOptions`, and `BehaviorTable`.
151
+
152
+ No consumer action is required. The vocabulary is a foundation; the contract-v2 cutover, a later
153
+ breaking release, migrates concepts off `defineFields` and carries the "Consumers must:" line then.
154
+
155
+ ## 0.65.0
156
+
157
+ <!-- release-size: minor -->
158
+
159
+ Build-time syntax highlighting moves into the engine render pipeline, and the public side gains the
160
+ Waymark design foundation in the showcase template (the scaffolder's Part B2).
161
+
162
+ Fenced code is now highlighted at build time. The render pipeline runs Shiki at build and SSR and
163
+ emits role-bound `.cairn-tok-*` token classes with no inline style and no client highlighter, so the
164
+ reading route ships no highlighter JavaScript and the colors come from the site's theme. The engine
165
+ owns the `.cairn-tok-*` class contract (the way it owns `.cairn-place-*` for figures); a site styles
166
+ the classes from its own `--cairn-code-*` variables. Adds `shiki` and `hast-util-to-string` to the
167
+ engine's dependencies.
168
+
169
+ GFM task-list checkboxes now carry an `aria-label` from their item text, so a screen reader names the
170
+ read-only control. This clears an axe `label` violation on every site while keeping the real disabled
171
+ input the design calls for.
172
+
173
+ No consumer action is required. A site gets highlighting automatically; to color the tokens, style the
174
+ `.cairn-tok-*` classes from a `--cairn-code-*` ramp (the Waymark showcase template does this, bound to
175
+ the DaisyUI roles). The broader Waymark design foundation (the oklch token layer, the bespoke reading
176
+ surface, the chrome, the `/styleguide` route, and the dual-gamut contrast, token-resolution, and
177
+ re-skin CI gates) ships in `examples/showcase`, the deployable starter, not the published engine.
178
+
179
+ ## 0.64.0
180
+
181
+ <!-- release-size: minor -->
182
+
183
+ A small pre-Part-B DX pass fixes two engine warts the scaffolder's template would otherwise bake in,
184
+ and retires a third item that was already resolved.
185
+
186
+ `readCommittedManifest`, exported from `/media`, reads a committed media manifest from an
187
+ `import.meta.glob` result and degrades a missing file to an empty manifest. A fresh site with no
188
+ `src/content/.cairn/media.json` no longer fails its build: the static import that crashed gives way to
189
+ the glob, which returns `{}` for no match. The showcase reads its manifest this way.
190
+
191
+ A new `media.resolver_absent` log event (level `warn`) makes a silently-broken public-image setup
192
+ diagnosable. The public route emits it once, at construction, when media is configured on but no
193
+ `resolveMedia` reached it, so a forgotten resolver wiring becomes a queryable Workers Logs event
194
+ instead of a bare `media:` token on every hero image. `PublicRoutesDeps` gains an optional
195
+ `assetsEnabled` flag a site threads from its resolved asset config.
196
+
197
+ No consumer action is required. A site that wants the no-crash manifest read can adopt
198
+ `readCommittedManifest`, and a site that wants the resolver diagnostic threads `assetsEnabled` into
199
+ `createPublicRoutes`.
200
+
201
+ ## 0.63.0
202
+
203
+ <!-- release-size: minor -->
204
+
205
+ The local-development fake backend moves out of the engine and the showcase into a separate, dev-only
206
+ package, `@glw907/cairn-cms-dev`, the first part of the `create-cairn-site` scaffolder. The package
207
+ holds the in-memory GitHub, R2, D1, and Anthropic doubles and a blessed `devBackendHandle()` that
208
+ installs them and an owner-session bypass, so a site runs `/admin` locally with no cloud accounts. A
209
+ consumer installs it as a `devDependency` and activates it from `hooks.server.ts` behind a
210
+ build-foldable `dev` gate, so a production build eliminates it from the bundle.
211
+
212
+ The auth guard gains a fail-closed tripwire. If `CAIRN_DEV_BACKEND` is set in a deployed runtime, the
213
+ guard refuses the request with a 503 and logs `guard.rejected` with `reason: "dev_backend_in_prod"`.
214
+ It reads the flag from both the Worker `platform.env` and `process.env`, so it fires on Cloudflare and
215
+ adapter-node alike. `AuthEnv` carries a new optional `CAIRN_DEV_BACKEND?: string | boolean` field for
216
+ it.
217
+
218
+ No consumer action is required. The tripwire fires only when the flag is set, and the new package is
219
+ opt-in for sites that want the local dev backend.
220
+
5
221
  ## 0.62.2
6
222
 
7
223
  The edit-load address-collision advisory now checks the published corpus only. It fires when an entry
package/dist/ambient.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type { Editor } from './auth/types.js';
2
+ import type { Backend } from './github/backend.js';
2
3
  declare global {
3
4
  namespace App {
4
5
  interface Locals {
5
6
  editor?: Editor | null;
7
+ backend?: Backend;
6
8
  }
7
9
  }
8
10
  }
@@ -11,6 +11,13 @@ export interface AuthEnv {
11
11
  AUTH_DB?: D1Database;
12
12
  /** Canonical origin for confirmation links, never read from a request header (spec 7.1, risk H3). */
13
13
  PUBLIC_ORIGIN?: string;
14
+ /**
15
+ * Dev-backend tripwire flag. The dev backend sets this in local development; if it is ever set in
16
+ * a deployed runtime the guard refuses (the build-foldable `dev` gate should have eliminated the
17
+ * dev backend, so a set flag signals a polluted environment). A string from a Worker var or a
18
+ * boolean.
19
+ */
20
+ CAIRN_DEV_BACKEND?: string | boolean;
14
21
  /** Cloudflare Email Sending binding. */
15
22
  EMAIL?: {
16
23
  send(message: {
@@ -2,8 +2,7 @@ import type { AdminData } from '../sveltekit/cairn-admin.js';
2
2
  import type { ContentFormFailure } from '../sveltekit/content-routes.js';
3
3
  import type { ComponentRegistry } from '../render/registry.js';
4
4
  import type { IconSet } from '../render/glyph.js';
5
- import type { LinkResolve } from '../content/links.js';
6
- import type { MediaResolve } from '../render/resolve-media.js';
5
+ import type { SiteRender } from '../content/types.js';
7
6
  interface Props {
8
7
  /** The discriminated view data from `createCairnAdmin`'s load. */
9
8
  data: AdminData;
@@ -16,11 +15,7 @@ interface Props {
16
15
  ok?: boolean;
17
16
  }) | null;
18
17
  /** The site's design-accurate render pipeline, for the edit view's preview pane. */
19
- render?: (md: string, opts?: {
20
- stagger?: boolean;
21
- resolve?: LinkResolve;
22
- resolveMedia?: MediaResolve;
23
- }) => string | Promise<string>;
18
+ render?: SiteRender;
24
19
  /** The site's component registry, for the edit view's insert palette. */
25
20
  registry?: ComponentRegistry;
26
21
  /** The site's icon set, for the edit view's guided form fields. */
@@ -26,7 +26,7 @@ let working = $state(untrack(() => initial ?? previewValues(def)));
26
26
  $effect(() => {
27
27
  values = working;
28
28
  });
29
- const attributes = $derived(def.attributes ?? []);
29
+ const attributes = $derived(Object.entries(def.attributes ?? {}));
30
30
  const flatSlots = $derived((def.slots ?? []).filter((s) => s.kind !== "repeatable"));
31
31
  const repeatableSlots = $derived((def.slots ?? []).filter((s) => s.kind === "repeatable"));
32
32
  function slotItems(name) {
@@ -59,7 +59,7 @@ function removeItem(name, index) {
59
59
  function rowLabel(slot, value, index) {
60
60
  const fallback = `${slot.label} ${index + 1}`;
61
61
  if (!slot.itemLabel) return fallback;
62
- const key = slot.itemFields?.[0]?.key ?? "text";
62
+ const key = Object.keys(slot.itemFields ?? {})[0] ?? "text";
63
63
  const derived = slot.itemLabel({ [key]: value }, index);
64
64
  return derived && derived.trim() ? derived : fallback;
65
65
  }
@@ -74,10 +74,26 @@ function slotString(name) {
74
74
  const v = working.slots[name];
75
75
  return typeof v === "string" ? v : "";
76
76
  }
77
+ function inputType(type) {
78
+ switch (type) {
79
+ case "number":
80
+ return "number";
81
+ case "date":
82
+ return "date";
83
+ case "datetime":
84
+ return "datetime-local";
85
+ case "url":
86
+ return "url";
87
+ case "email":
88
+ return "email";
89
+ default:
90
+ return "text";
91
+ }
92
+ }
77
93
  const incompleteState = $derived.by(() => {
78
- for (const field of attributes) {
94
+ for (const [name, field] of attributes) {
79
95
  if (!field.required || field.type === "boolean") continue;
80
- if (asString(field.key) === "") return true;
96
+ if (asString(name) === "") return true;
81
97
  }
82
98
  for (const slot of def.slots ?? []) {
83
99
  if (!slot.required) continue;
@@ -97,9 +113,9 @@ function markTouched(key) {
97
113
  }
98
114
  const errors = $derived.by(() => {
99
115
  const out = {};
100
- for (const field of attributes) {
101
- if (field.required && field.type !== "boolean" && touched[field.key] && asString(field.key) === "") {
102
- out[field.key] = `${field.label} is required.`;
116
+ for (const [name, field] of attributes) {
117
+ if (field.required && field.type !== "boolean" && touched[name] && asString(name) === "") {
118
+ out[name] = `${field.label} is required.`;
103
119
  }
104
120
  }
105
121
  for (const slot of def.slots ?? []) {
@@ -127,16 +143,16 @@ async function submit() {
127
143
  </script>
128
144
 
129
145
  <div class="flex flex-col gap-3" bind:this={formEl}>
130
- {#each attributes as field (field.key)}
146
+ {#each attributes as [name, field] (name)}
131
147
  {#if field.type === 'boolean'}
132
148
  <label class="label cursor-pointer justify-start gap-2">
133
149
  <input
134
150
  class="checkbox checkbox-sm"
135
151
  type="checkbox"
136
- aria-invalid={Boolean(errors[field.key])}
137
- aria-describedby={errors[field.key] ? `err-${field.key}` : undefined}
138
- checked={asBool(field.key)}
139
- onchange={(e) => (working.attributes[field.key] = e.currentTarget.checked)}
152
+ aria-invalid={Boolean(errors[name])}
153
+ aria-describedby={errors[name] ? `err-${name}` : undefined}
154
+ checked={asBool(name)}
155
+ onchange={(e) => (working.attributes[name] = e.currentTarget.checked)}
140
156
  />
141
157
  <span class="text-sm">{field.label}</span>
142
158
  </label>
@@ -146,14 +162,14 @@ async function submit() {
146
162
  <select
147
163
  class="select"
148
164
  aria-required={field.required ? 'true' : undefined}
149
- aria-invalid={Boolean(errors[field.key])}
150
- aria-describedby={errors[field.key] ? `err-${field.key}` : undefined}
151
- value={asString(field.key)}
165
+ aria-invalid={Boolean(errors[name])}
166
+ aria-describedby={errors[name] ? `err-${name}` : undefined}
167
+ value={asString(name)}
152
168
  onchange={(e) => {
153
- working.attributes[field.key] = e.currentTarget.value;
154
- markTouched(field.key);
169
+ working.attributes[name] = e.currentTarget.value;
170
+ markTouched(name);
155
171
  }}
156
- onblur={() => markTouched(field.key)}
172
+ onblur={() => markTouched(name)}
157
173
  >
158
174
  {#if !field.required}<option value="">—</option>{/if}
159
175
  {#each field.options ?? [] as opt (opt)}<option value={opt}>{opt}</option>{/each}
@@ -165,9 +181,9 @@ async function submit() {
165
181
  <IconPicker
166
182
  {icons}
167
183
  label={field.label}
168
- value={asString(field.key)}
184
+ value={asString(name)}
169
185
  required={field.required ?? false}
170
- onChange={(name) => (working.attributes[field.key] = name)}
186
+ onChange={(glyph) => (working.attributes[name] = glyph)}
171
187
  />
172
188
  </div>
173
189
  {:else}
@@ -175,19 +191,20 @@ async function submit() {
175
191
  <span class="text-sm font-medium">{field.label}{#if field.required}<span data-testid="cairn-pk-req" class="text-error" aria-hidden="true">*</span>{/if}</span>
176
192
  <input
177
193
  class="input"
194
+ type={inputType(field.type)}
178
195
  aria-required={field.required ? 'true' : undefined}
179
- aria-invalid={Boolean(errors[field.key])}
180
- aria-describedby={errors[field.key] ? `err-${field.key}` : undefined}
181
- value={asString(field.key)}
196
+ aria-invalid={Boolean(errors[name])}
197
+ aria-describedby={errors[name] ? `err-${name}` : undefined}
198
+ value={asString(name)}
182
199
  oninput={(e) => {
183
- working.attributes[field.key] = e.currentTarget.value;
184
- markTouched(field.key);
200
+ working.attributes[name] = e.currentTarget.value;
201
+ markTouched(name);
185
202
  }}
186
- onblur={() => markTouched(field.key)}
203
+ onblur={() => markTouched(name)}
187
204
  />
188
205
  </label>
189
206
  {/if}
190
- {#if errors[field.key]}<span id={`err-${field.key}`} role="alert" class="text-error text-xs">{errors[field.key]}</span>{/if}
207
+ {#if errors[name]}<span id={`err-${name}`} role="alert" class="text-error text-xs">{errors[name]}</span>{/if}
191
208
  {/each}
192
209
 
193
210
  {#each flatSlots as slot (slot.name)}
@@ -7,7 +7,7 @@ trapping and Escape, following the dropdown's a11y conventions used elsewhere in
7
7
  -->
8
8
  <script module lang="ts">const SEARCH_THRESHOLD = 8;
9
9
  export function hasSchema(def) {
10
- return (def.attributes?.length ?? 0) > 0 || (def.slots?.length ?? 0) > 0;
10
+ return Object.keys(def.attributes ?? {}).length > 0 || (def.slots?.length ?? 0) > 0;
11
11
  }
12
12
  export function insertableDefs(registry) {
13
13
  return (registry?.defs ?? []).filter(
@@ -49,10 +49,10 @@ let previewDoc = $state("");
49
49
  const emptyRequired = $derived.by(() => {
50
50
  if (!picked || !formValues) return [];
51
51
  const out = [];
52
- for (const field of picked.attributes ?? []) {
52
+ for (const [name, field] of Object.entries(picked.attributes ?? {})) {
53
53
  if (!field.required || field.type === "boolean") continue;
54
- const v = formValues.attributes[field.key];
55
- if (typeof v !== "string" || v === "") out.push(field.label);
54
+ const v = formValues.attributes[name];
55
+ if (typeof v !== "string" || v === "") out.push(field.label ?? name);
56
56
  }
57
57
  for (const slot of picked.slots ?? []) {
58
58
  if (!slot.required) continue;
@@ -75,7 +75,7 @@ $effect(() => {
75
75
  previewState = "settling";
76
76
  const handle = setTimeout(async () => {
77
77
  try {
78
- const html = await render(md);
78
+ const html = await render({ body: md });
79
79
  if (run === previewRun) {
80
80
  previewDoc = buildPreviewDoc(html, preview);
81
81
  previewState = "settled";
@@ -197,11 +197,16 @@ function onSearchKeydown(e) {
197
197
 
198
198
  {#if defs.length > 0}
199
199
  <dialog class="modal" aria-labelledby="cairn-insert-dialog-title" bind:this={dialog} onclose={onClose} oncancel={onCancel}>
200
- <div class="modal-box {twoPane ? 'max-w-3xl' : ''}">
200
+ <!-- The box caps at 85vh and is a flex column so its header holds while only the body scrolls,
201
+ per the design system's dialog-sizing recipe. The cap rides Tailwind utilities (the utilities
202
+ layer) so it beats DaisyUI's `.modal-box` max-height: 100vh; a components-layer rule loses the
203
+ cascade. overflow-hidden keeps the box from being a second scroll container. Matches TidyReview. -->
204
+ <div class="modal-box flex max-h-[85vh] flex-col overflow-hidden {twoPane ? 'max-w-3xl' : ''}">
201
205
  <!-- The shared header: at the configure step it carries the Back control and the
202
206
  "Insert > group" eyebrow breadcrumb above the component label; while browsing it is the
203
- plain "Insert a component" title. -->
204
- <div class="mb-3 flex items-center gap-3">
207
+ plain "Insert a component" title. It holds (flex-none) while the body scrolls, per the
208
+ design system's dialog-sizing recipe. -->
209
+ <div class="mb-3 flex flex-none items-center gap-3">
205
210
  {#if picked && !editing}
206
211
  <button type="button" class="btn btn-ghost btn-sm btn-square" aria-label="Back to components" onclick={back}>
207
212
  <svg class="h-4 w-4" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M165.7 202.3a8 8 0 0 1-11.4 11.4l-80-80a8 8 0 0 1 0-11.4l80-80a8 8 0 0 1 11.4 11.4L91.3 128Z" /></svg>
@@ -219,6 +224,9 @@ function onSearchKeydown(e) {
219
224
  </div>
220
225
 
221
226
  {#if picked}
227
+ <!-- The configure body is the box's scroll container (flex-1, min-h-0): the shared header
228
+ above holds while the form scrolls within the 85vh cap. -->
229
+ <div class="-mr-1 flex min-h-0 flex-1 flex-col overflow-y-auto pr-1">
222
230
  {#key picked}
223
231
  {#if twoPane}
224
232
  <!-- Two panes: the form on the left, the live preview on the right. Below the breakpoint
@@ -275,9 +283,10 @@ function onSearchKeydown(e) {
275
283
  {@render configureForm(picked)}
276
284
  {/if}
277
285
  {/key}
286
+ </div>
278
287
  {:else}
279
288
  {#if showSearch}
280
- <div class="mb-3 flex items-center gap-2 rounded-field border border-[var(--cairn-card-border)] bg-base-100 px-3 py-2">
289
+ <div class="mb-3 flex flex-none items-center gap-2 rounded-field border border-[var(--cairn-card-border)] bg-base-100 px-3 py-2">
281
290
  <svg class="ec-glyph h-4 w-4 text-[var(--color-muted)]" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M229.7 218.3 179.6 168.2A92.2 92.2 0 1 0 168.2 179.6l50.1 50.1a8 8 0 0 0 11.4-11.4ZM40 112a72 72 0 1 1 72 72 72.1 72.1 0 0 1-72-72Z" /></svg>
282
291
  <input
283
292
  type="search"
@@ -301,8 +310,10 @@ function onSearchKeydown(e) {
301
310
  <button type="button" class="text-[0.8125rem] font-medium text-primary underline [text-underline-offset:2px]" onclick={() => (query = '')}>Clear search</button>
302
311
  </div>
303
312
  {:else}
304
- <!-- One scroll region holds every group, so the arrow keys roam the whole catalog. -->
305
- <div data-cairn-pk-list>
313
+ <!-- One scroll region holds every group, so the arrow keys roam the whole catalog. It
314
+ is the box's scroll container (flex-1, min-h-0): the header above holds while the
315
+ list scrolls within the 85vh cap. -->
316
+ <div data-cairn-pk-list class="-mr-1 min-h-0 flex-1 overflow-y-auto pr-1">
306
317
  {#each groups as group (group.heading)}
307
318
  <div class="mt-3 first:mt-0">
308
319
  {#if group.heading}
@@ -10,8 +10,7 @@ export declare function hasSchema(def: ComponentDef): boolean;
10
10
  export declare function insertableDefs(registry?: ComponentRegistry): ComponentDef[];
11
11
  import type { IconSet } from '../render/glyph.js';
12
12
  import type { ComponentValues } from '../render/registry.js';
13
- import type { ResolvedPreview } from '../content/types.js';
14
- import type { LinkResolve } from '../content/links.js';
13
+ import type { ResolvedPreview, SiteRender } from '../content/types.js';
15
14
  interface Props {
16
15
  /** The site's component registry. */
17
16
  registry?: ComponentRegistry;
@@ -29,10 +28,7 @@ interface Props {
29
28
  * `preview`, the configure step splits to two panes and renders the configured directive
30
29
  * through this into a sandboxed iframe (the same path EditPage's preview uses). Optional: a
31
30
  * host that passes none simply gets no preview pane. */
32
- render?: (md: string, opts?: {
33
- stagger?: boolean;
34
- resolve?: LinkResolve;
35
- }) => string | Promise<string>;
31
+ render?: SiteRender;
36
32
  /** The adapter's resolved preview knob (stylesheets and container class), threaded to
37
33
  * buildPreviewDoc so the preview frame links the site's own CSS, the same as EditPage. */
38
34
  preview?: ResolvedPreview | null;
@@ -120,6 +120,22 @@ const sortButton = `inline-flex items-center gap-1 ${headerLabel} hover:text-bas
120
120
  const publishedAllMessage = $derived(
121
121
  data.publishedAll !== null && data.publishedAll > 0 ? `Published ${data.publishedAll} ${data.publishedAll === 1 ? "entry" : "entries"}.` : ""
122
122
  );
123
+ const lifecycleError = $derived(
124
+ deleteRefused ? `This ${data.label.toLowerCase()} could not be deleted. ${deleteRefused.inboundLinks.length} ${deleteRefused.inboundLinks.length === 1 ? "page links" : "pages link"} to it.` : data.formError ?? data.error ?? ""
125
+ );
126
+ let announceNonce = $state(0);
127
+ function nonce() {
128
+ return announceNonce % 2 === 0 ? "" : "​";
129
+ }
130
+ let lastSubmit;
131
+ $effect(() => {
132
+ const submit = form ?? data;
133
+ if (submit !== lastSubmit) {
134
+ lastSubmit = submit;
135
+ if (lifecycleError) announceNonce++;
136
+ }
137
+ });
138
+ const liveError = $derived(lifecycleError ? `${lifecycleError}${nonce()}` : "");
123
139
  </script>
124
140
 
125
141
  <!-- The non-color selected cue for the triage controls (WCAG 1.4.1): a small check glyph that
@@ -145,20 +161,25 @@ const publishedAllMessage = $derived(
145
161
  {#if}-gated role element inserted fresh is announced inconsistently, so the visible alert
146
162
  below keeps its styling without a role and the message is announced once. -->
147
163
  <div class="sr-only" aria-live="polite">{publishedAllMessage}</div>
164
+ <!-- One persistent polite region announces the lifecycle errors, re-announcing a repeat through the
165
+ nonce. The visible alerts below keep their styling and drop the live `role` (a fresh-inserted
166
+ role element announces inconsistently and clobbers a repeat), so the message is announced once. -->
167
+ <div class="sr-only" aria-live="polite">{liveError}</div>
148
168
  {#if publishedAllMessage}
149
169
  <div class="alert alert-success mb-4 text-sm">{publishedAllMessage}</div>
150
170
  {/if}
151
171
  {#if data.formError}
152
- <div role="alert" class="alert alert-error mb-4 text-sm">{data.formError}</div>
172
+ <div class="alert alert-error mb-4 text-sm">{data.formError}</div>
153
173
  {/if}
154
174
  {#if data.error}
155
- <div role="alert" class="alert alert-warning mb-4 text-sm">{data.error}</div>
175
+ <div class="alert alert-warning mb-4 text-sm">{data.error}</div>
156
176
  {/if}
157
177
 
158
178
  {#if deleteRefused}
159
179
  <!-- A `?/delete` was refused: name the blockers up front, matching the editor's refusal banner,
160
- so the author sees why without re-opening a dialog. -->
161
- <div role="alert" aria-label="This {data.label.toLowerCase()} could not be deleted" class="alert alert-error mb-4 flex-col items-start text-sm">
180
+ so the author sees why without re-opening a dialog. The polite region above announces it, so
181
+ the box itself carries no role or label (a bare div with an aria-label gets no accessible name). -->
182
+ <div class="alert alert-error mb-4 flex-col items-start text-sm">
162
183
  <p class="font-medium">This {data.label.toLowerCase()} could not be deleted.</p>
163
184
  <p>{deleteRefused.inboundLinks.length} {deleteRefused.inboundLinks.length === 1 ? 'page links' : 'pages link'} to it. Remove or repoint the {deleteRefused.inboundLinks.length === 1 ? 'link' : 'links'} listed below, then delete again.</p>
164
185
  <ul class="mt-1 w-full">