@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.
- package/CHANGELOG.md +216 -0
- package/dist/ambient.d.ts +2 -0
- package/dist/auth/types.d.ts +7 -0
- package/dist/components/CairnAdmin.svelte.d.ts +2 -7
- package/dist/components/ComponentForm.svelte +44 -27
- package/dist/components/ComponentInsertDialog.svelte +22 -11
- package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
- package/dist/components/ConceptList.svelte +25 -4
- package/dist/components/EditPage.svelte +29 -107
- package/dist/components/EditPage.svelte.d.ts +2 -7
- package/dist/components/EntryPicker.svelte +117 -0
- package/dist/components/EntryPicker.svelte.d.ts +35 -0
- package/dist/components/FieldInput.svelte +218 -0
- package/dist/components/FieldInput.svelte.d.ts +51 -0
- package/dist/components/IconPicker.svelte +2 -2
- package/dist/components/IconPicker.svelte.d.ts +2 -0
- package/dist/components/LinkPicker.svelte +8 -75
- package/dist/components/LinkPicker.svelte.d.ts +4 -5
- package/dist/components/MediaHeroField.svelte +8 -5
- package/dist/components/MediaHeroField.svelte.d.ts +4 -0
- package/dist/components/ObjectGroupField.svelte +54 -0
- package/dist/components/ObjectGroupField.svelte.d.ts +47 -0
- package/dist/components/ReferenceField.svelte +94 -0
- package/dist/components/ReferenceField.svelte.d.ts +27 -0
- package/dist/components/RepeatableField.svelte +221 -0
- package/dist/components/RepeatableField.svelte.d.ts +53 -0
- package/dist/components/cairn-admin.css +179 -2
- package/dist/components/preview-doc.js +5 -1
- package/dist/components/tidy-validate.js +1 -1
- package/dist/content/adapter.js +18 -0
- package/dist/content/advisories.d.ts +2 -2
- package/dist/content/advisories.js +3 -5
- package/dist/content/compose.d.ts +7 -6
- package/dist/content/compose.js +26 -20
- package/dist/content/concepts.d.ts +21 -15
- package/dist/content/concepts.js +55 -32
- package/dist/content/field-rules.d.ts +15 -0
- package/dist/content/field-rules.js +38 -0
- package/dist/content/fields.d.ts +169 -0
- package/dist/content/fields.js +41 -0
- package/dist/content/fieldset.d.ts +107 -0
- package/dist/content/fieldset.js +386 -0
- package/dist/content/frontmatter-region.d.ts +38 -0
- package/dist/content/frontmatter-region.js +75 -0
- package/dist/content/frontmatter.d.ts +35 -2
- package/dist/content/frontmatter.js +232 -11
- package/dist/content/manifest.d.ts +34 -0
- package/dist/content/manifest.js +80 -4
- package/dist/content/media-refs.d.ts +2 -2
- package/dist/content/media-rewrite.js +1 -69
- package/dist/content/reference-index.d.ts +56 -0
- package/dist/content/reference-index.js +95 -0
- package/dist/content/references.d.ts +40 -0
- package/dist/content/references.js +0 -0
- package/dist/content/standard-schema.d.ts +30 -0
- package/dist/content/standard-schema.js +4 -0
- package/dist/content/types.d.ts +127 -178
- package/dist/delivery/data.d.ts +2 -2
- package/dist/delivery/data.js +1 -1
- package/dist/delivery/public-routes.d.ts +10 -5
- package/dist/delivery/public-routes.js +25 -2
- package/dist/delivery/site-descriptors.d.ts +5 -1
- package/dist/delivery/site-descriptors.js +8 -3
- package/dist/delivery/site-indexes.d.ts +2 -2
- package/dist/delivery/site-resolver.d.ts +25 -0
- package/dist/delivery/site-resolver.js +49 -0
- package/dist/doctor/checks-local.js +6 -11
- package/dist/github/backend.d.ts +83 -0
- package/dist/github/backend.js +76 -0
- package/dist/github/credentials.d.ts +11 -5
- package/dist/github/credentials.js +3 -3
- package/dist/github/repo.d.ts +8 -19
- package/dist/github/repo.js +69 -80
- package/dist/github/types.d.ts +1 -1
- package/dist/github/types.js +4 -4
- package/dist/index.d.ts +18 -10
- package/dist/index.js +9 -5
- package/dist/islands/index.d.ts +12 -0
- package/dist/islands/index.js +83 -0
- package/dist/islands/types.d.ts +7 -0
- package/dist/islands/types.js +1 -0
- package/dist/log/events.d.ts +1 -1
- package/dist/media/index.d.ts +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/manifest.d.ts +11 -0
- package/dist/media/manifest.js +13 -0
- package/dist/media/rewrite-plan.d.ts +2 -3
- package/dist/media/rewrite-plan.js +2 -3
- package/dist/media/usage.d.ts +2 -2
- package/dist/media/usage.js +3 -5
- package/dist/nav/site-config.d.ts +0 -6
- package/dist/nav/site-config.js +6 -4
- package/dist/render/component-grammar.js +11 -11
- package/dist/render/component-reference.js +5 -3
- package/dist/render/component-validate.d.ts +4 -1
- package/dist/render/component-validate.js +10 -35
- package/dist/render/highlight.d.ts +9 -0
- package/dist/render/highlight.js +206 -0
- package/dist/render/pipeline.d.ts +0 -6
- package/dist/render/pipeline.js +13 -2
- package/dist/render/registry.d.ts +44 -36
- package/dist/render/registry.js +47 -6
- package/dist/render/rehype-dispatch.d.ts +6 -10
- package/dist/render/rehype-dispatch.js +38 -17
- package/dist/render/remark-directives.js +4 -5
- package/dist/render/sanitize-schema.d.ts +10 -0
- package/dist/render/sanitize-schema.js +30 -1
- package/dist/sveltekit/cairn-admin.d.ts +5 -5
- package/dist/sveltekit/cairn-admin.js +3 -4
- package/dist/sveltekit/content-routes.d.ts +10 -8
- package/dist/sveltekit/content-routes.js +269 -181
- package/dist/sveltekit/guard.js +10 -0
- package/dist/sveltekit/health.d.ts +7 -3
- package/dist/sveltekit/health.js +9 -3
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/nav-routes.d.ts +6 -5
- package/dist/sveltekit/nav-routes.js +22 -20
- package/dist/sveltekit/types.d.ts +2 -0
- package/dist/vite/index.d.ts +3 -3
- package/dist/vite/index.js +17 -8
- package/package.json +17 -2
- package/src/lib/ambient.ts +7 -0
- package/src/lib/auth/types.ts +7 -0
- package/src/lib/components/CairnAdmin.svelte +2 -6
- package/src/lib/components/ComponentForm.svelte +48 -27
- package/src/lib/components/ComponentInsertDialog.svelte +26 -14
- package/src/lib/components/ConceptList.svelte +41 -4
- package/src/lib/components/EditPage.svelte +43 -119
- package/src/lib/components/EntryPicker.svelte +154 -0
- package/src/lib/components/FieldInput.svelte +262 -0
- package/src/lib/components/IconPicker.svelte +4 -2
- package/src/lib/components/LinkPicker.svelte +10 -81
- package/src/lib/components/MediaHeroField.svelte +12 -5
- package/src/lib/components/ObjectGroupField.svelte +97 -0
- package/src/lib/components/ReferenceField.svelte +126 -0
- package/src/lib/components/RepeatableField.svelte +310 -0
- package/src/lib/components/preview-doc.ts +5 -1
- package/src/lib/components/tidy-validate.ts +1 -1
- package/src/lib/content/adapter.ts +21 -0
- package/src/lib/content/advisories.ts +4 -7
- package/src/lib/content/compose.ts +30 -23
- package/src/lib/content/concepts.ts +68 -40
- package/src/lib/content/field-rules.ts +39 -0
- package/src/lib/content/fields.ts +178 -0
- package/src/lib/content/fieldset.ts +470 -0
- package/src/lib/content/frontmatter-region.ts +90 -0
- package/src/lib/content/frontmatter.ts +231 -15
- package/src/lib/content/manifest.ts +101 -4
- package/src/lib/content/media-refs.ts +2 -2
- package/src/lib/content/media-rewrite.ts +7 -80
- package/src/lib/content/reference-index.ts +159 -0
- package/src/lib/content/references.ts +0 -0
- package/src/lib/content/standard-schema.ts +25 -0
- package/src/lib/content/types.ts +128 -195
- package/src/lib/delivery/data.ts +2 -2
- package/src/lib/delivery/public-routes.ts +36 -4
- package/src/lib/delivery/site-descriptors.ts +8 -3
- package/src/lib/delivery/site-indexes.ts +2 -2
- package/src/lib/delivery/site-resolver.ts +64 -0
- package/src/lib/doctor/checks-local.ts +6 -14
- package/src/lib/github/backend.ts +161 -0
- package/src/lib/github/credentials.ts +10 -7
- package/src/lib/github/repo.ts +79 -83
- package/src/lib/github/types.ts +5 -5
- package/src/lib/index.ts +40 -18
- package/src/lib/islands/index.ts +84 -0
- package/src/lib/islands/types.ts +11 -0
- package/src/lib/log/events.ts +1 -0
- package/src/lib/media/index.ts +1 -0
- package/src/lib/media/manifest.ts +14 -0
- package/src/lib/media/rewrite-plan.ts +4 -6
- package/src/lib/media/usage.ts +4 -7
- package/src/lib/nav/site-config.ts +8 -9
- package/src/lib/render/component-grammar.ts +10 -10
- package/src/lib/render/component-reference.ts +4 -3
- package/src/lib/render/component-validate.ts +10 -35
- package/src/lib/render/highlight.ts +259 -0
- package/src/lib/render/pipeline.ts +13 -8
- package/src/lib/render/registry.ts +88 -42
- package/src/lib/render/rehype-dispatch.ts +47 -16
- package/src/lib/render/remark-directives.ts +4 -5
- package/src/lib/render/sanitize-schema.ts +32 -1
- package/src/lib/sveltekit/cairn-admin.ts +8 -9
- package/src/lib/sveltekit/content-routes.ts +330 -221
- package/src/lib/sveltekit/guard.ts +15 -0
- package/src/lib/sveltekit/health.ts +13 -6
- package/src/lib/sveltekit/index.ts +2 -2
- package/src/lib/sveltekit/nav-routes.ts +33 -29
- package/src/lib/sveltekit/types.ts +5 -1
- package/src/lib/vite/index.ts +20 -11
- package/dist/content/schema.d.ts +0 -87
- package/dist/content/schema.js +0 -89
- package/dist/content/validate.d.ts +0 -17
- package/dist/content/validate.js +0 -93
- package/src/lib/content/schema.ts +0 -167
- 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
package/dist/auth/types.d.ts
CHANGED
|
@@ -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 {
|
|
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?:
|
|
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
|
|
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(
|
|
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[
|
|
102
|
-
out[
|
|
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 (
|
|
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[
|
|
137
|
-
aria-describedby={errors[
|
|
138
|
-
checked={asBool(
|
|
139
|
-
onchange={(e) => (working.attributes[
|
|
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[
|
|
150
|
-
aria-describedby={errors[
|
|
151
|
-
value={asString(
|
|
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[
|
|
154
|
-
markTouched(
|
|
169
|
+
working.attributes[name] = e.currentTarget.value;
|
|
170
|
+
markTouched(name);
|
|
155
171
|
}}
|
|
156
|
-
onblur={() => markTouched(
|
|
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(
|
|
184
|
+
value={asString(name)}
|
|
169
185
|
required={field.required ?? false}
|
|
170
|
-
onChange={(
|
|
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[
|
|
180
|
-
aria-describedby={errors[
|
|
181
|
-
value={asString(
|
|
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[
|
|
184
|
-
markTouched(
|
|
200
|
+
working.attributes[name] = e.currentTarget.value;
|
|
201
|
+
markTouched(name);
|
|
185
202
|
}}
|
|
186
|
-
onblur={() => markTouched(
|
|
203
|
+
onblur={() => markTouched(name)}
|
|
187
204
|
/>
|
|
188
205
|
</label>
|
|
189
206
|
{/if}
|
|
190
|
-
{#if errors[
|
|
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
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?:
|
|
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
|
|
172
|
+
<div class="alert alert-error mb-4 text-sm">{data.formError}</div>
|
|
153
173
|
{/if}
|
|
154
174
|
{#if data.error}
|
|
155
|
-
<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
|
-
|
|
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">
|