@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
@@ -0,0 +1,184 @@
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 { get } from './path.js';
25
+ /** Branch under a structured value that holds a locale's string overlay. */
26
+ export const TRANSLATIONS_KEY = '$t';
27
+ /** Overlay id that addresses the structured value's root fields. */
28
+ export const ROOT_ID = '_';
29
+ export const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
30
+ export const defineStructured = (schema) => ({
31
+ ...schema,
32
+ read: (raw, fallback) => {
33
+ if (raw === undefined)
34
+ return fallback === undefined ? schema.empty() : fallback;
35
+ if (raw === null)
36
+ return schema.empty();
37
+ const overlay = extractTranslations(raw);
38
+ const value = schema.normalize(stripTranslations(raw));
39
+ return overlay ? applyTranslations(value, overlay, schema) : value;
40
+ }
41
+ });
42
+ /** Path of one translated string: `<name>.$t.<id>.<field>`. */
43
+ export const translationPath = (name, id, field) => `${name}.${TRANSLATIONS_KEY}.${id}.${field}`;
44
+ /** The `$t` branch of a stored value, when it is one. */
45
+ export const extractTranslations = (raw) => {
46
+ if (!isPlainObject(raw))
47
+ return undefined;
48
+ const t = raw[TRANSLATIONS_KEY];
49
+ if (!isPlainObject(t))
50
+ return undefined;
51
+ const out = {};
52
+ for (const [id, fields] of Object.entries(t)) {
53
+ if (!isPlainObject(fields))
54
+ continue;
55
+ const strings = {};
56
+ for (const [field, v] of Object.entries(fields)) {
57
+ if (typeof v === 'string')
58
+ strings[field] = v;
59
+ }
60
+ out[id] = strings;
61
+ }
62
+ return out;
63
+ };
64
+ /** A copy of `raw` without its `$t` branch, so a whole-object write in the
65
+ * default locale never carries another locale's strings. */
66
+ export const stripTranslations = (raw) => {
67
+ if (!isPlainObject(raw) || !(TRANSLATIONS_KEY in raw))
68
+ return raw;
69
+ const { [TRANSLATIONS_KEY]: _omit, ...rest } = raw;
70
+ return rest;
71
+ };
72
+ const overlayString = (overlay, id, field) => {
73
+ const v = overlay[id]?.[field];
74
+ return typeof v === 'string' && v !== '' ? v : undefined;
75
+ };
76
+ /**
77
+ * Fold a locale's string overlay into a normalized default-locale value.
78
+ * Root fields come from the `_` id; item fields from the item's own `id`.
79
+ * Only non-empty strings override, so an untranslated field shows the
80
+ * default-locale text rather than a blank.
81
+ */
82
+ export const applyTranslations = (value, overlay, schema) => {
83
+ if (!overlay)
84
+ return value;
85
+ const out = { ...value };
86
+ for (const field of schema.translatable) {
87
+ const s = overlayString(overlay, ROOT_ID, field);
88
+ if (s !== undefined)
89
+ out[field] = s;
90
+ }
91
+ if (schema.items) {
92
+ const list = value[schema.items.key];
93
+ if (Array.isArray(list)) {
94
+ out[schema.items.key] = list.map((item) => {
95
+ if (!isPlainObject(item) || typeof item.id !== 'string')
96
+ return item;
97
+ const next = { ...item };
98
+ for (const field of schema.items.translatable) {
99
+ const s = overlayString(overlay, item.id, field);
100
+ if (s !== undefined)
101
+ next[field] = s;
102
+ }
103
+ return next;
104
+ });
105
+ }
106
+ }
107
+ return out;
108
+ };
109
+ /**
110
+ * Every translatable, non-empty string in a normalized default-locale value.
111
+ * Drives the popover's translation tab and the Locales panel's counts.
112
+ */
113
+ export const translatableFields = (value, schema) => {
114
+ const out = [];
115
+ for (const field of schema.translatable) {
116
+ const s = value[field];
117
+ if (typeof s === 'string' && s !== '')
118
+ out.push({ id: ROOT_ID, field, source: s });
119
+ }
120
+ if (schema.items) {
121
+ const list = value[schema.items.key];
122
+ if (Array.isArray(list)) {
123
+ for (const item of list) {
124
+ if (!isPlainObject(item) || typeof item.id !== 'string')
125
+ continue;
126
+ for (const field of schema.items.translatable) {
127
+ const s = item[field];
128
+ if (typeof s === 'string' && s !== '')
129
+ out.push({ id: item.id, field, source: s });
130
+ }
131
+ }
132
+ }
133
+ }
134
+ return out;
135
+ };
136
+ /** How many translatable strings a locale's overlay still lacks. */
137
+ export const countTranslations = (value, overlay, schema) => {
138
+ const fields = translatableFields(value, schema);
139
+ let missing = 0;
140
+ for (const f of fields) {
141
+ if (!overlay || overlayString(overlay, f.id, f.field) === undefined)
142
+ missing += 1;
143
+ }
144
+ return { total: fields.length, missing };
145
+ };
146
+ // ---------------------------------------------------------------------------
147
+ // Normalizer building blocks
148
+ // ---------------------------------------------------------------------------
149
+ export const asString = (v, fallback = '') => typeof v === 'string' ? v : fallback;
150
+ export const asNumber = (v, fallback = null) => typeof v === 'number' && Number.isFinite(v) ? v : fallback;
151
+ export const asBoolean = (v, fallback = false) => typeof v === 'boolean' ? v : fallback;
152
+ /**
153
+ * Normalize a list of items, guaranteeing every item carries a string `id`.
154
+ * An item without one (a hand-written seed, say) gets a deterministic
155
+ * `item-<index>` so reads stay stable until the editor next writes the list,
156
+ * at which point the ids are persisted.
157
+ */
158
+ export const asItems = (raw, normalizeItem) => {
159
+ if (!Array.isArray(raw))
160
+ return [];
161
+ const out = [];
162
+ raw.forEach((entry, index) => {
163
+ if (!isPlainObject(entry))
164
+ return;
165
+ const id = typeof entry.id === 'string' && entry.id !== '' ? entry.id : `item-${index}`;
166
+ out.push({ id, ...normalizeItem(entry, index) });
167
+ });
168
+ return out;
169
+ };
170
+ /** A short random id for a newly added item. Stable once written. */
171
+ export const newItemId = () => {
172
+ const bytes = new Uint8Array(6);
173
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues)
174
+ crypto.getRandomValues(bytes);
175
+ else
176
+ for (let i = 0; i < bytes.length; i++)
177
+ bytes[i] = Math.floor(Math.random() * 256);
178
+ let out = '';
179
+ for (const b of bytes)
180
+ out += b.toString(16).padStart(2, '0');
181
+ return out;
182
+ };
183
+ /** Read a structured value's overlay for one locale out of a whole scope tree. */
184
+ export const overlayAt = (tree, name) => extractTranslations(get(tree, name));
@@ -11,7 +11,7 @@
11
11
  * `published_at` stay snake_case because that is what the JSON carries.
