@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
@@ -0,0 +1,325 @@
1
+ import { asBoolean, asItems, asString, isPlainObject } from '../structured.js';
2
+ import { defineForm } from './form.js';
3
+ export const DAY_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
4
+ const TIME = /^([01]\d|2[0-3]):[0-5]\d$/;
5
+ const asTime = (v, fallback) => typeof v === 'string' && TIME.test(v) ? v : fallback;
6
+ const asRanges = (v) => {
7
+ if (!Array.isArray(v))
8
+ return [];
9
+ const out = [];
10
+ for (const r of v) {
11
+ if (!isPlainObject(r))
12
+ continue;
13
+ out.push({ open: asTime(r.open, '09:00'), close: asTime(r.close, '17:00') });
14
+ }
15
+ return out.slice(0, 2);
16
+ };
17
+ const asDay = (v) => {
18
+ if (!isPlainObject(v))
19
+ return { closed: true, ranges: [] };
20
+ const ranges = asRanges(v.ranges);
21
+ return { closed: asBoolean(v.closed, ranges.length === 0), ranges };
22
+ };
23
+ export const DEFAULT_HOURS_LABELS = {
24
+ closed: 'Closed',
25
+ today: 'Today',
26
+ openNow: 'Open now',
27
+ closedNow: 'Closed now'
28
+ };
29
+ const emptyDays = () => {
30
+ const out = {};
31
+ for (const k of DAY_KEYS)
32
+ out[k] = { closed: true, ranges: [] };
33
+ return out;
34
+ };
35
+ export const cmsHours = defineForm({
36
+ component: 'CmsHours',
37
+ label: 'Opening hours',
38
+ version: 1,
39
+ translatable: ['note', 'labels.closed', 'labels.today', 'labels.openNow', 'labels.closedNow'],
40
+ items: { key: 'exceptions', translatable: ['label'] },
41
+ normalize: (raw) => {
42
+ const r = isPlainObject(raw) ? raw : {};
43
+ const days = isPlainObject(r.days) ? r.days : {};
44
+ const labels = isPlainObject(r.labels) ? r.labels : {};
45
+ const out = {
46
+ v: 1,
47
+ timezone: asString(r.timezone),
48
+ days: emptyDays(),
49
+ note: asString(r.note),
50
+ exceptions: asItems(r.exceptions, (e) => {
51
+ const ranges = asRanges(e.ranges);
52
+ return {
53
+ date: asString(e.date),
54
+ label: asString(e.label),
55
+ closed: asBoolean(e.closed, ranges.length === 0),
56
+ ranges
57
+ };
58
+ }),
59
+ labels: {
60
+ closed: asString(labels.closed, DEFAULT_HOURS_LABELS.closed),
61
+ today: asString(labels.today, DEFAULT_HOURS_LABELS.today),
62
+ openNow: asString(labels.openNow, DEFAULT_HOURS_LABELS.openNow),
63
+ closedNow: asString(labels.closedNow, DEFAULT_HOURS_LABELS.closedNow)
64
+ }
65
+ };
66
+ for (const k of DAY_KEYS)
67
+ if (k in days)
68
+ out.days[k] = asDay(days[k]);
69
+ return out;
70
+ },
71
+ empty: () => ({
72
+ v: 1,
73
+ timezone: '',
74
+ days: emptyDays(),
75
+ note: '',
76
+ exceptions: [],
77
+ labels: { ...DEFAULT_HOURS_LABELS }
78
+ }),
79
+ // The week grid is a bespoke editor (cms-hours-editable); these are the
80
+ // fields it renders below the grid through the generic form.
81
+ fields: [
82
+ { key: 'note', label: 'Note', type: 'text', placeholder: 'Closed on public holidays' },
83
+ {
84
+ key: 'exceptions',
85
+ label: 'Exceptions',
86
+ type: 'list',
87
+ itemLabel: 'exception',
88
+ titleKey: 'label',
89
+ blank: () => ({ date: '', label: '', closed: true, ranges: [] }),
90
+ fields: [
91
+ { key: 'date', label: 'Date', type: 'date', half: true },
92
+ { key: 'label', label: 'Label', type: 'text', placeholder: 'Christmas Day' },
93
+ { key: 'closed', label: 'Closed all day', type: 'boolean' }
94
+ ]
95
+ },
96
+ {
97
+ key: 'timezone',
98
+ label: 'Timezone',
99
+ type: 'text',
100
+ placeholder: 'Europe/Madrid',
101
+ group: 'Labels'
102
+ },
103
+ { key: 'labels.closed', label: '“Closed”', type: 'text', half: true, group: 'Labels' },
104
+ { key: 'labels.today', label: '“Today”', type: 'text', group: 'Labels' },
105
+ { key: 'labels.openNow', label: '“Open now”', type: 'text', half: true, group: 'Labels' },
106
+ { key: 'labels.closedNow', label: '“Closed now”', type: 'text', group: 'Labels' }
107
+ ]
108
+ });
109
+ // ---------------------------------------------------------------------------
110
+ // Derived views
111
+ // ---------------------------------------------------------------------------
112
+ const minutesOf = (hhmm) => {
113
+ const [h, m] = hhmm.split(':').map(Number);
114
+ return h * 60 + m;
115
+ };
116
+ /** `09:00` → `9:00 AM` (en) / `9:00` (es). */
117
+ export const formatTime = (hhmm, locale) => {
118
+ if (!TIME.test(hhmm))
119
+ return hhmm;
120
+ const [h, m] = hhmm.split(':').map(Number);
121
+ try {
122
+ return new Intl.DateTimeFormat(locale, {
123
+ hour: 'numeric',
124
+ minute: '2-digit',
125
+ timeZone: 'UTC'
126
+ }).format(new Date(Date.UTC(2024, 0, 1, h, m)));
127
+ }
128
+ catch {
129
+ return hhmm;
130
+ }
131
+ };
132
+ /** `9:00 AM – 5:00 PM, 6:00 – 10:00 PM`, or the closed label. */
133
+ export const formatRanges = (ranges, locale, closedLabel) => ranges.length === 0
134
+ ? closedLabel
135
+ : ranges
136
+ .map((r) => `${formatTime(r.open, locale)} – ${formatTime(r.close, locale)}`)
137
+ .join(', ');
138
+ /** Short weekday names in the visitor's locale, Monday first. */
139
+ export const dayNames = (locale, weekday = 'short') => {
140
+ const out = {};
141
+ let fmt = null;
142
+ try {
143
+ fmt = new Intl.DateTimeFormat(locale, { weekday, timeZone: 'UTC' });
144
+ }
145
+ catch {
146
+ fmt = null;
147
+ }
148
+ DAY_KEYS.forEach((k, i) => {
149
+ // 2024-01-01 is a Monday.
150
+ out[k] = fmt ? fmt.format(new Date(Date.UTC(2024, 0, 1 + i))) : k;
151
+ });
152
+ return out;
153
+ };
154
+ /** The current weekday, date and minute in `timezone` (the visitor's zone when empty or invalid). */
155
+ export const zonedNow = (timezone, now = new Date()) => {
156
+ const keyOf = (weekday) => {
157
+ const k = weekday.slice(0, 3).toLowerCase();
158
+ return DAY_KEYS.includes(k) ? k : 'mon';
159
+ };
160
+ if (timezone) {
161
+ try {
162
+ const parts = new Intl.DateTimeFormat('en-US', {
163
+ timeZone: timezone,
164
+ weekday: 'short',
165
+ year: 'numeric',
166
+ month: '2-digit',
167
+ day: '2-digit',
168
+ hour: '2-digit',
169
+ minute: '2-digit',
170
+ hourCycle: 'h23'
171
+ }).formatToParts(now);
172
+ const p = (t) => parts.find((x) => x.type === t)?.value ?? '';
173
+ return {
174
+ key: keyOf(p('weekday')),
175
+ date: `${p('year')}-${p('month')}-${p('day')}`,
176
+ minutes: Number(p('hour')) * 60 + Number(p('minute'))
177
+ };
178
+ }
179
+ catch {
180
+ /* fall through to the visitor's zone */
181
+ }
182
+ }
183
+ const pad = (n) => String(n).padStart(2, '0');
184
+ const day = (now.getDay() + 6) % 7; // Sunday → 6
185
+ return {
186
+ key: DAY_KEYS[day],
187
+ date: `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`,
188
+ minutes: now.getHours() * 60 + now.getMinutes()
189
+ };
190
+ };
191
+ const sameHours = (a, b) => a.closed === b.closed &&
192
+ a.ranges.length === b.ranges.length &&
193
+ a.ranges.every((r, i) => r.open === b.ranges[i].open && r.close === b.ranges[i].close);
194
+ const effective = (d) => d.closed ? [] : d.ranges;
195
+ const isOpenAt = (ranges, minutes) => ranges.some((r) => {
196
+ const open = minutesOf(r.open);
197
+ const close = minutesOf(r.close);
198
+ if (close <= open)
199
+ return minutes >= open || minutes < close; // overnight
200
+ return minutes >= open && minutes < close;
201
+ });
202
+ export const formatDate = (iso, locale) => {
203
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
204
+ if (!m)
205
+ return iso;
206
+ try {
207
+ return new Intl.DateTimeFormat(locale, {
208
+ day: 'numeric',
209
+ month: 'short',
210
+ timeZone: 'UTC'
211
+ }).format(new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]))));
212
+ }
213
+ catch {
214
+ return iso;
215
+ }
216
+ };
217
+ export const hoursView = (value, locale, now = new Date()) => {
218
+ const names = dayNames(locale);
219
+ const closedLabel = value.labels.closed;
220
+ const rows = [];
221
+ for (const key of DAY_KEYS) {
222
+ const day = value.days[key];
223
+ const last = rows[rows.length - 1];
224
+ if (last && sameHours(value.days[last.keys[last.keys.length - 1]], day)) {
225
+ last.keys.push(key);
226
+ continue;
227
+ }
228
+ rows.push({
229
+ id: key,
230
+ keys: [key],
231
+ days: '',
232
+ text: formatRanges(effective(day), locale, closedLabel),
233
+ closed: day.closed
234
+ });
235
+ }
236
+ for (const row of rows) {
237
+ const first = names[row.keys[0]];
238
+ const lastName = names[row.keys[row.keys.length - 1]];
239
+ row.days =
240
+ row.keys.length === 1
241
+ ? first
242
+ : row.keys.length === 2
243
+ ? `${first}, ${lastName}`
244
+ : `${first} – ${lastName}`;
245
+ }
246
+ const z = zonedNow(value.timezone, now);
247
+ const exception = value.exceptions.find((e) => e.date === z.date) ?? null;
248
+ const todayRanges = effective(exception ?? value.days[z.key]);
249
+ const today = {
250
+ key: z.key,
251
+ date: z.date,
252
+ label: value.labels.today,
253
+ day: names[z.key],
254
+ text: formatRanges(todayRanges, locale, closedLabel),
255
+ closed: todayRanges.length === 0,
256
+ exception
257
+ };
258
+ const isOpenNow = isOpenAt(todayRanges, z.minutes);
259
+ const exceptionRows = value.exceptions
260
+ .filter((e) => e.date !== '')
261
+ .map((e) => ({
262
+ ...e,
263
+ dateText: formatDate(e.date, locale),
264
+ text: formatRanges(effective(e), locale, closedLabel)
265
+ }));
266
+ return {
267
+ ...value,
268
+ rows,
269
+ today,
270
+ isOpenNow,
271
+ status: isOpenNow ? value.labels.openNow : value.labels.closedNow,
272
+ exceptionRows
273
+ };
274
+ };
275
+ const SCHEMA_DAYS = {
276
+ mon: 'Monday',
277
+ tue: 'Tuesday',
278
+ wed: 'Wednesday',
279
+ thu: 'Thursday',
280
+ fri: 'Friday',
281
+ sat: 'Saturday',
282
+ sun: 'Sunday'
283
+ };
284
+ /** schema.org `openingHoursSpecification` entries, one per distinct range. */
285
+ export const toOpeningHoursSpecification = (value) => {
286
+ const byRange = new Map();
287
+ for (const key of DAY_KEYS) {
288
+ for (const r of effective(value.days[key])) {
289
+ const k = `${r.open}-${r.close}`;
290
+ const entry = byRange.get(k) ?? { opens: r.open, closes: r.close, days: [] };
291
+ entry.days.push(SCHEMA_DAYS[key]);
292
+ byRange.set(k, entry);
293
+ }
294
+ }
295
+ const out = [...byRange.values()].map((e) => ({
296
+ '@type': 'OpeningHoursSpecification',
297
+ dayOfWeek: e.days,
298
+ opens: e.opens,
299
+ closes: e.closes
300
+ }));
301
+ for (const e of value.exceptions) {
302
+ if (!e.date)
303
+ continue;
304
+ if (e.closed || e.ranges.length === 0) {
305
+ out.push({
306
+ '@type': 'OpeningHoursSpecification',
307
+ validFrom: e.date,
308
+ validThrough: e.date,
309
+ opens: '00:00',
310
+ closes: '00:00'
311
+ });
312
+ continue;
313
+ }
314
+ for (const r of e.ranges) {
315
+ out.push({
316
+ '@type': 'OpeningHoursSpecification',
317
+ validFrom: e.date,
318
+ validThrough: e.date,
319
+ opens: r.open,
320
+ closes: r.close
321
+ });
322
+ }
323
+ }
324
+ return out;
325
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Stored shape of an image slot. Seeds and fallbacks may pass a bare URL
3
+ * string; it is normalized to `{ url }`. A cleared slot stores `url: null`.
4
+ */
5
+ export type CmsImageValue = {
6
+ url?: string | null;
7
+ alt?: string | null;
8
+ width?: number;
9
+ height?: number;
10
+ };
11
+ export declare const normalizeImage: (raw: unknown) => CmsImageValue;
12
+ /** An image inside a structured value: `null` when there is no URL. */
13
+ export declare const asImage: (raw: unknown) => CmsImageValue | null;
14
+ /** A list of images, dropping entries without a URL. */
15
+ export declare const asImages: (raw: unknown) => CmsImageValue[];
@@ -0,0 +1,27 @@
1
+ export const normalizeImage = (raw) => {
2
+ if (raw == null)
3
+ return {};
4
+ if (typeof raw === 'string')
5
+ return { url: raw };
6
+ if (typeof raw === 'object' && !Array.isArray(raw)) {
7
+ const r = raw;
8
+ const out = {};
9
+ if (typeof r.url === 'string')
10
+ out.url = r.url;
11
+ if (typeof r.alt === 'string')
12
+ out.alt = r.alt;
13
+ if (typeof r.width === 'number')
14
+ out.width = r.width;
15
+ if (typeof r.height === 'number')
16
+ out.height = r.height;
17
+ return out;
18
+ }
19
+ return {};
20
+ };
21
+ /** An image inside a structured value: `null` when there is no URL. */
22
+ export const asImage = (raw) => {
23
+ const img = normalizeImage(raw);
24
+ return img.url ? img : null;
25
+ };
26
+ /** A list of images, dropping entries without a URL. */
27
+ export const asImages = (raw) => Array.isArray(raw) ? raw.map(asImage).filter((i) => i !== null) : [];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The structured value shapes behind the editor components. Importing this
3
+ * module registers every schema, so the Locales panel can count translatable
4
+ * strings for any usage the manifest recorded.
5
+ */
6
+ export type { FormField, FormFieldType, FormShape, ListValue } from './form.js';
7
+ export { defineForm } from './form.js';
8
+ export { normalizeLink, asLink, linkHref } from './link.js';
9
+ export type { CmsLinkValue } from './link.js';
10
+ export { normalizeImage, asImage, asImages } from './image.js';
11
+ export type { CmsImageValue } from './image.js';
12
+ export { cmsNav, navView, isActive } from './nav.js';
13
+ export type { CmsNavValue, NavItem, NavItemView } from './nav.js';
14
+ export { cmsHours, hoursView, formatTime, formatRanges, formatDate, dayNames, zonedNow, toOpeningHoursSpecification, DAY_KEYS, DEFAULT_HOURS_LABELS } from './hours.js';
15
+ export type { CmsHoursValue, DayKey, DayHours, HoursRange, HoursException, HoursLabels, HoursRow, HoursToday, HoursExceptionRow, HoursView } from './hours.js';
16
+ export { cmsContact, contactView, formatAddress, telHref, whatsappHref, toPostalAddress, DEFAULT_CONTACT_LABELS } from './contact.js';
17
+ export type { CmsContactValue, ContactAddress, ContactEmail, ContactLabels, ContactPhone, ContactView } from './contact.js';
18
+ export { cmsSocialLinks, socialLinksView, toSameAs, SOCIAL_PLATFORMS, SOCIAL_PLATFORM_NAMES } from './social-links.js';
19
+ export type { CmsSocialLinksValue, SocialLink, SocialLinkView, SocialPlatform } from './social-links.js';
20
+ export { cmsTeam, teamView } from './team.js';
21
+ export type { CmsTeamValue, TeamLink, TeamMember } from './team.js';
22
+ export { cmsTestimonials, testimonialsView, toReview } from './testimonials.js';
23
+ export type { CmsTestimonialsValue, Testimonial } from './testimonials.js';
24
+ export { cmsCollection, collectionView, collectionTags, formatCollectionPrice, toItemList } from './collection.js';
25
+ export type { CmsCollectionValue, CollectionDetail, CollectionItem, CollectionItemView, CollectionPrice, CollectionSlice } from './collection.js';
26
+ export { cmsPricing, pricingView, formatPrice } from './pricing.js';
27
+ export type { CmsPricingValue, PricingFeature, PricingTier, PricingTierView } from './pricing.js';
28
+ export { cmsFaq, faqView, toFaqPage } from './faq.js';
29
+ export type { CmsFaqValue, FaqItem } from './faq.js';
30
+ export { cmsStats, cmsSteps, cmsTimeline, cmsGallery, cmsLogos, cmsSchedule, logosView, scheduleByDay } from './presets.js';
31
+ export type { CmsStatsValue, StatItem, CmsStepsValue, StepItem, CmsTimelineValue, TimelineItem, CmsGalleryValue, GalleryItem, CmsLogosValue, LogoItem, LogoItemView, CmsScheduleValue, ScheduleItem } from './presets.js';
@@ -0,0 +1,13 @@
1
+ export { defineForm } from './form.js';
2
+ export { normalizeLink, asLink, linkHref } from './link.js';
3
+ export { normalizeImage, asImage, asImages } from './image.js';
4
+ export { cmsNav, navView, isActive } from './nav.js';
5
+ export { cmsHours, hoursView, formatTime, formatRanges, formatDate, dayNames, zonedNow, toOpeningHoursSpecification, DAY_KEYS, DEFAULT_HOURS_LABELS } from './hours.js';
6
+ export { cmsContact, contactView, formatAddress, telHref, whatsappHref, toPostalAddress, DEFAULT_CONTACT_LABELS } from './contact.js';
7
+ export { cmsSocialLinks, socialLinksView, toSameAs, SOCIAL_PLATFORMS, SOCIAL_PLATFORM_NAMES } from './social-links.js';
8
+ export { cmsTeam, teamView } from './team.js';
9
+ export { cmsTestimonials, testimonialsView, toReview } from './testimonials.js';
10
+ export { cmsCollection, collectionView, collectionTags, formatCollectionPrice, toItemList } from './collection.js';
11
+ export { cmsPricing, pricingView, formatPrice } from './pricing.js';
12
+ export { cmsFaq, faqView, toFaqPage } from './faq.js';
13
+ export { cmsStats, cmsSteps, cmsTimeline, cmsGallery, cmsLogos, cmsSchedule, logosView, scheduleByDay } from './presets.js';
@@ -0,0 +1,15 @@
1
+ export type CmsLinkValue = {
2
+ label?: string;
3
+ /** Raw URL — used when `routeId` is absent. */
4
+ href?: string;
5
+ /** SvelteKit route id, e.g. `/blog/[slug]`. Resolved at render time. */
6
+ routeId?: string;
7
+ params?: Record<string, string>;
8
+ newTab?: boolean;
9
+ };
10
+ export declare const normalizeLink: (raw: unknown) => CmsLinkValue;
11
+ /** A link inside a structured value: `null` when nothing points anywhere. */
12
+ export declare const asLink: (raw: unknown) => CmsLinkValue | null;
13
+ /** The URL a link points at, or `fallback` when it points nowhere or the
14
+ * route cannot be resolved. */
15
+ export declare const linkHref: (link: CmsLinkValue | null | undefined, fallback?: string) => string;
@@ -0,0 +1,42 @@
1
+ import { resolveRouteUrl } from '../../components/admin-bar/resolve-route.js';
2
+ import { isPlainObject } from '../structured.js';
3
+ export const normalizeLink = (raw) => {
4
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
5
+ return {};
6
+ const r = raw;
7
+ const out = {};
8
+ if (typeof r.label === 'string')
9
+ out.label = r.label;
10
+ if (typeof r.href === 'string')
11
+ out.href = r.href;
12
+ if (typeof r.routeId === 'string')
13
+ out.routeId = r.routeId;
14
+ if (r.params && typeof r.params === 'object' && !Array.isArray(r.params)) {
15
+ out.params = r.params;
16
+ }
17
+ if (typeof r.newTab === 'boolean')
18
+ out.newTab = r.newTab;
19
+ return out;
20
+ };
21
+ /** A link inside a structured value: `null` when nothing points anywhere. */
22
+ export const asLink = (raw) => {
23
+ if (!isPlainObject(raw))
24
+ return null;
25
+ const link = normalizeLink(raw);
26
+ return link.href || link.routeId ? link : null;
27
+ };
28
+ /** The URL a link points at, or `fallback` when it points nowhere or the
29
+ * route cannot be resolved. */
30
+ export const linkHref = (link, fallback = '') => {
31
+ if (!link)
32
+ return fallback;
33
+ if (link.routeId) {
34
+ try {
35
+ return resolveRouteUrl(link.routeId, link.params ?? {});
36
+ }
37
+ catch {
38
+ return fallback;
39
+ }
40
+ }
41
+ return link.href ?? fallback;
42
+ };
@@ -0,0 +1,21 @@
1
+ import { type ListValue } from './form.js';
2
+ import { type CmsLinkValue } from './link.js';
3
+ export type NavItem = {
4
+ id: string;
5
+ label: string;
6
+ link: CmsLinkValue | null;
7
+ children: NavItem[];
8
+ };
9
+ export type CmsNavValue = ListValue<NavItem>;
10
+ export type NavItemView = {
11
+ id: string;
12
+ label: string;
13
+ href: string;
14
+ newTab: boolean;
15
+ children: NavItemView[];
16
+ };
17
+ export declare const cmsNav: import("./form.js").FormShape<CmsNavValue>;
18
+ /** Items with resolved hrefs, dropping items that have no label. */
19
+ export declare const navView: (value: CmsNavValue) => NavItemView[];
20
+ /** Whether `item` (or one of its children) points at the current page. */
21
+ export declare const isActive: (item: NavItemView, url: URL | string) => boolean;
@@ -0,0 +1,71 @@
1
+ import { asBoolean, asItems, asString, isPlainObject } from '../structured.js';
2
+ import { defineForm } from './form.js';
3
+ import { asLink, linkHref } from './link.js';
4
+ const normalizeItem = (raw, depth) => ({
5
+ label: asString(raw.label),
6
+ link: asLink(raw.link),
7
+ children: depth === 0 ? asItems(raw.children, (child) => normalizeItem(child, depth + 1)) : []
8
+ });
9
+ const blankItem = () => ({ label: '', link: null, children: [] });
10
+ const linkField = { key: 'link', label: 'Link', type: 'link' };
11
+ export const cmsNav = defineForm({
12
+ component: 'CmsNav',
13
+ label: 'Navigation',
14
+ version: 1,
15
+ translatable: [],
16
+ items: {
17
+ key: 'items',
18
+ translatable: ['label'],
19
+ items: { key: 'children', translatable: ['label'] }
20
+ },
21
+ normalize: (raw) => {
22
+ const items = Array.isArray(raw) ? raw : isPlainObject(raw) ? raw.items : [];
23
+ return { v: 1, items: asItems(items, (item) => normalizeItem(item, 0)) };
24
+ },
25
+ empty: () => ({ v: 1, items: [] }),
26
+ fields: [
27
+ {
28
+ key: 'items',
29
+ label: 'Links',
30
+ type: 'list',
31
+ itemLabel: 'link',
32
+ titleKey: 'label',
33
+ blank: blankItem,
34
+ fields: [
35
+ { key: 'label', label: 'Label', type: 'text' },
36
+ linkField,
37
+ {
38
+ key: 'children',
39
+ label: 'Sub-links',
40
+ type: 'list',
41
+ itemLabel: 'sub-link',
42
+ titleKey: 'label',
43
+ blank: blankItem,
44
+ fields: [{ key: 'label', label: 'Label', type: 'text' }, linkField]
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ });
50
+ const viewItem = (item) => ({
51
+ id: item.id,
52
+ label: item.label,
53
+ href: linkHref(item.link, '#'),
54
+ newTab: asBoolean(item.link?.newTab),
55
+ children: item.children.map(viewItem)
56
+ });
57
+ /** Items with resolved hrefs, dropping items that have no label. */
58
+ export const navView = (value) => value.items.filter((i) => i.label !== '').map(viewItem);
59
+ /** Whether `item` (or one of its children) points at the current page. */
60
+ export const isActive = (item, url) => {
61
+ const pathname = typeof url === 'string' ? url : url.pathname;
62
+ const path = pathname.replace(/\/+$/, '') || '/';
63
+ const href = item.href.replace(/\/+$/, '') || '/';
64
+ if (href === '#')
65
+ return false;
66
+ if (href === path)
67
+ return true;
68
+ if (href !== '/' && path.startsWith(href + '/'))
69
+ return true;
70
+ return item.children.some((c) => isActive(c, url));
71
+ };
@@ -0,0 +1,65 @@
1
+ import { type FormShape, type ListValue } from './form.js';
2
+ import { type CmsImageValue } from './image.js';
3
+ import { type CmsLinkValue } from './link.js';
4
+ export type StatItem = {
5
+ id: string;
6
+ value: string;
7
+ label: string;
8
+ note: string;
9
+ };
10
+ export type CmsStatsValue = ListValue<StatItem>;
11
+ export declare const cmsStats: FormShape<ListValue<StatItem>>;
12
+ export type StepItem = {
13
+ id: string;
14
+ title: string;
15
+ body: string;
16
+ image: CmsImageValue | null;
17
+ };
18
+ export type CmsStepsValue = ListValue<StepItem>;
19
+ export declare const cmsSteps: FormShape<ListValue<StepItem>>;
20
+ export type TimelineItem = {
21
+ id: string;
22
+ /** Free text: `2019`, `March 2024`. */
23
+ date: string;
24
+ title: string;
25
+ body: string;
26
+ image: CmsImageValue | null;
27
+ };
28
+ export type CmsTimelineValue = ListValue<TimelineItem>;
29
+ export declare const cmsTimeline: FormShape<ListValue<TimelineItem>>;
30
+ export type GalleryItem = {
31
+ id: string;
32
+ image: CmsImageValue | null;
33
+ caption: string;
34
+ };
35
+ export type CmsGalleryValue = ListValue<GalleryItem>;
36
+ export declare const cmsGallery: FormShape<ListValue<GalleryItem>>;
37
+ export type LogoItem = {
38
+ id: string;
39
+ image: CmsImageValue | null;
40
+ name: string;
41
+ link: CmsLinkValue | null;
42
+ };
43
+ export type CmsLogosValue = ListValue<LogoItem>;
44
+ export type LogoItemView = LogoItem & {
45
+ href: string | null;
46
+ };
47
+ export declare const cmsLogos: FormShape<ListValue<LogoItem>>;
48
+ export declare const logosView: (value: CmsLogosValue) => LogoItemView[];
49
+ export type ScheduleItem = {
50
+ id: string;
51
+ /** Free text: `Monday`, `Sat 12 Oct`. */
52
+ day: string;
53
+ /** Free text: `10:00`, `7 – 9pm`. */
54
+ time: string;
55
+ title: string;
56
+ body: string;
57
+ location: string;
58
+ };
59
+ export type CmsScheduleValue = ListValue<ScheduleItem>;
60
+ export declare const cmsSchedule: FormShape<ListValue<ScheduleItem>>;
61
+ /** Sessions grouped by `day`, in first-seen order. */
62
+ export declare const scheduleByDay: (value: CmsScheduleValue) => Array<{
63
+ day: string;
64
+ items: ScheduleItem[];
65
+ }>;