@velastack/cms 0.4.0 → 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.
- package/README.md +107 -18
- 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 +220 -0
- package/dist/components/admin-bar/field-input.svelte.d.ts +29 -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/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/reorder-buttons.svelte +52 -0
- package/dist/components/admin-bar/reorder-buttons.svelte.d.ts +10 -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 +250 -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-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-image-editable.svelte +13 -4
- package/dist/components/cms/cms-image.svelte +37 -30
- package/dist/components/cms/cms-image.svelte.d.ts +11 -4
- package/dist/components/cms/cms-link.svelte +26 -19
- package/dist/components/cms/cms-link.svelte.d.ts +2 -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-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-repeater.svelte +15 -18
- package/dist/components/cms/cms-repeater.svelte.d.ts +1 -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-store.svelte.d.ts +14 -0
- package/dist/components/cms/cms-store.svelte.js +39 -2
- package/dist/components/cms/cms-text.svelte +23 -30
- package/dist/components/cms/cms-text.svelte.d.ts +3 -1
- package/dist/components/cms/scope.d.ts +25 -5
- package/dist/components/cms/structured-registry.d.ts +10 -0
- package/dist/components/cms/structured-registry.js +6 -0
- 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/structured.d.ts +108 -0
- package/dist/core/structured.js +184 -0
- package/dist/core/wire.d.ts +31 -1
- package/dist/editor.d.ts +22 -0
- package/dist/editor.js +21 -0
- package/dist/index.d.ts +12 -4
- package/dist/index.js +13 -3
- 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
|
@@ -10,37 +10,43 @@
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
export type CmsLinkRenderProps = { label: string; href: string; newTab: boolean };
|
|
13
|
+
|
|
14
|
+
export const normalizeLink = (raw: unknown): CmsLinkValue => {
|
|
15
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
16
|
+
const r = raw as Record<string, unknown>;
|
|
17
|
+
const out: CmsLinkValue = {};
|
|
18
|
+
if (typeof r.label === 'string') out.label = r.label;
|
|
19
|
+
if (typeof r.href === 'string') out.href = r.href;
|
|
20
|
+
if (typeof r.routeId === 'string') out.routeId = r.routeId;
|
|
21
|
+
if (r.params && typeof r.params === 'object' && !Array.isArray(r.params)) {
|
|
22
|
+
out.params = r.params as Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
if (typeof r.newTab === 'boolean') out.newTab = r.newTab;
|
|
25
|
+
return out;
|
|
26
|
+
};
|
|
13
27
|
</script>
|
|
14
28
|
|
|
15
29
|
<script lang="ts">
|
|
16
30
|
import type { Snippet } from 'svelte';
|
|
17
|
-
import {
|
|
18
|
-
import { cmsStore, type CmsScopeRef } from './cms-store.svelte.js';
|
|
31
|
+
import { useCmsField } from './use-cms-field.svelte.js';
|
|
19
32
|
import { resolveRouteUrl } from '../admin-bar/resolve-route.js';
|
|
20
33
|
|
|
21
34
|
type Props = {
|
|
22
35
|
name: string;
|
|
36
|
+
scope?: string;
|
|
23
37
|
fallback?: CmsLinkValue;
|
|
24
38
|
value?: unknown;
|
|
25
39
|
class?: string;
|
|
26
40
|
children?: Snippet<[CmsLinkRenderProps]>;
|
|
27
41
|
};
|
|
28
|
-
let { name, fallback, value, class: className, children }: Props = $props();
|
|
42
|
+
let { name, scope, fallback, value, class: className, children }: Props = $props();
|
|
29
43
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
const field = useCmsField(
|
|
45
|
+
() => ({ name, scope, value }),
|
|
46
|
+
(raw): CmsLinkValue => (raw === undefined ? (fallback ?? {}) : normalizeLink(raw))
|
|
33
47
|
);
|
|
34
48
|
|
|
35
|
-
const
|
|
36
|
-
if (value !== undefined) return value;
|
|
37
|
-
return ref ? cmsStore.getValue(ref, name) : undefined;
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const link = $derived.by((): CmsLinkValue => {
|
|
41
|
-
if (resolved && typeof resolved === 'object') return resolved as CmsLinkValue;
|
|
42
|
-
return fallback ?? {};
|
|
43
|
-
});
|
|
49
|
+
const link = $derived(field.current);
|
|
44
50
|
|
|
45
51
|
const href = $derived.by(() => {
|
|
46
52
|
if (link.routeId) {
|
|
@@ -55,12 +61,11 @@
|
|
|
55
61
|
|
|
56
62
|
const label = $derived(link.label ?? '');
|
|
57
63
|
const newTab = $derived(!!link.newTab);
|
|
58
|
-
const editable = $derived(cmsStore.isEditing && value === undefined && !!ref);
|
|
59
64
|
</script>
|
|
60
65
|
|
|
61
|
-
{#if editable && ref}
|
|
66
|
+
{#if field.editable && field.ref}
|
|
62
67
|
{#await import('./cms-link-editable.svelte') then { default: Editable }}
|
|
63
|
-
<Editable scope={ref} {name} initial={link} {className} {children} />
|
|
68
|
+
<Editable scope={field.ref} {name} initial={link} {className} {children} />
|
|
64
69
|
{/await}
|
|
65
70
|
{:else if children}
|
|
66
71
|
{@render children({ label, href, newTab })}
|
|
@@ -73,8 +78,10 @@
|
|
|
73
78
|
>
|
|
74
79
|
{label || name}
|
|
75
80
|
</a>
|
|
81
|
+
{:else if field.raw === null}
|
|
82
|
+
<!-- cleared by the editor -->
|
|
76
83
|
{:else}
|
|
77
|
-
<span class="cms-missing" data-cms-name={name} data-cms-scope={
|
|
84
|
+
<span class="cms-missing" data-cms-name={name} data-cms-scope={field.ref?.scopeId ?? '?'}>
|
|
78
85
|
{name}
|
|
79
86
|
</span>
|
|
80
87
|
{/if}
|
|
@@ -12,9 +12,11 @@ export type CmsLinkRenderProps = {
|
|
|
12
12
|
href: string;
|
|
13
13
|
newTab: boolean;
|
|
14
14
|
};
|
|
15
|
+
export declare const normalizeLink: (raw: unknown) => CmsLinkValue;
|
|
15
16
|
import type { Snippet } from 'svelte';
|
|
16
17
|
type Props = {
|
|
17
18
|
name: string;
|
|
19
|
+
scope?: string;
|
|
18
20
|
fallback?: CmsLinkValue;
|
|
19
21
|
value?: unknown;
|
|
20
22
|
class?: string;
|
|
@@ -1,42 +1,41 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { marked } from 'marked';
|
|
4
|
-
import {
|
|
5
|
-
import { cmsStore, type CmsScopeRef } from './cms-store.svelte.js';
|
|
4
|
+
import { useCmsField } from './use-cms-field.svelte.js';
|
|
6
5
|
|
|
7
6
|
type Props = {
|
|
8
7
|
name: string;
|
|
8
|
+
scope?: string;
|
|
9
9
|
fallback?: string;
|
|
10
10
|
value?: unknown;
|
|
11
11
|
children?: Snippet;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
let { name, fallback, value, children }: Props = $props();
|
|
14
|
+
let { name, scope, fallback, value, children }: Props = $props();
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const field = useCmsField(
|
|
17
|
+
() => ({ name, scope, value }),
|
|
18
|
+
(raw): string | undefined => {
|
|
19
|
+
if (raw === undefined) return fallback;
|
|
20
|
+
return typeof raw === 'string' ? raw : '';
|
|
21
|
+
}
|
|
19
22
|
);
|
|
20
23
|
|
|
21
|
-
const
|
|
22
|
-
if (value !== undefined) return value;
|
|
23
|
-
return ref ? cmsStore.getValue(ref, name) : undefined;
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const source = $derived(typeof resolved === 'string' ? resolved : (fallback ?? ''));
|
|
27
|
-
const editable = $derived(cmsStore.isEditing && value === undefined && !!ref);
|
|
24
|
+
const source = $derived(field.current ?? '');
|
|
28
25
|
</script>
|
|
29
26
|
|
|
30
|
-
{#if editable && ref}
|
|
27
|
+
{#if field.editable && field.ref}
|
|
31
28
|
{#await import('./cms-markdown-editable.svelte') then { default: Editable }}
|
|
32
|
-
<Editable scope={ref} {name} initial={source} />
|
|
29
|
+
<Editable scope={field.ref} {name} initial={source} />
|
|
33
30
|
{/await}
|
|
34
31
|
{:else if source}
|
|
35
32
|
<div class="cms-markdown">{@html marked.parse(source, { async: false })}</div>
|
|
33
|
+
{:else if field.raw === null}
|
|
34
|
+
<!-- cleared by the editor -->
|
|
36
35
|
{:else if children}
|
|
37
36
|
{@render children()}
|
|
38
37
|
{:else}
|
|
39
|
-
<div class="cms-missing" data-cms-name={name} data-cms-scope={
|
|
38
|
+
<div class="cms-missing" data-cms-name={name} data-cms-scope={field.ref?.scopeId ?? '?'}>
|
|
40
39
|
{name}
|
|
41
40
|
</div>
|
|
42
41
|
{/if}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
const live = $derived(cmsStore.getValue(scope, name));
|
|
17
17
|
const display = $derived.by(() => {
|
|
18
|
-
const v = typeof live === 'number' ? live : initial;
|
|
18
|
+
const v = typeof live === 'number' ? live : live === null ? undefined : initial;
|
|
19
19
|
return typeof v === 'number' && Number.isFinite(v) ? String(v) : '';
|
|
20
20
|
});
|
|
21
21
|
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
|
|
35
35
|
const onInput = (e: Event) => {
|
|
36
36
|
const raw = (e.currentTarget as HTMLInputElement).value;
|
|
37
|
-
|
|
37
|
+
// An emptied input clears the field explicitly (`null`), so the
|
|
38
|
+
// published value no longer shows through.
|
|
39
|
+
if (raw === '') return cmsStore.setValue(scope, name, null);
|
|
38
40
|
const n = Number(raw);
|
|
39
41
|
if (!Number.isFinite(n)) return;
|
|
40
42
|
cmsStore.setValue(scope, name, integer ? Math.trunc(n) : n);
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
export type CmsNumberProps = {
|
|
5
5
|
name: string;
|
|
6
|
+
scope?: string;
|
|
6
7
|
fallback?: number;
|
|
7
|
-
/** Per-item override used by
|
|
8
|
+
/** Per-item override used by structured components. */
|
|
8
9
|
value?: unknown;
|
|
9
10
|
min?: number;
|
|
10
11
|
max?: number;
|
|
@@ -16,37 +17,37 @@
|
|
|
16
17
|
</script>
|
|
17
18
|
|
|
18
19
|
<script lang="ts">
|
|
19
|
-
import {
|
|
20
|
-
import { cmsStore, type CmsScopeRef } from './cms-store.svelte.js';
|
|
20
|
+
import { useCmsField } from './use-cms-field.svelte.js';
|
|
21
21
|
|
|
22
|
-
let { name, fallback, value, min, max, step, integer, children }: CmsNumberProps =
|
|
22
|
+
let { name, scope, fallback, value, min, max, step, integer, children }: CmsNumberProps =
|
|
23
|
+
$props();
|
|
23
24
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const field = useCmsField(
|
|
26
|
+
() => ({ name, scope, value }),
|
|
27
|
+
(raw): number | undefined => {
|
|
28
|
+
if (raw === undefined) return fallback;
|
|
29
|
+
return typeof raw === 'number' && Number.isFinite(raw) ? raw : undefined;
|
|
30
|
+
}
|
|
27
31
|
);
|
|
28
32
|
|
|
29
|
-
const
|
|
30
|
-
if (value !== undefined) return value;
|
|
31
|
-
return ref ? cmsStore.getValue(ref, name) : undefined;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const current = $derived(typeof resolved === 'number' ? resolved : fallback);
|
|
35
|
-
const editable = $derived(cmsStore.isEditing && value === undefined && !!ref);
|
|
33
|
+
const current = $derived(field.current);
|
|
36
34
|
</script>
|
|
37
35
|
|
|
38
|
-
{#if editable && ref}
|
|
36
|
+
{#if field.editable && field.ref}
|
|
39
37
|
{#await import('./cms-number-editable.svelte') then { default: Editable }}
|
|
40
|
-
<Editable scope={ref} {name} initial={current} {min} {max} {step} {integer} />
|
|
38
|
+
<Editable scope={field.ref} {name} initial={current} {min} {max} {step} {integer} />
|
|
41
39
|
{/await}
|
|
42
40
|
{:else if current !== undefined && children}
|
|
43
41
|
{@render children(current)}
|
|
44
42
|
{:else if current !== undefined}
|
|
45
43
|
{current}
|
|
44
|
+
{:else if field.raw === null}
|
|
45
|
+
<!-- cleared by the editor -->
|
|
46
46
|
{:else if children}
|
|
47
47
|
{@render children(0)}
|
|
48
48
|
{:else}
|
|
49
|
-
<span class="cms-missing" data-cms-name={name} data-cms-scope={
|
|
49
|
+
<span class="cms-missing" data-cms-name={name} data-cms-scope={field.ref?.scopeId ?? '?'}
|
|
50
|
+
>{name}</span
|
|
50
51
|
>
|
|
51
52
|
{/if}
|
|
52
53
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
export type CmsNumberProps = {
|
|
3
3
|
name: string;
|
|
4
|
+
scope?: string;
|
|
4
5
|
fallback?: number;
|
|
5
|
-
/** Per-item override used by
|
|
6
|
+
/** Per-item override used by structured components. */
|
|
6
7
|
value?: unknown;
|
|
7
8
|
min?: number;
|
|
8
9
|
max?: number;
|
|
@@ -1,43 +1,40 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends Record<string, unknown> = Record<string, unknown>">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import {
|
|
4
|
-
import { cmsStore, type CmsScopeRef } from './cms-store.svelte.js';
|
|
3
|
+
import { useCmsField } from './use-cms-field.svelte.js';
|
|
5
4
|
|
|
6
5
|
type Props = {
|
|
7
6
|
name: string;
|
|
7
|
+
scope?: string;
|
|
8
8
|
fallback?: T[];
|
|
9
9
|
value?: unknown;
|
|
10
10
|
children: Snippet<[T, number]>;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
let { name, fallback, value, children }: Props = $props();
|
|
13
|
+
let { name, scope, fallback, value, children }: Props = $props();
|
|
14
14
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const field = useCmsField(
|
|
16
|
+
() => ({ name, scope, value }),
|
|
17
|
+
(raw): T[] => {
|
|
18
|
+
if (raw === undefined) return Array.isArray(fallback) ? fallback : [];
|
|
19
|
+
return Array.isArray(raw) ? (raw as T[]) : [];
|
|
20
|
+
}
|
|
18
21
|
);
|
|
19
22
|
|
|
20
|
-
const
|
|
21
|
-
if (value !== undefined) return value;
|
|
22
|
-
return ref ? cmsStore.getValue(ref, name) : undefined;
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const items = $derived(
|
|
26
|
-
Array.isArray(resolved) ? (resolved as T[]) : Array.isArray(fallback) ? fallback : []
|
|
27
|
-
);
|
|
28
|
-
const editable = $derived(cmsStore.isEditing && value === undefined && !!ref);
|
|
23
|
+
const items = $derived(field.current);
|
|
29
24
|
</script>
|
|
30
25
|
|
|
31
|
-
{#if editable && ref}
|
|
26
|
+
{#if field.editable && field.ref}
|
|
32
27
|
{#await import('./cms-repeater-editable.svelte') then { default: Editable }}
|
|
33
|
-
<Editable scope={ref} {name} {items} {children} />
|
|
28
|
+
<Editable scope={field.ref} {name} {items} {children} />
|
|
34
29
|
{/await}
|
|
35
30
|
{:else if items.length}
|
|
36
31
|
{#each items as item, i}
|
|
37
32
|
{@render children(item, i)}
|
|
38
33
|
{/each}
|
|
34
|
+
{:else if field.raw === null}
|
|
35
|
+
<!-- cleared by the editor -->
|
|
39
36
|
{:else}
|
|
40
|
-
<span class="cms-missing" data-cms-name={name} data-cms-scope={
|
|
37
|
+
<span class="cms-missing" data-cms-name={name} data-cms-scope={field.ref?.scopeId ?? '?'}>
|
|
41
38
|
{name}
|
|
42
39
|
</span>
|
|
43
40
|
{/if}
|
|
@@ -1,48 +1,41 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import {
|
|
4
|
-
import { cmsStore, type CmsScopeRef } from './cms-store.svelte.js';
|
|
3
|
+
import { useCmsField } from './use-cms-field.svelte.js';
|
|
5
4
|
|
|
6
5
|
type Props = {
|
|
7
6
|
name: string;
|
|
7
|
+
scope?: string;
|
|
8
8
|
fallback?: string;
|
|
9
9
|
value?: unknown;
|
|
10
10
|
children?: Snippet;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
let { name, fallback, value, children }: Props = $props();
|
|
13
|
+
let { name, scope, fallback, value, children }: Props = $props();
|
|
14
14
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const field = useCmsField(
|
|
16
|
+
() => ({ name, scope, value }),
|
|
17
|
+
(raw): string | undefined => {
|
|
18
|
+
if (raw === undefined) return fallback;
|
|
19
|
+
return typeof raw === 'string' ? raw : '';
|
|
20
|
+
}
|
|
18
21
|
);
|
|
19
22
|
|
|
20
|
-
const
|
|
21
|
-
if (value !== undefined) return value;
|
|
22
|
-
return ref ? cmsStore.getValue(ref, name) : undefined;
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const html = $derived(typeof resolved === 'string' ? resolved : (fallback ?? ''));
|
|
26
|
-
const editable = $derived(cmsStore.isEditing && value === undefined && !!ref);
|
|
23
|
+
const html = $derived(field.current ?? '');
|
|
27
24
|
|
|
28
25
|
let childrenEl = $state<HTMLDivElement>();
|
|
29
26
|
let initialFromChildren = $state<string | undefined>(undefined);
|
|
30
27
|
|
|
31
28
|
$effect.pre(() => {
|
|
32
|
-
if (editable && childrenEl && initialFromChildren === undefined) {
|
|
29
|
+
if (field.editable && childrenEl && initialFromChildren === undefined) {
|
|
33
30
|
const inner = childrenEl.innerHTML.trim();
|
|
34
31
|
initialFromChildren = inner || undefined;
|
|
35
32
|
}
|
|
36
33
|
});
|
|
37
34
|
|
|
38
|
-
const editInitial = $derived.
|
|
39
|
-
if (typeof resolved === 'string') return resolved;
|
|
40
|
-
if (fallback !== undefined) return fallback;
|
|
41
|
-
return initialFromChildren ?? '';
|
|
42
|
-
});
|
|
35
|
+
const editInitial = $derived(field.current ?? initialFromChildren ?? '');
|
|
43
36
|
</script>
|
|
44
37
|
|
|
45
|
-
{#if editable && ref}
|
|
38
|
+
{#if field.editable && field.ref}
|
|
46
39
|
<!-- Mounted straight into edit mode (navigation while editing): the
|
|
47
40
|
children branch below never rendered, so render it hidden once to
|
|
48
41
|
capture the default markup. Removed as soon as it's captured. -->
|
|
@@ -50,14 +43,16 @@
|
|
|
50
43
|
<div bind:this={childrenEl} class="cms-rich-text-capture">{@render children()}</div>
|
|
51
44
|
{/if}
|
|
52
45
|
{#await import('./cms-rich-text-editable.svelte') then { default: Editable }}
|
|
53
|
-
<Editable scope={ref} {name} initial={editInitial} />
|
|
46
|
+
<Editable scope={field.ref} {name} initial={editInitial} />
|
|
54
47
|
{/await}
|
|
55
48
|
{:else if html}
|
|
56
49
|
<div class="cms-rich-text">{@html html}</div>
|
|
50
|
+
{:else if field.raw === null}
|
|
51
|
+
<!-- cleared by the editor -->
|
|
57
52
|
{:else if children}
|
|
58
53
|
<div bind:this={childrenEl} class="cms-rich-text-children">{@render children()}</div>
|
|
59
54
|
{:else}
|
|
60
|
-
<div class="cms-missing" data-cms-name={name} data-cms-scope={
|
|
55
|
+
<div class="cms-missing" data-cms-name={name} data-cms-scope={field.ref?.scopeId ?? '?'}>
|
|
61
56
|
{name}
|
|
62
57
|
</div>
|
|
63
58
|
{/if}
|
|
@@ -38,9 +38,23 @@ declare class CmsStore {
|
|
|
38
38
|
private _activeLocale;
|
|
39
39
|
private overlayFetchToken;
|
|
40
40
|
private entriesFetchToken;
|
|
41
|
+
private localeOverlayTokens;
|
|
41
42
|
toggleEdit(): void;
|
|
42
43
|
setActiveLocale(locale: string | null): void;
|
|
43
44
|
private effectiveLocale;
|
|
45
|
+
/** The locale edits currently target: the editor's `?locale=` override, else the page's. */
|
|
46
|
+
get activeLocale(): string;
|
|
47
|
+
/** The project's default locale, `cms.locales[0]`. */
|
|
48
|
+
get defaultLocale(): string;
|
|
49
|
+
/** Every supported locale from the payload. */
|
|
50
|
+
get locales(): string[];
|
|
51
|
+
/**
|
|
52
|
+
* Make sure the overlay for `locale` is loaded for the current route's
|
|
53
|
+
* scopes, so a structured editor's translation tab and the Locales panel
|
|
54
|
+
* can read another locale's strings without switching the page. Uses the
|
|
55
|
+
* open release's preview key when there is one.
|
|
56
|
+
*/
|
|
57
|
+
loadLocaleOverlay(locale: string): Promise<void>;
|
|
44
58
|
/** Base doc (server-rendered) for a scope. Already locale-resolved by `resolveCmsPayload`. */
|
|
45
59
|
private baseDoc;
|
|
46
60
|
/** Merged read tree: base ∪ overlay (∪ draft when editing) for the given locale. */
|
|
@@ -105,6 +105,7 @@ class CmsStore {
|
|
|
105
105
|
_activeLocale = $state(null);
|
|
106
106
|
overlayFetchToken = 0;
|
|
107
107
|
entriesFetchToken = 0;
|
|
108
|
+
localeOverlayTokens = {};
|
|
108
109
|
toggleEdit() {
|
|
109
110
|
this.isEditing = !this.isEditing;
|
|
110
111
|
}
|
|
@@ -114,6 +115,38 @@ class CmsStore {
|
|
|
114
115
|
effectiveLocale() {
|
|
115
116
|
return this._activeLocale ?? pageLocale();
|
|
116
117
|
}
|
|
118
|
+
/** The locale edits currently target: the editor's `?locale=` override, else the page's. */
|
|
119
|
+
get activeLocale() {
|
|
120
|
+
return this.effectiveLocale();
|
|
121
|
+
}
|
|
122
|
+
/** The project's default locale, `cms.locales[0]`. */
|
|
123
|
+
get defaultLocale() {
|
|
124
|
+
return pageDefaultLocale();
|
|
125
|
+
}
|
|
126
|
+
/** Every supported locale from the payload. */
|
|
127
|
+
get locales() {
|
|
128
|
+
return page.data?.cms?.locales ?? [];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Make sure the overlay for `locale` is loaded for the current route's
|
|
132
|
+
* scopes, so a structured editor's translation tab and the Locales panel
|
|
133
|
+
* can read another locale's strings without switching the page. Uses the
|
|
134
|
+
* open release's preview key when there is one.
|
|
135
|
+
*/
|
|
136
|
+
async loadLocaleOverlay(locale) {
|
|
137
|
+
const cms = page.data?.cms;
|
|
138
|
+
if (!cms)
|
|
139
|
+
return;
|
|
140
|
+
const scopes = Object.values(cms.scopes);
|
|
141
|
+
const loaded = scopes.every((s) => composeKey({ scopeId: s.scopeId, routeId: s.routeId, params: s.params }, locale) in
|
|
142
|
+
this.overlay);
|
|
143
|
+
if (loaded)
|
|
144
|
+
return;
|
|
145
|
+
await this.loadAndApplyOverlay(cms.endpoint, scopes, this.openRelease?.preview_key ?? null, {
|
|
146
|
+
locale,
|
|
147
|
+
defaultLocale: cms.locales[0]
|
|
148
|
+
});
|
|
149
|
+
}
|
|
117
150
|
/** Base doc (server-rendered) for a scope. Already locale-resolved by `resolveCmsPayload`. */
|
|
118
151
|
baseDoc(scope) {
|
|
119
152
|
const cms = page.data?.cms;
|
|
@@ -234,7 +267,11 @@ class CmsStore {
|
|
|
234
267
|
* overlays applied during navigation while new-page scopes load.
|
|
235
268
|
*/
|
|
236
269
|
async loadAndApplyOverlay(endpoint, scopes, previewKey, opts) {
|
|
237
|
-
|
|
270
|
+
// One in-flight fetch per locale: a translation tab loading `es` must
|
|
271
|
+
// not cancel the page's own `en` overlay load, and vice versa.
|
|
272
|
+
const token = (this.localeOverlayTokens[opts.locale] =
|
|
273
|
+
(this.localeOverlayTokens[opts.locale] ?? 0) + 1);
|
|
274
|
+
this.overlayFetchToken = token;
|
|
238
275
|
const versionKey = opts.versionKey ?? null;
|
|
239
276
|
const locale = opts.locale;
|
|
240
277
|
const defaultLocale = opts.defaultLocale ?? pageDefaultLocale() ?? locale;
|
|
@@ -271,7 +308,7 @@ class CmsStore {
|
|
|
271
308
|
? Promise.all(scopes.map((s) => fetchScope(s, defaultLocale)))
|
|
272
309
|
: Promise.resolve(null)
|
|
273
310
|
]);
|
|
274
|
-
if (token !== this.
|
|
311
|
+
if (token !== this.localeOverlayTokens[locale])
|
|
275
312
|
return;
|
|
276
313
|
const merged = mergeLocaleDocs(collect(requestedPairs), fallbackPairs ? collect(fallbackPairs) : null);
|
|
277
314
|
const scopeIndex = new Map();
|
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import {
|
|
4
|
-
import { cmsStore, type CmsScopeRef } from './cms-store.svelte.js';
|
|
3
|
+
import { useCmsField } from './use-cms-field.svelte.js';
|
|
5
4
|
|
|
6
5
|
type Props = {
|
|
7
6
|
name: string;
|
|
7
|
+
/** `'root'` for the root layout scope, or an explicit scope id. */
|
|
8
|
+
scope?: string;
|
|
8
9
|
fallback?: string;
|
|
9
|
-
/** Per-item override used by
|
|
10
|
+
/** Per-item override used by structured components. */
|
|
10
11
|
value?: unknown;
|
|
11
12
|
children?: Snippet;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
let { name, fallback, value, children }: Props = $props();
|
|
15
|
+
let { name, scope, fallback, value, children }: Props = $props();
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
17
|
+
// `undefined` → fallback (or children); `null` → cleared, shown empty.
|
|
18
|
+
const field = useCmsField(
|
|
19
|
+
() => ({ name, scope, value }),
|
|
20
|
+
(raw): string | undefined => {
|
|
21
|
+
if (raw === undefined) return fallback;
|
|
22
|
+
return typeof raw === 'string' ? raw : '';
|
|
23
|
+
}
|
|
19
24
|
);
|
|
20
25
|
|
|
21
|
-
const
|
|
22
|
-
if (value !== undefined) return value;
|
|
23
|
-
return ref ? cmsStore.getValue(ref, name) : undefined;
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const display = $derived(typeof resolved === 'string' ? resolved : (fallback ?? ''));
|
|
27
|
-
const editable = $derived(cmsStore.isEditing && value === undefined && !!ref);
|
|
26
|
+
const display = $derived(field.current ?? '');
|
|
28
27
|
|
|
29
28
|
let el = $state<HTMLSpanElement>();
|
|
30
29
|
let childrenEl = $state<HTMLElement>();
|
|
@@ -32,17 +31,13 @@
|
|
|
32
31
|
let composing = false;
|
|
33
32
|
|
|
34
33
|
$effect.pre(() => {
|
|
35
|
-
if (editable && childrenEl && initialFromChildren === undefined) {
|
|
34
|
+
if (field.editable && childrenEl && initialFromChildren === undefined) {
|
|
36
35
|
const text = childrenEl.textContent?.trim();
|
|
37
36
|
initialFromChildren = text || undefined;
|
|
38
37
|
}
|
|
39
38
|
});
|
|
40
39
|
|
|
41
|
-
const editValue = $derived.
|
|
42
|
-
if (typeof resolved === 'string') return resolved;
|
|
43
|
-
if (fallback !== undefined) return fallback;
|
|
44
|
-
return initialFromChildren ?? '';
|
|
45
|
-
});
|
|
40
|
+
const editValue = $derived(field.current ?? initialFromChildren ?? '');
|
|
46
41
|
|
|
47
42
|
$effect(() => {
|
|
48
43
|
if (!el) return;
|
|
@@ -52,14 +47,9 @@
|
|
|
52
47
|
el.textContent = target;
|
|
53
48
|
});
|
|
54
49
|
|
|
55
|
-
const writeValue = (text: string) => {
|
|
56
|
-
if (!ref) return;
|
|
57
|
-
cmsStore.setValue(ref, name, text);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
50
|
const onInput = (e: Event) => {
|
|
61
51
|
if (composing) return;
|
|
62
|
-
|
|
52
|
+
field.set((e.currentTarget as HTMLElement).textContent ?? '');
|
|
63
53
|
};
|
|
64
54
|
|
|
65
55
|
const onKeydown = (e: KeyboardEvent) => {
|
|
@@ -68,11 +58,11 @@
|
|
|
68
58
|
|
|
69
59
|
const onCompositionEnd = (e: CompositionEvent) => {
|
|
70
60
|
composing = false;
|
|
71
|
-
|
|
61
|
+
field.set((e.currentTarget as HTMLElement).textContent ?? '');
|
|
72
62
|
};
|
|
73
63
|
</script>
|
|
74
64
|
|
|
75
|
-
{#if editable && ref}
|
|
65
|
+
{#if field.editable && field.ref}
|
|
76
66
|
<!-- Mounted straight into edit mode (navigation while editing): the
|
|
77
67
|
children branch below never rendered, so render it hidden once to
|
|
78
68
|
capture the default text. Removed as soon as it's captured. -->
|
|
@@ -85,7 +75,7 @@
|
|
|
85
75
|
contenteditable="plaintext-only"
|
|
86
76
|
data-placeholder={name}
|
|
87
77
|
data-cms-name={name}
|
|
88
|
-
data-cms-scope={
|
|
78
|
+
data-cms-scope={field.ref.scopeId}
|
|
89
79
|
role="textbox"
|
|
90
80
|
tabindex="0"
|
|
91
81
|
aria-multiline="false"
|
|
@@ -98,10 +88,13 @@
|
|
|
98
88
|
></span>
|
|
99
89
|
{:else if display}
|
|
100
90
|
{display}
|
|
91
|
+
{:else if field.raw === null}
|
|
92
|
+
<!-- cleared by the editor: render nothing -->
|
|
101
93
|
{:else if children}
|
|
102
94
|
<span bind:this={childrenEl} class="cms-text-children">{@render children()}</span>
|
|
103
95
|
{:else}
|
|
104
|
-
<span class="cms-missing" data-cms-name={name} data-cms-scope={
|
|
96
|
+
<span class="cms-missing" data-cms-name={name} data-cms-scope={field.ref?.scopeId ?? '?'}
|
|
97
|
+
>{name}</span
|
|
105
98
|
>
|
|
106
99
|
{/if}
|
|
107
100
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
type Props = {
|
|
3
3
|
name: string;
|
|
4
|
+
/** `'root'` for the root layout scope, or an explicit scope id. */
|
|
5
|
+
scope?: string;
|
|
4
6
|
fallback?: string;
|
|
5
|
-
/** Per-item override used by
|
|
7
|
+
/** Per-item override used by structured components. */
|
|
6
8
|
value?: unknown;
|
|
7
9
|
children?: Snippet;
|
|
8
10
|
};
|