@velastack/cms 0.5.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 (99) hide show
  1. package/README.md +66 -43
  2. package/dist/components/admin-bar/admin-bar.css +1 -1
  3. package/dist/components/admin-bar/field-input.svelte +20 -1
  4. package/dist/components/admin-bar/field-input.svelte.d.ts +2 -1
  5. package/dist/components/admin-bar/fields-form.svelte +218 -0
  6. package/dist/components/admin-bar/fields-form.svelte.d.ts +16 -0
  7. package/dist/components/admin-bar/link-input.svelte +102 -0
  8. package/dist/components/admin-bar/link-input.svelte.d.ts +9 -0
  9. package/dist/components/admin-bar/list-field.svelte +93 -0
  10. package/dist/components/admin-bar/list-field.svelte.d.ts +20 -0
  11. package/dist/components/admin-bar/locales-panel.svelte +1 -1
  12. package/dist/components/admin-bar/page-options.d.ts +24 -0
  13. package/dist/components/admin-bar/page-options.js +53 -0
  14. package/dist/components/admin-bar/rich-text-input.svelte +92 -0
  15. package/dist/components/admin-bar/rich-text-input.svelte.d.ts +9 -0
  16. package/dist/components/admin-bar/structured-editor-popover.svelte +15 -6
  17. package/dist/components/cms/cms-collection.svelte +46 -0
  18. package/dist/components/cms/cms-collection.svelte.d.ts +25 -0
  19. package/dist/components/cms/cms-contact.svelte +26 -0
  20. package/dist/components/cms/cms-contact.svelte.d.ts +14 -0
  21. package/dist/components/cms/cms-faq.svelte +22 -0
  22. package/dist/components/cms/cms-faq.svelte.d.ts +15 -0
  23. package/dist/components/cms/cms-gallery.svelte +31 -0
  24. package/dist/components/cms/cms-gallery.svelte.d.ts +16 -0
  25. package/dist/components/cms/cms-hours-editable.svelte +160 -0
  26. package/dist/components/cms/cms-hours-editable.svelte.d.ts +17 -0
  27. package/dist/components/cms/cms-hours.svelte +42 -0
  28. package/dist/components/cms/cms-hours.svelte.d.ts +19 -0
  29. package/dist/components/cms/cms-image.svelte +3 -25
  30. package/dist/components/cms/cms-image.svelte.d.ts +3 -11
  31. package/dist/components/cms/cms-link-editable.svelte +4 -60
  32. package/dist/components/cms/cms-link.svelte +3 -23
  33. package/dist/components/cms/cms-link.svelte.d.ts +3 -10
  34. package/dist/components/cms/cms-logos.svelte +29 -0
  35. package/dist/components/cms/cms-logos.svelte.d.ts +16 -0
  36. package/dist/components/cms/cms-nav.svelte +22 -0
  37. package/dist/components/cms/cms-nav.svelte.d.ts +15 -0
  38. package/dist/components/cms/cms-pricing.svelte +36 -0
  39. package/dist/components/cms/cms-pricing.svelte.d.ts +14 -0
  40. package/dist/components/cms/cms-schedule.svelte +35 -0
  41. package/dist/components/cms/cms-schedule.svelte.d.ts +16 -0
  42. package/dist/components/cms/cms-social-links.svelte +35 -0
  43. package/dist/components/cms/cms-social-links.svelte.d.ts +15 -0
  44. package/dist/components/cms/cms-stats.svelte +31 -0
  45. package/dist/components/cms/cms-stats.svelte.d.ts +16 -0
  46. package/dist/components/cms/cms-steps.svelte +31 -0
  47. package/dist/components/cms/cms-steps.svelte.d.ts +16 -0
  48. package/dist/components/cms/cms-structured-editable.svelte +29 -0
  49. package/dist/components/cms/{cms-repeater-editable.svelte.d.ts → cms-structured-editable.svelte.d.ts} +12 -8
  50. package/dist/components/cms/cms-structured.svelte +50 -0
  51. package/dist/components/cms/cms-structured.svelte.d.ts +49 -0
  52. package/dist/components/cms/cms-team.svelte +22 -0
  53. package/dist/components/cms/cms-team.svelte.d.ts +15 -0
  54. package/dist/components/cms/cms-testimonials.svelte +36 -0
  55. package/dist/components/cms/cms-testimonials.svelte.d.ts +15 -0
  56. package/dist/components/cms/cms-timeline.svelte +35 -0
  57. package/dist/components/cms/cms-timeline.svelte.d.ts +16 -0
  58. package/dist/components/cms/index.d.ts +15 -1
  59. package/dist/components/cms/index.js +15 -1
  60. package/dist/core/shapes/collection.d.ts +60 -0
  61. package/dist/core/shapes/collection.js +194 -0
  62. package/dist/core/shapes/contact.d.ts +70 -0
  63. package/dist/core/shapes/contact.js +214 -0
  64. package/dist/core/shapes/faq.d.ts +12 -0
  65. package/dist/core/shapes/faq.js +48 -0
  66. package/dist/core/shapes/form.d.ts +46 -0
  67. package/dist/core/shapes/form.js +7 -0
  68. package/dist/core/shapes/hours.d.ts +91 -0
  69. package/dist/core/shapes/hours.js +325 -0
  70. package/dist/core/shapes/image.d.ts +15 -0
  71. package/dist/core/shapes/image.js +27 -0
  72. package/dist/core/shapes/index.d.ts +31 -0
  73. package/dist/core/shapes/index.js +13 -0
  74. package/dist/core/shapes/link.d.ts +15 -0
  75. package/dist/core/shapes/link.js +42 -0
  76. package/dist/core/shapes/nav.d.ts +21 -0
  77. package/dist/core/shapes/nav.js +71 -0
  78. package/dist/core/shapes/presets.d.ts +65 -0
  79. package/dist/core/shapes/presets.js +152 -0
  80. package/dist/core/shapes/pricing.d.ts +40 -0
  81. package/dist/core/shapes/pricing.js +115 -0
  82. package/dist/core/shapes/social-links.d.ts +19 -0
  83. package/dist/core/shapes/social-links.js +83 -0
  84. package/dist/core/shapes/team.d.ts +22 -0
  85. package/dist/core/shapes/team.js +70 -0
  86. package/dist/core/shapes/testimonials.d.ts +24 -0
  87. package/dist/core/shapes/testimonials.js +86 -0
  88. package/dist/{components/cms → core}/structured-registry.d.ts +2 -2
  89. package/dist/core/structured.d.ts +33 -10
  90. package/dist/core/structured.js +98 -47
  91. package/dist/editor.d.ts +4 -0
  92. package/dist/editor.js +4 -0
  93. package/dist/index.d.ts +22 -8
  94. package/dist/index.js +24 -5
  95. package/package.json +1 -1
  96. package/dist/components/cms/cms-repeater-editable.svelte +0 -137
  97. package/dist/components/cms/cms-repeater.svelte +0 -52
  98. package/dist/components/cms/cms-repeater.svelte.d.ts +0 -31
  99. /package/dist/{components/cms → core}/structured-registry.js +0 -0
