@velastack/cms 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +107 -18
  2. package/dist/backend/index.d.ts +1 -1
  3. package/dist/backend/routes/release.js +2 -2
  4. package/dist/backend/routes/seed.d.ts +6 -0
  5. package/dist/backend/routes/seed.js +96 -0
  6. package/dist/backend/routes/table.js +3 -0
  7. package/dist/backend/store/migrations/0004-release-publisher.d.ts +1 -0
  8. package/dist/backend/store/migrations/0004-release-publisher.js +15 -0
  9. package/dist/backend/store/migrations/index.js +3 -1
  10. package/dist/backend/store/queries.d.ts +9 -3
  11. package/dist/backend/store/queries.js +105 -11
  12. package/dist/cms.js +13 -16
  13. package/dist/components/admin-bar/admin-bar-internal.svelte +4 -4
  14. package/dist/components/admin-bar/admin-bar.css +1 -1
  15. package/dist/components/admin-bar/field-input.svelte +220 -0
  16. package/dist/components/admin-bar/field-input.svelte.d.ts +29 -0
  17. package/dist/components/admin-bar/history-panel.svelte +4 -5
  18. package/dist/components/admin-bar/icons/pencil.svelte +19 -0
  19. package/dist/components/admin-bar/icons/pencil.svelte.d.ts +3 -0
  20. package/dist/components/admin-bar/keyboard-shortcuts-dialog.svelte +1 -1
  21. package/dist/components/admin-bar/locales-panel.svelte +42 -1
  22. package/dist/components/admin-bar/page-config.d.ts +8 -1
  23. package/dist/components/admin-bar/page-config.js +15 -2
  24. package/dist/components/admin-bar/reorder-buttons.svelte +52 -0
  25. package/dist/components/admin-bar/reorder-buttons.svelte.d.ts +10 -0
  26. package/dist/components/admin-bar/seo-panel.svelte +144 -189
  27. package/dist/components/admin-bar/site-options-panel.svelte +115 -0
  28. package/dist/components/admin-bar/site-options-panel.svelte.d.ts +7 -0
  29. package/dist/components/admin-bar/structured-editor-popover.svelte +250 -0
  30. package/dist/components/admin-bar/structured-editor-popover.svelte.d.ts +48 -0
  31. package/dist/components/admin-bar/ui/checkbox/checkbox.svelte +36 -0
  32. package/dist/components/admin-bar/ui/checkbox/checkbox.svelte.d.ts +5 -0
  33. package/dist/components/admin-bar/ui/checkbox/index.d.ts +2 -0
  34. package/dist/components/admin-bar/ui/checkbox/index.js +2 -0
  35. package/dist/components/admin-bar/ui/popover/index.d.ts +5 -0
  36. package/dist/components/admin-bar/ui/popover/index.js +7 -0
  37. package/dist/components/admin-bar/ui/popover/popover-close.svelte +7 -0
  38. package/dist/components/admin-bar/ui/popover/popover-close.svelte.d.ts +4 -0
  39. package/dist/components/admin-bar/ui/popover/popover-content.svelte +35 -0
  40. package/dist/components/admin-bar/ui/popover/popover-content.svelte.d.ts +10 -0
  41. package/dist/components/admin-bar/ui/popover/popover-trigger.svelte +7 -0
  42. package/dist/components/admin-bar/ui/popover/popover-trigger.svelte.d.ts +4 -0
  43. package/dist/components/admin-bar/ui/popover/popover.svelte +7 -0
  44. package/dist/components/admin-bar/ui/popover/popover.svelte.d.ts +3 -0
  45. package/dist/components/admin-bar/ui/sheet/index.d.ts +10 -0
  46. package/dist/components/admin-bar/ui/sheet/index.js +12 -0
  47. package/dist/components/admin-bar/ui/sheet/sheet-close.svelte +7 -0
  48. package/dist/components/admin-bar/ui/sheet/sheet-close.svelte.d.ts +4 -0
  49. package/dist/components/admin-bar/ui/sheet/sheet-content.svelte +64 -0
  50. package/dist/components/admin-bar/ui/sheet/sheet-content.svelte.d.ts +17 -0
  51. package/dist/components/admin-bar/ui/sheet/sheet-description.svelte +17 -0
  52. package/dist/components/admin-bar/ui/sheet/sheet-description.svelte.d.ts +4 -0
  53. package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte +23 -0
  54. package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  55. package/dist/components/admin-bar/ui/sheet/sheet-header.svelte +20 -0
  56. package/dist/components/admin-bar/ui/sheet/sheet-header.svelte.d.ts +5 -0
  57. package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte +20 -0
  58. package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
  59. package/dist/components/admin-bar/ui/sheet/sheet-title.svelte +17 -0
  60. package/dist/components/admin-bar/ui/sheet/sheet-title.svelte.d.ts +4 -0
  61. package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte +7 -0
  62. package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
  63. package/dist/components/admin-bar/ui/sheet/sheet.svelte +7 -0
  64. package/dist/components/admin-bar/ui/sheet/sheet.svelte.d.ts +3 -0
  65. package/dist/components/admin-bar/ui/switch/index.d.ts +2 -0
  66. package/dist/components/admin-bar/ui/switch/index.js +2 -0
  67. package/dist/components/admin-bar/ui/switch/switch.svelte +27 -0
  68. package/dist/components/admin-bar/ui/switch/switch.svelte.d.ts +5 -0
  69. package/dist/components/admin-bar/ui/tabs/index.d.ts +5 -0
  70. package/dist/components/admin-bar/ui/tabs/index.js +7 -0
  71. package/dist/components/admin-bar/ui/tabs/tabs-content.svelte +17 -0
  72. package/dist/components/admin-bar/ui/tabs/tabs-content.svelte.d.ts +4 -0
  73. package/dist/components/admin-bar/ui/tabs/tabs-list.svelte +16 -0
  74. package/dist/components/admin-bar/ui/tabs/tabs-list.svelte.d.ts +4 -0
  75. package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte +20 -0
  76. package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  77. package/dist/components/admin-bar/ui/tabs/tabs.svelte +19 -0
  78. package/dist/components/admin-bar/ui/tabs/tabs.svelte.d.ts +4 -0
  79. package/dist/components/cms/cms-boolean.svelte +9 -18
  80. package/dist/components/cms/cms-boolean.svelte.d.ts +1 -0
  81. package/dist/components/cms/cms-date-time-editable.svelte +3 -3
  82. package/dist/components/cms/cms-date-time.svelte +17 -17
  83. package/dist/components/cms/cms-date-time.svelte.d.ts +2 -1
  84. package/dist/components/cms/cms-image-editable.svelte +13 -4
  85. package/dist/components/cms/cms-image.svelte +37 -30
  86. package/dist/components/cms/cms-image.svelte.d.ts +11 -4
  87. package/dist/components/cms/cms-link.svelte +26 -19
  88. package/dist/components/cms/cms-link.svelte.d.ts +2 -0
  89. package/dist/components/cms/cms-markdown.svelte +15 -16
  90. package/dist/components/cms/cms-markdown.svelte.d.ts +1 -0
  91. package/dist/components/cms/cms-number-editable.svelte +4 -2
  92. package/dist/components/cms/cms-number.svelte +18 -17
  93. package/dist/components/cms/cms-number.svelte.d.ts +2 -1
  94. package/dist/components/cms/cms-repeater.svelte +15 -18
  95. package/dist/components/cms/cms-repeater.svelte.d.ts +1 -0
  96. package/dist/components/cms/cms-rich-text.svelte +17 -22
  97. package/dist/components/cms/cms-rich-text.svelte.d.ts +1 -0
  98. package/dist/components/cms/cms-store.svelte.d.ts +14 -0
  99. package/dist/components/cms/cms-store.svelte.js +39 -2
  100. package/dist/components/cms/cms-text.svelte +23 -30
  101. package/dist/components/cms/cms-text.svelte.d.ts +3 -1
  102. package/dist/components/cms/scope.d.ts +25 -5
  103. package/dist/components/cms/structured-registry.d.ts +10 -0
  104. package/dist/components/cms/structured-registry.js +6 -0
  105. package/dist/components/cms/use-cms-field.svelte.d.ts +54 -0
  106. package/dist/components/cms/use-cms-field.svelte.js +83 -0
  107. package/dist/core/metadata.d.ts +44 -0
  108. package/dist/core/metadata.js +59 -0
  109. package/dist/core/structured.d.ts +108 -0
  110. package/dist/core/structured.js +184 -0
  111. package/dist/core/wire.d.ts +42 -2
  112. package/dist/editor.d.ts +22 -0
  113. package/dist/editor.js +21 -0
  114. package/dist/index.d.ts +12 -4
  115. package/dist/index.js +13 -3
  116. package/dist/server/load-cms.js +1 -0
  117. package/package.json +15 -6
  118. package/plugin/README.md +22 -2
  119. package/plugin/dist/build.d.ts +74 -0
  120. package/plugin/dist/build.js +210 -0
  121. package/plugin/dist/build.js.map +1 -0
  122. package/plugin/dist/index.d.ts +10 -1
  123. package/plugin/dist/index.js +63 -4
  124. package/plugin/dist/index.js.map +1 -1
  125. package/plugin/dist/manifest.d.ts +31 -0
  126. package/plugin/dist/manifest.js +146 -41
  127. package/plugin/dist/manifest.js.map +1 -1
  128. package/plugin/dist/parse-svelte.d.ts +15 -0
  129. package/plugin/dist/parse-svelte.js +26 -16
  130. package/plugin/dist/parse-svelte.js.map +1 -1
  131. package/dist/components/admin-bar/site-settings-panel.svelte +0 -225
  132. package/dist/components/admin-bar/site-settings-panel.svelte.d.ts +0 -7
