@velastack/cms 0.4.0 → 0.5.1
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/README.md +140 -28
- package/dist/backend/index.d.ts +1 -1
- package/dist/backend/routes/seed.d.ts +6 -0
- package/dist/backend/routes/seed.js +96 -0
- package/dist/backend/routes/table.js +3 -0
- package/dist/backend/store/queries.d.ts +6 -1
- package/dist/backend/store/queries.js +89 -0
- package/dist/cms.js +13 -16
- package/dist/components/admin-bar/admin-bar-internal.svelte +4 -4
- package/dist/components/admin-bar/admin-bar.css +1 -1
- package/dist/components/admin-bar/field-input.svelte +239 -0
- package/dist/components/admin-bar/field-input.svelte.d.ts +30 -0
- package/dist/components/admin-bar/fields-form.svelte +218 -0
- package/dist/components/admin-bar/fields-form.svelte.d.ts +16 -0
- package/dist/components/admin-bar/icons/pencil.svelte +19 -0
- package/dist/components/admin-bar/icons/pencil.svelte.d.ts +3 -0
- package/dist/components/admin-bar/keyboard-shortcuts-dialog.svelte +1 -1
- package/dist/components/admin-bar/link-input.svelte +102 -0
- package/dist/components/admin-bar/link-input.svelte.d.ts +9 -0
- package/dist/components/admin-bar/list-field.svelte +93 -0
- package/dist/components/admin-bar/list-field.svelte.d.ts +20 -0
- package/dist/components/admin-bar/locales-panel.svelte +42 -1
- package/dist/components/admin-bar/page-config.d.ts +8 -1
- package/dist/components/admin-bar/page-config.js +15 -2
- package/dist/components/admin-bar/page-options.d.ts +24 -0
- package/dist/components/admin-bar/page-options.js +53 -0
- package/dist/components/admin-bar/reorder-buttons.svelte +52 -0
- package/dist/components/admin-bar/reorder-buttons.svelte.d.ts +10 -0
- package/dist/components/admin-bar/rich-text-input.svelte +92 -0
- package/dist/components/admin-bar/rich-text-input.svelte.d.ts +9 -0
- package/dist/components/admin-bar/seo-panel.svelte +144 -189
- package/dist/components/admin-bar/site-options-panel.svelte +115 -0
- package/dist/components/admin-bar/site-options-panel.svelte.d.ts +7 -0
- package/dist/components/admin-bar/structured-editor-popover.svelte +259 -0
- package/dist/components/admin-bar/structured-editor-popover.svelte.d.ts +48 -0
- package/dist/components/admin-bar/ui/checkbox/checkbox.svelte +36 -0
- package/dist/components/admin-bar/ui/checkbox/checkbox.svelte.d.ts +5 -0
- package/dist/components/admin-bar/ui/checkbox/index.d.ts +2 -0
- package/dist/components/admin-bar/ui/checkbox/index.js +2 -0
- package/dist/components/admin-bar/ui/popover/index.d.ts +5 -0
- package/dist/components/admin-bar/ui/popover/index.js +7 -0
- package/dist/components/admin-bar/ui/popover/popover-close.svelte +7 -0
- package/dist/components/admin-bar/ui/popover/popover-close.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/popover/popover-content.svelte +35 -0
- package/dist/components/admin-bar/ui/popover/popover-content.svelte.d.ts +10 -0
- package/dist/components/admin-bar/ui/popover/popover-trigger.svelte +7 -0
- package/dist/components/admin-bar/ui/popover/popover-trigger.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/popover/popover.svelte +7 -0
- package/dist/components/admin-bar/ui/popover/popover.svelte.d.ts +3 -0
- package/dist/components/admin-bar/ui/sheet/index.d.ts +10 -0
- package/dist/components/admin-bar/ui/sheet/index.js +12 -0
- package/dist/components/admin-bar/ui/sheet/sheet-close.svelte +7 -0
- package/dist/components/admin-bar/ui/sheet/sheet-close.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet-content.svelte +64 -0
- package/dist/components/admin-bar/ui/sheet/sheet-content.svelte.d.ts +17 -0
- package/dist/components/admin-bar/ui/sheet/sheet-description.svelte +17 -0
- package/dist/components/admin-bar/ui/sheet/sheet-description.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte +23 -0
- package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte.d.ts +5 -0
- package/dist/components/admin-bar/ui/sheet/sheet-header.svelte +20 -0
- package/dist/components/admin-bar/ui/sheet/sheet-header.svelte.d.ts +5 -0
- package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte +20 -0
- package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet-title.svelte +17 -0
- package/dist/components/admin-bar/ui/sheet/sheet-title.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte +7 -0
- package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet.svelte +7 -0
- package/dist/components/admin-bar/ui/sheet/sheet.svelte.d.ts +3 -0
- package/dist/components/admin-bar/ui/switch/index.d.ts +2 -0
- package/dist/components/admin-bar/ui/switch/index.js +2 -0
- package/dist/components/admin-bar/ui/switch/switch.svelte +27 -0
- package/dist/components/admin-bar/ui/switch/switch.svelte.d.ts +5 -0
- package/dist/components/admin-bar/ui/tabs/index.d.ts +5 -0
- package/dist/components/admin-bar/ui/tabs/index.js +7 -0
- package/dist/components/admin-bar/ui/tabs/tabs-content.svelte +17 -0
- package/dist/components/admin-bar/ui/tabs/tabs-content.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/tabs/tabs-list.svelte +16 -0
- package/dist/components/admin-bar/ui/tabs/tabs-list.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte +20 -0
- package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/tabs/tabs.svelte +19 -0
- package/dist/components/admin-bar/ui/tabs/tabs.svelte.d.ts +4 -0
- package/dist/components/cms/cms-boolean.svelte +9 -18
- package/dist/components/cms/cms-boolean.svelte.d.ts +1 -0
- package/dist/components/cms/cms-collection.svelte +46 -0
- package/dist/components/cms/cms-collection.svelte.d.ts +25 -0
- package/dist/components/cms/cms-contact.svelte +26 -0
- package/dist/components/cms/cms-contact.svelte.d.ts +14 -0
- package/dist/components/cms/cms-date-time-editable.svelte +3 -3
- package/dist/components/cms/cms-date-time.svelte +17 -17
- package/dist/components/cms/cms-date-time.svelte.d.ts +2 -1
- package/dist/components/cms/cms-faq.svelte +22 -0
- package/dist/components/cms/cms-faq.svelte.d.ts +15 -0
- package/dist/components/cms/cms-gallery.svelte +31 -0
- package/dist/components/cms/cms-gallery.svelte.d.ts +16 -0
- package/dist/components/cms/cms-hours-editable.svelte +160 -0
- package/dist/components/cms/cms-hours-editable.svelte.d.ts +17 -0
- package/dist/components/cms/cms-hours.svelte +42 -0
- package/dist/components/cms/cms-hours.svelte.d.ts +19 -0
- package/dist/components/cms/cms-image-editable.svelte +13 -4
- package/dist/components/cms/cms-image.svelte +19 -34
- package/dist/components/cms/cms-image.svelte.d.ts +7 -8
- package/dist/components/cms/cms-link-editable.svelte +4 -60
- package/dist/components/cms/cms-link.svelte +15 -28
- package/dist/components/cms/cms-link.svelte.d.ts +4 -9
- package/dist/components/cms/cms-logos.svelte +29 -0
- package/dist/components/cms/cms-logos.svelte.d.ts +16 -0
- package/dist/components/cms/cms-markdown.svelte +15 -16
- package/dist/components/cms/cms-markdown.svelte.d.ts +1 -0
- package/dist/components/cms/cms-nav.svelte +22 -0
- package/dist/components/cms/cms-nav.svelte.d.ts +15 -0
- package/dist/components/cms/cms-number-editable.svelte +4 -2
- package/dist/components/cms/cms-number.svelte +18 -17
- package/dist/components/cms/cms-number.svelte.d.ts +2 -1
- package/dist/components/cms/cms-pricing.svelte +36 -0
- package/dist/components/cms/cms-pricing.svelte.d.ts +14 -0
- package/dist/components/cms/cms-rich-text.svelte +17 -22
- package/dist/components/cms/cms-rich-text.svelte.d.ts +1 -0
- package/dist/components/cms/cms-schedule.svelte +35 -0
- package/dist/components/cms/cms-schedule.svelte.d.ts +16 -0
- package/dist/components/cms/cms-social-links.svelte +35 -0
- package/dist/components/cms/cms-social-links.svelte.d.ts +15 -0
- package/dist/components/cms/cms-stats.svelte +31 -0
- package/dist/components/cms/cms-stats.svelte.d.ts +16 -0
- package/dist/components/cms/cms-steps.svelte +31 -0
- package/dist/components/cms/cms-steps.svelte.d.ts +16 -0
- package/dist/components/cms/cms-store.svelte.d.ts +14 -0
- package/dist/components/cms/cms-store.svelte.js +39 -2
- package/dist/components/cms/cms-structured-editable.svelte +29 -0
- package/dist/components/cms/{cms-repeater-editable.svelte.d.ts → cms-structured-editable.svelte.d.ts} +12 -8
- package/dist/components/cms/cms-structured.svelte +50 -0
- package/dist/components/cms/cms-structured.svelte.d.ts +49 -0
- package/dist/components/cms/cms-team.svelte +22 -0
- package/dist/components/cms/cms-team.svelte.d.ts +15 -0
- package/dist/components/cms/cms-testimonials.svelte +36 -0
- package/dist/components/cms/cms-testimonials.svelte.d.ts +15 -0
- package/dist/components/cms/cms-text.svelte +23 -30
- package/dist/components/cms/cms-text.svelte.d.ts +3 -1
- package/dist/components/cms/cms-timeline.svelte +35 -0
- package/dist/components/cms/cms-timeline.svelte.d.ts +16 -0
- package/dist/components/cms/index.d.ts +15 -1
- package/dist/components/cms/index.js +15 -1
- package/dist/components/cms/scope.d.ts +25 -5
- package/dist/components/cms/use-cms-field.svelte.d.ts +54 -0
- package/dist/components/cms/use-cms-field.svelte.js +83 -0
- package/dist/core/metadata.d.ts +44 -0
- package/dist/core/metadata.js +59 -0
- package/dist/core/shapes/collection.d.ts +60 -0
- package/dist/core/shapes/collection.js +194 -0
- package/dist/core/shapes/contact.d.ts +70 -0
- package/dist/core/shapes/contact.js +214 -0
- package/dist/core/shapes/faq.d.ts +12 -0
- package/dist/core/shapes/faq.js +48 -0
- package/dist/core/shapes/form.d.ts +46 -0
- package/dist/core/shapes/form.js +7 -0
- package/dist/core/shapes/hours.d.ts +91 -0
- package/dist/core/shapes/hours.js +325 -0
- package/dist/core/shapes/image.d.ts +15 -0
- package/dist/core/shapes/image.js +27 -0
- package/dist/core/shapes/index.d.ts +31 -0
- package/dist/core/shapes/index.js +13 -0
- package/dist/core/shapes/link.d.ts +15 -0
- package/dist/core/shapes/link.js +42 -0
- package/dist/core/shapes/nav.d.ts +21 -0
- package/dist/core/shapes/nav.js +71 -0
- package/dist/core/shapes/presets.d.ts +65 -0
- package/dist/core/shapes/presets.js +152 -0
- package/dist/core/shapes/pricing.d.ts +40 -0
- package/dist/core/shapes/pricing.js +115 -0
- package/dist/core/shapes/social-links.d.ts +19 -0
- package/dist/core/shapes/social-links.js +83 -0
- package/dist/core/shapes/team.d.ts +22 -0
- package/dist/core/shapes/team.js +70 -0
- package/dist/core/shapes/testimonials.d.ts +24 -0
- package/dist/core/shapes/testimonials.js +86 -0
- package/dist/core/structured-registry.d.ts +10 -0
- package/dist/core/structured-registry.js +6 -0
- package/dist/core/structured.d.ts +131 -0
- package/dist/core/structured.js +235 -0
- package/dist/core/wire.d.ts +31 -1
- package/dist/editor.d.ts +26 -0
- package/dist/editor.js +25 -0
- package/dist/index.d.ts +26 -4
- package/dist/index.js +31 -2
- package/dist/server/load-cms.js +1 -0
- package/package.json +12 -3
- package/plugin/README.md +22 -2
- package/plugin/dist/build.d.ts +74 -0
- package/plugin/dist/build.js +210 -0
- package/plugin/dist/build.js.map +1 -0
- package/plugin/dist/index.d.ts +10 -1
- package/plugin/dist/index.js +63 -4
- package/plugin/dist/index.js.map +1 -1
- package/plugin/dist/manifest.d.ts +31 -0
- package/plugin/dist/manifest.js +146 -41
- package/plugin/dist/manifest.js.map +1 -1
- package/plugin/dist/parse-svelte.d.ts +15 -0
- package/plugin/dist/parse-svelte.js +26 -16
- package/plugin/dist/parse-svelte.js.map +1 -1
- package/dist/components/admin-bar/site-settings-panel.svelte +0 -225
- package/dist/components/admin-bar/site-settings-panel.svelte.d.ts +0 -7
- package/dist/components/cms/cms-repeater-editable.svelte +0 -137
- package/dist/components/cms/cms-repeater.svelte +0 -55
- package/dist/components/cms/cms-repeater.svelte.d.ts +0 -30
|
@@ -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,259 @@
|
|
|
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 { Textarea } from './ui/textarea/index.js';
|
|
28
|
+
import * as Popover from './ui/popover/index.js';
|
|
29
|
+
import * as Sheet from './ui/sheet/index.js';
|
|
30
|
+
import * as Tabs from './ui/tabs/index.js';
|
|
31
|
+
|
|
32
|
+
type Props = {
|
|
33
|
+
field: CmsField<T>;
|
|
34
|
+
schema: Structured<T>;
|
|
35
|
+
/** Human label for the header, e.g. "Opening hours". */
|
|
36
|
+
label: string;
|
|
37
|
+
open?: boolean;
|
|
38
|
+
/** What the visitor sees; rendered in place with the edit affordance. */
|
|
39
|
+
preview: Snippet;
|
|
40
|
+
/** The default-locale form over a local draft. */
|
|
41
|
+
editor: Snippet<[T, (next: T) => void]>;
|
|
42
|
+
};
|
|
43
|
+
let { field, schema, label, open = $bindable(false), preview, editor }: Props = $props();
|
|
44
|
+
|
|
45
|
+
const locales = $derived(cmsStore.locales);
|
|
46
|
+
const defaultLocale = $derived(cmsStore.defaultLocale);
|
|
47
|
+
const otherLocales = $derived(locales.filter((l) => l !== defaultLocale));
|
|
48
|
+
|
|
49
|
+
// Local drafts: structure for the default locale, strings per other locale.
|
|
50
|
+
let draft = $state<T>({} as T);
|
|
51
|
+
let translations = $state<Record<string, Record<string, string>>>({});
|
|
52
|
+
let tab = $state('');
|
|
53
|
+
|
|
54
|
+
const reset = () => {
|
|
55
|
+
draft = structuredClone($state.snapshot(field.current)) as T;
|
|
56
|
+
translations = {};
|
|
57
|
+
tab = locales.includes(cmsStore.activeLocale) ? cmsStore.activeLocale : defaultLocale;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
$effect(() => {
|
|
61
|
+
if (open) reset();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
$effect(() => {
|
|
65
|
+
if (open && tab && tab !== defaultLocale) void cmsStore.loadLocaleOverlay(tab);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const update = (next: T) => {
|
|
69
|
+
draft = next;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const fields = $derived<TranslatableField[]>(translatableFields(draft, schema));
|
|
73
|
+
|
|
74
|
+
/** Stored translation for a field in `locale`, unless edited in this session. */
|
|
75
|
+
const translationValue = (locale: string, f: TranslatableField): string => {
|
|
76
|
+
const local = translations[locale]?.[`${f.id}.${f.field}`];
|
|
77
|
+
if (local !== undefined) return local;
|
|
78
|
+
const stored = field.getIn(locale, `$t.${f.id}.${f.field}`);
|
|
79
|
+
return typeof stored === 'string' ? stored : '';
|
|
80
|
+
};
|
|
81
|
+
const setTranslation = (locale: string, f: TranslatableField, value: string) => {
|
|
82
|
+
translations = {
|
|
83
|
+
...translations,
|
|
84
|
+
[locale]: { ...(translations[locale] ?? {}), [`${f.id}.${f.field}`]: value }
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const done = () => {
|
|
89
|
+
field.set(stripTranslations($state.snapshot(draft)), defaultLocale);
|
|
90
|
+
for (const [locale, strings] of Object.entries(translations)) {
|
|
91
|
+
for (const [key, value] of Object.entries(strings)) {
|
|
92
|
+
const dot = key.indexOf('.');
|
|
93
|
+
const id = key.slice(0, dot);
|
|
94
|
+
const f = key.slice(dot + 1);
|
|
95
|
+
field.setAt(translationPath('', id, f).slice(1), value === '' ? null : value, locale);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
open = false;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const clear = () => {
|
|
102
|
+
field.set(null, defaultLocale);
|
|
103
|
+
open = false;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Under 600px the popover becomes a bottom sheet (DESIGN.md §8.2).
|
|
107
|
+
let isMobile = $state(false);
|
|
108
|
+
onMount(() => {
|
|
109
|
+
const mq = window.matchMedia('(max-width: 600px)');
|
|
110
|
+
const sync = () => (isMobile = mq.matches);
|
|
111
|
+
sync();
|
|
112
|
+
mq.addEventListener('change', sync);
|
|
113
|
+
return () => mq.removeEventListener('change', sync);
|
|
114
|
+
});
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
{#snippet body()}
|
|
118
|
+
<div class="vela:flex vela:items-center vela:justify-between vela:px-4 vela:pt-3.5 vela:pb-2">
|
|
119
|
+
<div class="vela:flex vela:items-baseline vela:gap-2 vela:min-w-0">
|
|
120
|
+
<h2 class="vela:text-[14px] vela:font-semibold vela:m-0">{label}</h2>
|
|
121
|
+
<span class="vela:font-mono vela:text-[11px] vela:text-bar-text-tertiary vela:truncate">
|
|
122
|
+
{field.name}
|
|
123
|
+
</span>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{#if otherLocales.length > 0}
|
|
128
|
+
<Tabs.Root bind:value={tab} class="vela:px-4">
|
|
129
|
+
<Tabs.List aria-label="Language">
|
|
130
|
+
{#each locales as locale (locale)}
|
|
131
|
+
<Tabs.Trigger value={locale} class="vela:font-mono vela:uppercase">{locale}</Tabs.Trigger>
|
|
132
|
+
{/each}
|
|
133
|
+
</Tabs.List>
|
|
134
|
+
</Tabs.Root>
|
|
135
|
+
{/if}
|
|
136
|
+
|
|
137
|
+
<div
|
|
138
|
+
class="vela:overflow-y-auto vela:max-h-[60vh] vela:px-4 vela:py-3 vela:flex vela:flex-col vela:gap-3"
|
|
139
|
+
>
|
|
140
|
+
{#if tab === defaultLocale || otherLocales.length === 0}
|
|
141
|
+
{@render editor(draft, update)}
|
|
142
|
+
{:else}
|
|
143
|
+
{#if fields.length === 0}
|
|
144
|
+
<p class="vela:text-[12px] vela:text-bar-text-tertiary vela:m-0">
|
|
145
|
+
Nothing to translate yet. Add content in
|
|
146
|
+
<span class="vela:font-mono vela:uppercase">{defaultLocale}</span> first.
|
|
147
|
+
</p>
|
|
148
|
+
{:else}
|
|
149
|
+
<p class="vela:text-[11px] vela:text-bar-text-tertiary vela:m-0">
|
|
150
|
+
Structure is shared with
|
|
151
|
+
<span class="vela:font-mono vela:uppercase">{defaultLocale}</span>; only the text
|
|
152
|
+
translates. Blank fields show the original.
|
|
153
|
+
</p>
|
|
154
|
+
{#each fields as f (`${f.id}.${f.field}`)}
|
|
155
|
+
<label class="vela:flex vela:flex-col vela:gap-1">
|
|
156
|
+
<span class="vela:text-[12px] vela:text-bar-text-secondary vela:truncate">
|
|
157
|
+
{f.source}
|
|
158
|
+
</span>
|
|
159
|
+
{#if f.source.length > 80 || f.source.includes('<')}
|
|
160
|
+
<Textarea
|
|
161
|
+
value={translationValue(tab, f)}
|
|
162
|
+
placeholder={f.source}
|
|
163
|
+
oninput={(e) => setTranslation(tab, f, e.currentTarget.value)}
|
|
164
|
+
/>
|
|
165
|
+
{:else}
|
|
166
|
+
<Input
|
|
167
|
+
type="text"
|
|
168
|
+
value={translationValue(tab, f)}
|
|
169
|
+
placeholder={f.source}
|
|
170
|
+
oninput={(e) => setTranslation(tab, f, e.currentTarget.value)}
|
|
171
|
+
/>
|
|
172
|
+
{/if}
|
|
173
|
+
</label>
|
|
174
|
+
{/each}
|
|
175
|
+
{/if}
|
|
176
|
+
{/if}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div
|
|
180
|
+
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"
|
|
181
|
+
>
|
|
182
|
+
<Button variant="ghost" size="pill" onclick={clear}>Clear</Button>
|
|
183
|
+
<div class="vela:flex vela:items-center vela:gap-1.5">
|
|
184
|
+
<Button variant="ghost" size="pill" onclick={() => (open = false)}>Cancel</Button>
|
|
185
|
+
<Button size="pill" onclick={done}>Done</Button>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
{/snippet}
|
|
189
|
+
|
|
190
|
+
<CssRoot>
|
|
191
|
+
<span class="cms-structured-edit" data-open={open ? '' : undefined} data-cms-name={field.name}>
|
|
192
|
+
<span class="cms-structured-edit__preview">{@render preview()}</span>
|
|
193
|
+
{#if isMobile}
|
|
194
|
+
<Sheet.Root bind:open>
|
|
195
|
+
<Sheet.Trigger class="cms-structured-edit__trigger" aria-label={`Edit ${label}`}>
|
|
196
|
+
<PencilIcon class="vela:size-3" />
|
|
197
|
+
{label}
|
|
198
|
+
</Sheet.Trigger>
|
|
199
|
+
<Sheet.Content side="bottom" class="vela:gap-0" showCloseButton={false}>
|
|
200
|
+
{@render body()}
|
|
201
|
+
</Sheet.Content>
|
|
202
|
+
</Sheet.Root>
|
|
203
|
+
{:else}
|
|
204
|
+
<Popover.Root bind:open>
|
|
205
|
+
<Popover.Trigger class="cms-structured-edit__trigger" aria-label={`Edit ${label}`}>
|
|
206
|
+
<PencilIcon class="vela:size-3" />
|
|
207
|
+
{label}
|
|
208
|
+
</Popover.Trigger>
|
|
209
|
+
<Popover.Content class="vela:w-[420px] vela:max-w-[calc(100vw-2rem)]">
|
|
210
|
+
{@render body()}
|
|
211
|
+
</Popover.Content>
|
|
212
|
+
</Popover.Root>
|
|
213
|
+
{/if}
|
|
214
|
+
</span>
|
|
215
|
+
</CssRoot>
|
|
216
|
+
|
|
217
|
+
<style>
|
|
218
|
+
.cms-structured-edit {
|
|
219
|
+
position: relative;
|
|
220
|
+
display: block;
|
|
221
|
+
border-radius: 0.125rem;
|
|
222
|
+
outline: 1px dashed rgba(127, 127, 127, 0.6);
|
|
223
|
+
outline-offset: 4px;
|
|
224
|
+
}
|
|
225
|
+
.cms-structured-edit:hover,
|
|
226
|
+
.cms-structured-edit:focus-within,
|
|
227
|
+
.cms-structured-edit[data-open] {
|
|
228
|
+
outline: 2px solid var(--cms-accent, #534ab7);
|
|
229
|
+
outline-offset: 4px;
|
|
230
|
+
}
|
|
231
|
+
.cms-structured-edit__preview {
|
|
232
|
+
display: contents;
|
|
233
|
+
}
|
|
234
|
+
:global(.cms-structured-edit__trigger) {
|
|
235
|
+
position: absolute;
|
|
236
|
+
top: -0.5rem;
|
|
237
|
+
right: -0.5rem;
|
|
238
|
+
z-index: 40;
|
|
239
|
+
display: none;
|
|
240
|
+
align-items: center;
|
|
241
|
+
gap: 0.25rem;
|
|
242
|
+
padding: 0.125rem 0.5rem;
|
|
243
|
+
background: var(--cms-bar-bg, #1f1f1f);
|
|
244
|
+
color: var(--cms-bar-text, #f5f5f5);
|
|
245
|
+
border: 1px solid var(--cms-bar-divider, #3a3a3a);
|
|
246
|
+
border-radius: 999px;
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
font: inherit;
|
|
249
|
+
font-size: 0.6875rem;
|
|
250
|
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
251
|
+
line-height: 1.4;
|
|
252
|
+
white-space: nowrap;
|
|
253
|
+
}
|
|
254
|
+
.cms-structured-edit:hover :global(.cms-structured-edit__trigger),
|
|
255
|
+
.cms-structured-edit:focus-within :global(.cms-structured-edit__trigger),
|
|
256
|
+
.cms-structured-edit[data-open] :global(.cms-structured-edit__trigger) {
|
|
257
|
+
display: inline-flex;
|
|
258
|
+
}
|
|
259
|
+
</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,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,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,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 };
|