@@ -0,0 +1,86 @@
1
+ /** `CmsTestimonials`: quotes with an author, a rating and a source, plus Review JSON-LD. */
2
+ import { asItems, asNumber, asString, isPlainObject } from '../structured.js';
3
+ import { defineForm } from './form.js';
4
+ import { asImage } from './image.js';
5
+ const asRating = (v) => {
6
+ const n = asNumber(typeof v === 'string' ? Number(v) : v);
7
+ return n !== null && n >= 1 && n <= 5 ? Math.round(n) : null;
8
+ };
9
+ export const cmsTestimonials = defineForm({
10
+ component: 'CmsTestimonials',
11
+ label: 'Testimonials',
12
+ version: 1,
13
+ translatable: [],
14
+ items: { key: 'items', translatable: ['quote', 'role'] },
15
+ normalize: (raw) => {
16
+ const items = Array.isArray(raw) ? raw : isPlainObject(raw) ? raw.items : [];
17
+ return {
18
+ v: 1,
19
+ items: asItems(items, (r) => {
20
+ const source = isPlainObject(r.source) ? r.source : {};
21
+ return {
22
+ quote: asString(r.quote),
23
+ author: asString(r.author),
24
+ role: asString(r.role),
25
+ company: asString(r.company),
26
+ photo: asImage(r.photo),
27
+ rating: asRating(r.rating),
28
+ source: { label: asString(source.label), url: asString(source.url) },
29
+ date: asString(r.date)
30
+ };
31
+ })
32
+ };
33
+ },
34
+ empty: () => ({ v: 1, items: [] }),
35
+ fields: [
36
+ {
37
+ key: 'items',
38
+ label: 'Quotes',
39
+ type: 'list',
40
+ itemLabel: 'testimonial',
41
+ titleKey: 'author',
42
+ blank: () => ({
43
+ quote: '',
44
+ author: '',
45
+ role: '',
46
+ company: '',
47
+ photo: null,
48
+ rating: null,
49
+ source: { label: '', url: '' },
50
+ date: ''
51
+ }),
52
+ fields: [
53
+ { key: 'quote', label: 'Quote', type: 'html' },
54
+ { key: 'author', label: 'Author', type: 'text', half: true },
55
+ { key: 'role', label: 'Role', type: 'text' },
56
+ { key: 'company', label: 'Company', type: 'text', half: true },
57
+ { key: 'date', label: 'Date', type: 'date' },
58
+ { key: 'rating', label: 'Rating', type: 'rating' },
59
+ { key: 'photo', label: 'Photo', type: 'image' },
60
+ { key: 'source.label', label: 'Source', type: 'text', half: true, placeholder: 'Google' },
61
+ { key: 'source.url', label: 'Source URL', type: 'url' }
62
+ ]
63
+ }
64
+ ]
65
+ });
66
+ const stripTags = (html) => html.replace(/<[^>]*>/g, '').trim();
67
+ /** Testimonials that have a quote. */
68
+ export const testimonialsView = (value) => value.items.filter((t) => stripTags(t.quote) !== '');
69
+ /** A schema.org `Review` node. `itemReviewed` is the business or product node. */
70
+ export const toReview = (item, itemReviewed) => {
71
+ const out = {
72
+ '@type': 'Review',
73
+ reviewBody: stripTags(item.quote),
74
+ author: { '@type': 'Person', name: item.author }
75
+ };
76
+ if (item.rating !== null) {
77
+ out.reviewRating = { '@type': 'Rating', ratingValue: item.rating, bestRating: 5 };
78
+ }
79
+ if (item.date)
80
+ out.datePublished = item.date;
81
+ if (item.source.label)
82
+ out.publisher = { '@type': 'Organization', name: item.source.label };
83
+ if (itemReviewed)
84
+ out.itemReviewed = itemReviewed;
85
+ return out;
86
+ };
@@ -4,7 +4,7 @@
4
4
  * count translatable strings for every usage the manifest recorded on the
