@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.
Files changed (127) hide show
  1. package/README.md +107 -18
  2. package/dist/backend/index.d.ts +1 -1
  3. package/dist/backend/routes/seed.d.ts +6 -0
  4. package/dist/backend/routes/seed.js +96 -0
  5. package/dist/backend/routes/table.js +3 -0
  6. package/dist/backend/store/queries.d.ts +6 -1
  7. package/dist/backend/store/queries.js +89 -0
  8. package/dist/cms.js +13 -16
  9. package/dist/components/admin-bar/admin-bar-internal.svelte +4 -4
  10. package/dist/components/admin-bar/admin-bar.css +1 -1
  11. package/dist/components/admin-bar/field-input.svelte +220 -0
  12. package/dist/components/admin-bar/field-input.svelte.d.ts +29 -0
  13. package/dist/components/admin-bar/icons/pencil.svelte +19 -0
  14. package/dist/components/admin-bar/icons/pencil.svelte.d.ts +3 -0
  15. package/dist/components/admin-bar/keyboard-shortcuts-dialog.svelte +1 -1
  16. package/dist/components/admin-bar/locales-panel.svelte +42 -1
  17. package/dist/components/admin-bar/page-config.d.ts +8 -1
  18. package/dist/components/admin-bar/page-config.js +15 -2
  19. package/dist/components/admin-bar/reorder-buttons.svelte +52 -0
  20. package/dist/components/admin-bar/reorder-buttons.svelte.d.ts +10 -0
  21. package/dist/components/admin-bar/seo-panel.svelte +144 -189
  22. package/dist/components/admin-bar/site-options-panel.svelte +115 -0
  23. package/dist/components/admin-bar/site-options-panel.svelte.d.ts +7 -0
  24. package/dist/components/admin-bar/structured-editor-popover.svelte +250 -0
  25. package/dist/components/admin-bar/structured-editor-popover.svelte.d.ts +48 -0
  26. package/dist/components/admin-bar/ui/checkbox/checkbox.svelte +36 -0
  27. package/dist/components/admin-bar/ui/checkbox/checkbox.svelte.d.ts +5 -0
  28. package/dist/components/admin-bar/ui/checkbox/index.d.ts +2 -0
  29. package/dist/components/admin-bar/ui/checkbox/index.js +2 -0
  30. package/dist/components/admin-bar/ui/popover/index.d.ts +5 -0
  31. package/dist/components/admin-bar/ui/popover/index.js +7 -0
  32. package/dist/components/admin-bar/ui/popover/popover-close.svelte +7 -0
  33. package/dist/components/admin-bar/ui/popover/popover-close.svelte.d.ts +4 -0
  34. package/dist/components/admin-bar/ui/popover/popover-content.svelte +35 -0
  35. package/dist/components/admin-bar/ui/popover/popover-content.svelte.d.ts +10 -0
  36. package/dist/components/admin-bar/ui/popover/popover-trigger.svelte +7 -0
  37. package/dist/components/admin-bar/ui/popover/popover-trigger.svelte.d.ts +4 -0
  38. package/dist/components/admin-bar/ui/popover/popover.svelte +7 -0
  39. package/dist/components/admin-bar/ui/popover/popover.svelte.d.ts +3 -0
  40. package/dist/components/admin-bar/ui/sheet/index.d.ts +10 -0
  41. package/dist/components/admin-bar/ui/sheet/index.js +12 -0
  42. package/dist/components/admin-bar/ui/sheet/sheet-close.svelte +7 -0
  43. package/dist/components/admin-bar/ui/sheet/sheet-close.svelte.d.ts +4 -0
  44. package/dist/components/admin-bar/ui/sheet/sheet-content.svelte +64 -0
  45. package/dist/components/admin-bar/ui/sheet/sheet-content.svelte.d.ts +17 -0
  46. package/dist/components/admin-bar/ui/sheet/sheet-description.svelte +17 -0
  47. package/dist/components/admin-bar/ui/sheet/sheet-description.svelte.d.ts +4 -0
  48. package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte +23 -0
  49. package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  50. package/dist/components/admin-bar/ui/sheet/sheet-header.svelte +20 -0
  51. package/dist/components/admin-bar/ui/sheet/sheet-header.svelte.d.ts +5 -0
  52. package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte +20 -0
  53. package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
  54. package/dist/components/admin-bar/ui/sheet/sheet-title.svelte +17 -0
  55. package/dist/components/admin-bar/ui/sheet/sheet-title.svelte.d.ts +4 -0
  56. package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte +7 -0
  57. package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
  58. package/dist/components/admin-bar/ui/sheet/sheet.svelte +7 -0
  59. package/dist/components/admin-bar/ui/sheet/sheet.svelte.d.ts +3 -0
  60. package/dist/components/admin-bar/ui/switch/index.d.ts +2 -0
  61. package/dist/components/admin-bar/ui/switch/index.js +2 -0
  62. package/dist/components/admin-bar/ui/switch/switch.svelte +27 -0
  63. package/dist/components/admin-bar/ui/switch/switch.svelte.d.ts +5 -0
  64. package/dist/components/admin-bar/ui/tabs/index.d.ts +5 -0
  65. package/dist/components/admin-bar/ui/tabs/index.js +7 -0
  66. package/dist/components/admin-bar/ui/tabs/tabs-content.svelte +17 -0
  67. package/dist/components/admin-bar/ui/tabs/tabs-content.svelte.d.ts +4 -0
  68. package/dist/components/admin-bar/ui/tabs/tabs-list.svelte +16 -0
  69. package/dist/components/admin-bar/ui/tabs/tabs-list.svelte.d.ts +4 -0
  70. package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte +20 -0
  71. package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  72. package/dist/components/admin-bar/ui/tabs/tabs.svelte +19 -0
  73. package/dist/components/admin-bar/ui/tabs/tabs.svelte.d.ts +4 -0
  74. package/dist/components/cms/cms-boolean.svelte +9 -18
  75. package/dist/components/cms/cms-boolean.svelte.d.ts +1 -0
  76. package/dist/components/cms/cms-date-time-editable.svelte +3 -3
  77. package/dist/components/cms/cms-date-time.svelte +17 -17
  78. package/dist/components/cms/cms-date-time.svelte.d.ts +2 -1
  79. package/dist/components/cms/cms-image-editable.svelte +13 -4
  80. package/dist/components/cms/cms-image.svelte +37 -30
  81. package/dist/components/cms/cms-image.svelte.d.ts +11 -4
  82. package/dist/components/cms/cms-link.svelte +26 -19
  83. package/dist/components/cms/cms-link.svelte.d.ts +2 -0
  84. package/dist/components/cms/cms-markdown.svelte +15 -16
  85. package/dist/components/cms/cms-markdown.svelte.d.ts +1 -0
  86. package/dist/components/cms/cms-number-editable.svelte +4 -2
  87. package/dist/components/cms/cms-number.svelte +18 -17
  88. package/dist/components/cms/cms-number.svelte.d.ts +2 -1
  89. package/dist/components/cms/cms-repeater.svelte +15 -18
  90. package/dist/components/cms/cms-repeater.svelte.d.ts +1 -0
  91. package/dist/components/cms/cms-rich-text.svelte +17 -22
  92. package/dist/components/cms/cms-rich-text.svelte.d.ts +1 -0
  93. package/dist/components/cms/cms-store.svelte.d.ts +14 -0
  94. package/dist/components/cms/cms-store.svelte.js +39 -2
  95. package/dist/components/cms/cms-text.svelte +23 -30
  96. package/dist/components/cms/cms-text.svelte.d.ts +3 -1
  97. package/dist/components/cms/scope.d.ts +25 -5
  98. package/dist/components/cms/structured-registry.d.ts +10 -0
  99. package/dist/components/cms/structured-registry.js +6 -0
  100. package/dist/components/cms/use-cms-field.svelte.d.ts +54 -0
  101. package/dist/components/cms/use-cms-field.svelte.js +83 -0
  102. package/dist/core/metadata.d.ts +44 -0
  103. package/dist/core/metadata.js +59 -0
  104. package/dist/core/structured.d.ts +108 -0
  105. package/dist/core/structured.js +184 -0
  106. package/dist/core/wire.d.ts +31 -1
  107. package/dist/editor.d.ts +22 -0
  108. package/dist/editor.js +21 -0
  109. package/dist/index.d.ts +12 -4
  110. package/dist/index.js +13 -3
  111. package/dist/server/load-cms.js +1 -0
  112. package/package.json +12 -3
  113. package/plugin/README.md +22 -2
  114. package/plugin/dist/build.d.ts +74 -0
  115. package/plugin/dist/build.js +210 -0
  116. package/plugin/dist/build.js.map +1 -0
  117. package/plugin/dist/index.d.ts +10 -1
  118. package/plugin/dist/index.js +63 -4
  119. package/plugin/dist/index.js.map +1 -1
  120. package/plugin/dist/manifest.d.ts +31 -0
  121. package/plugin/dist/manifest.js +146 -41
  122. package/plugin/dist/manifest.js.map +1 -1
  123. package/plugin/dist/parse-svelte.d.ts +15 -0
  124. package/plugin/dist/parse-svelte.js +26 -16
  125. package/plugin/dist/parse-svelte.js.map +1 -1
  126. package/dist/components/admin-bar/site-settings-panel.svelte +0 -225
  127. 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 { getCmsScope } from './scope.js';
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 scope = getCmsScope();
31
- const ref = $derived<CmsScopeRef | null>(
32
- scope ? { scopeId: scope.scopeId, routeId: scope.routeId, params: scope.params } : null
44
+ const field = useCmsField(
45
+ () => ({ name, scope, value }),
46
+ (raw): CmsLinkValue => (raw === undefined ? (fallback ?? {}) : normalizeLink(raw))
33
47
  );
34
48
 
35
- const resolved = $derived.by(() => {
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={scope?.scopeId ?? '?'}>
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 { getCmsScope } from './scope.js';
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 scope = getCmsScope();
17
- const ref = $derived<CmsScopeRef | null>(
18
- scope ? { scopeId: scope.scopeId, routeId: scope.routeId, params: scope.params } : null
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 resolved = $derived.by(() => {
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={scope?.scopeId ?? '?'}>
38
+ <div class="cms-missing" data-cms-name={name} data-cms-scope={field.ref?.scopeId ?? '?'}>
40
39
  {name}
41
40
  </div>
42
41
  {/if}
@@ -1,6 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type Props = {
3
3
  name: string;
4
+ scope?: string;
4
5
  fallback?: string;
5
6
  value?: unknown;
6
7
  children?: Snippet;
@@ -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
- if (raw === '') return;
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 `CmsRepeater`. */
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 { getCmsScope } from './scope.js';
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 = $props();
22
+ let { name, scope, fallback, value, min, max, step, integer, children }: CmsNumberProps =
23
+ $props();
23
24
 
24
- const scope = getCmsScope();
25
- const ref = $derived<CmsScopeRef | null>(
26
- scope ? { scopeId: scope.scopeId, routeId: scope.routeId, params: scope.params } : null
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 resolved = $derived.by(() => {
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={scope?.scopeId ?? '?'}>{name}</span
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 `CmsRepeater`. */
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 { getCmsScope } from './scope.js';
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 scope = getCmsScope();
16
- const ref = $derived<CmsScopeRef | null>(
17
- scope ? { scopeId: scope.scopeId, routeId: scope.routeId, params: scope.params } : null
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 resolved = $derived.by(() => {
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={scope?.scopeId ?? '?'}>
37
+ <span class="cms-missing" data-cms-name={name} data-cms-scope={field.ref?.scopeId ?? '?'}>
41
38
  {name}
42
39
  </span>
43
40
  {/if}
@@ -2,6 +2,7 @@ import type { Snippet } from 'svelte';
2
2
  declare function $$render<T extends Record<string, unknown> = Record<string, unknown>>(): {
3
3
  props: {
4
4
  name: string;
5
+ scope?: string;
5
6
  fallback?: T[];
6
7
  value?: unknown;
7
8
  children: Snippet<[T, number]>;
@@ -1,48 +1,41 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import { getCmsScope } from './scope.js';
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 scope = getCmsScope();
16
- const ref = $derived<CmsScopeRef | null>(
17
- scope ? { scopeId: scope.scopeId, routeId: scope.routeId, params: scope.params } : null
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 resolved = $derived.by(() => {
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.by(() => {
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={scope?.scopeId ?? '?'}>
55
+ <div class="cms-missing" data-cms-name={name} data-cms-scope={field.ref?.scopeId ?? '?'}>
61
56
  {name}
62
57
  </div>
63
58
  {/if}
@@ -1,6 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type Props = {
3
3
  name: string;
4
+ scope?: string;
4
5
  fallback?: string;
5
6
  value?: unknown;
6
7
  children?: Snippet;
@@ -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
- const token = ++this.overlayFetchToken;
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.overlayFetchToken)
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 { getCmsScope } from './scope.js';
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 `CmsRepeater`. */
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
- const scope = getCmsScope();
17
- const ref = $derived<CmsScopeRef | null>(
18
- scope ? { scopeId: scope.scopeId, routeId: scope.routeId, params: scope.params } : null
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 resolved = $derived.by(() => {
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.by(() => {
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
- writeValue((e.currentTarget as HTMLElement).textContent ?? '');
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
- writeValue((e.currentTarget as HTMLElement).textContent ?? '');
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={scope?.scopeId ?? '?'}
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={scope?.scopeId ?? '?'}>{name}</span
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 `CmsRepeater`. */
7
+ /** Per-item override used by structured components. */
6
8
  value?: unknown;
7
9
  children?: Snippet;
8
10
  };