@@ -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
  };
@@ -5,6 +5,16 @@ export type CmsScope = {
5
5
  routeId: string;
6
6
  params: Record<string, string>;
7
7
  };
8
+ /**
9
+ * One CMS component usage as recorded by the Vite plugin: which value it
10
+ * reads and which component type reads it.
11
+ */
12
+ export type CmsManifestUsage = {
13
+ name: string;
14
+ /** Canonical exported component name (`CmsText`, `CmsHours`, …). */
15
+ component: string;
16
+ preset?: string;
17
+ };
8
18
  export type CmsScopeEntry = {
9
19
  scopeId: string;
10
20
  kind: 'layout' | 'page';
@@ -15,23 +25,31 @@ export type CmsScopeEntry = {
15
25
  * `metadata` branch shows up here too (e.g. `'metadata.title'`).
16
26
  */
17
27
  fields: string[];
28
+ /** The usages behind `fields`, with component types. Absent on hand-built manifests. */
29
+ usages?: CmsManifestUsage[];
18
30
  };
19
31
  /**
20
- * Schema for the project-wide site scope. Authored on `createCms({ site })`.
21
- * Top-level keys are sections rendered as groups in the Site Settings panel;
22
- * each section's `fields` keys become path segments in the stored tree
23
- * (`branding.name`, `social.twitter`).
32
+ * Schema for the project-wide site scope: the **Site Options** panel.
33
+ * Authored on `createCms({ site })`. Top-level keys are sections rendered as
34
+ * groups; each section's `fields` keys become path segments in the stored
35
+ * tree (`seo.schemaType`, `seo.shareImage`).
36
+ *
37
+ * Site options are for values that never render on the page — the schema.org
38
+ * type, the default share image, enabled locales. Anything a visitor sees
39
+ * belongs in the root layout scope, edited where it renders.
24
40
  *
25
41
  * `type` selects the input widget. Storage is unconstrained — the schema is
26
42
  * a UI hint, not an enforced type for the underlying tree (to keep the data
27
43
  * model uniform with pages/layouts and avoid destructive prunes when fields
28
44
  * are added/removed).
29
45
  */
30
- export type SiteFieldType = 'text' | 'markdown' | 'number' | 'datetime' | 'url' | 'color' | 'image';
46
+ export type SiteFieldType = 'text' | 'markdown' | 'number' | 'datetime' | 'url' | 'color' | 'image' | 'enum' | 'boolean';
31
47
  export type SiteFieldSchema = {
32
48
  type: SiteFieldType;
33
49
  label: string;
34
50
  placeholder?: string;
51
+ /** Options for `enum` fields. */
52
+ values?: readonly string[];
35
53
  };
36
54
  export type SiteSectionSchema = {
37
55
  label: string;
@@ -104,6 +122,8 @@ export type CmsManifestScope = {
104
122
  * `metadata` branch shows up here too (e.g. `'metadata.title'`).
105
123
  */
106
124
  fields: string[];
125
+ /** The usages behind `fields`, with component types. Absent on hand-built manifests. */
126
+ usages?: CmsManifestUsage[];
107
127
  };
108
128
  export type CmsManifestRoute = {
109
129
  scopes: CmsManifestScope[];
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Which exported component reads which structured shape. Structured
3
+ * components register their schema at module load, so the Locales panel can
4
+ * count translatable strings for every usage the manifest recorded on the
5
+ * current route without importing the components themselves.
6
+ */
7
+ import type { Structured } from '../../core/structured.js';
8
+ import type { Tree } from '../../core/path.js';
9
+ export declare const registerStructured: <T extends Tree>(schema: Structured<T>) => Structured<T>;
10
+ export declare const getStructured: (component: string) => Structured<Tree> | undefined;
@@ -0,0 +1,6 @@
1
+ const registry = new Map();
2
+ export const registerStructured = (schema) => {
3
+ registry.set(schema.component, schema);
4
+ return schema;
5
+ };
6
+ export const getStructured = (component) => registry.get(component);
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The one piece of plumbing every display component shares: find the scope,
3
+ * read the value, decide whether it is editable, write it back.
4
+ *
5
+ * Scope resolution (`scope` prop):
6
+ * - absent → the enclosing route scope from context, installed by the plugin
7
+ * into every `+layout.svelte` / `+page.svelte`.
8
+ * - `'root'` → the root layout scope `layout:/`, so a page can read and write
9
+ * a value that lives site-wide (hours, contact, navigation, branding). The
10
+ * payload already carries every enclosing layout tree, and the root layout
11
+ * encloses everything.
12
+ * - any other string → that scope id verbatim (`layout:/(public)`), for a
13
+ * value owned by an intermediate layout. Only param-less scopes can be
14
+ * addressed this way.
15
+ *
16
+ * Read rule (see `core/structured.ts`): `undefined` means nothing is stored
17
+ * and the component shows its fallback; `null` means the editor cleared the
18
+ * field, and the component shows it empty. Never conflate the two — that is
19
+ * the bug where a cleared field reverts to the template's demo copy.
20
+ */
21
+ import { type CmsScope } from './scope.js';
22
+ import { type CmsScopeRef } from './cms-store.svelte.js';
23
+ export declare const ROOT_SCOPE_ID = "layout:/";
24
+ export type CmsFieldInput = {
25
+ name: string;
26
+ scope?: string;
27
+ /** Per-item override: disables editing and scope lookup. */
28
+ value?: unknown;
29
+ };
30
+ export declare const resolveScopeRef: (scope: string | undefined, context: CmsScope | undefined) => CmsScopeRef | null;
31
+ export type CmsField<T> = {
32
+ /** Resolved scope, or `null` outside any route scope. */
33
+ readonly ref: CmsScopeRef | null;
34
+ /** Raw stored value: `undefined` (nothing), `null` (cleared) or the value. */
35
+ readonly raw: unknown;
36
+ /** What to render, per the component's `read`. */
37
+ readonly current: T;
38
+ readonly editable: boolean;
39
+ readonly name: string;
40
+ /** Write the whole value into the draft. Pass `null` to clear. `locale`
41
+ * defaults to the locale being edited; structured editors pass the
42
+ * default locale for structure and another locale for its `$t` strings. */
43
+ set: (next: unknown, locale?: string) => void;
44
+ /** Write a value at a sub-path of this field, e.g. a `$t` string. */
45
+ setAt: (subPath: string, next: unknown, locale?: string) => void;
46
+ /** Read this field (or a sub-path of it) in a specific locale. */
47
+ getIn: (locale: string, subPath?: string) => unknown;
48
+ clear: () => void;
49
+ };
50
+ /**
51
+ * Call once at component init. `input` is an accessor so prop changes flow
52
+ * through; `read` turns the raw stored value into what the component renders.
53
+ */
54
+ export declare const useCmsField: <T>(input: () => CmsFieldInput, read: (raw: unknown) => T) => CmsField<T>;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The one piece of plumbing every display component shares: find the scope,
3
+ * read the value, decide whether it is editable, write it back.
4
+ *
5
+ * Scope resolution (`scope` prop):
6
+ * - absent → the enclosing route scope from context, installed by the plugin
7
+ * into every `+layout.svelte` / `+page.svelte`.
8
+ * - `'root'` → the root layout scope `layout:/`, so a page can read and write
9
+ * a value that lives site-wide (hours, contact, navigation, branding). The
10
+ * payload already carries every enclosing layout tree, and the root layout
11
+ * encloses everything.
12
+ * - any other string → that scope id verbatim (`layout:/(public)`), for a
13
+ * value owned by an intermediate layout. Only param-less scopes can be
14
+ * addressed this way.
15
+ *
16
+ * Read rule (see `core/structured.ts`): `undefined` means nothing is stored
17
+ * and the component shows its fallback; `null` means the editor cleared the
18
+ * field, and the component shows it empty. Never conflate the two — that is
19
+ * the bug where a cleared field reverts to the template's demo copy.
20
+ */
21
+ import { getCmsScope } from './scope.js';
22
+ import { cmsStore } from './cms-store.svelte.js';
23
+ export const ROOT_SCOPE_ID = 'layout:/';
24
+ export const resolveScopeRef = (scope, context) => {
25
+ if (scope === 'root')
26
+ return { scopeId: ROOT_SCOPE_ID, routeId: '/', params: {} };
27
+ if (scope) {
28
+ const colon = scope.indexOf(':');
29
+ const routeId = colon === -1 ? scope : scope.slice(colon + 1);
30
+ return { scopeId: colon === -1 ? `layout:${scope}` : scope, routeId, params: {} };
31
+ }
32
+ if (!context)
33
+ return null;
34
+ return { scopeId: context.scopeId, routeId: context.routeId, params: context.params };
35
+ };
36
+ /**
37
+ * Call once at component init. `input` is an accessor so prop changes flow
38
+ * through; `read` turns the raw stored value into what the component renders.
39
+ */
40
+ export const useCmsField = (input, read) => {
41
+ const context = getCmsScope();
42
+ const ref = $derived(resolveScopeRef(input().scope, context));
43
+ const raw = $derived.by(() => {
44
+ const { name, value } = input();
45
+ if (value !== undefined)
46
+ return value;
47
+ return ref ? cmsStore.getValue(ref, name) : undefined;
48
+ });
49
+ const current = $derived(read(raw));
50
+ const editable = $derived(cmsStore.isEditing && input().value === undefined && !!ref);
51
+ const set = (next, locale) => {
52
+ if (ref)
53
+ cmsStore.setValue(ref, input().name, next, locale);
54
+ };
55
+ const setAt = (subPath, next, locale) => {
56
+ if (ref)
57
+ cmsStore.setValue(ref, `${input().name}.${subPath}`, next, locale);
58
+ };
59
+ const getIn = (locale, subPath) => ref
60
+ ? cmsStore.getValue(ref, subPath ? `${input().name}.${subPath}` : input().name, locale)
61
+ : undefined;
62
+ return {
63
+ get ref() {
64
+ return ref;
65
+ },
66
+ get raw() {
67
+ return raw;
68
+ },
69
+ get current() {
70
+ return current;
71
+ },
72
+ get editable() {
73
+ return editable;
74
+ },
75
+ get name() {
76
+ return input().name;
77
+ },
78
+ set,
79
+ setAt,
80
+ getIn,
81
+ clear: () => set(null)
82
+ };
83
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Page metadata → `svelte-meta-tags` props.
3
+ *
4
+ * The page scope's `metadata` branch holds what the SEO panel edits (see
5
+ * `DEFAULT_METADATA_SCHEMA`). This maps it onto the object
6
+ * `definePageMetaTags` / `<MetaTags>` accept, so a template's root layout is
7
+ * one line: `deepMerge(baseMetaTags, toMetaTags(cms.metadata, { siteName }))`.
8
+ * Share fields fall back to the search fields, and the site name — read from
9
+ * the root layout's `branding.name` — supplies the title template and the
10
+ * Open Graph site name.
11
+ */
12
+ export type MetaTagsInput = Record<string, unknown>;
13
+ export type MetaTagsOptions = {
14
+ /** The root layout's `branding.name`; drives `titleTemplate` and `openGraph.siteName`. */
15
+ siteName?: string;
16
+ /** Title template when `siteName` is set. `%s` is the page title. Default `'%s · <siteName>'`. */
17
+ titleTemplate?: string;
18
+ /** Absolute URL of the page, for `openGraph.url` when no canonical is set. */
19
+ url?: string;
20
+ };
21
+ export type MetaTagsOutput = {
22
+ title?: string;
23
+ titleTemplate?: string;
24
+ description?: string;
25
+ canonical?: string;
26
+ robots?: string;
27
+ openGraph?: {
28
+ title?: string;
29
+ description?: string;
30
+ url?: string;
31
+ siteName?: string;
32
+ images?: Array<{
33
+ url: string;
34
+ alt?: string;
35
+ }>;
36
+ };
37
+ twitter?: {
38
+ cardType?: 'summary' | 'summary_large_image';
39
+ title?: string;
40
+ description?: string;
41
+ image?: string;
42
+ };
43
+ };
44
+ export declare const toMetaTags: (metadata: MetaTagsInput, options?: MetaTagsOptions) => MetaTagsOutput;
@@ -0,0 +1,59 @@
1
+ const str = (v) => (typeof v === 'string' && v !== '' ? v : undefined);
2
+ const imageUrl = (v) => {
3
+ if (typeof v === 'string')
4
+ return v ? { url: v } : undefined;
5
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
6
+ const r = v;
7
+ if (typeof r.url === 'string' && r.url) {
8
+ return typeof r.alt === 'string' && r.alt ? { url: r.url, alt: r.alt } : { url: r.url };
9
+ }
10
+ }
11
+ return undefined;
12
+ };
13
+ export const toMetaTags = (metadata, options = {}) => {
14
+ const title = str(metadata.title);
15
+ const description = str(metadata.description);
16
+ const ogTitle = str(metadata.ogTitle) ?? title;
17
+ const ogDescription = str(metadata.ogDescription) ?? description;
18
+ const image = imageUrl(metadata.ogImage);
19
+ const canonical = str(metadata.canonical);
20
+ const card = str(metadata.twitterCard);
21
+ const out = {};
22
+ if (title)
23
+ out.title = title;
24
+ if (options.siteName)
25
+ out.titleTemplate = options.titleTemplate ?? `%s · ${options.siteName}`;
26
+ if (description)
27
+ out.description = description;
28
+ if (canonical)
29
+ out.canonical = canonical;
30
+ if (metadata.noindex === true)
31
+ out.robots = 'noindex, nofollow';
32
+ const og = {};
33
+ if (ogTitle)
34
+ og.title = ogTitle;
35
+ if (ogDescription)
36
+ og.description = ogDescription;
37
+ if (canonical ?? options.url)
38
+ og.url = canonical ?? options.url;
39
+ if (options.siteName)
40
+ og.siteName = options.siteName;
41
+ if (image)
42
+ og.images = [image];
43
+ if (Object.keys(og).length > 0)
44
+ out.openGraph = og;
45
+ const tw = {};
46
+ if (card === 'summary' || card === 'summary_large_image')
47
+ tw.cardType = card;
48
+ else if (image)
49
+ tw.cardType = 'summary_large_image';
50
+ if (ogTitle)
51
+ tw.title = ogTitle;
52
+ if (ogDescription)
53
+ tw.description = ogDescription;
54
+ if (image)
55
+ tw.image = image.url;
56
+ if (Object.keys(tw).length > 0)
57
+ out.twitter = tw;
58
+ return out;
59
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Structured values: the shapes behind the editor components (hours, team,
3
+ * testimonials, …) and the two rules every one of them follows.
4
+ *
5
+ * 1. **Versioned shapes, whole-object writes.** A structured value is a plain
6
+ * object with a `v` version number. Components never patch a leaf inside
7
+ * it; they read the whole value through the schema's `normalize`, change
8
+ * it, and write the whole object back. A field the editor clears is
9
+ * written as `null`, never dropped: the draft and release trees deep-merge
10
+ * over the published tree, and a missing key merges to "unchanged" while
11
+ * `null` merges to "cleared". That is what stops a cleared field from
12
+ * reverting to the template's demo copy.
13
+ *
14
+ * 2. **Localisation is a string overlay, never a second structure.** The
15
+ * default locale stores the full value at `<name>`. Every other locale
16
+ * stores only strings, at `<name>.$t.<id>.<field>`, keyed by the stable
17
+ * `id` of the list item they translate (the reserved id `_` addresses the
18
+ * value's own root fields). Because objects deep-merge across locales,
19
+ * reading `es` yields the default structure plus the `es` overlay, and
20
+ * {@link applyTranslations} folds the strings in. Structure edits happen in
21
+ * the default locale only, so the item list can never fork between
22
+ * languages.
23
+ */
24
+ import { type Tree } from './path.js';
25
+ /** Branch under a structured value that holds a locale's string overlay. */
26
+ export declare const TRANSLATIONS_KEY = "$t";
27
+ /** Overlay id that addresses the structured value's root fields. */
28
+ export declare const ROOT_ID = "_";
29
+ /** `{ [itemId]: { [field]: string } }` — what a non-default locale stores. */
30
+ export type TranslationOverlay = Record<string, Record<string, string>>;
31
+ export type StructuredItem = {
32
+ id: string;
33
+ } & Record<string, unknown>;
34
+ export type StructuredSchema<T extends Tree> = {
35
+ /** Exported component name, e.g. `'CmsHours'`. Recorded in the manifest. */
36
+ component: string;
37
+ /** Current shape version, written as `v` on every value. */
38
+ version: number;
39
+ /** Root-level fields holding translatable strings, e.g. `['note']`. */
40
+ translatable: readonly string[];
41
+ /** The item list, when the value has one, and the item fields that translate. */
42
+ items?: {
43
+ key: string;
44
+ translatable: readonly string[];
45
+ };
46
+ /**
47
+ * Turn anything the store may hold — an older version, a hand-seeded
48
+ * partial object, garbage — into the current shape. Must never throw.
49
+ */
50
+ normalize: (raw: unknown) => T;
51
+ /** The cleared value: what an editor gets after "Clear". */
52
+ empty: () => T;
53
+ };
54
+ export type Structured<T extends Tree> = StructuredSchema<T> & {
55
+ /**
56
+ * Resolve a stored value the way a display component reads it:
57
+ * `undefined` (nothing stored) → the fallback; `null` (cleared) → empty;
58
+ * anything else → normalized, with the locale overlay applied.
59
+ */
60
+ read: (raw: unknown, fallback: T | undefined) => T;
61
+ };
62
+ export declare const isPlainObject: (v: unknown) => v is Tree;
63
+ export declare const defineStructured: <T extends Tree>(schema: StructuredSchema<T>) => Structured<T>;
64
+ /** Path of one translated string: `<name>.$t.<id>.<field>`. */
65
+ export declare const translationPath: (name: string, id: string, field: string) => string;
66
+ /** The `$t` branch of a stored value, when it is one. */
67
+ export declare const extractTranslations: (raw: unknown) => TranslationOverlay | undefined;
68
+ /** A copy of `raw` without its `$t` branch, so a whole-object write in the
69
+ * default locale never carries another locale's strings. */
70
+ export declare const stripTranslations: (raw: unknown) => unknown;
71
+ /**
72
+ * Fold a locale's string overlay into a normalized default-locale value.
73
+ * Root fields come from the `_` id; item fields from the item's own `id`.
74
+ * Only non-empty strings override, so an untranslated field shows the
75
+ * default-locale text rather than a blank.
76
+ */
77
+ export declare const applyTranslations: <T extends Tree>(value: T, overlay: TranslationOverlay | undefined, schema: Pick<StructuredSchema<T>, "translatable" | "items">) => T;
78
+ export type TranslatableField = {
79
+ /** Overlay id: an item id, or `_` for a root field. */
80
+ id: string;
81
+ field: string;
82
+ /** The default-locale string being translated. */
83
+ source: string;
84
+ };
85
+ /**
86
+ * Every translatable, non-empty string in a normalized default-locale value.
87
+ * Drives the popover's translation tab and the Locales panel's counts.
88
+ */
89
+ export declare const translatableFields: <T extends Tree>(value: T, schema: Pick<StructuredSchema<T>, "translatable" | "items">) => TranslatableField[];
90
+ /** How many translatable strings a locale's overlay still lacks. */
91
+ export declare const countTranslations: <T extends Tree>(value: T, overlay: TranslationOverlay | undefined, schema: Pick<StructuredSchema<T>, "translatable" | "items">) => {
92
+ total: number;
93
+ missing: number;
94
+ };
95
+ export declare const asString: (v: unknown, fallback?: string) => string;
96
+ export declare const asNumber: (v: unknown, fallback?: number | null) => number | null;
97
+ export declare const asBoolean: (v: unknown, fallback?: boolean) => boolean;
98
+ /**
99
+ * Normalize a list of items, guaranteeing every item carries a string `id`.
100
+ * An item without one (a hand-written seed, say) gets a deterministic
101
+ * `item-<index>` so reads stay stable until the editor next writes the list,
102
+ * at which point the ids are persisted.
103
+ */
104
+ export declare const asItems: <I extends StructuredItem>(raw: unknown, normalizeItem: (item: Tree, index: number) => Omit<I, "id">) => I[];
105
+ /** A short random id for a newly added item. Stable once written. */
106
+ export declare const newItemId: () => string;
107
+ /** Read a structured value's overlay for one locale out of a whole scope tree. */
108
+ export declare const overlayAt: (tree: Tree | undefined, name: string) => TranslationOverlay | undefined;