@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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { LinkTarget } from '../content/manifest.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The site's link targets, from the committed manifest (editLoad ships them). */
|
|
4
|
+
targets: LinkTarget[];
|
|
5
|
+
/** Called with the target the user picked; the host decides what to do with it. */
|
|
6
|
+
choose: (target: LinkTarget) => void;
|
|
7
|
+
/** Narrow the list to a single concept (the reference field's concept). */
|
|
8
|
+
conceptFilter?: string;
|
|
9
|
+
/** Ids the host already holds; matching rows render as already-selected and do not re-fire choose. */
|
|
10
|
+
selectedIds?: string[];
|
|
11
|
+
/** Render the built-in trigger button. False mounts only the dialog, for a host that supplies its
|
|
12
|
+
* own trigger and opens the dialog through the exported open(). */
|
|
13
|
+
trigger?: boolean;
|
|
14
|
+
/** The dialog title. Defaults to the editor link control's wording; a reference field passes a
|
|
15
|
+
* concept-appropriate heading. */
|
|
16
|
+
heading?: string;
|
|
17
|
+
/** The search input's accessible name. Defaults to the link control's wording. */
|
|
18
|
+
searchLabel?: string;
|
|
19
|
+
/** The empty-state text shown when no target matches. Defaults to the link control's wording. */
|
|
20
|
+
emptyText?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The search + concept-grouped target list shared by the editor's "Link to page" control and the
|
|
24
|
+
* reference field picker. It lists link targets from the committed manifest, grouped by concept with
|
|
25
|
+
* Pages first then Posts then any other concept, each post showing its date and each draft marked, and
|
|
26
|
+
* fires choose() with the picked target. It knows nothing about cairn: tokens or the editor cursor; the
|
|
27
|
+
* host decides what a chosen target means. An optional conceptFilter narrows the list to one concept,
|
|
28
|
+
* and selectedIds marks rows the host already holds. Built on a native <dialog>, following the component
|
|
29
|
+
* dialog's a11y conventions.
|
|
30
|
+
*/
|
|
31
|
+
declare const EntryPicker: import("svelte").Component<Props, {
|
|
32
|
+
open: () => void;
|
|
33
|
+
}, "">;
|
|
34
|
+
type EntryPicker = ReturnType<typeof EntryPicker>;
|
|
35
|
+
export default EntryPicker;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The leaf-field dispatcher for the edit page's Details panel. It renders one leaf field (a scalar,
|
|
4
|
+
an image, or a reference) as the matching input, picked by `field.type`.
|
|
5
|
+
|
|
6
|
+
It is name-prefixable so a container row can reuse it one level down. The `name` prop is the form
|
|
7
|
+
input name: it defaults to `field.name` at the top level, and a container caller passes a prefixed
|
|
8
|
+
path (`${parent}.${index}` for an array element, `${parent}.${leafKey}` for an object leaf) so the
|
|
9
|
+
nested decode in `frontmatter.ts` reads the value back from the right slot. Each arm reads its value
|
|
10
|
+
from `frontmatter[field.name]`, so a nested caller passes the row or object slice as `frontmatter`
|
|
11
|
+
and the leaf key as `field.name`, leaving the reads unchanged.
|
|
12
|
+
|
|
13
|
+
An `object` field renders a labeled `ObjectGroupField`, and a non-reference `array` renders a
|
|
14
|
+
`RepeatableField`; both recurse one level back through this dispatcher for their leaves, which the
|
|
15
|
+
one-level nesting cap (the declaration guard) bounds so the recursion terminates. An
|
|
16
|
+
`array(reference)` stays on `ReferenceField`.
|
|
17
|
+
-->
|
|
18
|
+
<script lang="ts">import MediaHeroField from "./MediaHeroField.svelte";
|
|
19
|
+
import ReferenceField from "./ReferenceField.svelte";
|
|
20
|
+
import ObjectGroupField from "./ObjectGroupField.svelte";
|
|
21
|
+
import RepeatableField from "./RepeatableField.svelte";
|
|
22
|
+
import IconPicker from "./IconPicker.svelte";
|
|
23
|
+
import { isClosedMultiselect } from "../content/frontmatter.js";
|
|
24
|
+
let {
|
|
25
|
+
field,
|
|
26
|
+
name = field.name,
|
|
27
|
+
frontmatter,
|
|
28
|
+
targets,
|
|
29
|
+
markFieldsDirty,
|
|
30
|
+
mediaLibrary,
|
|
31
|
+
conceptId,
|
|
32
|
+
id,
|
|
33
|
+
heroFieldRefs,
|
|
34
|
+
onuploaded,
|
|
35
|
+
onheroneedsalt,
|
|
36
|
+
icons
|
|
37
|
+
} = $props();
|
|
38
|
+
function str(v) {
|
|
39
|
+
return v == null ? "" : String(v);
|
|
40
|
+
}
|
|
41
|
+
function inputType(fieldType) {
|
|
42
|
+
switch (fieldType) {
|
|
43
|
+
case "url":
|
|
44
|
+
return "url";
|
|
45
|
+
case "email":
|
|
46
|
+
return "email";
|
|
47
|
+
case "datetime":
|
|
48
|
+
return "datetime-local";
|
|
49
|
+
default:
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const DATE_PUBLISH_HINT = "Sets the date for this post. Publishing is a separate step you choose.";
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
{#snippet fieldHint(hintName: string, text: string)}
|
|
57
|
+
<p id={`${hintName}-hint`} class="fld-hint mt-1 text-sm text-[var(--color-muted)]">
|
|
58
|
+
{text}
|
|
59
|
+
</p>
|
|
60
|
+
{/snippet}
|
|
61
|
+
|
|
62
|
+
{#if field.type === 'textarea'}
|
|
63
|
+
{@const f = field as NamedField & TextareaField}
|
|
64
|
+
<label class="flex flex-col gap-1">
|
|
65
|
+
<span class="text-sm font-medium">{f.label}</span>
|
|
66
|
+
<textarea class="textarea textarea-sm" {name} aria-label={f.label} aria-describedby={f.help ? `${f.name}-hint` : undefined} rows={f.rows ?? 3}>{str(frontmatter[f.name])}</textarea>
|
|
67
|
+
{#if f.help}
|
|
68
|
+
{@render fieldHint(f.name, f.help)}
|
|
69
|
+
{/if}
|
|
70
|
+
</label>
|
|
71
|
+
{:else if field.type === 'number'}
|
|
72
|
+
{@const f = field as NamedField & NumberField}
|
|
73
|
+
<label class="flex flex-col gap-1">
|
|
74
|
+
<span class="text-sm font-medium">{f.label}</span>
|
|
75
|
+
<input
|
|
76
|
+
class="input input-sm"
|
|
77
|
+
type="number"
|
|
78
|
+
{name}
|
|
79
|
+
aria-label={f.label}
|
|
80
|
+
aria-describedby={f.help ? `${f.name}-hint` : undefined}
|
|
81
|
+
min={f.min}
|
|
82
|
+
max={f.max}
|
|
83
|
+
step={f.integer ? 1 : undefined}
|
|
84
|
+
value={str(frontmatter[f.name])}
|
|
85
|
+
required={f.required}
|
|
86
|
+
/>
|
|
87
|
+
{#if f.help}
|
|
88
|
+
{@render fieldHint(f.name, f.help)}
|
|
89
|
+
{/if}
|
|
90
|
+
</label>
|
|
91
|
+
{:else if field.type === 'select'}
|
|
92
|
+
{@const f = field as NamedField & SelectField}
|
|
93
|
+
<label class="flex flex-col gap-1">
|
|
94
|
+
<span class="text-sm font-medium">{f.label}</span>
|
|
95
|
+
<select class="select select-sm" {name} aria-label={f.label} aria-describedby={f.help ? `${f.name}-hint` : undefined} required={f.required}>
|
|
96
|
+
<!-- A leading empty option submits '' (the key is dropped on save); a required select
|
|
97
|
+
leaves it unselected so an unset value fails the required check with a clear message. -->
|
|
98
|
+
<option value="">— none —</option>
|
|
99
|
+
{#each f.options as option (option)}
|
|
100
|
+
<option value={option} selected={str(frontmatter[f.name]) === option}>{option}</option>
|
|
101
|
+
{/each}
|
|
102
|
+
</select>
|
|
103
|
+
{#if f.help}
|
|
104
|
+
{@render fieldHint(f.name, f.help)}
|
|
105
|
+
{/if}
|
|
106
|
+
</label>
|
|
107
|
+
{:else if field.type === 'date'}
|
|
108
|
+
<label class="flex flex-col gap-1">
|
|
109
|
+
<span class="text-sm font-medium">{field.label}</span>
|
|
110
|
+
<!-- A date field always carries a hint: the adapter's help when set, else the
|
|
111
|
+
built-in publish-clarity default. So aria-describedby always points at the paragraph. -->
|
|
112
|
+
<input class="input input-sm" type="date" {name} aria-label={field.label} aria-describedby={`${field.name}-hint`} value={str(frontmatter[field.name])} />
|
|
113
|
+
{@render fieldHint(field.name, field.help || DATE_PUBLISH_HINT)}
|
|
114
|
+
</label>
|
|
115
|
+
{:else if field.type === 'boolean'}
|
|
116
|
+
<div class="flex flex-col gap-1">
|
|
117
|
+
<label class="label cursor-pointer justify-start gap-2">
|
|
118
|
+
<input class="checkbox checkbox-sm" type="checkbox" {name} aria-label={field.label} aria-describedby={field.help ? `${field.name}-hint` : undefined} checked={frontmatter[field.name] === true} />
|
|
119
|
+
<span class="text-sm">{field.label}</span>
|
|
120
|
+
</label>
|
|
121
|
+
{#if field.help}
|
|
122
|
+
{@render fieldHint(field.name, field.help)}
|
|
123
|
+
{/if}
|
|
124
|
+
</div>
|
|
125
|
+
{:else if field.type === 'multiselect' && isClosedMultiselect(field)}
|
|
126
|
+
{@const f = field as NamedField & MultiselectField & { options: readonly string[] }}
|
|
127
|
+
{@const selected = (frontmatter[f.name] ?? []) as string[]}
|
|
128
|
+
<fieldset class="fieldset" aria-describedby={f.help ? `${f.name}-hint` : undefined}>
|
|
129
|
+
<legend class="fieldset-legend">{f.label}</legend>
|
|
130
|
+
{#if f.help}
|
|
131
|
+
{@render fieldHint(f.name, f.help)}
|
|
132
|
+
{/if}
|
|
133
|
+
<div class="flex flex-wrap gap-2">
|
|
134
|
+
{#each f.options as option (option)}
|
|
135
|
+
<label class="label cursor-pointer justify-start gap-2">
|
|
136
|
+
<input
|
|
137
|
+
class="checkbox checkbox-sm"
|
|
138
|
+
type="checkbox"
|
|
139
|
+
{name}
|
|
140
|
+
value={option}
|
|
141
|
+
checked={selected.includes(option)}
|
|
142
|
+
/>
|
|
143
|
+
<span class="text-sm">{option}</span>
|
|
144
|
+
</label>
|
|
145
|
+
{/each}
|
|
146
|
+
</div>
|
|
147
|
+
</fieldset>
|
|
148
|
+
{:else if field.type === 'multiselect'}
|
|
149
|
+
{@const f = field as NamedField & MultiselectField}
|
|
150
|
+
{@const tagValue = ((frontmatter[f.name] ?? []) as string[]).join(', ')}
|
|
151
|
+
<label class="flex flex-col gap-1">
|
|
152
|
+
<span class="text-sm font-medium">{f.label}</span>
|
|
153
|
+
<input
|
|
154
|
+
class="input input-sm"
|
|
155
|
+
{name}
|
|
156
|
+
aria-label={f.label}
|
|
157
|
+
aria-describedby={f.help ? `${f.name}-hint` : undefined}
|
|
158
|
+
placeholder={f.placeholder ?? (f.help ? undefined : 'Separate values with commas')}
|
|
159
|
+
value={tagValue}
|
|
160
|
+
/>
|
|
161
|
+
{#if f.help}
|
|
162
|
+
{@render fieldHint(f.name, f.help)}
|
|
163
|
+
{/if}
|
|
164
|
+
</label>
|
|
165
|
+
{:else if field.type === 'image'}
|
|
166
|
+
{@const heroValue = frontmatter[field.name] as ImageValue | undefined}
|
|
167
|
+
<!-- The binding_property_non_reactive warning this logs is benign: the parent owns the $state
|
|
168
|
+
proxy and mutates it by reference, and the hero-alt focus flow reads the same prefixed key. -->
|
|
169
|
+
<MediaHeroField
|
|
170
|
+
bind:this={heroFieldRefs[name]}
|
|
171
|
+
field={{ name, label: field.label }}
|
|
172
|
+
value={heroValue}
|
|
173
|
+
decorative={heroValue?.decorative ?? false}
|
|
174
|
+
lead={field.type === 'image' && field.seo === true}
|
|
175
|
+
mediaLibrary={mediaLibrary}
|
|
176
|
+
conceptId={conceptId}
|
|
177
|
+
id={id}
|
|
178
|
+
onuploaded={onuploaded}
|
|
179
|
+
ondirty={markFieldsDirty}
|
|
180
|
+
onneedsaltchange={(n) => onheroneedsalt(name, n)}
|
|
181
|
+
/>
|
|
182
|
+
{:else if field.type === 'reference'}
|
|
183
|
+
<ReferenceField {field} value={(frontmatter[field.name] ?? '') as string} {targets} ondirty={markFieldsDirty} />
|
|
184
|
+
{:else if field.type === 'array' && field.item.type === 'reference'}
|
|
185
|
+
<ReferenceField {field} value={(frontmatter[field.name] ?? []) as string[]} {targets} ondirty={markFieldsDirty} />
|
|
186
|
+
{:else if field.type === 'object'}
|
|
187
|
+
<ObjectGroupField {field} {name} frontmatter={(frontmatter[field.name] ?? {}) as Record<string, unknown>} {markFieldsDirty} {mediaLibrary} {conceptId} {id} {heroFieldRefs} {targets} {onuploaded} {onheroneedsalt} {icons} />
|
|
188
|
+
{:else if field.type === 'array' && field.item.type !== 'reference'}
|
|
189
|
+
<RepeatableField {field} {name} rows={(frontmatter[field.name] ?? []) as unknown[]} {markFieldsDirty} {mediaLibrary} {conceptId} {id} {heroFieldRefs} {targets} {onuploaded} {onheroneedsalt} {icons} />
|
|
190
|
+
{:else if field.type === 'icon' && icons}
|
|
191
|
+
<div class="flex flex-col gap-1">
|
|
192
|
+
<span class="text-sm font-medium">{field.label}</span>
|
|
193
|
+
<IconPicker
|
|
194
|
+
{icons}
|
|
195
|
+
label={field.label}
|
|
196
|
+
describedby={field.help ? `${field.name}-hint` : undefined}
|
|
197
|
+
value={typeof frontmatter[field.name] === 'string' ? (frontmatter[field.name] as string) : ''}
|
|
198
|
+
required={field.required ?? false}
|
|
199
|
+
onChange={(glyph) => {
|
|
200
|
+
frontmatter[field.name] = glyph;
|
|
201
|
+
markFieldsDirty();
|
|
202
|
+
}}
|
|
203
|
+
/>
|
|
204
|
+
{#if field.help}
|
|
205
|
+
{@render fieldHint(field.name, field.help)}
|
|
206
|
+
{/if}
|
|
207
|
+
</div>
|
|
208
|
+
{:else}
|
|
209
|
+
<!-- The plain single-line text input arm: url, email, datetime, and the text fallback. They share
|
|
210
|
+
one shape and differ only in the input type inputType() resolves. -->
|
|
211
|
+
<label class="flex flex-col gap-1">
|
|
212
|
+
<span class="text-sm font-medium">{field.label}</span>
|
|
213
|
+
<input class="input input-sm" type={inputType(field.type)} {name} aria-label={field.label} aria-describedby={field.help ? `${field.name}-hint` : undefined} value={str(frontmatter[field.name])} required={field.required} />
|
|
214
|
+
{#if field.help}
|
|
215
|
+
{@render fieldHint(field.name, field.help)}
|
|
216
|
+
{/if}
|
|
217
|
+
</label>
|
|
218
|
+
{/if}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import MediaHeroField from './MediaHeroField.svelte';
|
|
2
|
+
import type { NamedField } from '../content/types.js';
|
|
3
|
+
import type { IconSet } from '../render/glyph.js';
|
|
4
|
+
import type { LinkTarget } from '../content/manifest.js';
|
|
5
|
+
import type { MediaEntry } from '../media/manifest.js';
|
|
6
|
+
import type { MediaLibraryEntry } from '../media/library-entry.js';
|
|
7
|
+
interface Props {
|
|
8
|
+
/** The leaf field to render; its `name` is the frontmatter key the arm reads its value from. */
|
|
9
|
+
field: NamedField;
|
|
10
|
+
/** The form input name. Defaults to `field.name`; a container caller passes a prefixed path. */
|
|
11
|
+
name?: string;
|
|
12
|
+
/** The frontmatter slice this field reads from, keyed by `field.name`. */
|
|
13
|
+
frontmatter: Record<string, unknown>;
|
|
14
|
+
/** The site link targets the reference arm offers. */
|
|
15
|
+
targets: LinkTarget[];
|
|
16
|
+
/** Mark the edit form dirty; the image arm wires it to the hero field's commit. */
|
|
17
|
+
markFieldsDirty: () => void;
|
|
18
|
+
/** The merged committed-plus-uploaded media library, keyed by content hash. */
|
|
19
|
+
mediaLibrary: Record<string, MediaLibraryEntry>;
|
|
20
|
+
/** The concept the entry belongs to (the upload action's route param). */
|
|
21
|
+
conceptId: string;
|
|
22
|
+
/** The entry id (the upload action's route param). */
|
|
23
|
+
id: string;
|
|
24
|
+
/** The host's hero-field refs, keyed by the prefixed `name` so two rows do not collide. */
|
|
25
|
+
heroFieldRefs: Record<string, MediaHeroField>;
|
|
26
|
+
/** Called with the server-owned record on a successful upload, so the host merges it. */
|
|
27
|
+
onuploaded: (record: MediaEntry) => void;
|
|
28
|
+
/** Called when a hero's needs-alt status changes, keyed by the prefixed `name`. */
|
|
29
|
+
onheroneedsalt: (name: string, needsAlt: boolean) => void;
|
|
30
|
+
/** The site's icon set, threaded to the icon arm's picker. Absent when the site ships none. */
|
|
31
|
+
icons?: IconSet;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The leaf-field dispatcher for the edit page's Details panel. It renders one leaf field (a scalar,
|
|
35
|
+
* an image, or a reference) as the matching input, picked by `field.type`.
|
|
36
|
+
*
|
|
37
|
+
* It is name-prefixable so a container row can reuse it one level down. The `name` prop is the form
|
|
38
|
+
* input name: it defaults to `field.name` at the top level, and a container caller passes a prefixed
|
|
39
|
+
* path (`${parent}.${index}` for an array element, `${parent}.${leafKey}` for an object leaf) so the
|
|
40
|
+
* nested decode in `frontmatter.ts` reads the value back from the right slot. Each arm reads its value
|
|
41
|
+
* from `frontmatter[field.name]`, so a nested caller passes the row or object slice as `frontmatter`
|
|
42
|
+
* and the leaf key as `field.name`, leaving the reads unchanged.
|
|
43
|
+
*
|
|
44
|
+
* An `object` field renders a labeled `ObjectGroupField`, and a non-reference `array` renders a
|
|
45
|
+
* `RepeatableField`; both recurse one level back through this dispatcher for their leaves, which the
|
|
46
|
+
* one-level nesting cap (the declaration guard) bounds so the recursion terminates. An
|
|
47
|
+
* `array(reference)` stays on `ReferenceField`.
|
|
48
|
+
*/
|
|
49
|
+
declare const FieldInput: import("svelte").Component<Props, {}, "">;
|
|
50
|
+
type FieldInput = ReturnType<typeof FieldInput>;
|
|
51
|
+
export default FieldInput;
|
|
@@ -7,7 +7,7 @@ keys move the selection, the standard radiogroup keyboard model. The glyph rende
|
|
|
7
7
|
IconSet path data, matching the renderer's 256-unit viewBox.
|
|
8
8
|
-->
|
|
9
9
|
<script lang="ts">import { tick } from "svelte";
|
|
10
|
-
let { icons, value, required, onChange, label = "Icon" } = $props();
|
|
10
|
+
let { icons, value, required, onChange, label = "Icon", describedby } = $props();
|
|
11
11
|
let group;
|
|
12
12
|
const names = $derived(Object.keys(icons));
|
|
13
13
|
const choices = $derived(required ? names : ["", ...names]);
|
|
@@ -32,7 +32,7 @@ function onKeydown(e) {
|
|
|
32
32
|
}
|
|
33
33
|
</script>
|
|
34
34
|
|
|
35
|
-
<div class="flex flex-wrap gap-2" role="radiogroup" aria-label={label} bind:this={group}>
|
|
35
|
+
<div class="flex flex-wrap gap-2" role="radiogroup" aria-label={label} aria-required={required ? 'true' : undefined} aria-describedby={describedby} bind:this={group}>
|
|
36
36
|
{#if !required}
|
|
37
37
|
<button
|
|
38
38
|
type="button"
|
|
@@ -10,6 +10,8 @@ interface Props {
|
|
|
10
10
|
onChange: (name: string) => void;
|
|
11
11
|
/** The group's accessible name, threaded from the field label. Defaults to Icon. */
|
|
12
12
|
label?: string;
|
|
13
|
+
/** The id of a describing element (a field hint), for the radiogroup's `aria-describedby`. */
|
|
14
|
+
describedby?: string;
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* A visual icon choice over the site's IconSet. The choices form a radiogroup; each glyph is a radio
|
|
@@ -1,45 +1,19 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
|
-
The "Link to page" control
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
conventions. The plain-URL link is the toolbar's Web link dialog; this is for an internal target.
|
|
3
|
+
The "Link to page" control. It wraps EntryPicker, the shared search + concept-grouped target list, and
|
|
4
|
+
turns the chosen target into an inline cairn: internal link through the editor's registerInsertLink
|
|
5
|
+
seam. EntryPicker owns the list and search; this component owns the cairn-token meaning and the trigger.
|
|
6
|
+
The plain-URL link is the toolbar's Web link dialog; this is for an internal target.
|
|
8
7
|
-->
|
|
9
8
|
<script lang="ts">import { formatCairnToken } from "../content/links.js";
|
|
9
|
+
import EntryPicker from "./EntryPicker.svelte";
|
|
10
10
|
let { linkTargets, insert, disabled = false, trigger = true } = $props();
|
|
11
|
-
let
|
|
12
|
-
let query = $state("");
|
|
13
|
-
const ORDER = { pages: 0, posts: 1 };
|
|
14
|
-
function rank(concept) {
|
|
15
|
-
return ORDER[concept] ?? 2;
|
|
16
|
-
}
|
|
17
|
-
function heading(concept) {
|
|
18
|
-
if (concept === "pages") return "Pages";
|
|
19
|
-
if (concept === "posts") return "Posts";
|
|
20
|
-
return concept.charAt(0).toUpperCase() + concept.slice(1);
|
|
21
|
-
}
|
|
22
|
-
const groups = $derived.by(() => {
|
|
23
|
-
const q = query.trim().toLowerCase();
|
|
24
|
-
const matched = q ? linkTargets.filter((t) => t.title.toLowerCase().includes(q)) : linkTargets;
|
|
25
|
-
const byConcept = /* @__PURE__ */ new Map();
|
|
26
|
-
for (const t of matched) {
|
|
27
|
-
const list = byConcept.get(t.concept) ?? [];
|
|
28
|
-
list.push(t);
|
|
29
|
-
byConcept.set(t.concept, list);
|
|
30
|
-
}
|
|
31
|
-
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));
|
|
32
|
-
});
|
|
11
|
+
let picker = $state(null);
|
|
33
12
|
export function open() {
|
|
34
|
-
|
|
35
|
-
dialog?.showModal();
|
|
36
|
-
}
|
|
37
|
-
function close() {
|
|
38
|
-
dialog?.close();
|
|
13
|
+
picker?.open();
|
|
39
14
|
}
|
|
40
15
|
function choose(target) {
|
|
41
16
|
insert(formatCairnToken(target), target.title);
|
|
42
|
-
close();
|
|
43
17
|
}
|
|
44
18
|
</script>
|
|
45
19
|
|
|
@@ -49,45 +23,4 @@ function choose(target) {
|
|
|
49
23
|
</button>
|
|
50
24
|
{/if}
|
|
51
25
|
|
|
52
|
-
<
|
|
53
|
-
<div class="modal-box">
|
|
54
|
-
<div class="mb-3 flex items-center justify-between">
|
|
55
|
-
<h2 id="cairn-link-dialog-title" class="text-base font-semibold">Link to a page</h2>
|
|
56
|
-
<button type="button" class="btn btn-ghost btn-sm" aria-label="Close" onclick={close}>✕</button>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<input
|
|
60
|
-
type="search"
|
|
61
|
-
class="input input-bordered mb-3 w-full"
|
|
62
|
-
placeholder="Search by title"
|
|
63
|
-
aria-label="Search pages and posts"
|
|
64
|
-
bind:value={query}
|
|
65
|
-
/>
|
|
66
|
-
|
|
67
|
-
{#if groups.length === 0}
|
|
68
|
-
<p class="text-sm text-[var(--color-muted)]">No pages or posts to link to.</p>
|
|
69
|
-
{:else}
|
|
70
|
-
{#each groups as group (group.concept)}
|
|
71
|
-
<h3 class="mt-2 mb-1 text-xs font-semibold tracking-wide text-[var(--color-muted)] uppercase">{group.heading}</h3>
|
|
72
|
-
<ul class="menu w-full">
|
|
73
|
-
{#each group.items as target (`${target.concept}/${target.id}`)}
|
|
74
|
-
<li>
|
|
75
|
-
<button type="button" onclick={() => choose(target)}>
|
|
76
|
-
<span class="flex flex-col items-start">
|
|
77
|
-
<span class="font-medium">{target.title}</span>
|
|
78
|
-
<span class="text-xs text-[var(--color-muted)]">
|
|
79
|
-
{#if target.draft}<span class="badge badge-ghost badge-sm mr-1">Draft</span>{/if}
|
|
80
|
-
{#if target.date}{target.date}{/if}
|
|
81
|
-
</span>
|
|
82
|
-
</span>
|
|
83
|
-
</button>
|
|
84
|
-
</li>
|
|
85
|
-
{/each}
|
|
86
|
-
</ul>
|
|
87
|
-
{/each}
|
|
88
|
-
{/if}
|
|
89
|
-
</div>
|
|
90
|
-
<form method="dialog" class="modal-backdrop">
|
|
91
|
-
<button tabindex="-1" aria-label="Close">close</button>
|
|
92
|
-
</form>
|
|
93
|
-
</dialog>
|
|
26
|
+
<EntryPicker bind:this={picker} targets={linkTargets} {choose} trigger={false} />
|
|
@@ -11,11 +11,10 @@ interface Props {
|
|
|
11
11
|
trigger?: boolean;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* The "Link to page" control
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* conventions. The plain-URL link is the toolbar's Web link dialog; this is for an internal target.
|
|
14
|
+
* The "Link to page" control. It wraps EntryPicker, the shared search + concept-grouped target list, and
|
|
15
|
+
* turns the chosen target into an inline cairn: internal link through the editor's registerInsertLink
|
|
16
|
+
* seam. EntryPicker owns the list and search; this component owns the cairn-token meaning and the trigger.
|
|
17
|
+
* The plain-URL link is the toolbar's Web link dialog; this is for an internal target.
|
|
19
18
|
*/
|
|
20
19
|
declare const LinkPicker: import("svelte").Component<Props, {
|
|
21
20
|
open: () => void;
|
|
@@ -53,6 +53,7 @@ let {
|
|
|
53
53
|
field,
|
|
54
54
|
value,
|
|
55
55
|
decorative: decorativeInitial = false,
|
|
56
|
+
lead = false,
|
|
56
57
|
mediaLibrary,
|
|
57
58
|
conceptId,
|
|
58
59
|
id,
|
|
@@ -323,13 +324,15 @@ function onDropzoneDragover(e) {
|
|
|
323
324
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /><circle cx="9" cy="9" r="2" /><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21" /></svg>
|
|
324
325
|
</span>
|
|
325
326
|
<span class="flex min-w-0 flex-col gap-px">
|
|
326
|
-
<span class="text-[0.8125rem] font-medium">Add
|
|
327
|
+
<span class="text-[0.8125rem] font-medium">Add {field.label.toLowerCase()}</span>
|
|
327
328
|
<span class="text-[0.6875rem] text-[var(--color-muted)]">Drop an image here, or pick from the library</span>
|
|
328
329
|
</span>
|
|
329
330
|
</button>
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
331
|
+
{#if lead}
|
|
332
|
+
<p class="text-[0.6875rem] leading-snug text-[var(--color-muted)]">
|
|
333
|
+
This image leads the page, and it is the picture shown when the post is shared.
|
|
334
|
+
</p>
|
|
335
|
+
{/if}
|
|
333
336
|
{/if}
|
|
334
337
|
|
|
335
338
|
<!-- The committed value rides four named hidden inputs the save path's decode arm reads. They sit
|
|
@@ -349,7 +352,7 @@ function onDropzoneDragover(e) {
|
|
|
349
352
|
<div class="modal-box max-w-md">
|
|
350
353
|
<div class="mb-3 flex items-center justify-between gap-2">
|
|
351
354
|
<h2 id={titleId} class="text-[0.9375rem] font-semibold">
|
|
352
|
-
{view === 'chooser' ?
|
|
355
|
+
{view === 'chooser' ? `Add ${field.label.toLowerCase()}` : field.label}
|
|
353
356
|
</h2>
|
|
354
357
|
<button type="button" class="btn btn-ghost btn-xs btn-square" aria-label="Close" onclick={closeDialog}>
|
|
355
358
|
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>
|
|
@@ -16,6 +16,10 @@ interface Props {
|
|
|
16
16
|
/** Whether the initial hero is an explicit decorative choice (an empty alt that is not debt).
|
|
17
17
|
* Defaults false; a fresh field with an empty alt reads as needs-alt. */
|
|
18
18
|
decorative?: boolean;
|
|
19
|
+
/** Whether this image leads the page and becomes the social card (a top-level SEO hero). When
|
|
20
|
+
* false (a gallery or nested image item), the empty state drops the hero-only social-card line
|
|
21
|
+
* and the copy reads neutrally. Defaults false. */
|
|
22
|
+
lead?: boolean;
|
|
19
23
|
/** The merged committed-plus-uploaded media library, keyed by content hash. */
|
|
20
24
|
mediaLibrary: Record<string, MediaLibraryEntry>;
|
|
21
25
|
/** The concept the entry belongs to (the upload action's route param). */
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
A labeled group of leaf fields, the editor arm for an `object` container. It renders each leaf one
|
|
4
|
+
level down through `FieldInput`, prefixing the leaf's form name with this group's name
|
|
5
|
+
(`${name}.${leafKey}`) so the nested decode in `frontmatter.ts` reads each value back into the right
|
|
6
|
+
sub-key. The group's value slice (the object, defaulting to `{}`) is passed straight through as the
|
|
7
|
+
leaf dispatcher's `frontmatter`, so a leaf reads `frontmatter[leafKey]` unchanged.
|
|
8
|
+
|
|
9
|
+
The legend uses the object's optional `label`; when the schema omits it (an object inside an array is
|
|
10
|
+
labeled by the array, so it carries no label of its own), a humanized field key stands in. The group
|
|
11
|
+
matches the Details fieldset/legend recipe so it reads as one of the panel's grouped sections.
|
|
12
|
+
-->
|
|
13
|
+
<script lang="ts">import FieldInput from "./FieldInput.svelte";
|
|
14
|
+
let {
|
|
15
|
+
field,
|
|
16
|
+
name,
|
|
17
|
+
frontmatter,
|
|
18
|
+
targets,
|
|
19
|
+
markFieldsDirty,
|
|
20
|
+
mediaLibrary,
|
|
21
|
+
conceptId,
|
|
22
|
+
id,
|
|
23
|
+
heroFieldRefs,
|
|
24
|
+
onuploaded,
|
|
25
|
+
onheroneedsalt,
|
|
26
|
+
icons
|
|
27
|
+
} = $props();
|
|
28
|
+
function humanize(key) {
|
|
29
|
+
const words = key.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
|
|
30
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
31
|
+
}
|
|
32
|
+
const legend = $derived(field.label ?? humanize(field.name));
|
|
33
|
+
const leaves = $derived(Object.entries(field.fields));
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<fieldset class="m-0 flex min-w-0 flex-col gap-3 border-0 p-0">
|
|
37
|
+
<legend class="text-sm font-medium">{legend}</legend>
|
|
38
|
+
{#each leaves as [leafKey, leaf] (leafKey)}
|
|
39
|
+
<FieldInput
|
|
40
|
+
field={{ ...leaf, name: leafKey }}
|
|
41
|
+
name={`${name}.${leafKey}`}
|
|
42
|
+
{frontmatter}
|
|
43
|
+
{targets}
|
|
44
|
+
{markFieldsDirty}
|
|
45
|
+
{mediaLibrary}
|
|
46
|
+
{conceptId}
|
|
47
|
+
{id}
|
|
48
|
+
{heroFieldRefs}
|
|
49
|
+
{onuploaded}
|
|
50
|
+
{onheroneedsalt}
|
|
51
|
+
{icons}
|
|
52
|
+
/>
|
|
53
|
+
{/each}
|
|
54
|
+
</fieldset>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { NamedField } from '../content/types.js';
|
|
2
|
+
import type { ObjectField } from '../content/fields.js';
|
|
3
|
+
import type { LinkTarget } from '../content/manifest.js';
|
|
4
|
+
import type { MediaEntry } from '../media/manifest.js';
|
|
5
|
+
import type { MediaLibraryEntry } from '../media/library-entry.js';
|
|
6
|
+
import type { IconSet } from '../render/glyph.js';
|
|
7
|
+
import type MediaHeroField from './MediaHeroField.svelte';
|
|
8
|
+
interface Props {
|
|
9
|
+
/** The object descriptor to render; its `fields` are the leaves this group holds. */
|
|
10
|
+
field: NamedField & ObjectField;
|
|
11
|
+
/** The form name prefix for this group; each leaf renders at `${name}.${leafKey}`. */
|
|
12
|
+
name: string;
|
|
13
|
+
/** The object value slice this group reads from, keyed by leaf sub-key. */
|
|
14
|
+
frontmatter: Record<string, unknown>;
|
|
15
|
+
/** The site link targets the reference arm offers (threaded through to each leaf). */
|
|
16
|
+
targets: LinkTarget[];
|
|
17
|
+
/** Mark the edit form dirty; threaded to each leaf's media or reference arm. */
|
|
18
|
+
markFieldsDirty: () => void;
|
|
19
|
+
/** The merged committed-plus-uploaded media library, keyed by content hash. */
|
|
20
|
+
mediaLibrary: Record<string, MediaLibraryEntry>;
|
|
21
|
+
/** The concept the entry belongs to (the upload action's route param). */
|
|
22
|
+
conceptId: string;
|
|
23
|
+
/** The entry id (the upload action's route param). */
|
|
24
|
+
id: string;
|
|
25
|
+
/** The host's hero-field refs, keyed by the prefixed `name` so two groups do not collide. */
|
|
26
|
+
heroFieldRefs: Record<string, MediaHeroField>;
|
|
27
|
+
/** Called with the server-owned record on a successful upload, so the host merges it. */
|
|
28
|
+
onuploaded: (record: MediaEntry) => void;
|
|
29
|
+
/** Called when a hero's needs-alt status changes, keyed by the prefixed `name`. */
|
|
30
|
+
onheroneedsalt: (name: string, needsAlt: boolean) => void;
|
|
31
|
+
/** The site's icon set, forwarded to each leaf's icon arm. */
|
|
32
|
+
icons?: IconSet;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A labeled group of leaf fields, the editor arm for an `object` container. It renders each leaf one
|
|
36
|
+
* level down through `FieldInput`, prefixing the leaf's form name with this group's name
|
|
37
|
+
* (`${name}.${leafKey}`) so the nested decode in `frontmatter.ts` reads each value back into the right
|
|
38
|
+
* sub-key. The group's value slice (the object, defaulting to `{}`) is passed straight through as the
|
|
39
|
+
* leaf dispatcher's `frontmatter`, so a leaf reads `frontmatter[leafKey]` unchanged.
|
|
40
|
+
*
|
|
41
|
+
* The legend uses the object's optional `label`; when the schema omits it (an object inside an array is
|
|
42
|
+
* labeled by the array, so it carries no label of its own), a humanized field key stands in. The group
|
|
43
|
+
* matches the Details fieldset/legend recipe so it reads as one of the panel's grouped sections.
|
|
44
|
+
*/
|
|
45
|
+
declare const ObjectGroupField: import("svelte").Component<Props, {}, "">;
|
|
46
|
+
type ObjectGroupField = ReturnType<typeof ObjectGroupField>;
|
|
47
|
+
export default ObjectGroupField;
|