12
12
  */
13
13
  import type { Tree } from './path.js';
14
- import type { PageDeleteOutcome } from './page-entry.js';
14
+ import type { PageDeleteOutcome, PageEntry } from './page-entry.js';
15
15
  export type { PageDeleteOutcome };
16
16
  /** One pending change in a release, without the bookkeeping the store adds. */
17
17
  export type ReleaseItemCore = {
@@ -54,10 +54,20 @@ export type PublishedReleaseItem = ReleaseItemCore & {
54
54
  priorTree: Tree | null;
55
55
  priorTombstone?: PageDeleteOutcome;
56
56
  };
57
+ /**
58
+ * The editor who published a release, captured at publish time. `email` and
59
+ * `name` are null only on rows published before they were recorded, where the
60
+ * account no longer resolves.
61
+ */
62
+ export type ReleasePublisher = {
63
+ id: string;
64
+ email: string | null;
65
+ name: string | null;
66
+ };
57
67
  export type PublishedRelease = {
58
68
  id: string;
59
69
  name?: string;
60
- publishedBy: string;
70
+ publishedBy: ReleasePublisher;
61
71
  publishedAt: string;
62
72
  preview_key: string;
63
73
  items: PublishedReleaseItem[];
@@ -172,6 +182,36 @@ export type CmsDeployState = {
172
182
  };
173
183
  latest?: CmsDeployRun | null;
174
184
  };
185
+ /**
186
+ * A project's published content in one object: `layouts` and `pages` keyed
187
+ * `[locale][routeId]`, plus the non-localised `site` tree. `POST /seed`
188
+ * accepts it and `GET /export` returns it, so a template's published
189
+ * `content/` can seed a fresh project and a project can be dumped back into
190
+ * the same shape. A page keyed by route id may be a bare tree (a static page,
191
+ * `params: {}`) or a full `PageEntry[]`.
192
+ */
193
+ export type CmsSeed = {
194
+ layouts?: Record<string, Record<string, Tree>>;
195
+ pages?: Record<string, Record<string, Tree | PageEntry[]>>;
196
+ site?: Tree;
197
+ };
198
+ export type SeedRequest = CmsSeed & {
199
+ /** Overwrite a project that already has published rows. */
200
+ force?: boolean;
201
+ };
202
+ export type SeedSummary = {
203
+ locales: string[];
204
+ layouts: number;
205
+ pages: number;
206
+ site: boolean;
207
+ };
208
+ export type SeedResponse = {
209
+ ok: true;
210
+ seeded: SeedSummary;
211
+ } | {
212
+ ok: false;
213
+ reason: 'already-seeded';
214
+ };
175
215
  export type DocsResponse = {
176
216
  contents: Tree;
177
217
  } | PageDeleteOutcome;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `@velastack/cms/editor` — the editing chrome a structured component's
3
+ * editable sibling imports lazily, so none of it reaches the public bundle:
4
+ *
5
+ * ```svelte
6
+ * {#await import('@velastack/cms/editor') then { StructuredEditorPopover, FieldInput, ReorderButtons }}
7
+ * ```
8
+ */
9
+ export { default as StructuredEditorPopover } from './components/admin-bar/structured-editor-popover.svelte';
10
+ export { default as FieldInput } from './components/admin-bar/field-input.svelte';
11
+ export type { FieldInputType } from './components/admin-bar/field-input.svelte';
12
+ export { default as ReorderButtons } from './components/admin-bar/reorder-buttons.svelte';
13
+ export { default as CssRoot } from './components/admin-bar/css-root.svelte';
14
+ export { Button } from './components/admin-bar/ui/button/index.js';
15
+ export { Input } from './components/admin-bar/ui/input/index.js';
16
+ export { Textarea } from './components/admin-bar/ui/textarea/index.js';
17
+ export { Switch } from './components/admin-bar/ui/switch/index.js';
18
+ export { Checkbox } from './components/admin-bar/ui/checkbox/index.js';
19
+ export * as Popover from './components/admin-bar/ui/popover/index.js';
20
+ export * as Tabs from './components/admin-bar/ui/tabs/index.js';
21
+ export * as Sheet from './components/admin-bar/ui/sheet/index.js';
22
+ export { default as CmsMediaPicker } from './components/cms/cms-media-picker.svelte';
package/dist/editor.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `@velastack/cms/editor` — the editing chrome a structured component's
3
+ * editable sibling imports lazily, so none of it reaches the public bundle:
4
+ *
5
+ * ```svelte
6
+ * {#await import('@velastack/cms/editor') then { StructuredEditorPopover, FieldInput, ReorderButtons }}
7
+ * ```
8
+ */
9
+ export { default as StructuredEditorPopover } from './components/admin-bar/structured-editor-popover.svelte';
10
+ export { default as FieldInput } from './components/admin-bar/field-input.svelte';
11
+ export { default as ReorderButtons } from './components/admin-bar/reorder-buttons.svelte';
12
+ export { default as CssRoot } from './components/admin-bar/css-root.svelte';
13
+ export { Button } from './components/admin-bar/ui/button/index.js';
14
+ export { Input } from './components/admin-bar/ui/input/index.js';
15
+ export { Textarea } from './components/admin-bar/ui/textarea/index.js';
16
+ export { Switch } from './components/admin-bar/ui/switch/index.js';
17
+ export { Checkbox } from './components/admin-bar/ui/checkbox/index.js';
18
+ export * as Popover from './components/admin-bar/ui/popover/index.js';
19
+ export * as Tabs from './components/admin-bar/ui/tabs/index.js';
20
+ export * as Sheet from './components/admin-bar/ui/sheet/index.js';
21
+ export { default as CmsMediaPicker } from './components/cms/cms-media-picker.svelte';
package/dist/index.d.ts CHANGED
@@ -7,21 +7,29 @@
7
7
  export { default as CmsText } from './components/cms/cms-text.svelte';
8
8
  export { default as CmsRichText } from './components/cms/cms-rich-text.svelte';
9
9
  export { default as CmsMarkdown } from './components/cms/cms-markdown.svelte';
10
- export { default as CmsImage } from './components/cms/cms-image.svelte';
10
+ export { default as CmsImage, normalizeImage } from './components/cms/cms-image.svelte';
11
+ export type { CmsImageValue } from './components/cms/cms-image.svelte';
11
12
  export { default as CmsBoolean } from './components/cms/cms-boolean.svelte';
12
13
  export { default as CmsNumber } from './components/cms/cms-number.svelte';
13
14
  export type { CmsNumberProps } from './components/cms/cms-number.svelte';
14
15
  export { default as CmsDateTime } from './components/cms/cms-date-time.svelte';
15
16
  export type { CmsDateTimeMode, CmsDateTimeProps } from './components/cms/cms-date-time.svelte';
16
- export { default as CmsLink } from './components/cms/cms-link.svelte';
17
+ export { default as CmsLink, normalizeLink } from './components/cms/cms-link.svelte';
17
18
  export type { CmsLinkValue, CmsLinkRenderProps } from './components/cms/cms-link.svelte';
18
19
  export { default as CmsRepeater } from './components/cms/cms-repeater.svelte';
19
20
  export { default as CmsEntries } from './components/cms/cms-entries.svelte';
20
21
  export { default as AdminBar } from './components/admin-bar/admin-bar.svelte';
21
- export { definePage } from './components/admin-bar/page-config.js';
22
+ export { useCmsField, resolveScopeRef, ROOT_SCOPE_ID } from './components/cms/use-cms-field.svelte.js';
23
+ export type { CmsField, CmsFieldInput } from './components/cms/use-cms-field.svelte.js';
24
+ export { defineStructured, applyTranslations, countTranslations, extractTranslations, stripTranslations, translatableFields, translationPath, asBoolean, asItems, asNumber, asString, newItemId, ROOT_ID, TRANSLATIONS_KEY } from './core/structured.js';
25
+ export type { Structured, StructuredItem, StructuredSchema, TranslatableField, TranslationOverlay } from './core/structured.js';
26
+ export { registerStructured, getStructured } from './components/cms/structured-registry.js';
27
+ export { toMetaTags } from './core/metadata.js';
28
+ export type { MetaTagsOptions, MetaTagsOutput } from './core/metadata.js';
29
+ export { definePage, DEFAULT_METADATA_SCHEMA, metadataSchemaFor } from './components/admin-bar/page-config.js';
22
30
  export type { CmsPageConfig, CmsCreatablePageConfig, CmsStaticPageConfig, CmsPageField, CmsPageMetadataSchema, MetadataFieldSchema, MetadataPrimitive } from './components/admin-bar/page-config.js';
23
31
  export { cmsStore } from './components/cms/cms-store.svelte.js';
24
32
  export { cms } from './components/cms/cms-store.svelte.js';
25
33
  export { CMS_SCOPE, getCmsScope, getCmsValue, getPageScope } from './components/cms/scope.js';
26
34
  export { installCmsScope } from './components/cms/install-scope.svelte.js';
27
- export type { CmsEntry, CmsManifest, CmsManifestRoute, CmsManifestScope, CmsPayload, CmsScope, CmsScopeEntry } from './components/cms/scope.js';
35
+ export type { CmsEntry, CmsManifest, CmsManifestRoute, CmsManifestScope, CmsManifestUsage, CmsPayload, CmsScope, CmsScopeEntry, SiteFieldSchema, SiteFieldType, SiteSchema } from './components/cms/scope.js';
package/dist/index.js CHANGED
@@ -8,17 +8,27 @@
8
8
  export { default as CmsText } from './components/cms/cms-text.svelte';
9
9
  export { default as CmsRichText } from './components/cms/cms-rich-text.svelte';
10
10
  export { default as CmsMarkdown } from './components/cms/cms-markdown.svelte';
11
- export { default as CmsImage } from './components/cms/cms-image.svelte';
11
+ export { default as CmsImage, normalizeImage } from './components/cms/cms-image.svelte';
12
12
  export { default as CmsBoolean } from './components/cms/cms-boolean.svelte';
13
13
  export { default as CmsNumber } from './components/cms/cms-number.svelte';
14
14
  export { default as CmsDateTime } from './components/cms/cms-date-time.svelte';
15
- export { default as CmsLink } from './components/cms/cms-link.svelte';
15
+ export { default as CmsLink, normalizeLink } from './components/cms/cms-link.svelte';
16
16
  export { default as CmsRepeater } from './components/cms/cms-repeater.svelte';
17
17
  export { default as CmsEntries } from './components/cms/cms-entries.svelte';
18
18
  // Admin bar (sync wrapper; admin-bar-internal + seo-panel are async chunks)
19
19
  export { default as AdminBar } from './components/admin-bar/admin-bar.svelte';
20
+ // Building blocks for structured components: the field hook, the value
21
+ // rules (versioned shapes, `$t` overlay) and the registry the Locales panel
22
+ // reads. The editor popover and field inputs are admin-bar chunks, imported
23
+ // lazily by editable siblings:
24
+ // `import('@velastack/cms/editor')`
25
+ export { useCmsField, resolveScopeRef, ROOT_SCOPE_ID } from './components/cms/use-cms-field.svelte.js';
26
+ export { defineStructured, applyTranslations, countTranslations, extractTranslations, stripTranslations, translatableFields, translationPath, asBoolean, asItems, asNumber, asString, newItemId, ROOT_ID, TRANSLATIONS_KEY } from './core/structured.js';
27
+ export { registerStructured, getStructured } from './components/cms/structured-registry.js';
28
+ // Page metadata → svelte-meta-tags props.
29
+ export { toMetaTags } from './core/metadata.js';
20
30
  // Per-route page configuration — declare in `page.cms.ts` next to `+page.svelte`.
21
- export { definePage } from './components/admin-bar/page-config.js';
31
+ export { definePage, DEFAULT_METADATA_SCHEMA, metadataSchemaFor } from './components/admin-bar/page-config.js';
22
32
  // Edit-mode store
23
33
  export { cmsStore } from './components/cms/cms-store.svelte.js';
24
34
  // Merged CMS read view (server payload + client overlay), shaped like CmsPayload
@@ -61,6 +61,7 @@ export const resolveCmsPayload = async (args) => {
61
61
  routeId: scope.routeId,
62
62
  params: scopeParams,
63
63
  fields: scope.fields,
64
+ ...(scope.usages ? { usages: scope.usages } : {}),
64
65
  locale: forLocale
65
66
  };
66
67
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velastack/cms",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "A scope-aware CMS for SvelteKit, for static or dynamic sites.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "type": "module",
24
24
  "engines": {
25
- "node": "^20.19.0 || >=22.12.0"
25
+ "node": ">=22.12.0"
26
26
  },
27
27
  "scripts": {
28
28
  "dev": "vite dev",
@@ -57,6 +57,11 @@
57
57
  "svelte": "./dist/index.js",
58
58
  "default": "./dist/index.js"
59
59
  },
60
+ "./editor": {
61
+ "types": "./dist/editor.d.ts",
62
+ "svelte": "./dist/editor.js",
63
+ "default": "./dist/editor.js"
64
+ },
60
65
  "./server": {
61
66
  "types": "./dist/server/index.d.ts",
62
67
  "default": "./dist/server/index.js"
@@ -72,11 +77,15 @@
72
77
  "./adapter": {
73
78
  "types": "./plugin/dist/adapter.d.ts",
74
79
  "default": "./plugin/dist/adapter.js"
80
+ },
81
+ "./build": {
82
+ "types": "./plugin/dist/build.d.ts",
83
+ "default": "./plugin/dist/build.js"
75
84
  }
76
85
  },
77
86
  "peerDependencies": {
78
87
  "@sveltejs/kit": "^2.0.0",
79
- "better-sqlite3": "^11.0.0 || ^12.0.0",
88
+ "better-sqlite3": "^12.0.0 || ^13.0.0",
80
89
  "marked": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
81
90
  "svelte": "^5.0.0",
82
91
  "vite": "^8.0.0"
@@ -98,7 +107,7 @@
98
107
  "@tailwindcss/cli": "^4.3.3",
99
108
  "@tailwindcss/vite": "^4.3.3",
100
109
  "@types/node": "^26.4.0",
101
- "better-sqlite3": "^12.11.1",
110
+ "better-sqlite3": "^13.0.3",
102
111
  "marked": "^18.0.11",
103
112
  "prettier": "^3.9.6",
104
113
  "prettier-plugin-svelte": "^4.1.1",
@@ -110,7 +119,7 @@
110
119
  "tw-animate-css": "^1.4.0",
111
120
  "typescript": "^6.0.3",
112
121
  "vite": "^8.2.2",
113
- "vitest": "^4.1.11"
122
+ "vitest": "^5.0.1"
114
123
  },
115
124
  "dependencies": {
116
125
  "@sveltejs/adapter-static": "^3.0.10",
@@ -119,7 +128,7 @@
119
128
  "@tiptap/pm": "^3.30.6",
120
129
  "@tiptap/starter-kit": "^3.30.6",
121
130
  "bits-ui": "^2.19.0",
122
- "devalue": "^5.9.2",
131
+ "devalue": "^6.0.0",
123
132
  "tailwind-merge": "^3.6.0"
124
133
  }
125
134
  }
