@velastack/cms 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +140 -28
  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 +239 -0
  12. package/dist/components/admin-bar/field-input.svelte.d.ts +30 -0
  13. package/dist/components/admin-bar/fields-form.svelte +218 -0
  14. package/dist/components/admin-bar/fields-form.svelte.d.ts +16 -0
  15. package/dist/components/admin-bar/icons/pencil.svelte +19 -0
  16. package/dist/components/admin-bar/icons/pencil.svelte.d.ts +3 -0
  17. package/dist/components/admin-bar/keyboard-shortcuts-dialog.svelte +1 -1
  18. package/dist/components/admin-bar/link-input.svelte +102 -0
  19. package/dist/components/admin-bar/link-input.svelte.d.ts +9 -0
  20. package/dist/components/admin-bar/list-field.svelte +93 -0
  21. package/dist/components/admin-bar/list-field.svelte.d.ts +20 -0
  22. package/dist/components/admin-bar/locales-panel.svelte +42 -1
  23. package/dist/components/admin-bar/page-config.d.ts +8 -1
  24. package/dist/components/admin-bar/page-config.js +15 -2
  25. package/dist/components/admin-bar/page-options.d.ts +24 -0
  26. package/dist/components/admin-bar/page-options.js +53 -0
  27. package/dist/components/admin-bar/reorder-buttons.svelte +52 -0
  28. package/dist/components/admin-bar/reorder-buttons.svelte.d.ts +10 -0
  29. package/dist/components/admin-bar/rich-text-input.svelte +92 -0
  30. package/dist/components/admin-bar/rich-text-input.svelte.d.ts +9 -0
  31. package/dist/components/admin-bar/seo-panel.svelte +144 -189
  32. package/dist/components/admin-bar/site-options-panel.svelte +115 -0
  33. package/dist/components/admin-bar/site-options-panel.svelte.d.ts +7 -0
  34. package/dist/components/admin-bar/structured-editor-popover.svelte +259 -0
  35. package/dist/components/admin-bar/structured-editor-popover.svelte.d.ts +48 -0
  36. package/dist/components/admin-bar/ui/checkbox/checkbox.svelte +36 -0
  37. package/dist/components/admin-bar/ui/checkbox/checkbox.svelte.d.ts +5 -0
  38. package/dist/components/admin-bar/ui/checkbox/index.d.ts +2 -0
  39. package/dist/components/admin-bar/ui/checkbox/index.js +2 -0
  40. package/dist/components/admin-bar/ui/popover/index.d.ts +5 -0
  41. package/dist/components/admin-bar/ui/popover/index.js +7 -0
  42. package/dist/components/admin-bar/ui/popover/popover-close.svelte +7 -0
  43. package/dist/components/admin-bar/ui/popover/popover-close.svelte.d.ts +4 -0
  44. package/dist/components/admin-bar/ui/popover/popover-content.svelte +35 -0
  45. package/dist/components/admin-bar/ui/popover/popover-content.svelte.d.ts +10 -0
  46. package/dist/components/admin-bar/ui/popover/popover-trigger.svelte +7 -0
  47. package/dist/components/admin-bar/ui/popover/popover-trigger.svelte.d.ts +4 -0
  48. package/dist/components/admin-bar/ui/popover/popover.svelte +7 -0
  49. package/dist/components/admin-bar/ui/popover/popover.svelte.d.ts +3 -0
  50. package/dist/components/admin-bar/ui/sheet/index.d.ts +10 -0
  51. package/dist/components/admin-bar/ui/sheet/index.js +12 -0
  52. package/dist/components/admin-bar/ui/sheet/sheet-close.svelte +7 -0
  53. package/dist/components/admin-bar/ui/sheet/sheet-close.svelte.d.ts +4 -0
  54. package/dist/components/admin-bar/ui/sheet/sheet-content.svelte +64 -0
  55. package/dist/components/admin-bar/ui/sheet/sheet-content.svelte.d.ts +17 -0
  56. package/dist/components/admin-bar/ui/sheet/sheet-description.svelte +17 -0
  57. package/dist/components/admin-bar/ui/sheet/sheet-description.svelte.d.ts +4 -0
  58. package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte +23 -0
  59. package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  60. package/dist/components/admin-bar/ui/sheet/sheet-header.svelte +20 -0
  61. package/dist/components/admin-bar/ui/sheet/sheet-header.svelte.d.ts +5 -0
  62. package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte +20 -0
  63. package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
  64. package/dist/components/admin-bar/ui/sheet/sheet-title.svelte +17 -0
  65. package/dist/components/admin-bar/ui/sheet/sheet-title.svelte.d.ts +4 -0
  66. package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte +7 -0
  67. package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
  68. package/dist/components/admin-bar/ui/sheet/sheet.svelte +7 -0
  69. package/dist/components/admin-bar/ui/sheet/sheet.svelte.d.ts +3 -0
  70. package/dist/components/admin-bar/ui/switch/index.d.ts +2 -0
  71. package/dist/components/admin-bar/ui/switch/index.js +2 -0
  72. package/dist/components/admin-bar/ui/switch/switch.svelte +27 -0
  73. package/dist/components/admin-bar/ui/switch/switch.svelte.d.ts +5 -0
  74. package/dist/components/admin-bar/ui/tabs/index.d.ts +5 -0
  75. package/dist/components/admin-bar/ui/tabs/index.js +7 -0
  76. package/dist/components/admin-bar/ui/tabs/tabs-content.svelte +17 -0
  77. package/dist/components/admin-bar/ui/tabs/tabs-content.svelte.d.ts +4 -0
  78. package/dist/components/admin-bar/ui/tabs/tabs-list.svelte +16 -0
  79. package/dist/components/admin-bar/ui/tabs/tabs-list.svelte.d.ts +4 -0
  80. package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte +20 -0
  81. package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  82. package/dist/components/admin-bar/ui/tabs/tabs.svelte +19 -0
  83. package/dist/components/admin-bar/ui/tabs/tabs.svelte.d.ts +4 -0
  84. package/dist/components/cms/cms-boolean.svelte +9 -18
  85. package/dist/components/cms/cms-boolean.svelte.d.ts +1 -0
  86. package/dist/components/cms/cms-collection.svelte +46 -0
  87. package/dist/components/cms/cms-collection.svelte.d.ts +25 -0
  88. package/dist/components/cms/cms-contact.svelte +26 -0
  89. package/dist/components/cms/cms-contact.svelte.d.ts +14 -0
  90. package/dist/components/cms/cms-date-time-editable.svelte +3 -3
  91. package/dist/components/cms/cms-date-time.svelte +17 -17
  92. package/dist/components/cms/cms-date-time.svelte.d.ts +2 -1
  93. package/dist/components/cms/cms-faq.svelte +22 -0
  94. package/dist/components/cms/cms-faq.svelte.d.ts +15 -0
  95. package/dist/components/cms/cms-gallery.svelte +31 -0
  96. package/dist/components/cms/cms-gallery.svelte.d.ts +16 -0
  97. package/dist/components/cms/cms-hours-editable.svelte +160 -0
  98. package/dist/components/cms/cms-hours-editable.svelte.d.ts +17 -0
  99. package/dist/components/cms/cms-hours.svelte +42 -0
  100. package/dist/components/cms/cms-hours.svelte.d.ts +19 -0
  101. package/dist/components/cms/cms-image-editable.svelte +13 -4
  102. package/dist/components/cms/cms-image.svelte +19 -34
  103. package/dist/components/cms/cms-image.svelte.d.ts +7 -8
  104. package/dist/components/cms/cms-link-editable.svelte +4 -60
  105. package/dist/components/cms/cms-link.svelte +15 -28
  106. package/dist/components/cms/cms-link.svelte.d.ts +4 -9
  107. package/dist/components/cms/cms-logos.svelte +29 -0
  108. package/dist/components/cms/cms-logos.svelte.d.ts +16 -0
  109. package/dist/components/cms/cms-markdown.svelte +15 -16
  110. package/dist/components/cms/cms-markdown.svelte.d.ts +1 -0
  111. package/dist/components/cms/cms-nav.svelte +22 -0
  112. package/dist/components/cms/cms-nav.svelte.d.ts +15 -0
  113. package/dist/components/cms/cms-number-editable.svelte +4 -2
  114. package/dist/components/cms/cms-number.svelte +18 -17
  115. package/dist/components/cms/cms-number.svelte.d.ts +2 -1
  116. package/dist/components/cms/cms-pricing.svelte +36 -0
  117. package/dist/components/cms/cms-pricing.svelte.d.ts +14 -0
  118. package/dist/components/cms/cms-rich-text.svelte +17 -22
  119. package/dist/components/cms/cms-rich-text.svelte.d.ts +1 -0
  120. package/dist/components/cms/cms-schedule.svelte +35 -0
  121. package/dist/components/cms/cms-schedule.svelte.d.ts +16 -0
  122. package/dist/components/cms/cms-social-links.svelte +35 -0
  123. package/dist/components/cms/cms-social-links.svelte.d.ts +15 -0
  124. package/dist/components/cms/cms-stats.svelte +31 -0
  125. package/dist/components/cms/cms-stats.svelte.d.ts +16 -0
  126. package/dist/components/cms/cms-steps.svelte +31 -0
  127. package/dist/components/cms/cms-steps.svelte.d.ts +16 -0
  128. package/dist/components/cms/cms-store.svelte.d.ts +14 -0
  129. package/dist/components/cms/cms-store.svelte.js +39 -2
  130. package/dist/components/cms/cms-structured-editable.svelte +29 -0
  131. package/dist/components/cms/{cms-repeater-editable.svelte.d.ts → cms-structured-editable.svelte.d.ts} +12 -8
  132. package/dist/components/cms/cms-structured.svelte +50 -0
  133. package/dist/components/cms/cms-structured.svelte.d.ts +49 -0
  134. package/dist/components/cms/cms-team.svelte +22 -0
  135. package/dist/components/cms/cms-team.svelte.d.ts +15 -0
  136. package/dist/components/cms/cms-testimonials.svelte +36 -0
  137. package/dist/components/cms/cms-testimonials.svelte.d.ts +15 -0
  138. package/dist/components/cms/cms-text.svelte +23 -30
  139. package/dist/components/cms/cms-text.svelte.d.ts +3 -1
  140. package/dist/components/cms/cms-timeline.svelte +35 -0
  141. package/dist/components/cms/cms-timeline.svelte.d.ts +16 -0
  142. package/dist/components/cms/index.d.ts +15 -1
  143. package/dist/components/cms/index.js +15 -1
  144. package/dist/components/cms/scope.d.ts +25 -5
  145. package/dist/components/cms/use-cms-field.svelte.d.ts +54 -0
  146. package/dist/components/cms/use-cms-field.svelte.js +83 -0
  147. package/dist/core/metadata.d.ts +44 -0
  148. package/dist/core/metadata.js +59 -0
  149. package/dist/core/shapes/collection.d.ts +60 -0
  150. package/dist/core/shapes/collection.js +194 -0
  151. package/dist/core/shapes/contact.d.ts +70 -0
  152. package/dist/core/shapes/contact.js +214 -0
  153. package/dist/core/shapes/faq.d.ts +12 -0
  154. package/dist/core/shapes/faq.js +48 -0
  155. package/dist/core/shapes/form.d.ts +46 -0
  156. package/dist/core/shapes/form.js +7 -0
  157. package/dist/core/shapes/hours.d.ts +91 -0
  158. package/dist/core/shapes/hours.js +325 -0
  159. package/dist/core/shapes/image.d.ts +15 -0
  160. package/dist/core/shapes/image.js +27 -0
  161. package/dist/core/shapes/index.d.ts +31 -0
  162. package/dist/core/shapes/index.js +13 -0
  163. package/dist/core/shapes/link.d.ts +15 -0
  164. package/dist/core/shapes/link.js +42 -0
  165. package/dist/core/shapes/nav.d.ts +21 -0
  166. package/dist/core/shapes/nav.js +71 -0
  167. package/dist/core/shapes/presets.d.ts +65 -0
  168. package/dist/core/shapes/presets.js +152 -0
  169. package/dist/core/shapes/pricing.d.ts +40 -0
  170. package/dist/core/shapes/pricing.js +115 -0
  171. package/dist/core/shapes/social-links.d.ts +19 -0
  172. package/dist/core/shapes/social-links.js +83 -0
  173. package/dist/core/shapes/team.d.ts +22 -0
  174. package/dist/core/shapes/team.js +70 -0
  175. package/dist/core/shapes/testimonials.d.ts +24 -0
  176. package/dist/core/shapes/testimonials.js +86 -0
  177. package/dist/core/structured-registry.d.ts +10 -0
  178. package/dist/core/structured-registry.js +6 -0
  179. package/dist/core/structured.d.ts +131 -0
  180. package/dist/core/structured.js +235 -0
  181. package/dist/core/wire.d.ts +31 -1
  182. package/dist/editor.d.ts +26 -0
  183. package/dist/editor.js +25 -0
  184. package/dist/index.d.ts +26 -4
  185. package/dist/index.js +31 -2
  186. package/dist/server/load-cms.js +1 -0
  187. package/package.json +12 -3
  188. package/plugin/README.md +22 -2
  189. package/plugin/dist/build.d.ts +74 -0
  190. package/plugin/dist/build.js +210 -0
  191. package/plugin/dist/build.js.map +1 -0
  192. package/plugin/dist/index.d.ts +10 -1
  193. package/plugin/dist/index.js +63 -4
  194. package/plugin/dist/index.js.map +1 -1
  195. package/plugin/dist/manifest.d.ts +31 -0
  196. package/plugin/dist/manifest.js +146 -41
  197. package/plugin/dist/manifest.js.map +1 -1
  198. package/plugin/dist/parse-svelte.d.ts +15 -0
  199. package/plugin/dist/parse-svelte.js +26 -16
  200. package/plugin/dist/parse-svelte.js.map +1 -1
  201. package/dist/components/admin-bar/site-settings-panel.svelte +0 -225
  202. package/dist/components/admin-bar/site-settings-panel.svelte.d.ts +0 -7
  203. package/dist/components/cms/cms-repeater-editable.svelte +0 -137
  204. package/dist/components/cms/cms-repeater.svelte +0 -55
  205. package/dist/components/cms/cms-repeater.svelte.d.ts +0 -30