5
5
  * current route without importing the components themselves.
6
6
  */
7
- import type { Structured } from '../../core/structured.js';
8
- import type { Tree } from '../../core/path.js';
7
+ import type { Structured } from './structured.js';
8
+ import type { Tree } from './path.js';
9
9
  export declare const registerStructured: <T extends Tree>(schema: Structured<T>) => Structured<T>;
10
10
  export declare const getStructured: (component: string) => Structured<Tree> | undefined;
@@ -31,18 +31,28 @@ export type TranslationOverlay = Record<string, Record<string, string>>;
31
31
  export type StructuredItem = {
32
32
  id: string;
33
33
  } & Record<string, unknown>;
34
+ /**
35
+ * One list of items inside a structured value. `key` names the array field,
36
+ * `translatable` the item fields holding translatable text (a string, or an
37
+ * array of strings, translated per index as `<field>.<i>`), and `items` any
38
+ * lists nested inside each item (a collection item's `details`, a pricing
39
+ * tier's `features`). Every item at every level carries a stable `id`.
40
+ */
41
+ export type ItemsSpec = {
42
+ key: string;
43
+ translatable: readonly string[];
44
+ items?: ItemsSpec | readonly ItemsSpec[];
45
+ };
34
46
  export type StructuredSchema<T extends Tree> = {
35
47
  /** Exported component name, e.g. `'CmsHours'`. Recorded in the manifest. */
36
48
  component: string;
37
49
  /** Current shape version, written as `v` on every value. */
38
50
  version: number;
39
- /** Root-level fields holding translatable strings, e.g. `['note']`. */
51
+ /** Root-level fields holding translatable strings, e.g. `['note']`. A
52
+ * dotted path addresses a nested field (`'labels.closed'`). */
40
53
  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
- };
54
+ /** The value's item lists, when it has any, and the item fields that translate. */
55
+ items?: ItemsSpec | readonly ItemsSpec[];
46
56
  /**
47
57
  * Turn anything the store may hold — an older version, a hand-seeded
48
58
  * partial object, garbage — into the current shape. Must never throw.
@@ -63,21 +73,26 @@ export declare const isPlainObject: (v: unknown) => v is Tree;
63
73
  export declare const defineStructured: <T extends Tree>(schema: StructuredSchema<T>) => Structured<T>;
64
74
  /** Path of one translated string: `<name>.$t.<id>.<field>`. */
65
75
  export declare const translationPath: (name: string, id: string, field: string) => string;
66
- /** The `$t` branch of a stored value, when it is one. */
76
+ /** The `$t` branch of a stored value, when it is one. A dotted field
77
+ * (`labels.closed`, `tags.0`) is stored nested by the path-based writes and
78
+ * read back flat here. */
67
79
  export declare const extractTranslations: (raw: unknown) => TranslationOverlay | undefined;