package/plugin/README.md CHANGED
@@ -31,6 +31,11 @@ type CmsPluginOptions = {
31
31
  libDir?: string; // default: 'src/lib'
32
32
  components?: ExternalCmsComponentSpec[];
33
33
  traverse?: (string | RegExp)[];
34
+ endpoint?: string; // media download at build time
35
+ locales?: string[]; // default: ['en']; locales[0] is the default locale
36
+ mediaDir?: string;
37
+ mediaPrefix?: string;
38
+ content?: string | false; // default: 'content' — see "Content manifest"
34
39
  };
35
40
 
36
41
  type ExternalCmsComponentSpec =
@@ -48,6 +53,14 @@ type ExternalCmsComponentSpec =
48
53
  `source === pattern || source.startsWith(pattern + '/')`. Auto-populated with
49
54
  every `components[].source`.
50
55
 
56
+ - `content` — directory of per-locale content manifests. See below.
57
+
58
+ ## Content manifest
59
+
60
+ When `<content>/<defaultLocale>.json` exists, the plugin reads it — an object keyed by scope id (`layout:/`, `page:/(public)/about`, `site`) — and injects each usage's value as a `fallback={…}` prop (`initial` for `CmsBoolean`) into every walked `.svelte` file, route entrypoints and shared components alike. A `scope="root"` usage reads `layout:/`; any other usage reads the scopes that reach its file, leaf first. Components in source stay name-only; the value the visitor sees before the CMS answers comes from the manifest. The file is watched, so edits apply in dev.
61
+
62
+ The same operation is available to other tooling from `@velastack/cms/build` (`scanUsages`, `injectFallbacks` / `inlineFallbacks`, `buildManifest`, `fallbackResolverFor`), so `pack` and `verify` in a templates repo share this parser instead of carrying their own.
63
+
51
64
  ## Virtual modules
