@velastack/cms 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +107 -18
  2. package/dist/backend/index.d.ts +1 -1
  3. package/dist/backend/routes/release.js +2 -2
  4. package/dist/backend/routes/seed.d.ts +6 -0
  5. package/dist/backend/routes/seed.js +96 -0
  6. package/dist/backend/routes/table.js +3 -0
  7. package/dist/backend/store/migrations/0004-release-publisher.d.ts +1 -0
  8. package/dist/backend/store/migrations/0004-release-publisher.js +15 -0
  9. package/dist/backend/store/migrations/index.js +3 -1
  10. package/dist/backend/store/queries.d.ts +9 -3
  11. package/dist/backend/store/queries.js +105 -11
  12. package/dist/cms.js +13 -16
  13. package/dist/components/admin-bar/admin-bar-internal.svelte +4 -4
  14. package/dist/components/admin-bar/admin-bar.css +1 -1
  15. package/dist/components/admin-bar/field-input.svelte +220 -0
  16. package/dist/components/admin-bar/field-input.svelte.d.ts +29 -0
  17. package/dist/components/admin-bar/history-panel.svelte +4 -5
  18. package/dist/components/admin-bar/icons/pencil.svelte +19 -0
  19. package/dist/components/admin-bar/icons/pencil.svelte.d.ts +3 -0
  20. package/dist/components/admin-bar/keyboard-shortcuts-dialog.svelte +1 -1
  21. package/dist/components/admin-bar/locales-panel.svelte +42 -1
  22. package/dist/components/admin-bar/page-config.d.ts +8 -1
  23. package/dist/components/admin-bar/page-config.js +15 -2
  24. package/dist/components/admin-bar/reorder-buttons.svelte +52 -0
  25. package/dist/components/admin-bar/reorder-buttons.svelte.d.ts +10 -0
  26. package/dist/components/admin-bar/seo-panel.svelte +144 -189
  27. package/dist/components/admin-bar/site-options-panel.svelte +115 -0
  28. package/dist/components/admin-bar/site-options-panel.svelte.d.ts +7 -0
  29. package/dist/components/admin-bar/structured-editor-popover.svelte +250 -0
  30. package/dist/components/admin-bar/structured-editor-popover.svelte.d.ts +48 -0
  31. package/dist/components/admin-bar/ui/checkbox/checkbox.svelte +36 -0
  32. package/dist/components/admin-bar/ui/checkbox/checkbox.svelte.d.ts +5 -0
  33. package/dist/components/admin-bar/ui/checkbox/index.d.ts +2 -0
  34. package/dist/components/admin-bar/ui/checkbox/index.js +2 -0
  35. package/dist/components/admin-bar/ui/popover/index.d.ts +5 -0
  36. package/dist/components/admin-bar/ui/popover/index.js +7 -0
  37. package/dist/components/admin-bar/ui/popover/popover-close.svelte +7 -0
  38. package/dist/components/admin-bar/ui/popover/popover-close.svelte.d.ts +4 -0
  39. package/dist/components/admin-bar/ui/popover/popover-content.svelte +35 -0
  40. package/dist/components/admin-bar/ui/popover/popover-content.svelte.d.ts +10 -0
  41. package/dist/components/admin-bar/ui/popover/popover-trigger.svelte +7 -0
  42. package/dist/components/admin-bar/ui/popover/popover-trigger.svelte.d.ts +4 -0
  43. package/dist/components/admin-bar/ui/popover/popover.svelte +7 -0
  44. package/dist/components/admin-bar/ui/popover/popover.svelte.d.ts +3 -0
  45. package/dist/components/admin-bar/ui/sheet/index.d.ts +10 -0
  46. package/dist/components/admin-bar/ui/sheet/index.js +12 -0
  47. package/dist/components/admin-bar/ui/sheet/sheet-close.svelte +7 -0
  48. package/dist/components/admin-bar/ui/sheet/sheet-close.svelte.d.ts +4 -0
  49. package/dist/components/admin-bar/ui/sheet/sheet-content.svelte +64 -0
  50. package/dist/components/admin-bar/ui/sheet/sheet-content.svelte.d.ts +17 -0
  51. package/dist/components/admin-bar/ui/sheet/sheet-description.svelte +17 -0
  52. package/dist/components/admin-bar/ui/sheet/sheet-description.svelte.d.ts +4 -0
  53. package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte +23 -0
  54. package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  55. package/dist/components/admin-bar/ui/sheet/sheet-header.svelte +20 -0
  56. package/dist/components/admin-bar/ui/sheet/sheet-header.svelte.d.ts +5 -0
  57. package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte +20 -0
  58. package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
  59. package/dist/components/admin-bar/ui/sheet/sheet-title.svelte +17 -0
  60. package/dist/components/admin-bar/ui/sheet/sheet-title.svelte.d.ts +4 -0
  61. package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte +7 -0
  62. package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
  63. package/dist/components/admin-bar/ui/sheet/sheet.svelte +7 -0
  64. package/dist/components/admin-bar/ui/sheet/sheet.svelte.d.ts +3 -0
  65. package/dist/components/admin-bar/ui/switch/index.d.ts +2 -0
  66. package/dist/components/admin-bar/ui/switch/index.js +2 -0
  67. package/dist/components/admin-bar/ui/switch/switch.svelte +27 -0
  68. package/dist/components/admin-bar/ui/switch/switch.svelte.d.ts +5 -0
  69. package/dist/components/admin-bar/ui/tabs/index.d.ts +5 -0
  70. package/dist/components/admin-bar/ui/tabs/index.js +7 -0
  71. package/dist/components/admin-bar/ui/tabs/tabs-content.svelte +17 -0
  72. package/dist/components/admin-bar/ui/tabs/tabs-content.svelte.d.ts +4 -0
  73. package/dist/components/admin-bar/ui/tabs/tabs-list.svelte +16 -0
  74. package/dist/components/admin-bar/ui/tabs/tabs-list.svelte.d.ts +4 -0
  75. package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte +20 -0
  76. package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  77. package/dist/components/admin-bar/ui/tabs/tabs.svelte +19 -0
  78. package/dist/components/admin-bar/ui/tabs/tabs.svelte.d.ts +4 -0
  79. package/dist/components/cms/cms-boolean.svelte +9 -18
  80. package/dist/components/cms/cms-boolean.svelte.d.ts +1 -0
  81. package/dist/components/cms/cms-date-time-editable.svelte +3 -3
  82. package/dist/components/cms/cms-date-time.svelte +17 -17
  83. package/dist/components/cms/cms-date-time.svelte.d.ts +2 -1
  84. package/dist/components/cms/cms-image-editable.svelte +13 -4
  85. package/dist/components/cms/cms-image.svelte +37 -30
  86. package/dist/components/cms/cms-image.svelte.d.ts +11 -4
  87. package/dist/components/cms/cms-link.svelte +26 -19
  88. package/dist/components/cms/cms-link.svelte.d.ts +2 -0
  89. package/dist/components/cms/cms-markdown.svelte +15 -16
  90. package/dist/components/cms/cms-markdown.svelte.d.ts +1 -0
  91. package/dist/components/cms/cms-number-editable.svelte +4 -2
  92. package/dist/components/cms/cms-number.svelte +18 -17
  93. package/dist/components/cms/cms-number.svelte.d.ts +2 -1
  94. package/dist/components/cms/cms-repeater.svelte +15 -18
  95. package/dist/components/cms/cms-repeater.svelte.d.ts +1 -0
  96. package/dist/components/cms/cms-rich-text.svelte +17 -22
  97. package/dist/components/cms/cms-rich-text.svelte.d.ts +1 -0
  98. package/dist/components/cms/cms-store.svelte.d.ts +14 -0
  99. package/dist/components/cms/cms-store.svelte.js +39 -2
  100. package/dist/components/cms/cms-text.svelte +23 -30
  101. package/dist/components/cms/cms-text.svelte.d.ts +3 -1
  102. package/dist/components/cms/scope.d.ts +25 -5
  103. package/dist/components/cms/structured-registry.d.ts +10 -0
  104. package/dist/components/cms/structured-registry.js +6 -0
  105. package/dist/components/cms/use-cms-field.svelte.d.ts +54 -0
  106. package/dist/components/cms/use-cms-field.svelte.js +83 -0
  107. package/dist/core/metadata.d.ts +44 -0
  108. package/dist/core/metadata.js +59 -0
  109. package/dist/core/structured.d.ts +108 -0
  110. package/dist/core/structured.js +184 -0
  111. package/dist/core/wire.d.ts +42 -2
  112. package/dist/editor.d.ts +22 -0
  113. package/dist/editor.js +21 -0
  114. package/dist/index.d.ts +12 -4
  115. package/dist/index.js +13 -3
  116. package/dist/server/load-cms.js +1 -0
  117. package/package.json +15 -6
  118. package/plugin/README.md +22 -2
  119. package/plugin/dist/build.d.ts +74 -0
  120. package/plugin/dist/build.js +210 -0
  121. package/plugin/dist/build.js.map +1 -0
  122. package/plugin/dist/index.d.ts +10 -1
  123. package/plugin/dist/index.js +63 -4
  124. package/plugin/dist/index.js.map +1 -1
  125. package/plugin/dist/manifest.d.ts +31 -0
  126. package/plugin/dist/manifest.js +146 -41
  127. package/plugin/dist/manifest.js.map +1 -1
  128. package/plugin/dist/parse-svelte.d.ts +15 -0
  129. package/plugin/dist/parse-svelte.js +26 -16
  130. package/plugin/dist/parse-svelte.js.map +1 -1
  131. package/dist/components/admin-bar/site-settings-panel.svelte +0 -225
  132. package/dist/components/admin-bar/site-settings-panel.svelte.d.ts +0 -7