@@ -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,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,60 @@
1
+ /**
2
+ * `CmsCollection`: services, rooms, programmes, tours, projects, case
3
+ * studies. One fixed core plus a `details` key-value list for kind-specific
4
+ * facts. Lists only: an item's `link` covers "read more".
5
+ */
6
+ import type { Tree } from '../path.js';
7
+ import { type ListValue } from './form.js';
8
+ import { type CmsImageValue } from './image.js';
9
+ import { type CmsLinkValue } from './link.js';
10
+ export type CollectionDetail = {
11
+ id: string;
12
+ label: string;
13
+ value: string;
14
+ };
15
+ export type CollectionPrice = {
16
+ amount: number | null;
17
+ /** ISO 4217, e.g. `EUR`. */
18
+ currency: string;
19
+ /** `night`, `person`, `hour`… shown after the amount. */
20
+ period: string;
21
+ /** `from`, shown before the amount. */
22
+ prefix: string;
23
+ };
24
+ export type CollectionItem = {
25
+ id: string;
26
+ title: string;
27
+ summary: string;
28
+ body: string;
29
+ image: CmsImageValue | null;
30
+ gallery: CmsImageValue[];
31
+ price: CollectionPrice;
32
+ duration: string;
33
+ link: CmsLinkValue | null;
34
+ tags: string[];
35
+ features: string[];
36
+ featured: boolean;
37
+ hidden: boolean;
38
+ details: CollectionDetail[];
39
+ };
40
+ export type CmsCollectionValue = ListValue<CollectionItem>;
41
+ export type CollectionItemView = CollectionItem & {
42
+ href: string | null;
43
+ };
44
+ export declare const cmsCollection: import("./form.js").FormShape<CmsCollectionValue>;
45
+ export type CollectionSlice = {
46
+ /** At most this many items. */
47
+ limit?: number;
48
+ /** Only featured items. */
49
+ featured?: boolean;
50
+ /** Only items carrying this tag. */
51
+ tag?: string;
52
+ };
53
+ /** Visible items (not hidden, with a title), optionally sliced. */
54
+ export declare const collectionView: (value: CmsCollectionValue, slice?: CollectionSlice) => CollectionItemView[];
55
+ /** Every tag in use, in first-seen order. */
56
+ export declare const collectionTags: (value: CmsCollectionValue) => string[];
57
+ /** `from €120 / night`, or empty when there is no amount. */
58
+ export declare const formatCollectionPrice: (price: CollectionPrice, locale: string) => string;
59
+ /** A schema.org `ItemList` of the visible items. */
60
+ export declare const toItemList: (value: CmsCollectionValue, baseUrl?: string) => Tree;
@@ -0,0 +1,194 @@
1
+ import { asBoolean, asItems, asNumber, asString, asStrings, isPlainObject } from '../structured.js';
2
+ import { defineForm } from './form.js';
3
+ import { asImage, asImages } from './image.js';
4
+ import { asLink, linkHref } from './link.js';
5
+ export const cmsCollection = defineForm({
6
+ component: 'CmsCollection',
7
+ label: 'Collection',
8
+ version: 1,
9
+ translatable: [],
10
+ items: {
11
+ key: 'items',
12
+ translatable: [
13
+ 'title',
14
+ 'summary',
15
+ 'body',
16
+ 'duration',
17
+ 'tags',
18
+ 'features',
19
+ 'price.period',
20
+ 'price.prefix'
21
+ ],
22
+ items: { key: 'details', translatable: ['label', 'value'] }
23
+ },
24
+ normalize: (raw) => {
25
+ const items = Array.isArray(raw) ? raw : isPlainObject(raw) ? raw.items : [];
26
+ return {
27
+ v: 1,
28
+ items: asItems(items, (r) => {
29
+ const p = isPlainObject(r.price) ? r.price : {};
30
+ return {
31
+ title: asString(r.title),
32
+ summary: asString(r.summary),
33
+ body: asString(r.body),
34
+ image: asImage(r.image),
35
+ gallery: asImages(r.gallery),
36
+ price: {
37
+ amount: asNumber(p.amount),
38
+ currency: asString(p.currency),
39
+ period: asString(p.period),
40
+ prefix: asString(p.prefix)
41
+ },
42
+ duration: asString(r.duration),
43
+ link: asLink(r.link),
44
+ tags: asStrings(r.tags).filter((t) => t !== ''),
45
+ features: asStrings(r.features).filter((f) => f !== ''),
46
+ featured: asBoolean(r.featured),
47
+ hidden: asBoolean(r.hidden),
48
+ details: asItems(r.details, (d) => ({
49
+ label: asString(d.label),
50
+ value: asString(d.value)
51
+ }))
52
+ };
53
+ })
54
+ };
55
+ },
56
+ empty: () => ({ v: 1, items: [] }),
57
+ fields: [
58
+ {
59
+ key: 'items',
60
+ label: 'Items',
61
+ type: 'list',
62
+ itemLabel: 'item',
63
+ titleKey: 'title',
64
+ blank: () => ({
65
+ title: '',
66
+ summary: '',
67
+ body: '',
68
+ image: null,
69
+ gallery: [],
70
+ price: { amount: null, currency: '', period: '', prefix: '' },
71
+ duration: '',
72
+ link: null,
73
+ tags: [],
74
+ features: [],
75
+ featured: false,
76
+ hidden: false,
77
+ details: []
78
+ }),
79
+ fields: [
80
+ { key: 'title', label: 'Title', type: 'text', group: 'Details' },
81
+ { key: 'summary', label: 'Summary', type: 'long-string', group: 'Details' },
82
+ { key: 'body', label: 'Body', type: 'html', group: 'Details' },
83
+ {
84
+ key: 'duration',
85
+ label: 'Duration',
86
+ type: 'text',
87
+ half: true,
88
+ placeholder: '3 hours',
89
+ group: 'Details'
90
+ },
91
+ { key: 'link', label: 'Link', type: 'link', group: 'Details' },
92
+ {
93
+ key: 'tags',
94
+ label: 'Tags',
95
+ type: 'strings',
96
+ placeholder: 'One per line',
97
+ group: 'Details'
98
+ },
99
+ {
100
+ key: 'features',
101
+ label: 'Features',
102
+ type: 'strings',
103
+ placeholder: 'One per line',
104
+ group: 'Details'
105
+ },
106
+ {
107
+ key: 'details',
108
+ label: 'Details',
109
+ type: 'list',
110
+ itemLabel: 'detail',
111
+ titleKey: 'label',
112
+ group: 'Details',
113
+ blank: () => ({ label: '', value: '' }),
114
+ fields: [
115
+ { key: 'label', label: 'Label', type: 'text', half: true, placeholder: 'Sleeps' },
116
+ { key: 'value', label: 'Value', type: 'text', placeholder: '4' }
117
+ ]
118
+ },
119
+ { key: 'featured', label: 'Featured', type: 'boolean', group: 'Details' },
120
+ { key: 'hidden', label: 'Hidden', type: 'boolean', group: 'Details' },
121
+ { key: 'image', label: 'Image', type: 'image', group: 'Media' },
122
+ { key: 'gallery', label: 'Gallery', type: 'images', group: 'Media' },
123
+ {
124
+ key: 'price.prefix',
125
+ label: 'Prefix',
126
+ type: 'text',
127
+ half: true,
128
+ placeholder: 'from',
129
+ group: 'Pricing'
130
+ },
131
+ { key: 'price.amount', label: 'Amount', type: 'number', group: 'Pricing' },
132
+ {
133
+ key: 'price.currency',
134
+ label: 'Currency',
135
+ type: 'text',
136
+ half: true,
137
+ placeholder: 'EUR',
138
+ group: 'Pricing'
139
+ },
140
+ { key: 'price.period', label: 'Per', type: 'text', placeholder: 'night', group: 'Pricing' }
141
+ ]
142
+ }
143
+ ]
144
+ });
145
+ /** Visible items (not hidden, with a title), optionally sliced. */
146
+ export const collectionView = (value, slice = {}) => {
147
+ let items = value.items.filter((i) => !i.hidden && i.title !== '');
148
+ if (slice.featured)
149
+ items = items.filter((i) => i.featured);
150
+ if (slice.tag)
151
+ items = items.filter((i) => i.tags.includes(slice.tag));
152
+ if (slice.limit !== undefined && slice.limit >= 0)
153
+ items = items.slice(0, slice.limit);
154
+ return items.map((i) => ({ ...i, href: i.link ? linkHref(i.link) || null : null }));
155
+ };
156
+ /** Every tag in use, in first-seen order. */
157
+ export const collectionTags = (value) => {
158
+ const out = [];
159
+ for (const item of value.items)
160
+ for (const t of item.tags)
161
+ if (!out.includes(t))
162
+ out.push(t);
163
+ return out;
164
+ };
165
+ /** `from €120 / night`, or empty when there is no amount. */
166
+ export const formatCollectionPrice = (price, locale) => {
167
+ if (price.amount === null)
168
+ return '';
169
+ let amount;
170
+ try {
171
+ amount = price.currency
172
+ ? new Intl.NumberFormat(locale, {
173
+ style: 'currency',
174
+ currency: price.currency,
175
+ maximumFractionDigits: Number.isInteger(price.amount) ? 0 : 2
176
+ }).format(price.amount)
177
+ : new Intl.NumberFormat(locale).format(price.amount);
178
+ }
179
+ catch {
180
+ amount = `${price.currency} ${price.amount}`.trim();
181
+ }
182
+ return [price.prefix, amount, price.period ? `/ ${price.period}` : ''].filter(Boolean).join(' ');
183
+ };
184
+ /** A schema.org `ItemList` of the visible items. */
185
+ export const toItemList = (value, baseUrl = '') => ({
186
+ '@context': 'https://schema.org',
187
+ '@type': 'ItemList',
188
+ itemListElement: collectionView(value).map((i, n) => {
189
+ const item = { '@type': 'ListItem', position: n + 1, name: i.title };
190
+ if (i.href)
191
+ item.url = baseUrl + i.href;
192
+ return item;
193
+ })
194
+ });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `CmsContact`: where and how to reach the business. Labels translate;
3
+ * addresses and numbers do not. `contactView` adds the hrefs and the
4
+ * formatted address a template renders.
5
+ */
6
+ import type { Tree } from '../path.js';
7
+ export type ContactPhone = {
8
+ id: string;
9
+ label: string;
10
+ number: string;
11
+ };
12
+ export type ContactEmail = {
13
+ id: string;
14
+ label: string;
15
+ address: string;
16
+ };
17
+ export type ContactAddress = {
18
+ lines: string[];
19
+ locality: string;
20
+ region: string;
21
+ postcode: string;
22
+ country: string;
23
+ };
24
+ export type ContactLabels = {
25
+ address: string;
26
+ phone: string;
27
+ email: string;
28
+ whatsapp: string;
29
+ directions: string;
30
+ };
31
+ export type CmsContactValue = {
32
+ v: 1;
33
+ name: string;
34
+ address: ContactAddress;
35
+ phones: ContactPhone[];
36
+ emails: ContactEmail[];
37
+ whatsapp: string;
38
+ directionsUrl: string;
39
+ geo: {
40
+ lat: number;
41
+ lng: number;
42
+ } | null;
43
+ labels: ContactLabels;
44
+ };
45
+ export declare const DEFAULT_CONTACT_LABELS: ContactLabels;
46
+ export declare const cmsContact: import("./form.js").FormShape<CmsContactValue>;
47
+ export declare const telHref: (number: string) => string;
48
+ export declare const whatsappHref: (number: string) => string;
49
+ /** Address lines in the country's conventional order, empty parts dropped. */
50
+ export declare const formatAddress: (address: ContactAddress) => string[];
51
+ export type ContactView = Omit<CmsContactValue, 'phones' | 'emails'> & {
52
+ phones: Array<ContactPhone & {
53
+ href: string;
54
+ }>;
55
+ emails: Array<ContactEmail & {
56
+ href: string;
57
+ }>;
58
+ /** First phone's `tel:` href, or `null`. */
59
+ telHref: string | null;
60
+ /** First email's `mailto:`, or `null`. */
61
+ mailto: string | null;
62
+ whatsappHref: string | null;
63
+ /** `directionsUrl`, else a Google Maps link from `geo` or the address. */
64
+ mapsHref: string | null;
65
+ formattedAddress: string[];
66
+ hasAddress: boolean;
67
+ };
68
+ export declare const contactView: (value: CmsContactValue) => ContactView;
69
+ /** A schema.org `PostalAddress` node, or `null` without an address. */
70
+ export declare const toPostalAddress: (value: CmsContactValue) => Tree | null;