@glw907/cairn-cms 0.68.0 → 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 +82 -0
- package/dist/ambient.d.ts +2 -0
- package/dist/components/CairnAdmin.svelte.d.ts +2 -7
- package/dist/components/ComponentForm.svelte +44 -27
- package/dist/components/ComponentInsertDialog.svelte +5 -5
- package/dist/components/ComponentInsertDialog.svelte.d.ts +2 -6
- 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 +4 -0
- 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.js +3 -4
- package/dist/content/fields.d.ts +49 -1
- package/dist/content/fields.js +11 -0
- package/dist/content/fieldset.d.ts +31 -10
- package/dist/content/fieldset.js +262 -109
- 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 +2 -5
- package/dist/delivery/public-routes.js +15 -1
- 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 +16 -12
- package/dist/index.js +7 -8
- 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/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/pipeline.d.ts +0 -6
- package/dist/render/pipeline.js +1 -1
- package/dist/render/registry.d.ts +34 -34
- package/dist/render/registry.js +26 -5
- package/dist/render/rehype-dispatch.d.ts +4 -4
- package/dist/render/rehype-dispatch.js +36 -11
- package/dist/render/remark-directives.js +4 -5
- package/dist/render/sanitize-schema.js +1 -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/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 +5 -1
- package/src/lib/ambient.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 +9 -8
- 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 +3 -4
- package/src/lib/content/fields.ts +52 -1
- package/src/lib/content/fieldset.ts +291 -128
- 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 +17 -3
- 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 +38 -23
- package/src/lib/islands/index.ts +84 -0
- package/src/lib/islands/types.ts +11 -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/pipeline.ts +1 -7
- package/src/lib/render/registry.ts +58 -39
- package/src/lib/render/rehype-dispatch.ts +45 -10
- package/src/lib/render/remark-directives.ts +4 -5
- package/src/lib/render/sanitize-schema.ts +1 -1
- package/src/lib/sveltekit/cairn-admin.ts +8 -9
- package/src/lib/sveltekit/content-routes.ts +330 -221
- 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 -85
- package/dist/content/validate.d.ts +0 -17
- package/dist/content/validate.js +0 -93
- package/src/lib/content/schema.ts +0 -163
- package/src/lib/content/validate.ts +0 -90
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,88 @@
|
|
|
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
|
+
|
|
5
87
|
## 0.68.0
|
|
6
88
|
|
|
7
89
|
<!-- release-size: minor -->
|
package/dist/ambient.d.ts
CHANGED
|
@@ -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";
|
|
@@ -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;
|
|
@@ -34,7 +34,7 @@ import ComponentInsertDialog, { insertableDefs, hasSchema } from "./ComponentIns
|
|
|
34
34
|
import LinkPicker from "./LinkPicker.svelte";
|
|
35
35
|
import WebLinkDialog from "./WebLinkDialog.svelte";
|
|
36
36
|
import MediaInsertPopover from "./MediaInsertPopover.svelte";
|
|
37
|
-
import
|
|
37
|
+
import FieldInput from "./FieldInput.svelte";
|
|
38
38
|
import MediaFigureControl from "./MediaFigureControl.svelte";
|
|
39
39
|
import DeleteDialog from "./DeleteDialog.svelte";
|
|
40
40
|
import RenameDialog from "./RenameDialog.svelte";
|
|
@@ -614,6 +614,10 @@ const draftWarning = $derived.by(() => {
|
|
|
614
614
|
const drafts = page.url.searchParams.get("drafts");
|
|
615
615
|
return drafts ? drafts.split(",").filter(Boolean).join(", ") : "";
|
|
616
616
|
});
|
|
617
|
+
const referenceWarning = $derived.by(() => {
|
|
618
|
+
const refs = page.url.searchParams.get("refs");
|
|
619
|
+
return refs ? refs.split(",").filter(Boolean).join(", ") : "";
|
|
620
|
+
});
|
|
617
621
|
const flash = $derived.by(() => {
|
|
618
622
|
if (data.saved && !draftWarning)
|
|
619
623
|
return "Saved. Your site keeps showing the published version until you publish.";
|
|
@@ -622,9 +626,11 @@ const flash = $derived.by(() => {
|
|
|
622
626
|
if (data.renamed) return `The URL is now ${data.slug}.`;
|
|
623
627
|
return "";
|
|
624
628
|
});
|
|
625
|
-
const politeMessage = $derived(
|
|
626
|
-
draftWarning
|
|
627
|
-
)
|
|
629
|
+
const politeMessage = $derived.by(() => {
|
|
630
|
+
if (draftWarning) return `Saved. This page links to unpublished pages: ${draftWarning}.`;
|
|
631
|
+
if (referenceWarning) return `Saved. This page references unpublished entries: ${referenceWarning}.`;
|
|
632
|
+
return flash;
|
|
633
|
+
});
|
|
628
634
|
const assertiveMessage = $derived.by(() => {
|
|
629
635
|
if (data.error) return data.error;
|
|
630
636
|
if (formError) return formError;
|
|
@@ -718,7 +724,7 @@ $effect(() => {
|
|
|
718
724
|
const run = ++previewRun;
|
|
719
725
|
const handle = setTimeout(async () => {
|
|
720
726
|
try {
|
|
721
|
-
const html = await render(md,
|
|
727
|
+
const html = await render({ body: md, concept: data.conceptId, frontmatter: data.frontmatter, resolve, resolveMedia: resolveMediaRef });
|
|
722
728
|
if (run === previewRun) {
|
|
723
729
|
previewHtml = html;
|
|
724
730
|
previewFailed = false;
|
|
@@ -742,7 +748,6 @@ const eyebrowClass = "mb-2 text-[0.6875rem] font-semibold uppercase tracking-[0.
|
|
|
742
748
|
const titleField = $derived(data.fields.find((f) => f.name === "title"));
|
|
743
749
|
const draftField = $derived(data.fields.find((f) => f.type === "boolean" && f.name === "draft"));
|
|
744
750
|
const detailFields = $derived(data.fields.filter((f) => f !== titleField && f !== draftField));
|
|
745
|
-
const DATE_PUBLISH_HINT = "Sets the date for this post. Publishing is a separate step you choose.";
|
|
746
751
|
</script>
|
|
747
752
|
|
|
748
753
|
<!-- The desk controls live in the one header band: AdminLayout renders this snippet through the
|
|
@@ -869,17 +874,6 @@ const DATE_PUBLISH_HINT = "Sets the date for this post. Publishing is a separate
|
|
|
869
874
|
</div>
|
|
870
875
|
{/snippet}
|
|
871
876
|
|
|
872
|
-
<!-- The author-facing hint under a Details field. The id pairs with the input's aria-describedby
|
|
873
|
-
(`<name>-hint`); its uniqueness rests on schema field names being unique within a concept, which
|
|
874
|
-
is also the loop key. So assistive tech announces the sentence without bloating the accessible
|
|
875
|
-
name. Each field branch decides whether and where to render it; this snippet holds the one shape.
|
|
876
|
-
The `fld-hint` class is a styling hook with no rule today; the Tailwind utilities do the work. -->
|
|
877
|
-
{#snippet fieldHint(name: string, text: string)}
|
|
878
|
-
<p id={`${name}-hint`} class="fld-hint mt-1 text-sm text-[var(--color-muted)]">
|
|
879
|
-
{text}
|
|
880
|
-
</p>
|
|
881
|
-
{/snippet}
|
|
882
|
-
|
|
883
877
|
<!-- The whole edit surface remounts when navigation lands on another entry (see the entryKey
|
|
884
878
|
reset above); script-level state and the beforeNavigate registration sit outside the block,
|
|
885
879
|
so only the template rebuilds. -->
|
|
@@ -987,6 +981,11 @@ const DATE_PUBLISH_HINT = "Sets the date for this post. Publishing is a separate
|
|
|
987
981
|
Saved. Note: this page links to unpublished {draftWarning.includes(',') ? 'pages' : 'a page'} ({draftWarning}), which will 404 until published.
|
|
988
982
|
</div>
|
|
989
983
|
{/if}
|
|
984
|
+
{#if referenceWarning}
|
|
985
|
+
<div class="alert alert-warning mb-4 text-sm">
|
|
986
|
+
Saved. Note: this page references {referenceWarning.includes(',') ? 'entries' : 'an entry'} ({referenceWarning}) not yet published, which the build will flag until published.
|
|
987
|
+
</div>
|
|
988
|
+
{/if}
|
|
990
989
|
|
|
991
990
|
<form
|
|
992
991
|
method="POST"
|
|
@@ -1368,96 +1367,19 @@ const DATE_PUBLISH_HINT = "Sets the date for this post. Publishing is a separate
|
|
|
1368
1367
|
the screen-reader grouping but hides visually, the way the mockup carries it once. -->
|
|
1369
1368
|
<legend class="sr-only">Details</legend>
|
|
1370
1369
|
{#each detailFields as field (field.name)}
|
|
1371
|
-
|
|
1372
|
-
{
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
built-in publish-clarity default. So aria-describedby always points at the paragraph. -->
|
|
1385
|
-
<input class="input input-sm" type="date" name={field.name} aria-label={field.label} aria-describedby={`${field.name}-hint`} value={str(data.frontmatter[field.name])} />
|
|
1386
|
-
{@render fieldHint(field.name, field.description || DATE_PUBLISH_HINT)}
|
|
1387
|
-
</label>
|
|
1388
|
-
{:else if field.type === 'boolean'}
|
|
1389
|
-
<div class="flex flex-col gap-1">
|
|
1390
|
-
<label class="label cursor-pointer justify-start gap-2">
|
|
1391
|
-
<input class="checkbox checkbox-sm" type="checkbox" name={field.name} aria-label={field.label} aria-describedby={field.description ? `${field.name}-hint` : undefined} checked={data.frontmatter[field.name] === true} />
|
|
1392
|
-
<span class="text-sm">{field.label}</span>
|
|
1393
|
-
</label>
|
|
1394
|
-
{#if field.description}
|
|
1395
|
-
{@render fieldHint(field.name, field.description)}
|
|
1396
|
-
{/if}
|
|
1397
|
-
</div>
|
|
1398
|
-
{:else if field.type === 'tags'}
|
|
1399
|
-
{@const f = field as TagsField}
|
|
1400
|
-
{@const selected = (data.frontmatter[f.name] ?? []) as string[]}
|
|
1401
|
-
<fieldset class="fieldset" aria-describedby={f.description ? `${f.name}-hint` : undefined}>
|
|
1402
|
-
<legend class="fieldset-legend">{f.label}</legend>
|
|
1403
|
-
{#if f.description}
|
|
1404
|
-
{@render fieldHint(f.name, f.description)}
|
|
1405
|
-
{/if}
|
|
1406
|
-
<div class="flex flex-wrap gap-2">
|
|
1407
|
-
{#each f.options as option (option)}
|
|
1408
|
-
<label class="label cursor-pointer justify-start gap-2">
|
|
1409
|
-
<input
|
|
1410
|
-
class="checkbox checkbox-sm"
|
|
1411
|
-
type="checkbox"
|
|
1412
|
-
name={f.name}
|
|
1413
|
-
value={option}
|
|
1414
|
-
checked={selected.includes(option)}
|
|
1415
|
-
/>
|
|
1416
|
-
<span class="text-sm">{option}</span>
|
|
1417
|
-
</label>
|
|
1418
|
-
{/each}
|
|
1419
|
-
</div>
|
|
1420
|
-
</fieldset>
|
|
1421
|
-
{:else if field.type === 'freetags'}
|
|
1422
|
-
{@const f = field as FreeTagsField}
|
|
1423
|
-
{@const tagValue = ((data.frontmatter[f.name] ?? []) as string[]).join(', ')}
|
|
1424
|
-
<label class="flex flex-col gap-1">
|
|
1425
|
-
<span class="text-sm font-medium">{f.label}</span>
|
|
1426
|
-
<input
|
|
1427
|
-
class="input input-sm"
|
|
1428
|
-
name={f.name}
|
|
1429
|
-
aria-label={f.label}
|
|
1430
|
-
aria-describedby={f.description ? `${f.name}-hint` : undefined}
|
|
1431
|
-
placeholder={f.placeholder}
|
|
1432
|
-
value={tagValue}
|
|
1433
|
-
/>
|
|
1434
|
-
{#if f.description}
|
|
1435
|
-
{@render fieldHint(f.name, f.description)}
|
|
1436
|
-
{/if}
|
|
1437
|
-
</label>
|
|
1438
|
-
{:else if field.type === 'image'}
|
|
1439
|
-
{@const heroValue = data.frontmatter[field.name] as ImageValue | undefined}
|
|
1440
|
-
<MediaHeroField
|
|
1441
|
-
bind:this={heroFieldRefs[field.name]}
|
|
1442
|
-
field={{ name: field.name, label: field.label }}
|
|
1443
|
-
value={heroValue}
|
|
1444
|
-
decorative={heroValue?.decorative ?? false}
|
|
1445
|
-
mediaLibrary={mediaLibrary}
|
|
1446
|
-
conceptId={data.conceptId}
|
|
1447
|
-
id={data.id}
|
|
1448
|
-
onuploaded={(record) => (uploadedRecords = [...uploadedRecords, record])}
|
|
1449
|
-
ondirty={markFieldsDirty}
|
|
1450
|
-
onneedsaltchange={(n) => (heroNeedsAlt = { ...heroNeedsAlt, [field.name]: n })}
|
|
1451
|
-
/>
|
|
1452
|
-
{:else}
|
|
1453
|
-
<label class="flex flex-col gap-1">
|
|
1454
|
-
<span class="text-sm font-medium">{field.label}</span>
|
|
1455
|
-
<input class="input input-sm" name={field.name} aria-label={field.label} aria-describedby={field.description ? `${field.name}-hint` : undefined} value={str(data.frontmatter[field.name])} required={field.required} />
|
|
1456
|
-
{#if field.description}
|
|
1457
|
-
{@render fieldHint(field.name, field.description)}
|
|
1458
|
-
{/if}
|
|
1459
|
-
</label>
|
|
1460
|
-
{/if}
|
|
1370
|
+
<FieldInput
|
|
1371
|
+
{field}
|
|
1372
|
+
frontmatter={data.frontmatter}
|
|
1373
|
+
targets={data.linkTargets}
|
|
1374
|
+
markFieldsDirty={markFieldsDirty}
|
|
1375
|
+
mediaLibrary={mediaLibrary}
|
|
1376
|
+
conceptId={data.conceptId}
|
|
1377
|
+
id={data.id}
|
|
1378
|
+
heroFieldRefs={heroFieldRefs}
|
|
1379
|
+
onuploaded={(record) => (uploadedRecords = [...uploadedRecords, record])}
|
|
1380
|
+
onheroneedsalt={(name, n) => (heroNeedsAlt = { ...heroNeedsAlt, [name]: n })}
|
|
1381
|
+
{icons}
|
|
1382
|
+
/>
|
|
1461
1383
|
{/each}
|
|
1462
1384
|
</fieldset>
|
|
1463
1385
|
{/if}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { ComponentRegistry } from '../render/registry.js';
|
|
2
2
|
import type { IconSet } from '../render/glyph.js';
|
|
3
3
|
import type { ContentFormFailure, EditData } from '../sveltekit/content-routes.js';
|
|
4
|
-
import type {
|
|
5
|
-
import type { MediaResolve } from '../render/resolve-media.js';
|
|
4
|
+
import type { SiteRender } from '../content/types.js';
|
|
6
5
|
interface Props {
|
|
7
6
|
/** The edit load's data, plus the site name for the heading. */
|
|
8
7
|
data: EditData & {
|
|
@@ -11,11 +10,7 @@ interface Props {
|
|
|
11
10
|
/** The site's component registry, for the insert palette. */
|
|
12
11
|
registry?: ComponentRegistry;
|
|
13
12
|
/** The site's design-accurate render pipeline; the preview pane renders its output, which the floored pipeline already sanitized. */
|
|
14
|
-
render?:
|
|
15
|
-
stagger?: boolean;
|
|
16
|
-
resolve?: LinkResolve;
|
|
17
|
-
resolveMedia?: MediaResolve;
|
|
18
|
-
}) => string | Promise<string>;
|
|
13
|
+
render?: SiteRender;
|
|
19
14
|
/** The site's icon set, for the guided form's icon fields. */
|
|
20
15
|
icons?: IconSet;
|
|
21
16
|
/** The last content action's failure: the save guard's broken links, the delete guard's
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The search + concept-grouped target list shared by the editor's "Link to page" control and the
|
|
4
|
+
reference field picker. It lists link targets from the committed manifest, grouped by concept with
|
|
5
|
+
Pages first then Posts then any other concept, each post showing its date and each draft marked, and
|
|
6
|
+
fires choose() with the picked target. It knows nothing about cairn: tokens or the editor cursor; the
|
|
7
|
+
host decides what a chosen target means. An optional conceptFilter narrows the list to one concept,
|
|
8
|
+
and selectedIds marks rows the host already holds. Built on a native <dialog>, following the component
|
|
9
|
+
dialog's a11y conventions.
|
|
10
|
+
-->
|
|
11
|
+
<script lang="ts">let {
|
|
12
|
+
targets,
|
|
13
|
+
choose,
|
|
14
|
+
conceptFilter,
|
|
15
|
+
selectedIds = [],
|
|
16
|
+
trigger = true,
|
|
17
|
+
heading: dialogHeading = "Link to a page",
|
|
18
|
+
searchLabel = "Search pages and posts",
|
|
19
|
+
emptyText = "No pages or posts to link to."
|
|
20
|
+
} = $props();
|
|
21
|
+
let dialog = $state(null);
|
|
22
|
+
let searchInput = $state(null);
|
|
23
|
+
let query = $state("");
|
|
24
|
+
const ORDER = { pages: 0, posts: 1 };
|
|
25
|
+
function rank(concept) {
|
|
26
|
+
return ORDER[concept] ?? 2;
|
|
27
|
+
}
|
|
28
|
+
function heading(concept) {
|
|
29
|
+
if (concept === "pages") return "Pages";
|
|
30
|
+
if (concept === "posts") return "Posts";
|
|
31
|
+
return concept.charAt(0).toUpperCase() + concept.slice(1);
|
|
32
|
+
}
|
|
33
|
+
const groups = $derived.by(() => {
|
|
34
|
+
const q = query.trim().toLowerCase();
|
|
35
|
+
const scoped = conceptFilter ? targets.filter((t) => t.concept === conceptFilter) : targets;
|
|
36
|
+
const matched = q ? scoped.filter((t) => t.title.toLowerCase().includes(q)) : scoped;
|
|
37
|
+
const byConcept = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const t of matched) {
|
|
39
|
+
const list = byConcept.get(t.concept) ?? [];
|
|
40
|
+
list.push(t);
|
|
41
|
+
byConcept.set(t.concept, list);
|
|
42
|
+
}
|
|
43
|
+
return [...byConcept.entries()].map(([concept, items]) => ({ concept, heading: heading(concept), items })).sort((a, b) => rank(a.concept) - rank(b.concept) || a.heading.localeCompare(b.heading));
|
|
44
|
+
});
|
|
45
|
+
function isSelected(target) {
|
|
46
|
+
return selectedIds.includes(target.id);
|
|
47
|
+
}
|
|
48
|
+
export function open() {
|
|
49
|
+
query = "";
|
|
50
|
+
dialog?.showModal();
|
|
51
|
+
queueMicrotask(() => searchInput?.focus());
|
|
52
|
+
}
|
|
53
|
+
function close() {
|
|
54
|
+
dialog?.close();
|
|
55
|
+
}
|
|
56
|
+
function pick(target) {
|
|
57
|
+
if (isSelected(target)) return;
|
|
58
|
+
choose(target);
|
|
59
|
+
close();
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
{#if trigger}
|
|
64
|
+
<button type="button" class="btn btn-sm btn-ghost" aria-haspopup="dialog" aria-label="Link to page" onclick={open}>
|
|
65
|
+
Link to page
|
|
66
|
+
</button>
|
|
67
|
+
{/if}
|
|
68
|
+
|
|
69
|
+
<dialog class="modal" aria-labelledby="cairn-entry-picker-title" bind:this={dialog}>
|
|
70
|
+
<div class="modal-box">
|
|
71
|
+
<div class="mb-3 flex items-center justify-between">
|
|
72
|
+
<h2 id="cairn-entry-picker-title" class="text-base font-semibold">{dialogHeading}</h2>
|
|
73
|
+
<button type="button" class="btn btn-ghost btn-sm" aria-label="Close" onclick={close}>✕</button>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<input
|
|
77
|
+
type="search"
|
|
78
|
+
class="input input-bordered mb-3 w-full"
|
|
79
|
+
placeholder="Search by title"
|
|
80
|
+
aria-label={searchLabel}
|
|
81
|
+
bind:this={searchInput}
|
|
82
|
+
bind:value={query}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
{#if groups.length === 0}
|
|
86
|
+
<p class="text-sm text-[var(--color-muted)]">{emptyText}</p>
|
|
87
|
+
{:else}
|
|
88
|
+
{#each groups as group (group.concept)}
|
|
89
|
+
<h3 class="mt-2 mb-1 text-xs font-semibold tracking-wide text-[var(--color-muted)] uppercase">{group.heading}</h3>
|
|
90
|
+
<ul class="menu w-full">
|
|
91
|
+
{#each group.items as target (`${target.concept}/${target.id}`)}
|
|
92
|
+
<li>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
aria-disabled={isSelected(target)}
|
|
96
|
+
aria-label={isSelected(target) ? `${target.title} (already selected)` : target.title}
|
|
97
|
+
onclick={() => pick(target)}
|
|
98
|
+
>
|
|
99
|
+
<span class="flex flex-col items-start">
|
|
100
|
+
<span class="font-medium">{target.title}</span>
|
|
101
|
+
<span class="text-xs text-[var(--color-muted)]">
|
|
102
|
+
{#if isSelected(target)}<span class="badge badge-ghost badge-sm mr-1">Selected</span>{/if}
|
|
103
|
+
{#if target.draft}<span class="badge badge-ghost badge-sm mr-1">Draft</span>{/if}
|
|
104
|
+
{#if target.date}{target.date}{/if}
|
|
105
|
+
</span>
|
|
106
|
+
</span>
|
|
107
|
+
</button>
|
|
108
|
+
</li>
|
|
109
|
+
{/each}
|
|
110
|
+
</ul>
|
|
111
|
+
{/each}
|
|
112
|
+
{/if}
|
|
113
|
+
</div>
|
|
114
|
+
<form method="dialog" class="modal-backdrop">
|
|
115
|
+
<button tabindex="-1" aria-label="Close">close</button>
|
|
116
|
+
</form>
|
|
117
|
+
</dialog>
|