@@ -0,0 +1,115 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Site Options: the few project-wide values that do not render on the
4
+ * page — the schema.org type, the default share image, which locales are
5
+ * enabled. Anything a visitor sees (hours, contact, navigation, branding)
6
+ * lives in the root layout scope and is edited where it renders.
7
+ *
8
+ * The schema comes from `createCms({ site })`; storage is one
9
+ * non-localised tree per project.
10
+ */
11
+ import { page } from '$app/state';
12
+ import { cmsStore } from '../cms/cms-store.svelte.js';
13
+ import type { CmsPayload, SiteFieldSchema, SiteSchema } from '../cms/scope.js';
14
+ import FieldInput from './field-input.svelte';
15
+ import PanelFooter from './panel-footer.svelte';
16
+ import PanelHeader from './panel-header.svelte';
17
+ import Panel from './panel.svelte';
18
+ import { Button } from './ui/button/index.js';
19
+
20
+ type Props = { onClose: () => void; onSave: () => void | Promise<void> };
21
+ let { onClose, onSave }: Props = $props();
22
+
23
+ const cms = $derived(page.data.cms as CmsPayload | undefined);
24
+ const schema = $derived<SiteSchema>(cms?.site?.schema ?? {});
25
+ const endpoint = $derived(cms?.endpoint ?? '/api/cms');
26
+
27
+ // Flat (path → schema) view for save/reset; sections preserved for layout.
28
+ const sections = $derived(Object.entries(schema));
29
+ const allFieldPaths = $derived.by(() => {
30
+ const out: Array<{ path: string; schema: SiteFieldSchema }> = [];
31
+ for (const [sectionKey, section] of sections) {
32
+ for (const [fieldKey, fieldSchema] of Object.entries(section.fields)) {
33
+ out.push({ path: `${sectionKey}.${fieldKey}`, schema: fieldSchema });
34
+ }
35
+ }
36
+ return out;
37
+ });
38
+
39
+ const persistedValue = (path: string): unknown => cmsStore.getSiteValue(path);
40
+
41
+ // Local form state — typing only updates `local`; nothing reaches
42
+ // `cmsStore.siteDraft` until Save fires (matches seo-panel.svelte).
43
+ let local = $state<Record<string, unknown>>({});
44
+
45
+ $effect(() => {
46
+ const next: Record<string, unknown> = {};
47
+ for (const { path } of allFieldPaths) next[path] = persistedValue(path);
48
+ local = next;
49
+ });
50
+
51
+ const handleSave = async () => {
52
+ for (const { path } of allFieldPaths) {
53
+ const v = local[path];
54
+ if (v !== persistedValue(path)) cmsStore.setSiteValue(path, v);
55
+ }
56
+ await onSave();
57
+ };
58
+ </script>
59
+
60
+ <Panel ariaLabel="Site options" {onClose}>
61
+ <PanelHeader title="Site options" {onClose}>
62
+ {#snippet subtitle()}
63
+ <span class="vela:text-[12px] vela:text-bar-text-secondary">
64
+ settings that don't render on the page
65
+ </span>
66
+ {/snippet}
67
+ </PanelHeader>
68
+
69
+ <form
70
+ class="vela:overflow-y-auto vela:px-4 vela:pb-4 vela:flex vela:flex-col vela:gap-5"
71
+ onsubmit={(e) => {
72
+ e.preventDefault();
73
+ handleSave();
74
+ }}
75
+ >
76
+ {#if sections.length === 0}
77
+ <p class="vela:text-[13px] vela:text-bar-text-tertiary vela:py-2">
78
+ No site options have been declared. Pass a <code>site</code> schema to
79
+ <code>createCms()</code> to add fields here. Text that renders on the page belongs in the root
80
+ layout, edited in place.
81
+ </p>
82
+ {:else}
83
+ {#each sections as [sectionKey, section] (sectionKey)}
84
+ <section class="vela:flex vela:flex-col vela:gap-3">
85
+ <h3
86
+ class="vela:text-[10px] vela:font-semibold vela:uppercase vela:tracking-widest vela:text-bar-text-tertiary vela:m-0"
87
+ >
88
+ {section.label}
89
+ </h3>
90
+
91
+ {#each Object.entries(section.fields) as [fieldKey, fieldSchema] (fieldKey)}
92
+ {@const path = `${sectionKey}.${fieldKey}`}
93
+ <FieldInput
94
+ type={fieldSchema.type}
95
+ label={fieldSchema.label}
96
+ placeholder={fieldSchema.placeholder}
97
+ values={fieldSchema.values ?? []}
98
+ value={local[path]}
99
+ onChange={(v) => (local[path] = v)}
100
+ {endpoint}
101
+ />
102
+ {/each}
103
+ </section>
104
+ {/each}
105
+ {/if}
106
+ </form>
107
+
108
+ <PanelFooter>
109
+ <span>Save to keep your changes</span>
110
+ <div class="vela:flex vela:items-center vela:gap-1.5">
111
+ <Button variant="ghost" size="pill" onclick={onClose}>Cancel</Button>
112
+ <Button size="pill" onclick={handleSave}>Save</Button>
113
+ </div>
114
+ </PanelFooter>
115
+ </Panel>
@@ -0,0 +1,7 @@
1
+ type Props = {
2
+ onClose: () => void;
3
+ onSave: () => void | Promise<void>;
4
+ };
5
+ declare const SiteOptionsPanel: import("svelte").Component<Props, {}, "">;
6
+ type SiteOptionsPanel = ReturnType<typeof SiteOptionsPanel>;
7
+ export default SiteOptionsPanel;
@@ -0,0 +1,250 @@
1
+ <script lang="ts" generics="T extends Record<string, unknown>">
2
+ /**
3
+ * The editor every structured component opens: a popover anchored to the
4
+ * slot (a bottom sheet under 600px, per DESIGN.md §8.2) holding a form
5
+ * over a local draft, committed to the CMS draft on Done.
6
+ *
7
+ * Locales are tabs. The default locale's tab is the component's own form
8
+ * (`editor` snippet) and owns the structure; every other locale's tab
9
+ * lists the translatable strings with the default text as reference and
10
+ * writes only `<name>.$t.<id>.<field>` (see `core/structured.ts`). The
11
+ * popover opens on the locale the editor is previewing.
12
+ */
13
+ import { onMount, type Snippet } from 'svelte';
14
+ import { cmsStore } from '../cms/cms-store.svelte.js';
15
+ import type { CmsField } from '../cms/use-cms-field.svelte.js';
16
+ import {
17
+ stripTranslations,
18
+ translatableFields,
19
+ translationPath,
20
+ type Structured,
21
+ type TranslatableField
22
+ } from '../../core/structured.js';
23
+ import CssRoot from './css-root.svelte';
24
+ import PencilIcon from './icons/pencil.svelte';
25
+ import { Button } from './ui/button/index.js';
26
+ import { Input } from './ui/input/index.js';
27
+ import * as Popover from './ui/popover/index.js';
28
+ import * as Sheet from './ui/sheet/index.js';
29
+ import * as Tabs from './ui/tabs/index.js';
30
+
31
+ type Props = {
32
+ field: CmsField<T>;
33
+ schema: Structured<T>;
34
+ /** Human label for the header, e.g. "Opening hours". */
35
+ label: string;
36
+ open?: boolean;
37
+ /** What the visitor sees; rendered in place with the edit affordance. */
38
+ preview: Snippet;
39
+ /** The default-locale form over a local draft. */
40
+ editor: Snippet<[T, (next: T) => void]>;
41
+ };
42
+ let { field, schema, label, open = $bindable(false), preview, editor }: Props = $props();
43
+
44
+ const locales = $derived(cmsStore.locales);
45
+ const defaultLocale = $derived(cmsStore.defaultLocale);
46
+ const otherLocales = $derived(locales.filter((l) => l !== defaultLocale));
47
+
48
+ // Local drafts: structure for the default locale, strings per other locale.
49
+ let draft = $state<T>({} as T);
50
+ let translations = $state<Record<string, Record<string, string>>>({});
51
+ let tab = $state('');
52
+
53
+ const reset = () => {
54
+ draft = structuredClone($state.snapshot(field.current)) as T;
55
+ translations = {};
56
+ tab = locales.includes(cmsStore.activeLocale) ? cmsStore.activeLocale : defaultLocale;
57
+ };
58
+
59
+ $effect(() => {
60
+ if (open) reset();
61
+ });
62
+
63
+ $effect(() => {
64
+ if (open && tab && tab !== defaultLocale) void cmsStore.loadLocaleOverlay(tab);
65
+ });
66
+
67
+ const update = (next: T) => {
68
+ draft = next;
69
+ };
70
+
71
+ const fields = $derived<TranslatableField[]>(translatableFields(draft, schema));
72
+
73
+ /** Stored translation for a field in `locale`, unless edited in this session. */
74
+ const translationValue = (locale: string, f: TranslatableField): string => {
75
+ const local = translations[locale]?.[`${f.id}.${f.field}`];
76
+ if (local !== undefined) return local;
77
+ const stored = field.getIn(locale, `$t.${f.id}.${f.field}`);
78
+ return typeof stored === 'string' ? stored : '';
79
+ };
80
+ const setTranslation = (locale: string, f: TranslatableField, value: string) => {
81
+ translations = {
82
+ ...translations,
83
+ [locale]: { ...(translations[locale] ?? {}), [`${f.id}.${f.field}`]: value }
84
+ };
85
+ };
86
+
87
+ const done = () => {
88
+ field.set(stripTranslations($state.snapshot(draft)), defaultLocale);
89
+ for (const [locale, strings] of Object.entries(translations)) {
90
+ for (const [key, value] of Object.entries(strings)) {
91
+ const dot = key.indexOf('.');
92
+ const id = key.slice(0, dot);
93
+ const f = key.slice(dot + 1);
94
+ field.setAt(translationPath('', id, f).slice(1), value === '' ? null : value, locale);
95
+ }
96
+ }
97
+ open = false;
98
+ };
99
+
100
+ const clear = () => {
101
+ field.set(null, defaultLocale);
102
+ open = false;
103
+ };
104
+
105
+ // Under 600px the popover becomes a bottom sheet (DESIGN.md §8.2).
106
+ let isMobile = $state(false);
107
+ onMount(() => {
108
+ const mq = window.matchMedia('(max-width: 600px)');
109
+ const sync = () => (isMobile = mq.matches);
110
+ sync();
111
+ mq.addEventListener('change', sync);
112
+ return () => mq.removeEventListener('change', sync);
113
+ });
114
+ </script>
115
+
116
+ {#snippet body()}
117
+ <div class="vela:flex vela:items-center vela:justify-between vela:px-4 vela:pt-3.5 vela:pb-2">
118
+ <div class="vela:flex vela:items-baseline vela:gap-2 vela:min-w-0">
119
+ <h2 class="vela:text-[14px] vela:font-semibold vela:m-0">{label}</h2>
120
+ <span class="vela:font-mono vela:text-[11px] vela:text-bar-text-tertiary vela:truncate">
121
+ {field.name}
122
+ </span>
123
+ </div>
124
+ </div>
125
+
126
+ {#if otherLocales.length > 0}
127
+ <Tabs.Root bind:value={tab} class="vela:px-4">
128
+ <Tabs.List aria-label="Language">
129
+ {#each locales as locale (locale)}
130
+ <Tabs.Trigger value={locale} class="vela:font-mono vela:uppercase">{locale}</Tabs.Trigger>
131
+ {/each}
132
+ </Tabs.List>
133
+ </Tabs.Root>
134
+ {/if}
135
+
136
+ <div
137
+ class="vela:overflow-y-auto vela:max-h-[60vh] vela:px-4 vela:py-3 vela:flex vela:flex-col vela:gap-3"
138
+ >
139
+ {#if tab === defaultLocale || otherLocales.length === 0}
140
+ {@render editor(draft, update)}
141
+ {:else}
142
+ {#if fields.length === 0}
143
+ <p class="vela:text-[12px] vela:text-bar-text-tertiary vela:m-0">
144
+ Nothing to translate yet. Add content in
145
+ <span class="vela:font-mono vela:uppercase">{defaultLocale}</span> first.
146
+ </p>
147
+ {:else}
148
+ <p class="vela:text-[11px] vela:text-bar-text-tertiary vela:m-0">
149
+ Structure is shared with
150
+ <span class="vela:font-mono vela:uppercase">{defaultLocale}</span>; only the text
151
+ translates. Blank fields show the original.
152
+ </p>
153
+ {#each fields as f (`${f.id}.${f.field}`)}
154
+ <label class="vela:flex vela:flex-col vela:gap-1">
155
+ <span class="vela:text-[12px] vela:text-bar-text-secondary vela:truncate">
156
+ {f.source}
157
+ </span>
158
+ <Input
159
+ type="text"
160
+ value={translationValue(tab, f)}
161
+ placeholder={f.source}
162
+ oninput={(e) => setTranslation(tab, f, e.currentTarget.value)}
163
+ />
164
+ </label>
165
+ {/each}
166
+ {/if}
167
+ {/if}
168
+ </div>
169
+
170
+ <div
171
+ class="vela:flex vela:items-center vela:justify-between vela:px-4 vela:py-2.5 vela:border-t vela:border-[var(--cms-bar-divider)] vela:text-[12px] vela:text-bar-text-tertiary"
172
+ >
173
+ <Button variant="ghost" size="pill" onclick={clear}>Clear</Button>
174
+ <div class="vela:flex vela:items-center vela:gap-1.5">
175
+ <Button variant="ghost" size="pill" onclick={() => (open = false)}>Cancel</Button>
176
+ <Button size="pill" onclick={done}>Done</Button>
177
+ </div>
178
+ </div>
179
+ {/snippet}
180
+
181
+ <CssRoot>
182
+ <span class="cms-structured-edit" data-open={open ? '' : undefined} data-cms-name={field.name}>
183
+ <span class="cms-structured-edit__preview">{@render preview()}</span>
184
+ {#if isMobile}
185
+ <Sheet.Root bind:open>
186
+ <Sheet.Trigger class="cms-structured-edit__trigger" aria-label={`Edit ${label}`}>
187
+ <PencilIcon class="vela:size-3" />
188
+ {label}
189
+ </Sheet.Trigger>
190
+ <Sheet.Content side="bottom" class="vela:gap-0" showCloseButton={false}>
191
+ {@render body()}
192
+ </Sheet.Content>
193
+ </Sheet.Root>
194
+ {:else}
195
+ <Popover.Root bind:open>
196
+ <Popover.Trigger class="cms-structured-edit__trigger" aria-label={`Edit ${label}`}>
197
+ <PencilIcon class="vela:size-3" />
198
+ {label}
199
+ </Popover.Trigger>
200
+ <Popover.Content class="vela:w-[420px] vela:max-w-[calc(100vw-2rem)]">
201
+ {@render body()}
202
+ </Popover.Content>
203
+ </Popover.Root>
204
+ {/if}
205
+ </span>
206
+ </CssRoot>
207
+
208
+ <style>
209
+ .cms-structured-edit {
210
+ position: relative;
211
+ display: block;
212
+ border-radius: 0.125rem;
213
+ outline: 1px dashed rgba(127, 127, 127, 0.6);
214
+ outline-offset: 4px;
215
+ }
216
+ .cms-structured-edit:hover,
217
+ .cms-structured-edit:focus-within,
218
+ .cms-structured-edit[data-open] {
219
+ outline: 2px solid var(--cms-accent, #534ab7);
220
+ outline-offset: 4px;
221
+ }
222
+ .cms-structured-edit__preview {
223
+ display: contents;
224
+ }
225
+ :global(.cms-structured-edit__trigger) {
226
+ position: absolute;
227
+ top: -0.5rem;
228
+ right: -0.5rem;
229
+ z-index: 40;
230
+ display: none;
231
+ align-items: center;
232
+ gap: 0.25rem;
233
+ padding: 0.125rem 0.5rem;
234
+ background: var(--cms-bar-bg, #1f1f1f);
235
+ color: var(--cms-bar-text, #f5f5f5);
236
+ border: 1px solid var(--cms-bar-divider, #3a3a3a);
237
+ border-radius: 999px;
238
+ cursor: pointer;
239
+ font: inherit;
240
+ font-size: 0.6875rem;
241
+ font-family: ui-sans-serif, system-ui, sans-serif;
242
+ line-height: 1.4;
243
+ white-space: nowrap;
244
+ }
245
+ .cms-structured-edit:hover :global(.cms-structured-edit__trigger),
246
+ .cms-structured-edit:focus-within :global(.cms-structured-edit__trigger),
247
+ .cms-structured-edit[data-open] :global(.cms-structured-edit__trigger) {
248
+ display: inline-flex;
249
+ }
250
+ </style>
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The editor every structured component opens: a popover anchored to the
3
+ * slot (a bottom sheet under 600px, per DESIGN.md §8.2) holding a form
4
+ * over a local draft, committed to the CMS draft on Done.
5
+ *
6
+ * Locales are tabs. The default locale's tab is the component's own form
7
+ * (`editor` snippet) and owns the structure; every other locale's tab
8
+ * lists the translatable strings with the default text as reference and
9
+ * writes only `<name>.$t.<id>.<field>` (see `core/structured.ts`). The
10
+ * popover opens on the locale the editor is previewing.
11
+ */
12
+ import { type Snippet } from 'svelte';
13
+ import type { CmsField } from '../cms/use-cms-field.svelte.js';
14
+ import { type Structured } from '../../core/structured.js';
15
+ declare function $$render<T extends Record<string, unknown>>(): {
16
+ props: {
17
+ field: CmsField<T>;
18
+ schema: Structured<T>;
19
+ /** Human label for the header, e.g. "Opening hours". */
20
+ label: string;
21
+ open?: boolean;
22
+ /** What the visitor sees; rendered in place with the edit affordance. */
23
+ preview: Snippet;
24
+ /** The default-locale form over a local draft. */
25
+ editor: Snippet<[T, (next: T) => void]>;
26
+ };
27
+ exports: {};
28
+ bindings: "open";
29
+ slots: {};
30
+ events: {};
31
+ };
32
+ declare class __sveltets_Render<T extends Record<string, unknown>> {
33
+ props(): ReturnType<typeof $$render<T>>['props'];
34
+ events(): ReturnType<typeof $$render<T>>['events'];
35
+ slots(): ReturnType<typeof $$render<T>>['slots'];
36
+ bindings(): "open";
37
+ exports(): {};
38
+ }
39
+ interface $$IsomorphicComponent {
40
+ new <T extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
41
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
42
+ } & ReturnType<__sveltets_Render<T>['exports']>;
43
+ <T extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
44
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
45
+ }
46
+ declare const StructuredEditorPopover: $$IsomorphicComponent;
47
+ type StructuredEditorPopover<T extends Record<string, unknown>> = InstanceType<typeof StructuredEditorPopover<T>>;
48
+ export default StructuredEditorPopover;
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import { Checkbox as CheckboxPrimitive } from 'bits-ui';
3
+ import CheckIcon from '../../icons/check.svelte';
4
+ import MinusIcon from '../../icons/minus.svelte';
5
+ import { cn, type WithoutChildrenOrChild } from '../../utils.js';
6
+
7
+ let {
8
+ ref = $bindable(null),
9
+ checked = $bindable(false),
10
+ indeterminate = $bindable(false),
11
+ class: className,
12
+ ...restProps
13
+ }: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
14
+ </script>
15
+
16
+ <CheckboxPrimitive.Root
17
+ bind:ref
18
+ bind:checked
19
+ bind:indeterminate
20
+ data-slot="checkbox"
21
+ class={cn(
22
+ 'vela:peer vela:size-4 vela:shrink-0 vela:rounded-[4px] vela:border vela:border-[var(--cms-bar-divider)] vela:bg-transparent vela:cursor-pointer vela:transition-colors vela:focus-visible:outline-none vela:focus-visible:ring-2 vela:focus-visible:ring-[var(--cms-accent)] vela:disabled:cursor-not-allowed vela:disabled:opacity-50 vela:data-[state=checked]:bg-[var(--cms-accent)] vela:data-[state=checked]:border-[var(--cms-accent)] vela:data-[state=checked]:text-white vela:data-[state=indeterminate]:bg-[var(--cms-accent)] vela:data-[state=indeterminate]:text-white',
23
+ className
24
+ )}
25
+ {...restProps}
26
+ >
27
+ {#snippet children({ checked, indeterminate })}
28
+ <span class="vela:flex vela:size-full vela:items-center vela:justify-center vela:text-current">
29
+ {#if indeterminate}
30
+ <MinusIcon class="vela:size-3" />
31
+ {:else if checked}
32
+ <CheckIcon class="vela:size-3" />
33
+ {/if}
34
+ </span>
35
+ {/snippet}
36
+ </CheckboxPrimitive.Root>
@@ -0,0 +1,5 @@
1
+ import { Checkbox as CheckboxPrimitive } from 'bits-ui';
2
+ import { type WithoutChildrenOrChild } from '../../utils.js';
3
+ declare const Checkbox: import("svelte").Component<WithoutChildrenOrChild<CheckboxPrimitive.RootProps>, {}, "ref" | "checked" | "indeterminate">;
4
+ type Checkbox = ReturnType<typeof Checkbox>;
5
+ export default Checkbox;
@@ -0,0 +1,2 @@
1
+ import Root from './checkbox.svelte';
2
+ export { Root, Root as Checkbox };
@@ -0,0 +1,2 @@
1
+ import Root from './checkbox.svelte';
2
+ export { Root, Root as Checkbox };
@@ -0,0 +1,5 @@
1
+ import Root from './popover.svelte';
2
+ import Content from './popover-content.svelte';
3
+ import Trigger from './popover-trigger.svelte';
4
+ import Close from './popover-close.svelte';
5
+ export { Root, Content, Trigger, Close, Root as Popover, Content as PopoverContent, Trigger as PopoverTrigger, Close as PopoverClose };
@@ -0,0 +1,7 @@
1
+ import Root from './popover.svelte';
2
+ import Content from './popover-content.svelte';
3
+ import Trigger from './popover-trigger.svelte';
4
+ import Close from './popover-close.svelte';
5
+ export { Root, Content, Trigger, Close,
6
+ //
7
+ Root as Popover, Content as PopoverContent, Trigger as PopoverTrigger, Close as PopoverClose };
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ import { Popover as PopoverPrimitive } from 'bits-ui';
3
+
4
+ let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
5
+ </script>
6
+
7
+ <PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />
@@ -0,0 +1,4 @@
1
+ import { Popover as PopoverPrimitive } from 'bits-ui';
2
+ declare const PopoverClose: import("svelte").Component<PopoverPrimitive.CloseProps, {}, "ref">;
3
+ type PopoverClose = ReturnType<typeof PopoverClose>;
4
+ export default PopoverClose;
@@ -0,0 +1,35 @@
1
+ <script lang="ts">
2
+ import { Popover as PopoverPrimitive } from 'bits-ui';
3
+ import { cn, type WithoutChildrenOrChild } from '../../utils.js';
4
+ import type { Snippet } from 'svelte';
5
+
6
+ let {
7
+ ref = $bindable(null),
8
+ class: className,
9
+ sideOffset = 6,
10
+ align = 'start',
11
+ portalProps,
12
+ children,
13
+ ...restProps
14
+ }: WithoutChildrenOrChild<PopoverPrimitive.ContentProps> & {
15
+ portalProps?: PopoverPrimitive.PortalProps;
16
+ children?: Snippet;
17
+ } = $props();
18
+ </script>
19
+
20
+ <!-- Portal targets `.vela-admin-bar` (not body) so the @scope-wrapped utilities apply. -->
21
+ <PopoverPrimitive.Portal to=".vela-admin-bar" {...portalProps}>
22
+ <PopoverPrimitive.Content
23
+ bind:ref
24
+ data-slot="popover-content"
25
+ {sideOffset}
26
+ {align}
27
+ class={cn(
28
+ 'vela:z-[9999] vela:w-80 vela:rounded-xl vela:border vela:border-[var(--cms-bar-divider)] vela:bg-bar-bg vela:text-bar-text vela:shadow-[0_12px_32px_rgba(0,0,0,0.45)] vela:outline-hidden vela:data-[state=open]:animate-in vela:data-[state=closed]:animate-out vela:data-[state=closed]:fade-out-0 vela:data-[state=open]:fade-in-0 vela:data-[state=closed]:zoom-out-95 vela:data-[state=open]:zoom-in-95 vela:duration-150',
29
+ className
30
+ )}
31
+ {...restProps}
32
+ >
33
+ {@render children?.()}
34
+ </PopoverPrimitive.Content>
35
+ </PopoverPrimitive.Portal>
@@ -0,0 +1,10 @@
1
+ import { Popover as PopoverPrimitive } from 'bits-ui';
2
+ import { type WithoutChildrenOrChild } from '../../utils.js';
3
+ import type { Snippet } from 'svelte';
4
+ type $$ComponentProps = WithoutChildrenOrChild<PopoverPrimitive.ContentProps> & {
5
+ portalProps?: PopoverPrimitive.PortalProps;
6
+ children?: Snippet;
7
+ };
8
+ declare const PopoverContent: import("svelte").Component<$$ComponentProps, {}, "ref">;
9
+ type PopoverContent = ReturnType<typeof PopoverContent>;
10
+ export default PopoverContent;
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ import { Popover as PopoverPrimitive } from 'bits-ui';
3
+
4
+ let { ref = $bindable(null), ...restProps }: PopoverPrimitive.TriggerProps = $props();
5
+ </script>
6
+
7
+ <PopoverPrimitive.Trigger bind:ref data-slot="popover-trigger" {...restProps} />
@@ -0,0 +1,4 @@
1
+ import { Popover as PopoverPrimitive } from 'bits-ui';
2
+ declare const PopoverTrigger: import("svelte").Component<PopoverPrimitive.TriggerProps, {}, "ref">;
3
+ type PopoverTrigger = ReturnType<typeof PopoverTrigger>;
4
+ export default PopoverTrigger;
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ import { Popover as PopoverPrimitive } from 'bits-ui';
3
+
4
+ let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
5
+ </script>
6
+
7
+ <PopoverPrimitive.Root bind:open {...restProps} />
@@ -0,0 +1,3 @@
1
+ declare const Popover: import("svelte").Component<import("bits-ui").PopoverRootPropsWithoutHTML, {}, "open">;
2
+ type Popover = ReturnType<typeof Popover>;
3
+ export default Popover;
@@ -0,0 +1,10 @@
1
+ import Root from './sheet.svelte';
2
+ import Close from './sheet-close.svelte';
3
+ import Trigger from './sheet-trigger.svelte';
4
+ import Overlay from './sheet-overlay.svelte';
5
+ import Content from './sheet-content.svelte';
6
+ import Header from './sheet-header.svelte';
7
+ import Footer from './sheet-footer.svelte';
8
+ import Title from './sheet-title.svelte';
9
+ import Description from './sheet-description.svelte';
10
+ export { Root, Close, Trigger, Overlay, Content, Header, Footer, Title, Description, Root as Sheet, Close as SheetClose, Trigger as SheetTrigger, Overlay as SheetOverlay, Content as SheetContent, Header as SheetHeader, Footer as SheetFooter, Title as SheetTitle, Description as SheetDescription };
@@ -0,0 +1,12 @@
1
+ import Root from './sheet.svelte';
2
+ import Close from './sheet-close.svelte';
3
+ import Trigger from './sheet-trigger.svelte';
4
+ import Overlay from './sheet-overlay.svelte';
5
+ import Content from './sheet-content.svelte';
6
+ import Header from './sheet-header.svelte';
7
+ import Footer from './sheet-footer.svelte';
8
+ import Title from './sheet-title.svelte';
9
+ import Description from './sheet-description.svelte';
10
+ export { Root, Close, Trigger, Overlay, Content, Header, Footer, Title, Description,
11
+ //
12
+ Root as Sheet, Close as SheetClose, Trigger as SheetTrigger, Overlay as SheetOverlay, Content as SheetContent, Header as SheetHeader, Footer as SheetFooter, Title as SheetTitle, Description as SheetDescription };
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ import { Dialog as SheetPrimitive } from 'bits-ui';
3
+
4
+ let { ref = $bindable(null), ...restProps }: SheetPrimitive.CloseProps = $props();
5
+ </script>
6
+
7
+ <SheetPrimitive.Close bind:ref data-slot="sheet-close" {...restProps} />
@@ -0,0 +1,4 @@
1
+ import { Dialog as SheetPrimitive } from 'bits-ui';
2
+ declare const SheetClose: import("svelte").Component<SheetPrimitive.TriggerProps, {}, "ref">;
3
+ type SheetClose = ReturnType<typeof SheetClose>;
4
+ export default SheetClose;