@glw907/cairn-cms 0.8.0 → 0.10.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/dist/components/ComponentForm.svelte +178 -0
- package/dist/components/ComponentForm.svelte.d.ts +20 -0
- package/dist/components/ComponentForm.svelte.d.ts.map +1 -0
- package/dist/components/ComponentInsertDialog.svelte +92 -0
- package/dist/components/ComponentInsertDialog.svelte.d.ts +20 -0
- package/dist/components/ComponentInsertDialog.svelte.d.ts.map +1 -0
- package/dist/components/EditPage.svelte +9 -8
- package/dist/components/EditPage.svelte.d.ts +4 -3
- package/dist/components/EditPage.svelte.d.ts.map +1 -1
- package/dist/components/EditorToolbar.svelte +61 -0
- package/dist/components/EditorToolbar.svelte.d.ts +15 -0
- package/dist/components/EditorToolbar.svelte.d.ts.map +1 -0
- package/dist/components/IconPicker.svelte +51 -0
- package/dist/components/IconPicker.svelte.d.ts +20 -0
- package/dist/components/IconPicker.svelte.d.ts.map +1 -0
- package/dist/components/MarkdownEditor.svelte +96 -57
- package/dist/components/MarkdownEditor.svelte.d.ts +5 -6
- package/dist/components/MarkdownEditor.svelte.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -1
- package/dist/components/markdown-format.d.ts +13 -0
- package/dist/components/markdown-format.d.ts.map +1 -0
- package/dist/components/markdown-format.js +23 -0
- package/dist/content/compose.d.ts.map +1 -1
- package/dist/content/compose.js +1 -0
- package/dist/content/types.d.ts +5 -0
- package/dist/content/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/render/component-grammar.d.ts +10 -0
- package/dist/render/component-grammar.d.ts.map +1 -0
- package/dist/render/component-grammar.js +140 -0
- package/dist/render/component-insert.d.ts +14 -0
- package/dist/render/component-insert.d.ts.map +1 -0
- package/dist/render/component-insert.js +9 -0
- package/dist/render/component-reference.d.ts +11 -0
- package/dist/render/component-reference.d.ts.map +1 -0
- package/dist/render/component-reference.js +34 -0
- package/dist/render/component-validate.d.ts +10 -0
- package/dist/render/component-validate.d.ts.map +1 -0
- package/dist/render/component-validate.js +30 -0
- package/dist/render/pipeline.d.ts +1 -1
- package/dist/render/pipeline.js +1 -1
- package/dist/render/registry.d.ts +45 -1
- package/dist/render/registry.d.ts.map +1 -1
- package/dist/render/registry.js +13 -0
- package/dist/render/sanitize.js +2 -2
- package/package.json +8 -3
- package/src/lib/components/ComponentForm.svelte +178 -0
- package/src/lib/components/ComponentInsertDialog.svelte +92 -0
- package/src/lib/components/EditPage.svelte +9 -8
- package/src/lib/components/EditorToolbar.svelte +61 -0
- package/src/lib/components/IconPicker.svelte +51 -0
- package/src/lib/components/MarkdownEditor.svelte +96 -57
- package/src/lib/components/index.ts +3 -1
- package/src/lib/components/markdown-format.ts +39 -0
- package/src/lib/content/compose.ts +1 -0
- package/src/lib/content/types.ts +5 -0
- package/src/lib/index.ts +16 -2
- package/src/lib/render/component-grammar.ts +167 -0
- package/src/lib/render/component-insert.ts +15 -0
- package/src/lib/render/component-reference.ts +38 -0
- package/src/lib/render/component-validate.ts +36 -0
- package/src/lib/render/pipeline.ts +1 -1
- package/src/lib/render/registry.ts +61 -1
- package/src/lib/render/sanitize.ts +2 -2
- package/dist/components/ComponentPalette.svelte +0 -50
- package/dist/components/ComponentPalette.svelte.d.ts +0 -16
- package/dist/components/ComponentPalette.svelte.d.ts.map +0 -1
- package/src/lib/components/ComponentPalette.svelte +0 -50
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The schema-driven fill form for one component. It holds the working ComponentValues, seeded from
|
|
4
|
+
emptyValues(def), and renders attribute fields and the title/body and other non-repeatable slots.
|
|
5
|
+
Submit (Task 6) serializes and validates through buildComponentInsert and calls onInsert with the
|
|
6
|
+
markdown. Back returns to the picker. This is not a nested HTML form; Insert calls a callback.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { untrack } from 'svelte';
|
|
10
|
+
import { emptyValues, type ComponentDef } from '../render/registry.js';
|
|
11
|
+
import { buildComponentInsert } from '../render/component-insert.js';
|
|
12
|
+
import type { IconSet } from '../render/glyph.js';
|
|
13
|
+
import IconPicker from './IconPicker.svelte';
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
def: ComponentDef;
|
|
17
|
+
icons?: IconSet;
|
|
18
|
+
/** Called with the serialized markdown when the form validates. */
|
|
19
|
+
onInsert: (markdown: string) => void;
|
|
20
|
+
/** Return to the picker. */
|
|
21
|
+
onBack: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let { def, icons, onInsert, onBack }: Props = $props();
|
|
25
|
+
|
|
26
|
+
// Working values, seeded once from the schema. $state makes the nested records deeply reactive.
|
|
27
|
+
// untrack marks the seed as a deliberate one-time read of the initial def, not a reactive miss.
|
|
28
|
+
let values = $state(untrack(() => emptyValues(def)));
|
|
29
|
+
|
|
30
|
+
const attributes = $derived(def.attributes ?? []);
|
|
31
|
+
// Non-repeatable slots render here; the repeatable list is handled separately.
|
|
32
|
+
const flatSlots = $derived((def.slots ?? []).filter((s) => s.kind !== 'repeatable'));
|
|
33
|
+
const repeatableSlots = $derived((def.slots ?? []).filter((s) => s.kind === 'repeatable'));
|
|
34
|
+
|
|
35
|
+
// The live $state proxy array for a repeatable slot, so push/splice stay reactive.
|
|
36
|
+
function slotItems(name: string): string[] {
|
|
37
|
+
const v = values.slots[name];
|
|
38
|
+
return Array.isArray(v) ? v : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Typed accessors over the unions so explicit value targets stay sound.
|
|
42
|
+
function asString(key: string): string {
|
|
43
|
+
const v = values.attributes[key];
|
|
44
|
+
return typeof v === 'string' ? v : '';
|
|
45
|
+
}
|
|
46
|
+
function asBool(key: string): boolean {
|
|
47
|
+
return values.attributes[key] === true;
|
|
48
|
+
}
|
|
49
|
+
function slotString(name: string): string {
|
|
50
|
+
const v = values.slots[name];
|
|
51
|
+
return typeof v === 'string' ? v : '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Field-keyed validation errors from the last submit, keyed by attribute key or slot name.
|
|
55
|
+
let errors = $state<Record<string, string>>({});
|
|
56
|
+
|
|
57
|
+
// Serialize and validate through the pure helper. On success clear errors and emit the markdown;
|
|
58
|
+
// on failure keep the field-keyed errors so each field can show its message and insert nothing.
|
|
59
|
+
async function submit() {
|
|
60
|
+
const result = await buildComponentInsert(def, values);
|
|
61
|
+
if (result.ok) {
|
|
62
|
+
errors = {};
|
|
63
|
+
onInsert(result.markdown);
|
|
64
|
+
} else {
|
|
65
|
+
errors = result.errors;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<div class="flex flex-col gap-3">
|
|
71
|
+
<div class="flex items-center justify-between">
|
|
72
|
+
<h3 class="text-sm font-semibold">{def.label}</h3>
|
|
73
|
+
<button type="button" class="btn btn-ghost btn-xs" onclick={onBack}>Back</button>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{#each attributes as field (field.key)}
|
|
77
|
+
{#if field.type === 'boolean'}
|
|
78
|
+
<label class="label cursor-pointer justify-start gap-2">
|
|
79
|
+
<input
|
|
80
|
+
class="checkbox checkbox-sm"
|
|
81
|
+
type="checkbox"
|
|
82
|
+
aria-label={field.label}
|
|
83
|
+
aria-invalid={Boolean(errors[field.key])}
|
|
84
|
+
aria-describedby={errors[field.key] ? `err-${field.key}` : undefined}
|
|
85
|
+
checked={asBool(field.key)}
|
|
86
|
+
onchange={(e) => (values.attributes[field.key] = e.currentTarget.checked)}
|
|
87
|
+
/>
|
|
88
|
+
<span class="text-sm">{field.label}</span>
|
|
89
|
+
</label>
|
|
90
|
+
{:else if field.type === 'select'}
|
|
91
|
+
<label class="flex flex-col gap-1">
|
|
92
|
+
<span class="text-sm font-medium">{field.label}</span>
|
|
93
|
+
<select
|
|
94
|
+
class="select"
|
|
95
|
+
aria-label={field.label}
|
|
96
|
+
aria-invalid={Boolean(errors[field.key])}
|
|
97
|
+
aria-describedby={errors[field.key] ? `err-${field.key}` : undefined}
|
|
98
|
+
value={asString(field.key)}
|
|
99
|
+
onchange={(e) => (values.attributes[field.key] = e.currentTarget.value)}
|
|
100
|
+
>
|
|
101
|
+
{#if !field.required}<option value="">—</option>{/if}
|
|
102
|
+
{#each field.options ?? [] as opt (opt)}<option value={opt}>{opt}</option>{/each}
|
|
103
|
+
</select>
|
|
104
|
+
</label>
|
|
105
|
+
{:else if field.type === 'icon' && icons}
|
|
106
|
+
<div class="flex flex-col gap-1">
|
|
107
|
+
<span class="text-sm font-medium">{field.label}</span>
|
|
108
|
+
<IconPicker
|
|
109
|
+
{icons}
|
|
110
|
+
value={asString(field.key)}
|
|
111
|
+
required={field.required ?? false}
|
|
112
|
+
onChange={(name) => (values.attributes[field.key] = name)}
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
{:else}
|
|
116
|
+
<label class="flex flex-col gap-1">
|
|
117
|
+
<span class="text-sm font-medium">{field.label}</span>
|
|
118
|
+
<input
|
|
119
|
+
class="input"
|
|
120
|
+
aria-label={field.label}
|
|
121
|
+
aria-invalid={Boolean(errors[field.key])}
|
|
122
|
+
aria-describedby={errors[field.key] ? `err-${field.key}` : undefined}
|
|
123
|
+
value={asString(field.key)}
|
|
124
|
+
oninput={(e) => (values.attributes[field.key] = e.currentTarget.value)}
|
|
125
|
+
/>
|
|
126
|
+
</label>
|
|
127
|
+
{/if}
|
|
128
|
+
{#if errors[field.key]}<span id={`err-${field.key}`} role="alert" class="text-error text-xs">{errors[field.key]}</span>{/if}
|
|
129
|
+
{/each}
|
|
130
|
+
|
|
131
|
+
{#each flatSlots as slot (slot.name)}
|
|
132
|
+
{#if slot.kind === 'markdown'}
|
|
133
|
+
<label class="flex flex-col gap-1">
|
|
134
|
+
<span class="text-sm font-medium">{slot.label}</span>
|
|
135
|
+
<textarea
|
|
136
|
+
class="textarea"
|
|
137
|
+
aria-label={slot.label}
|
|
138
|
+
aria-invalid={Boolean(errors[slot.name])}
|
|
139
|
+
aria-describedby={errors[slot.name] ? `err-${slot.name}` : undefined}
|
|
140
|
+
rows={3}
|
|
141
|
+
value={slotString(slot.name)}
|
|
142
|
+
oninput={(e) => (values.slots[slot.name] = e.currentTarget.value)}
|
|
143
|
+
></textarea>
|
|
144
|
+
</label>
|
|
145
|
+
{:else}
|
|
146
|
+
<label class="flex flex-col gap-1">
|
|
147
|
+
<span class="text-sm font-medium">{slot.label}</span>
|
|
148
|
+
<input
|
|
149
|
+
class="input"
|
|
150
|
+
aria-label={slot.label}
|
|
151
|
+
aria-invalid={Boolean(errors[slot.name])}
|
|
152
|
+
aria-describedby={errors[slot.name] ? `err-${slot.name}` : undefined}
|
|
153
|
+
value={slotString(slot.name)}
|
|
154
|
+
oninput={(e) => (values.slots[slot.name] = e.currentTarget.value)}
|
|
155
|
+
/>
|
|
156
|
+
</label>
|
|
157
|
+
{/if}
|
|
158
|
+
{#if errors[slot.name]}<span id={`err-${slot.name}`} role="alert" class="text-error text-xs">{errors[slot.name]}</span>{/if}
|
|
159
|
+
{/each}
|
|
160
|
+
|
|
161
|
+
{#each repeatableSlots as slot (slot.name)}
|
|
162
|
+
{@const items = slotItems(slot.name)}
|
|
163
|
+
<fieldset class="rounded-box border border-base-300 flex flex-col gap-2 p-2">
|
|
164
|
+
<legend class="text-sm font-medium">{slot.label}</legend>
|
|
165
|
+
<!-- Index key is deliberate: items are bare strings with no stable id, so the value bindings and splice/push are index-based by design. -->
|
|
166
|
+
{#each items as _, i (i)}
|
|
167
|
+
<div class="flex items-center gap-2">
|
|
168
|
+
<input class="input input-sm flex-1" aria-label={`${slot.label} item`} bind:value={items[i]} />
|
|
169
|
+
<button type="button" class="btn btn-ghost btn-sm" aria-label={`Remove item ${i + 1}`} onclick={() => items.splice(i, 1)}>✕</button>
|
|
170
|
+
</div>
|
|
171
|
+
{/each}
|
|
172
|
+
<button type="button" class="btn btn-sm self-start" onclick={() => items.push('')}>Add item</button>
|
|
173
|
+
{#if errors[slot.name]}<span id={`err-${slot.name}`} role="alert" class="text-error text-xs">{errors[slot.name]}</span>{/if}
|
|
174
|
+
</fieldset>
|
|
175
|
+
{/each}
|
|
176
|
+
|
|
177
|
+
<button type="button" class="btn btn-primary btn-sm mt-2" onclick={submit}>Insert</button>
|
|
178
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ComponentDef } from '../render/registry.js';
|
|
2
|
+
import type { IconSet } from '../render/glyph.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
def: ComponentDef;
|
|
5
|
+
icons?: IconSet;
|
|
6
|
+
/** Called with the serialized markdown when the form validates. */
|
|
7
|
+
onInsert: (markdown: string) => void;
|
|
8
|
+
/** Return to the picker. */
|
|
9
|
+
onBack: () => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The schema-driven fill form for one component. It holds the working ComponentValues, seeded from
|
|
13
|
+
* emptyValues(def), and renders attribute fields and the title/body and other non-repeatable slots.
|
|
14
|
+
* Submit (Task 6) serializes and validates through buildComponentInsert and calls onInsert with the
|
|
15
|
+
* markdown. Back returns to the picker. This is not a nested HTML form; Insert calls a callback.
|
|
16
|
+
*/
|
|
17
|
+
declare const ComponentForm: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type ComponentForm = ReturnType<typeof ComponentForm>;
|
|
19
|
+
export default ComponentForm;
|
|
20
|
+
//# sourceMappingURL=ComponentForm.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComponentForm.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ComponentForm.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEvE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAIhD,UAAU,KAAK;IACb,GAAG,EAAE,YAAY,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,mEAAmE;IACnE,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,4BAA4B;IAC5B,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AA8HH;;;;;GAKG;AACH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The Insert control and its modal. The picker lists each actionable component with its description and
|
|
4
|
+
intended use. A component with a schema opens the guided ComponentForm; a template-only component
|
|
5
|
+
inserts directly; a component with neither is not listed. Built on a native <dialog> for focus
|
|
6
|
+
trapping and Escape, following the dropdown's a11y conventions used elsewhere in the admin.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import type { ComponentRegistry, ComponentDef } from '../render/registry.js';
|
|
10
|
+
import type { IconSet } from '../render/glyph.js';
|
|
11
|
+
import ComponentForm from './ComponentForm.svelte';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** The site's component registry. */
|
|
15
|
+
registry?: ComponentRegistry;
|
|
16
|
+
/** Insert markdown at the editor cursor. */
|
|
17
|
+
insert: (text: string) => void;
|
|
18
|
+
/** The site's icon set, for icon fields. */
|
|
19
|
+
icons?: IconSet;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { registry, insert, icons }: Props = $props();
|
|
23
|
+
|
|
24
|
+
let dialog = $state<HTMLDialogElement | null>(null);
|
|
25
|
+
let picked = $state<ComponentDef | null>(null);
|
|
26
|
+
|
|
27
|
+
function hasSchema(def: ComponentDef): boolean {
|
|
28
|
+
return (def.attributes?.length ?? 0) > 0 || (def.slots?.length ?? 0) > 0;
|
|
29
|
+
}
|
|
30
|
+
function actionable(def: ComponentDef): boolean {
|
|
31
|
+
return hasSchema(def) || Boolean(def.insertTemplate);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const defs = $derived((registry?.defs ?? []).filter(actionable));
|
|
35
|
+
|
|
36
|
+
function open() {
|
|
37
|
+
picked = null;
|
|
38
|
+
dialog?.showModal();
|
|
39
|
+
}
|
|
40
|
+
function close() {
|
|
41
|
+
picked = null;
|
|
42
|
+
dialog?.close();
|
|
43
|
+
}
|
|
44
|
+
function choose(def: ComponentDef) {
|
|
45
|
+
if (hasSchema(def)) {
|
|
46
|
+
picked = def;
|
|
47
|
+
} else {
|
|
48
|
+
insert(def.insertTemplate ?? '');
|
|
49
|
+
close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function onInsert(markdown: string) {
|
|
53
|
+
insert(markdown);
|
|
54
|
+
close();
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
{#if defs.length > 0}
|
|
59
|
+
<button type="button" class="btn btn-sm btn-ghost" aria-haspopup="dialog" aria-label="Insert component" onclick={open}>Insert</button>
|
|
60
|
+
|
|
61
|
+
<dialog class="modal" aria-labelledby="cairn-insert-dialog-title" bind:this={dialog} onclose={() => (picked = null)}>
|
|
62
|
+
<div class="modal-box">
|
|
63
|
+
<div class="mb-3 flex items-center justify-between">
|
|
64
|
+
<h2 id="cairn-insert-dialog-title" class="text-base font-semibold">Insert component</h2>
|
|
65
|
+
<button type="button" class="btn btn-ghost btn-sm" aria-label="Close" onclick={close}>✕</button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{#if picked}
|
|
69
|
+
{#key picked}
|
|
70
|
+
<ComponentForm def={picked} {icons} {onInsert} onBack={() => (picked = null)} />
|
|
71
|
+
{/key}
|
|
72
|
+
{:else}
|
|
73
|
+
<ul class="menu w-full">
|
|
74
|
+
{#each defs as def (def.name)}
|
|
75
|
+
<li>
|
|
76
|
+
<button type="button" onclick={() => choose(def)}>
|
|
77
|
+
<span class="flex flex-col items-start">
|
|
78
|
+
<span class="font-medium">{def.label}</span>
|
|
79
|
+
{#if def.description}<span class="text-xs text-[var(--color-muted)]">{def.description}</span>{/if}
|
|
80
|
+
{#if def.use}<span class="text-xs text-[var(--color-muted)]">{def.use}</span>{/if}
|
|
81
|
+
</span>
|
|
82
|
+
</button>
|
|
83
|
+
</li>
|
|
84
|
+
{/each}
|
|
85
|
+
</ul>
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
<form method="dialog" class="modal-backdrop">
|
|
89
|
+
<button tabindex="-1" aria-label="Close">close</button>
|
|
90
|
+
</form>
|
|
91
|
+
</dialog>
|
|
92
|
+
{/if}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ComponentRegistry } from '../render/registry.js';
|
|
2
|
+
import type { IconSet } from '../render/glyph.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** The site's component registry. */
|
|
5
|
+
registry?: ComponentRegistry;
|
|
6
|
+
/** Insert markdown at the editor cursor. */
|
|
7
|
+
insert: (text: string) => void;
|
|
8
|
+
/** The site's icon set, for icon fields. */
|
|
9
|
+
icons?: IconSet;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The Insert control and its modal. The picker lists each actionable component with its description and
|
|
13
|
+
* intended use. A component with a schema opens the guided ComponentForm; a template-only component
|
|
14
|
+
* inserts directly; a component with neither is not listed. Built on a native <dialog> for focus
|
|
15
|
+
* trapping and Escape, following the dropdown's a11y conventions used elsewhere in the admin.
|
|
16
|
+
*/
|
|
17
|
+
declare const ComponentInsertDialog: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type ComponentInsertDialog = ReturnType<typeof ComponentInsertDialog>;
|
|
19
|
+
export default ComponentInsertDialog;
|
|
20
|
+
//# sourceMappingURL=ComponentInsertDialog.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComponentInsertDialog.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ComponentInsertDialog.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAgB,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAIhD,UAAU,KAAK;IACb,qCAAqC;IACrC,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,4CAA4C;IAC5C,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,4CAA4C;IAC5C,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAkFH;;;;;GAKG;AACH,QAAA,MAAM,qBAAqB,2CAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
|
-
The differentiated editor: the per-concept frontmatter form (from `data.fields`) beside the
|
|
3
|
+
The differentiated editor: the per-concept frontmatter form (from `data.fields`) beside the
|
|
4
4
|
markdown editor and a live, design-accurate preview. The whole surface is one form posting to the
|
|
5
5
|
`?/save` action; the preview toggle persists per user in localStorage (spec §7.6).
|
|
6
6
|
-->
|
|
7
7
|
<script lang="ts">
|
|
8
8
|
import { untrack } from 'svelte';
|
|
9
9
|
import MarkdownEditor from './MarkdownEditor.svelte';
|
|
10
|
-
import
|
|
10
|
+
import ComponentInsertDialog from './ComponentInsertDialog.svelte';
|
|
11
11
|
import type { ComponentRegistry } from '../render/registry.js';
|
|
12
|
+
import type { IconSet } from '../render/glyph.js';
|
|
12
13
|
import type { EditData } from '../sveltekit/content-routes.js';
|
|
13
14
|
import type { TextareaField, TagsField, FreeTagsField } from '../content/types.js';
|
|
14
15
|
import { sanitizePreviewHtml } from '../render/sanitize.js';
|
|
@@ -18,13 +19,13 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
18
19
|
data: EditData & { siteName: string };
|
|
19
20
|
/** The site's component registry, for the insert palette. */
|
|
20
21
|
registry?: ComponentRegistry;
|
|
21
|
-
/** Carta preview plugins from the adapter, for the design-accurate preview. */
|
|
22
|
-
preview?: unknown[];
|
|
23
22
|
/** The site's design-accurate render pipeline; the preview pane sanitizes its output. */
|
|
24
23
|
render?: (md: string, opts?: { stagger?: boolean }) => string | Promise<string>;
|
|
24
|
+
/** The site's icon set, for the guided form's icon fields. */
|
|
25
|
+
icons?: IconSet;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
let { data, registry,
|
|
28
|
+
let { data, registry, render, icons }: Props = $props();
|
|
28
29
|
|
|
29
30
|
// `body` is local editor state seeded once from the prop; it diverges as the user types.
|
|
30
31
|
// untrack() captures the initial value without subscribing to future prop changes.
|
|
@@ -46,7 +47,7 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
// Render the design-accurate preview as the body changes, debounced, and sanitize before the DOM.
|
|
49
|
-
// The sanitize is the one barrier between editor-authored markdown and the page (
|
|
50
|
+
// The sanitize is the one barrier between editor-authored markdown and the page (the editor is unsanitized).
|
|
50
51
|
// previewRun is a plain counter (not reactive state) used as a latest-wins guard: if a slow earlier
|
|
51
52
|
// async render call resolves after a newer one has started, the stale result is discarded.
|
|
52
53
|
let previewRun = 0;
|
|
@@ -78,7 +79,7 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
78
79
|
<p class="text-xs text-[var(--color-muted)]">{data.label}: {data.id}</p>
|
|
79
80
|
</div>
|
|
80
81
|
<div class="flex items-center gap-2">
|
|
81
|
-
<
|
|
82
|
+
<ComponentInsertDialog {registry} {insert} {icons} />
|
|
82
83
|
<button
|
|
83
84
|
type="button"
|
|
84
85
|
class="btn btn-sm btn-ghost"
|
|
@@ -103,7 +104,7 @@ markdown editor and a live, design-accurate preview. The whole surface is one fo
|
|
|
103
104
|
|
|
104
105
|
<div class="lg:order-1">
|
|
105
106
|
<div class="rounded-box border border-base-300 bg-base-100 overflow-hidden">
|
|
106
|
-
<MarkdownEditor bind:value={body} name="body"
|
|
107
|
+
<MarkdownEditor bind:value={body} name="body" registerInsert={(fn) => (insert = fn)} />
|
|
107
108
|
</div>
|
|
108
109
|
{#if showPreview}
|
|
109
110
|
<section
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentRegistry } from '../render/registry.js';
|
|
2
|
+
import type { IconSet } from '../render/glyph.js';
|
|
2
3
|
import type { EditData } from '../sveltekit/content-routes.js';
|
|
3
4
|
interface Props {
|
|
4
5
|
/** The edit load's data, plus the site name for the heading. */
|
|
@@ -7,15 +8,15 @@ interface Props {
|
|
|
7
8
|
};
|
|
8
9
|
/** The site's component registry, for the insert palette. */
|
|
9
10
|
registry?: ComponentRegistry;
|
|
10
|
-
/** Carta preview plugins from the adapter, for the design-accurate preview. */
|
|
11
|
-
preview?: unknown[];
|
|
12
11
|
/** The site's design-accurate render pipeline; the preview pane sanitizes its output. */
|
|
13
12
|
render?: (md: string, opts?: {
|
|
14
13
|
stagger?: boolean;
|
|
15
14
|
}) => string | Promise<string>;
|
|
15
|
+
/** The site's icon set, for the guided form's icon fields. */
|
|
16
|
+
icons?: IconSet;
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
18
|
-
* The differentiated editor: the per-concept frontmatter form (from `data.fields`) beside the
|
|
19
|
+
* The differentiated editor: the per-concept frontmatter form (from `data.fields`) beside the
|
|
19
20
|
* markdown editor and a live, design-accurate preview. The whole surface is one form posting to the
|
|
20
21
|
* `?/save` action; the preview toggle persists per user in localStorage (spec §7.6).
|
|
21
22
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditPage.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAK7D,UAAU,KAAK;IACb,gEAAgE;IAChE,IAAI,EAAE,QAAQ,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B
|
|
1
|
+
{"version":3,"file":"EditPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditPage.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAK7D,UAAU,KAAK;IACb,gEAAgE;IAChE,IAAI,EAAE,QAAQ,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,yFAAyF;IACzF,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChF,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAsJH;;;;GAIG;AACH,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The editor's formatting toolbar: bold, italic, heading, link, bulleted list, quote, code. Each button
|
|
4
|
+
asks the host to apply a markdown transform to the current selection. Carta supplied this row before;
|
|
5
|
+
cairn owns it now so the edit surface stays swappable. The glyphs are stroke SVG icons in the admin's
|
|
6
|
+
house style (24x24 viewBox, `currentColor`, round caps), so the row matches the rest of the surface.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import type { FormatKind } from './markdown-format.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Apply a markdown transform to the editor's current selection. */
|
|
13
|
+
format: (kind: FormatKind) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { format }: Props = $props();
|
|
17
|
+
|
|
18
|
+
// Each icon is a set of stroke `<path>` d-strings rendered into the shared 24x24 svg below, so the
|
|
19
|
+
// markup stays declarative (no per-icon raw html). Paths follow the house outline style.
|
|
20
|
+
const buttons: { kind: FormatKind; label: string; paths: string[] }[] = [
|
|
21
|
+
{ kind: 'bold', label: 'Bold', paths: ['M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8'] },
|
|
22
|
+
{ kind: 'italic', label: 'Italic', paths: ['M19 4h-9', 'M14 20H5', 'M15 4 9 20'] },
|
|
23
|
+
{ kind: 'heading', label: 'Heading', paths: ['M6 4v16', 'M18 4v16', 'M6 12h12'] },
|
|
24
|
+
{
|
|
25
|
+
kind: 'link',
|
|
26
|
+
label: 'Link',
|
|
27
|
+
paths: [
|
|
28
|
+
'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71',
|
|
29
|
+
'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71',
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{ kind: 'ul', label: 'Bulleted list', paths: ['M8 6h13', 'M8 12h13', 'M8 18h13', 'M3 6h.01', 'M3 12h.01', 'M3 18h.01'] },
|
|
33
|
+
{
|
|
34
|
+
kind: 'quote',
|
|
35
|
+
label: 'Quote',
|
|
36
|
+
paths: [
|
|
37
|
+
'M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1 1 1 0 0 0 1 1 4 4 0 0 0 4-4V5a2 2 0 0 0-2-2z',
|
|
38
|
+
'M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1 1 1 0 0 0 1 1 4 4 0 0 0 4-4V5a2 2 0 0 0-2-2z',
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{ kind: 'code', label: 'Code', paths: ['M16 18l6-6-6-6', 'M8 6l-6 6 6 6'] },
|
|
42
|
+
];
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<div class="border-base-300 bg-base-200 flex gap-1 border-b p-1" role="toolbar" aria-label="Formatting">
|
|
46
|
+
{#each buttons as button (button.kind)}
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
class="btn btn-ghost btn-sm btn-square"
|
|
50
|
+
aria-label={button.label}
|
|
51
|
+
title={button.label}
|
|
52
|
+
onclick={() => format(button.kind)}
|
|
53
|
+
>
|
|
54
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
55
|
+
{#each button.paths as d (d)}
|
|
56
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={d} />
|
|
57
|
+
{/each}
|
|
58
|
+
</svg>
|
|
59
|
+
</button>
|
|
60
|
+
{/each}
|
|
61
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FormatKind } from './markdown-format.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Apply a markdown transform to the editor's current selection. */
|
|
4
|
+
format: (kind: FormatKind) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The editor's formatting toolbar: bold, italic, heading, link, bulleted list, quote, code. Each button
|
|
8
|
+
* asks the host to apply a markdown transform to the current selection. Carta supplied this row before;
|
|
9
|
+
* cairn owns it now so the edit surface stays swappable. The glyphs are stroke SVG icons in the admin's
|
|
10
|
+
* house style (24x24 viewBox, `currentColor`, round caps), so the row matches the rest of the surface.
|
|
11
|
+
*/
|
|
12
|
+
declare const EditorToolbar: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
type EditorToolbar = ReturnType<typeof EditorToolbar>;
|
|
14
|
+
export default EditorToolbar;
|
|
15
|
+
//# sourceMappingURL=EditorToolbar.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EditorToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditorToolbar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGrD,UAAU,KAAK;IACb,oEAAoE;IACpE,MAAM,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;CACpC;AAiDH;;;;;GAKG;AACH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
A visual icon choice over the site's IconSet. Each glyph is a toggle button; the selected one carries
|
|
4
|
+
aria-pressed. When the field is optional, a None button clears the value. The glyph renders inline from
|
|
5
|
+
the IconSet path data, matching the renderer's 256-unit viewBox.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import type { IconSet } from '../render/glyph.js';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/** The site's glyph name to SVG path-data map. */
|
|
12
|
+
icons: IconSet;
|
|
13
|
+
/** The currently selected glyph name, or '' for none. */
|
|
14
|
+
value: string;
|
|
15
|
+
/** When false, a None choice is offered. */
|
|
16
|
+
required: boolean;
|
|
17
|
+
/** Called with the new glyph name (or '' for none). */
|
|
18
|
+
onChange: (name: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let { icons, value, required, onChange }: Props = $props();
|
|
22
|
+
|
|
23
|
+
const names = $derived(Object.keys(icons));
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div class="flex flex-wrap gap-2" role="group" aria-label="Icon">
|
|
27
|
+
{#if !required}
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
class="btn btn-sm"
|
|
31
|
+
class:btn-primary={value === ''}
|
|
32
|
+
aria-pressed={value === ''}
|
|
33
|
+
onclick={() => onChange('')}
|
|
34
|
+
>None</button>
|
|
35
|
+
{/if}
|
|
36
|
+
{#each names as name (name)}
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
class="btn btn-sm gap-1"
|
|
40
|
+
class:btn-primary={value === name}
|
|
41
|
+
aria-pressed={value === name}
|
|
42
|
+
aria-label={name}
|
|
43
|
+
onclick={() => onChange(name)}
|
|
44
|
+
>
|
|
45
|
+
<svg class="ec-glyph" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true" width="16" height="16">
|
|
46
|
+
<path d={icons[name]} />
|
|
47
|
+
</svg>
|
|
48
|
+
<span class="text-xs">{name}</span>
|
|
49
|
+
</button>
|
|
50
|
+
{/each}
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IconSet } from '../render/glyph.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The site's glyph name to SVG path-data map. */
|
|
4
|
+
icons: IconSet;
|
|
5
|
+
/** The currently selected glyph name, or '' for none. */
|
|
6
|
+
value: string;
|
|
7
|
+
/** When false, a None choice is offered. */
|
|
8
|
+
required: boolean;
|
|
9
|
+
/** Called with the new glyph name (or '' for none). */
|
|
10
|
+
onChange: (name: string) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A visual icon choice over the site's IconSet. Each glyph is a toggle button; the selected one carries
|
|
14
|
+
* aria-pressed. When the field is optional, a None button clears the value. The glyph renders inline from
|
|
15
|
+
* the IconSet path data, matching the renderer's 256-unit viewBox.
|
|
16
|
+
*/
|
|
17
|
+
declare const IconPicker: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type IconPicker = ReturnType<typeof IconPicker>;
|
|
19
|
+
export default IconPicker;
|
|
20
|
+
//# sourceMappingURL=IconPicker.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IconPicker.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/IconPicker.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAGhD,UAAU,KAAK;IACb,kDAAkD;IAClD,KAAK,EAAE,OAAO,CAAC;IACf,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,QAAQ,EAAE,OAAO,CAAC;IAClB,uDAAuD;IACvD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AA2BH;;;;GAIG;AACH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|