52
65
 
53
66
  ### `virtual:vela-cms/manifest`
@@ -57,6 +70,7 @@ import { cmsManifest } from 'virtual:vela-cms/manifest';
57
70
 
58
71
  type CmsManifest = {
59
72
  version: 1;
73
+ // each scope lists `fields` (names) and `usages` ({ name, component, preset? })
60
74
  routes: Record<string, { scopes: CmsManifestScope[] }>;
61
75
  };
62
76
 
@@ -107,6 +121,8 @@ For each route entrypoint (`+page.svelte` or `+layout.svelte`), the plugin
107
121
  parses with `svelte/compiler` and follows static imports of `.svelte` files.
108
122
  Imports get classified as CMS components by:
109
123
 
124
+ 0. **`@velastack/cms` by name** — every named import from the package
125
+ specifier, with no resolver needed.
110
126
  1. **In-tree barrel** — anything from `<libDir>/components/cms/index.{ts,js}`.
111
127
  Named imports are CMS components; default isn't.
112
128
  2. **In-tree files** — `.svelte` files directly under `<libDir>/components/cms/`.
@@ -116,8 +132,12 @@ Imports get classified as CMS components by:
116
132
  4. **External `components` spec** — user-declared packs.
117
133
 
118
134
  Component usages with a `name=<static>` prop are recorded as field