68
80
  /** A copy of `raw` without its `$t` branch, so a whole-object write in the
69
81
  * default locale never carries another locale's strings. */
70
82
  export declare const stripTranslations: (raw: unknown) => unknown;
83
+ /** `set` that copies along the path instead of mutating shared branches. */
84
+ export declare const setIn: (obj: Tree, path: string, value: unknown) => Tree;
71
85
  /**
72
86
  * 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.
87
+ * Root fields come from the `_` id; item fields from the item's own `id`,
88
+ * at every nesting level. Only non-empty strings override, so an
89
+ * untranslated field shows the default-locale text rather than a blank.
76
90
  */
77
91
  export declare const applyTranslations: <T extends Tree>(value: T, overlay: TranslationOverlay | undefined, schema: Pick<StructuredSchema<T>, "translatable" | "items">) => T;
78
92
  export type TranslatableField = {
79
93
  /** Overlay id: an item id, or `_` for a root field. */
80
94
  id: string;
95
+ /** Field path within the object the id addresses (`note`, `labels.closed`, `tags.0`). */
81
96
  field: string;
82
97
  /** The default-locale string being translated. */
83
98
  source: string;
@@ -106,3 +121,11 @@ export declare const asItems: <I extends StructuredItem>(raw: unknown, normalize
106
121
  export declare const newItemId: () => string;
107
122
  /** Read a structured value's overlay for one locale out of a whole scope tree. */
108
123
  export declare const overlayAt: (tree: Tree | undefined, name: string) => TranslationOverlay | undefined;
124
+ /** A list of strings; anything else is dropped. */
125
+ export declare const asStrings: (v: unknown) => string[];
126
+ /** A nullable string: `null` (cleared) stays `null`; anything else is a string. */
127
+ export declare const asOptionalString: (v: unknown) => string | null;
128
+ /** One of a fixed set of strings, else the fallback. */
129
+ export declare const asEnum: <E extends string>(v: unknown, values: readonly E[], fallback: E) => E;
130
+ /** Move an item within a list, returning a new list. */
131
+ export declare const moveItem: <I>(list: readonly I[], from: number, to: number) => I[];
@@ -21,7 +21,7 @@
21
21
  * the default locale only, so the item list can never fork between
22
22
  * languages.
23
23
  */
24
- import { get } from './path.js';
24
+ import { get, leafPaths } from './path.js';
25
25
  /** Branch under a structured value that holds a locale's string overlay. */
26
26
  export const TRANSLATIONS_KEY = '$t';
27
27
  /** Overlay id that addresses the structured value's root fields. */
@@ -31,7 +31,7 @@ export const defineStructured = (schema) => ({
31
31
  ...schema,
32
32
  read: (raw, fallback) => {
33
33
  if (raw === undefined)
34
- return fallback === undefined ? schema.empty() : fallback;
34
+ return fallback === undefined ? schema.empty() : schema.normalize(fallback);
35
35
  if (raw === null)
36
36
  return schema.empty();
37
37
  const overlay = extractTranslations(raw);
@@ -41,7 +41,9 @@ export const defineStructured = (schema) => ({
41
41
  });
42
42
  /** Path of one translated string: `<name>.$t.<id>.<field>`. */
43
43
  export const translationPath = (name, id, field) => `${name}.${TRANSLATIONS_KEY}.${id}.${field}`;
44
- /** The `$t` branch of a stored value, when it is one. */
44
+ /** The `$t` branch of a stored value, when it is one. A dotted field
45
+ * (`labels.closed`, `tags.0`) is stored nested by the path-based writes and
46
+ * read back flat here. */
45
47
  export const extractTranslations = (raw) => {
46
48
  if (!isPlainObject(raw))
47
49
  return undefined;
@@ -53,9 +55,10 @@ export const extractTranslations = (raw) => {
53
55
  if (!isPlainObject(fields))
54
56
  continue;
55
57
  const strings = {};
56
- for (const [field, v] of Object.entries(fields)) {
58
+ for (const path of leafPaths(fields)) {
59
+ const v = get(fields, path);
57
60
  if (typeof v === 'string')
58
- strings[field] = v;
61
+ strings[path] = v;
59
62
  }
60
63
  out[id] = strings;
61
64
  }
@@ -73,38 +76,91 @@ const overlayString = (overlay, id, field) => {
73
76
  const v = overlay[id]?.[field];
74
77
  return typeof v === 'string' && v !== '' ? v : undefined;
75
78
  };
79
+ const specList = (items) => items === undefined ? [] : Array.isArray(items) ? items : [items];
80
+ const isStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === 'string');
81
+ /** `set` that copies along the path instead of mutating shared branches. */
82
+ export const setIn = (obj, path, value) => {
83
+ const [head, ...rest] = path.split('.');
84
+ const out = { ...obj };
85
+ if (rest.length === 0) {
86
+ out[head] = value;
87
+ return out;
88
+ }
89
+ const child = obj[head];
90
+ out[head] = setIn(isPlainObject(child) ? child : {}, rest.join('.'), value);
91
+ return out;
92
+ };
93
+ /**
94
+ * Fold a locale's string overlay into one object (the root value or an item)
95
+ * and, recursively, into the item lists it holds.
96
+ */
97
+ const applyTo = (obj, id, translatable, items, overlay) => {
98
+ let out = { ...obj };
99
+ for (const field of translatable) {
100
+ const cur = get(obj, field);
101
+ if (typeof cur === 'string') {
102
+ const s = overlayString(overlay, id, field);
103
+ if (s !== undefined)
104
+ out = setIn(out, field, s);
105
+ }
106
+ else if (isStringArray(cur)) {
107
+ let changed = false;
108
+ const next = cur.map((v, i) => {
109
+ const s = overlayString(overlay, id, `${field}.${i}`);
110
+ if (s === undefined)
111
+ return v;
112
+ changed = true;
113
+ return s;
114
+ });
115
+ if (changed)
116
+ out = setIn(out, field, next);
117
+ }
118
+ }
119
+ for (const spec of specList(items)) {
120
+ const list = obj[spec.key];
121
+ if (!Array.isArray(list))
122
+ continue;
123
+ out[spec.key] = list.map((item) => isPlainObject(item) && typeof item.id === 'string'
124
+ ? applyTo(item, item.id, spec.translatable, spec.items, overlay)
125
+ : item);
126
+ }
127
+ return out;
128
+ };
76
129
  /**
77
130
  * 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.
131
+ * Root fields come from the `_` id; item fields from the item's own `id`,
132
+ * at every nesting level. Only non-empty strings override, so an
133
+ * untranslated field shows the default-locale text rather than a blank.
81
134
  */
82
135
  export const applyTranslations = (value, overlay, schema) => {
83
136
  if (!overlay)
84
137
  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;
138
+ return applyTo(value, ROOT_ID, schema.translatable, schema.items, overlay);
139
+ };
140
+ const collectFrom = (obj, id, translatable, items, out) => {
141
+ for (const field of translatable) {
142
+ const cur = get(obj, field);
143
+ if (typeof cur === 'string') {
144
+ if (cur !== '')
145
+ out.push({ id, field, source: cur });
146
+ }
147
+ else if (isStringArray(cur)) {
148
+ cur.forEach((s, i) => {
149
+ if (s !== '')
150
+ out.push({ id, field: `${field}.${i}`, source: s });
104
151
  });
105
152
  }
106
153
  }
107
- return out;
154
+ for (const spec of specList(items)) {
155
+ const list = obj[spec.key];
156
+ if (!Array.isArray(list))
157
+ continue;
158
+ for (const item of list) {
159
+ if (!isPlainObject(item) || typeof item.id !== 'string')
160
+ continue;
161
+ collectFrom(item, item.id, spec.translatable, spec.items, out);
162
+ }
163
+ }
108
164
  };
109
165
  /**
110
166
  * Every translatable, non-empty string in a normalized default-locale value.
@@ -112,25 +168,7 @@ export const applyTranslations = (value, overlay, schema) => {
112
168
  */
113
169
  export const translatableFields = (value, schema) => {
114
170
  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
- }
171
+ collectFrom(value, ROOT_ID, schema.translatable, schema.items, out);
134
172
  return out;
135
173
  };
136
174
  /** How many translatable strings a locale's overlay still lacks. */
@@ -182,3 +220,16 @@ export const newItemId = () => {
182
220
  };
183
221
  /** Read a structured value's overlay for one locale out of a whole scope tree. */
184
222
  export const overlayAt = (tree, name) => extractTranslations(get(tree, name));
223
+ /** A list of strings; anything else is dropped. */
224
+ export const asStrings = (v) => Array.isArray(v) ? v.filter((s) => typeof s === 'string') : [];
225
+ /** A nullable string: `null` (cleared) stays `null`; anything else is a string. */
226
+ export const asOptionalString = (v) => typeof v === 'string' && v !== '' ? v : null;
227
+ /** One of a fixed set of strings, else the fallback. */
228
+ export const asEnum = (v, values, fallback) => typeof v === 'string' && values.includes(v) ? v : fallback;
229
+ /** Move an item within a list, returning a new list. */
230
+ export const moveItem = (list, from, to) => {
231
+ const next = [...list];
232
+ const [item] = next.splice(from, 1);
233
+ next.splice(to, 0, item);
234
+ return next;
235
+ };
package/dist/editor.d.ts CHANGED
@@ -10,6 +10,10 @@ export { default as StructuredEditorPopover } from './components/admin-bar/struc
10
10
  export { default as FieldInput } from './components/admin-bar/field-input.svelte';
11
11
  export type { FieldInputType } from './components/admin-bar/field-input.svelte';
12
12
  export { default as ReorderButtons } from './components/admin-bar/reorder-buttons.svelte';
13
+ export { default as FieldsForm } from './components/admin-bar/fields-form.svelte';
14
+ export { default as ListField } from './components/admin-bar/list-field.svelte';
15
+ export { default as LinkInput } from './components/admin-bar/link-input.svelte';
16
+ export { default as RichTextInput } from './components/admin-bar/rich-text-input.svelte';
13
17
  export { default as CssRoot } from './components/admin-bar/css-root.svelte';
14
18
  export { Button } from './components/admin-bar/ui/button/index.js';
15
19
  export { Input } from './components/admin-bar/ui/input/index.js';
package/dist/editor.js CHANGED
@@ -9,6 +9,10 @@
9
9
  export { default as StructuredEditorPopover } from './components/admin-bar/structured-editor-popover.svelte';
10
10
  export { default as FieldInput } from './components/admin-bar/field-input.svelte';
11
11
  export { default as ReorderButtons } from './components/admin-bar/reorder-buttons.svelte';
12
+ export { default as FieldsForm } from './components/admin-bar/fields-form.svelte';
13
+ export { default as ListField } from './components/admin-bar/list-field.svelte';
14
+ export { default as LinkInput } from './components/admin-bar/link-input.svelte';
15
+ export { default as RichTextInput } from './components/admin-bar/rich-text-input.svelte';
12
16
  export { default as CssRoot } from './components/admin-bar/css-root.svelte';
13
17
  export { Button } from './components/admin-bar/ui/button/index.js';
14
18
  export { Input } from './components/admin-bar/ui/input/index.js';
package/dist/index.d.ts CHANGED
@@ -7,23 +7,37 @@
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, normalizeImage } from './components/cms/cms-image.svelte';
11
- export type { CmsImageValue } from './components/cms/cms-image.svelte';
10
+ export { default as CmsImage } from './components/cms/cms-image.svelte';
12
11
  export { default as CmsBoolean } from './components/cms/cms-boolean.svelte';
13
12
  export { default as CmsNumber } from './components/cms/cms-number.svelte';
14
13
  export type { CmsNumberProps } from './components/cms/cms-number.svelte';
15
14
  export { default as CmsDateTime } from './components/cms/cms-date-time.svelte';
16
15
  export type { CmsDateTimeMode, CmsDateTimeProps } from './components/cms/cms-date-time.svelte';
17
- export { default as CmsLink, normalizeLink } from './components/cms/cms-link.svelte';
18
- export type { CmsLinkValue, CmsLinkRenderProps } from './components/cms/cms-link.svelte';
19
- export { default as CmsRepeater } from './components/cms/cms-repeater.svelte';
16
+ export { default as CmsLink } from './components/cms/cms-link.svelte';
17
+ export type { CmsLinkRenderProps } from './components/cms/cms-link.svelte';
20
18
  export { default as CmsEntries } from './components/cms/cms-entries.svelte';
19
+ export { default as CmsNav } from './components/cms/cms-nav.svelte';
20
+ export { default as CmsHours } from './components/cms/cms-hours.svelte';
21
+ export { default as CmsContact } from './components/cms/cms-contact.svelte';
22
+ export { default as CmsSocialLinks } from './components/cms/cms-social-links.svelte';
23
+ export { default as CmsCollection } from './components/cms/cms-collection.svelte';
24
+ export { default as CmsTeam } from './components/cms/cms-team.svelte';
25
+ export { default as CmsTestimonials } from './components/cms/cms-testimonials.svelte';
26
+ export { default as CmsPricing } from './components/cms/cms-pricing.svelte';
27
+ export { default as CmsFaq } from './components/cms/cms-faq.svelte';
28
+ export { default as CmsStats } from './components/cms/cms-stats.svelte';
29
+ export { default as CmsSteps } from './components/cms/cms-steps.svelte';
30
+ export { default as CmsTimeline } from './components/cms/cms-timeline.svelte';
31
+ export { default as CmsGallery } from './components/cms/cms-gallery.svelte';
32
+ export { default as CmsLogos } from './components/cms/cms-logos.svelte';
33
+ export { default as CmsSchedule } from './components/cms/cms-schedule.svelte';
34
+ export * from './core/shapes/index.js';
21
35
  export { default as AdminBar } from './components/admin-bar/admin-bar.svelte';
22
36
  export { useCmsField, resolveScopeRef, ROOT_SCOPE_ID } from './components/cms/use-cms-field.svelte.js';
23
37
  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';
38
+ export { defineStructured, applyTranslations, countTranslations, extractTranslations, stripTranslations, translatableFields, translationPath, asBoolean, asEnum, asItems, asNumber, asOptionalString, asString, asStrings, moveItem, newItemId, setIn, ROOT_ID, TRANSLATIONS_KEY } from './core/structured.js';
39
+ export type { ItemsSpec, Structured, StructuredItem, StructuredSchema, TranslatableField, TranslationOverlay } from './core/structured.js';
40
+ export { registerStructured, getStructured } from './core/structured-registry.js';
27
41
  export { toMetaTags } from './core/metadata.js';
28
42
  export type { MetaTagsOptions, MetaTagsOutput } from './core/metadata.js';
29
43
  export { definePage, DEFAULT_METADATA_SCHEMA, metadataSchemaFor } from './components/admin-bar/page-config.js';
package/dist/index.js CHANGED
@@ -8,13 +8,32 @@
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, normalizeImage } from './components/cms/cms-image.svelte';
11
+ export { default as CmsImage } 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, normalizeLink } from './components/cms/cms-link.svelte';
16
- export { default as CmsRepeater } from './components/cms/cms-repeater.svelte';
15
+ export { default as CmsLink } from './components/cms/cms-link.svelte';
17
16
  export { default as CmsEntries } from './components/cms/cms-entries.svelte';
17
+ // Structured editor components. Each reads one versioned value, opens a
18
+ // popover editor in edit mode and hands the typed view to `children`.
19
+ // Their value types, normalizers and JSON-LD helpers come from the shapes.
20
+ export { default as CmsNav } from './components/cms/cms-nav.svelte';
21
+ export { default as CmsHours } from './components/cms/cms-hours.svelte';
22
+ export { default as CmsContact } from './components/cms/cms-contact.svelte';
23
+ export { default as CmsSocialLinks } from './components/cms/cms-social-links.svelte';
24
+ export { default as CmsCollection } from './components/cms/cms-collection.svelte';
25
+ export { default as CmsTeam } from './components/cms/cms-team.svelte';
26
+ export { default as CmsTestimonials } from './components/cms/cms-testimonials.svelte';
27
+ export { default as CmsPricing } from './components/cms/cms-pricing.svelte';
28
+ export { default as CmsFaq } from './components/cms/cms-faq.svelte';
29
+ // List presets: fixed item shapes; a new shape is a new preset here.
30
+ export { default as CmsStats } from './components/cms/cms-stats.svelte';
31
+ export { default as CmsSteps } from './components/cms/cms-steps.svelte';
32
+ export { default as CmsTimeline } from './components/cms/cms-timeline.svelte';
33
+ export { default as CmsGallery } from './components/cms/cms-gallery.svelte';
34
+ export { default as CmsLogos } from './components/cms/cms-logos.svelte';
35
+ export { default as CmsSchedule } from './components/cms/cms-schedule.svelte';
36
+ export * from './core/shapes/index.js';
18
37
  // Admin bar (sync wrapper; admin-bar-internal + seo-panel are async chunks)
19
38
  export { default as AdminBar } from './components/admin-bar/admin-bar.svelte';
20
39
  // Building blocks for structured components: the field hook, the value
@@ -23,8 +42,8 @@ export { default as AdminBar } from './components/admin-bar/admin-bar.svelte';
23
42
  // lazily by editable siblings:
24
43
  // `import('@velastack/cms/editor')`
25
44
  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';
45
+ export { defineStructured, applyTranslations, countTranslations, extractTranslations, stripTranslations, translatableFields, translationPath, asBoolean, asEnum, asItems, asNumber, asOptionalString, asString, asStrings, moveItem, newItemId, setIn, ROOT_ID, TRANSLATIONS_KEY } from './core/structured.js';
46
+ export { registerStructured, getStructured } from './core/structured-registry.js';
28
47
  // Page metadata → svelte-meta-tags props.
29
48
  export { toMetaTags } from './core/metadata.js';
30
49
  // Per-route page configuration — declare in `page.cms.ts` next to `+page.svelte`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velastack/cms",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "A scope-aware CMS for SvelteKit, for static or dynamic sites.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -1,137 +0,0 @@
1
- <script lang="ts" generics="T extends Record<string, unknown>">
2
- import type { Snippet } from 'svelte';
3
- import { cmsStore, type CmsScopeRef } from './cms-store.svelte.js';
4
-
5
- type Props = {
6
- scope: CmsScopeRef;
7
- name: string;
8
- items: T[];
9
- children: Snippet<[T, number]>;
10
- };
11
-
12
- let { scope, name, items, children }: Props = $props();
13
-
14
- /** Build a blank item from the keys of the first existing item. */
15
- const blankItem = (): T => {
16
- const template = items[0];
17
- const out: Record<string, unknown> = {};
18
- if (template) for (const k of Object.keys(template)) out[k] = '';
19
- return out as T;
20
- };
21
-
22
- const commit = (next: T[]) => cmsStore.setValue(scope, name, next);
23
-
24
- const removeItem = (index: number) => commit(items.filter((_, i) => i !== index));
25
- const addItem = () => commit([...items, blankItem()]);
26
- const updateItem = (index: number, patch: Partial<T>) =>
27
- commit(items.map((item, i) => (i === index ? ({ ...item, ...patch } as T) : item)));
28
- </script>
29
-
30
- <ul class="cms-repeater-editable">
31
- {#each items as item, i (i)}
32
- <li class="cms-repeater-editable__item">
33
- <div class="cms-repeater-editable__preview">
34
- {@render children(item, i)}
35
- </div>
36
- <details class="cms-repeater-editable__fields">
37
- <summary>Fields</summary>
38
- {#each Object.keys(item) as key}
39
- <label>
40
- <span>{key}</span>
41
- <input
42
- type="text"
43
- value={String(item[key] ?? '')}
44
- oninput={(e) =>
45
- updateItem(i, { [key]: (e.target as HTMLInputElement).value } as Partial<T>)}
46
- />
47
- </label>
48
- {/each}
49
- </details>
50
- <button
51
- type="button"
52
- class="cms-repeater-editable__remove"
53
- aria-label="Remove item"
54
- onclick={() => removeItem(i)}
55
- >
56
- ×
57
- </button>
58
- </li>
59
- {/each}
60
- </ul>
61
- <button type="button" class="cms-repeater-editable__add" onclick={addItem}>+ Add item</button>
62
-
63
- <style>
64
- .cms-repeater-editable {
65
- list-style: none;
66
- padding: 0;
67
- margin: 0;
68
- display: flex;
69
- flex-direction: column;
70
- gap: 0.5rem;
71
- }
72
- .cms-repeater-editable__item {
73
- position: relative;
74
- padding: 0.5rem;
75
- border: 1px dashed rgba(127, 127, 127, 0.6);
76
- border-radius: 0.25rem;
77
- background: rgba(127, 127, 127, 0.06);
78
- }
79
- .cms-repeater-editable__remove {
80
- position: absolute;
81
- top: 0.25rem;
82
- right: 0.25rem;
83
- width: 1.5rem;
84
- height: 1.5rem;
85
- border: 0;
86
- border-radius: 9999px;
87
- background: rgba(220, 38, 38, 0.1);
88
- color: rgb(220, 38, 38);
89
- cursor: pointer;
90
- font-size: 1rem;
91
- line-height: 1;
92
- }
93
- .cms-repeater-editable__remove:hover {
94
- background: rgba(220, 38, 38, 0.2);
95
- }
96
- .cms-repeater-editable__fields {
97
- margin-top: 0.5rem;
98
- font-size: 0.85em;
99
- }
100
- .cms-repeater-editable__fields summary {
101
- cursor: pointer;
102
- color: rgba(127, 127, 127, 0.9);
103
- }
104
- .cms-repeater-editable__fields label {
105
- display: flex;
106
- align-items: center;
107
- gap: 0.5rem;
108
- margin-top: 0.375rem;
109
- }
110
- .cms-repeater-editable__fields label span {
111
- flex: 0 0 6rem;
112
- font-family: ui-monospace, monospace;
113
- color: rgba(127, 127, 127, 0.9);
114
- }
115
- .cms-repeater-editable__fields input {
116
- flex: 1;
117
- padding: 0.25rem 0.5rem;
118
- border: 1px solid rgba(127, 127, 127, 0.4);
119
- border-radius: 0.25rem;
120
- background: canvas;
121
- color: inherit;
122
- font: inherit;
123
- }
124
- .cms-repeater-editable__add {
125
- margin-top: 0.5rem;
126
- padding: 0.375rem 0.75rem;
127
- border: 1px dashed rgba(127, 127, 127, 0.6);
128
- border-radius: 0.25rem;
129
- background: transparent;
130
- color: inherit;
131
- cursor: pointer;
132
- font: inherit;
133
- }
134
- .cms-repeater-editable__add:hover {
135
- background: rgba(127, 127, 127, 0.06);
136
- }
137
- </style>