119
- references; `value=<…>` opts the call out (it's a runtime override, not a
120
- field). Dynamic `name={expr}` props are ignored.
135
+ references with their component type; `value=<…>` opts the call out (it's a
136
+ runtime override, not a field). A `scope="root"` (or `scope="layout:/x"`)
137
+ attribute records the usage under that layout scope instead of the file's
138
+ own. Dynamic `name={expr}` props and `scope=` attributes naming a scope
139
+ outside the route's chain are reported as warnings on the build result and
140
+ logged by the plugin.
121
141
 
122
142
  ## HMR
123
143
 
@@ -0,0 +1,74 @@
1
+ import { buildManifest, resolveScopeAttr, ROOT_SCOPE_ID, type BuildManifestOptions, type BuildManifestResult, type CmsManifest, type CmsManifestScope, type CmsManifestUsage } from './manifest.js';
2
+ export { buildManifest, resolveScopeAttr, ROOT_SCOPE_ID };
3
+ export type Tree = Record<string, unknown>;
4
+ export type { BuildManifestOptions, BuildManifestResult, CmsManifest, CmsManifestScope, CmsManifestUsage };
5
+ export type CmsUsage = {
6
+ /** Static `name=` literal, or `null` when absent or dynamic. */
7
+ name: string | null;
8
+ /** Canonical exported component name (`CmsText`, `CmsHours`, …). */
9
+ component: string;
10
+ /** Local binding used in the template. */
11
+ local: string;
12
+ /** Resolved scope id from `scope=`, or `null` for the enclosing route scope. */
13
+ scope: string | null;
14
+ preset: string | null;
15
+ /** `name=` was present but not a static string. */
16
+ dynamic: boolean;
17
+ hasValue: boolean;
18
+ hasFallback: boolean;
19
+ line: number;
20
+ /** Source offsets of the component node. */
21
+ start: number;
22
+ end: number;
23
+ };
24
+ export type ScanOptions = {
25
+ filename?: string;
26
+ /**
27
+ * Extra local names to treat as CMS components, for wrappers a template
28
+ * declares itself. Package imports and `$lib/components/cms/*` are always
29
+ * recognized.
30
+ */
31
+ components?: string[];
32
+ };
33
+ export type ScanResult = {
34
+ usages: CmsUsage[];
35
+ /** Dynamic names, which no manifest can record. */
36
+ warnings: string[];
37
+ };
38
+ /** Every CMS component usage in one `.svelte` source. */
39
+ export declare const scanUsages: (source: string, options?: ScanOptions) => ScanResult;
40
+ /** Returns the value to inject for a usage, or `undefined` to leave it alone. */
41
+ export type FallbackResolver = (usage: CmsUsage) => unknown;
42
+ export type InjectOptions = ScanOptions & {
43
+ /** Replace an existing `fallback` / `initial` prop instead of skipping it. */
44
+ overwrite?: boolean;
45
+ };
46
+ export type InjectResult = {
47
+ code: string;
48
+ /** Names that received a fallback. */
49
+ injected: string[];
50
+ /** Static names the resolver had no value for. */
51
+ missing: string[];
52
+ };
53
+ /**
54
+ * Write values into a file's CMS usages as `fallback={…}` props (the JSON
55
+ * literal of the value), leaving the source otherwise untouched. `resolve`
56
+ * is a content tree keyed by name, or a function for callers that know
57
+ * which scope each usage's value lives in (see {@link fallbackResolverFor}).
58
+ *
59
+ * Usages with a `value=` prop, a dynamic name, or (unless `overwrite`) an
60
+ * existing fallback are skipped. The plugin calls this in memory at build
61
+ * time in the templates repo; `pack` calls it to write the tarball's
62
+ * components, which is why it is also exported as {@link inlineFallbacks}.
63
+ */
64
+ export declare const injectFallbacks: (source: string, resolve: Tree | FallbackResolver, options?: InjectOptions) => InjectResult;
65
+ /** Same operation as {@link injectFallbacks}, named for the `pack` step that
66
+ * writes the result to disk. */
67
+ export declare const inlineFallbacks: (source: string, resolve: Tree | FallbackResolver, options?: InjectOptions) => InjectResult;
68
+ /**
69
+ * A resolver for one file, given a built manifest and a content manifest
70
+ * keyed by scope id (`layout:/`, `page:/(public)/about`, …). A usage with
71
+ * `scope=` reads that scope; otherwise the scopes that reach the file are
72
+ * tried leaf first, and the first one holding the value wins.
73
+ */
74
+ export declare const fallbackResolverFor: (result: Pick<BuildManifestResult, "scopesByFile">, file: string, content: Record<string, Tree>) => FallbackResolver;