@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,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
+ }>;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The six list presets: fixed item shapes behind exported components, so a
3
+ * template never invents a schema of its own. A new shape is a new preset.
4
+ */
5
+ import { asItems, asString, isPlainObject } from '../structured.js';
6
+ import { defineForm } from './form.js';
7
+ import { asImage } from './image.js';
8
+ import { asLink, linkHref } from './link.js';
9
+ const definePreset = (o) => defineForm({
10
+ component: o.component,
11
+ label: o.label,
12
+ version: 1,
13
+ translatable: [],
14
+ items: { key: 'items', translatable: o.translatable },
15
+ normalize: (raw) => {
16
+ const items = Array.isArray(raw) ? raw : isPlainObject(raw) ? raw.items : [];
17
+ return { v: 1, items: asItems(items, o.normalizeItem) };
18
+ },
19
+ empty: () => ({ v: 1, items: [] }),
20
+ fields: [
21
+ {
22
+ key: 'items',
23
+ label: o.label,
24
+ type: 'list',
25
+ itemLabel: o.itemLabel,
26
+ titleKey: o.titleKey,
27
+ blank: o.blank,
28
+ fields: o.fields
29
+ }
30
+ ]
31
+ });
32
+ export const cmsStats = definePreset({
33
+ component: 'CmsStats',
34
+ label: 'Stats',
35
+ itemLabel: 'stat',
36
+ titleKey: 'label',
37
+ translatable: ['value', 'label', 'note'],
38
+ blank: () => ({ value: '', label: '', note: '' }),
39
+ fields: [
40
+ { key: 'value', label: 'Value', type: 'text', half: true, placeholder: '12k' },
41
+ { key: 'label', label: 'Label', type: 'text', placeholder: 'Guests a year' },
42
+ { key: 'note', label: 'Note', type: 'text' }
43
+ ],
44
+ normalizeItem: (r) => ({
45
+ value: asString(r.value),
46
+ label: asString(r.label),
47
+ note: asString(r.note)
48
+ })
49
+ });
50
+ export const cmsSteps = definePreset({
51
+ component: 'CmsSteps',
52
+ label: 'Steps',
53
+ itemLabel: 'step',
54
+ titleKey: 'title',
55
+ translatable: ['title', 'body'],
56
+ blank: () => ({ title: '', body: '', image: null }),
57
+ fields: [
58
+ { key: 'title', label: 'Title', type: 'text' },
59
+ { key: 'body', label: 'Body', type: 'html' },
60
+ { key: 'image', label: 'Image', type: 'image' }
61
+ ],
62
+ normalizeItem: (r) => ({
63
+ title: asString(r.title),
64
+ body: asString(r.body),
65
+ image: asImage(r.image)
66
+ })
67
+ });
68
+ export const cmsTimeline = definePreset({
69
+ component: 'CmsTimeline',
70
+ label: 'Timeline',
71
+ itemLabel: 'entry',
72
+ titleKey: 'title',
73
+ translatable: ['date', 'title', 'body'],
74
+ blank: () => ({ date: '', title: '', body: '', image: null }),
75
+ fields: [
76
+ { key: 'date', label: 'Date', type: 'text', half: true, placeholder: '2019' },
77
+ { key: 'title', label: 'Title', type: 'text' },
78
+ { key: 'body', label: 'Body', type: 'html' },
79
+ { key: 'image', label: 'Image', type: 'image' }
80
+ ],
81
+ normalizeItem: (r) => ({
82
+ date: asString(r.date),
83
+ title: asString(r.title),
84
+ body: asString(r.body),
85
+ image: asImage(r.image)
86
+ })
87
+ });
88
+ export const cmsGallery = definePreset({
89
+ component: 'CmsGallery',
90
+ label: 'Gallery',
91
+ itemLabel: 'photo',
92
+ titleKey: 'caption',
93
+ translatable: ['caption'],
94
+ blank: () => ({ image: null, caption: '' }),
95
+ fields: [
96
+ { key: 'image', label: 'Photo', type: 'image' },
97
+ { key: 'caption', label: 'Caption', type: 'text' }
98
+ ],
99
+ normalizeItem: (r) => ({ image: asImage(r.image ?? r.src), caption: asString(r.caption) })
100
+ });
101
+ export const cmsLogos = definePreset({
102
+ component: 'CmsLogos',
103
+ label: 'Logos',
104
+ itemLabel: 'logo',
105
+ titleKey: 'name',
106
+ translatable: [],
107
+ blank: () => ({ image: null, name: '', link: null }),
108
+ fields: [
109
+ { key: 'name', label: 'Name', type: 'text' },
110
+ { key: 'image', label: 'Logo', type: 'image' },
111
+ { key: 'link', label: 'Link', type: 'link' }
112
+ ],
113
+ normalizeItem: (r) => ({ image: asImage(r.image), name: asString(r.name), link: asLink(r.link) })
114
+ });
115
+ export const logosView = (value) => value.items
116
+ .filter((l) => l.image !== null)
117
+ .map((l) => ({ ...l, href: l.link ? linkHref(l.link) || null : null }));
118
+ export const cmsSchedule = definePreset({
119
+ component: 'CmsSchedule',
120
+ label: 'Schedule',
121
+ itemLabel: 'session',
122
+ titleKey: 'title',
123
+ translatable: ['day', 'time', 'title', 'body', 'location'],
124
+ blank: () => ({ day: '', time: '', title: '', body: '', location: '' }),
125
+ fields: [
126
+ { key: 'day', label: 'Day', type: 'text', half: true, placeholder: 'Monday' },
127
+ { key: 'time', label: 'Time', type: 'text', placeholder: '10:00 – 11:00' },
128
+ { key: 'title', label: 'Title', type: 'text' },
129
+ { key: 'location', label: 'Location', type: 'text' },
130
+ { key: 'body', label: 'Details', type: 'html' }
131
+ ],
132
+ normalizeItem: (r) => ({
133
+ day: asString(r.day),
134
+ time: asString(r.time),
135
+ title: asString(r.title),
136
+ body: asString(r.body),
137
+ location: asString(r.location)
138
+ })
139
+ });
140
+ /** Sessions grouped by `day`, in first-seen order. */
141
+ export const scheduleByDay = (value) => {
142
+ const out = [];
143
+ for (const item of value.items) {
144
+ if (item.title === '')
145
+ continue;
146
+ let group = out.find((g) => g.day === item.day);
147
+ if (!group)
148
+ out.push((group = { day: item.day, items: [] }));
149
+ group.items.push(item);
150
+ }
151
+ return out;
152
+ };
@@ -0,0 +1,40 @@
1
+ import { type CmsLinkValue } from './link.js';
2
+ export type PricingFeature = {
3
+ id: string;
4
+ text: string;
5
+ included: boolean;
6
+ };
7
+ export type PricingTier = {
8
+ id: string;
9
+ name: string;
10
+ price: {
11
+ amount: number | null;
12
+ /** A quote, not a number: the template shows `labels.custom`. */
13
+ custom: boolean;
14
+ /** `month`, `person`… shown after the amount. */
15
+ period: string;
16
+ };
17
+ description: string;
18
+ features: PricingFeature[];
19
+ cta: CmsLinkValue | null;
20
+ highlighted: boolean;
21
+ badge: string;
22
+ };
23
+ export type CmsPricingValue = {
24
+ v: 1;
25
+ /** ISO 4217, e.g. `EUR`. */
26
+ currency: string;
27
+ labels: {
28
+ custom: string;
29
+ };
30
+ items: PricingTier[];
31
+ };
32
+ export type PricingTierView = PricingTier & {
33
+ priceText: string;
34
+ href: string | null;
35
+ };
36
+ export declare const cmsPricing: import("./form.js").FormShape<CmsPricingValue>;
37
+ /** `€29 / month`, the custom label, or empty without an amount. */
38
+ export declare const formatPrice: (tier: PricingTier, value: Pick<CmsPricingValue, "currency" | "labels">, locale: string) => string;
39
+ /** Tiers with a name, each with its formatted price and CTA href. */
40
+ export declare const pricingView: (value: CmsPricingValue, locale: string) => PricingTierView[];
@@ -0,0 +1,115 @@
1
+ /** `CmsPricing`: tiers with a feature checklist and a call to action. */
2
+ import { asBoolean, asItems, asNumber, asString, isPlainObject } from '../structured.js';
3
+ import { defineForm } from './form.js';
4
+ import { asLink, linkHref } from './link.js';
5
+ export const cmsPricing = defineForm({
6
+ component: 'CmsPricing',
7
+ label: 'Pricing',
8
+ version: 1,
9
+ translatable: ['labels.custom'],
10
+ items: {
11
+ key: 'items',
12
+ translatable: ['name', 'description', 'badge', 'price.period'],
13
+ items: { key: 'features', translatable: ['text'] }
14
+ },
15
+ normalize: (raw) => {
16
+ const r = isPlainObject(raw) ? raw : {};
17
+ const labels = isPlainObject(r.labels) ? r.labels : {};
18
+ return {
19
+ v: 1,
20
+ currency: asString(r.currency),
21
+ labels: { custom: asString(labels.custom, 'Custom') },
22
+ items: asItems(r.items, (t) => {
23
+ const p = isPlainObject(t.price) ? t.price : {};
24
+ return {
25
+ name: asString(t.name),
26
+ price: {
27
+ amount: asNumber(p.amount),
28
+ custom: asBoolean(p.custom, p.amount === 'custom'),
29
+ period: asString(p.period)
30
+ },
31
+ description: asString(t.description),
32
+ features: asItems(t.features, (f) => ({
33
+ text: asString(f.text),
34
+ included: asBoolean(f.included, true)
35
+ })),
36
+ cta: asLink(t.cta),
37
+ highlighted: asBoolean(t.highlighted),
38
+ badge: asString(t.badge)
39
+ };
40
+ })
41
+ };
42
+ },
43
+ empty: () => ({ v: 1, currency: '', labels: { custom: 'Custom' }, items: [] }),
44
+ fields: [
45
+ { key: 'currency', label: 'Currency', type: 'text', half: true, placeholder: 'EUR' },
46
+ { key: 'labels.custom', label: '“Custom” price label', type: 'text' },
47
+ {
48
+ key: 'items',
49
+ label: 'Tiers',
50
+ type: 'list',
51
+ itemLabel: 'tier',
52
+ titleKey: 'name',
53
+ blank: () => ({
54
+ name: '',
55
+ price: { amount: null, custom: false, period: '' },
56
+ description: '',
57
+ features: [],
58
+ cta: null,
59
+ highlighted: false,
60
+ badge: ''
61
+ }),
62
+ fields: [
63
+ { key: 'name', label: 'Name', type: 'text', half: true },
64
+ { key: 'badge', label: 'Badge', type: 'text', placeholder: 'Most popular' },
65
+ { key: 'price.amount', label: 'Price', type: 'number', half: true },
66
+ { key: 'price.period', label: 'Per', type: 'text', placeholder: 'month' },
67
+ { key: 'price.custom', label: 'Custom price (quote)', type: 'boolean' },
68
+ { key: 'description', label: 'Description', type: 'long-string' },
69
+ {
70
+ key: 'features',
71
+ label: 'Features',
72
+ type: 'list',
73
+ itemLabel: 'feature',
74
+ titleKey: 'text',
75
+ blank: () => ({ text: '', included: true }),
76
+ fields: [
77
+ { key: 'text', label: 'Feature', type: 'text' },
78
+ { key: 'included', label: 'Included', type: 'boolean' }
79
+ ]
80
+ },
81
+ { key: 'cta', label: 'Call to action', type: 'link' },
82
+ { key: 'highlighted', label: 'Highlighted', type: 'boolean' }
83
+ ]
84
+ }
85
+ ]
86
+ });
87
+ /** `€29 / month`, the custom label, or empty without an amount. */
88
+ export const formatPrice = (tier, value, locale) => {
89
+ if (tier.price.custom)
90
+ return value.labels.custom;
91
+ if (tier.price.amount === null)
92
+ return '';
93
+ let amount;
94
+ try {
95
+ amount = value.currency
96
+ ? new Intl.NumberFormat(locale, {
97
+ style: 'currency',
98
+ currency: value.currency,
99
+ maximumFractionDigits: Number.isInteger(tier.price.amount) ? 0 : 2
100
+ }).format(tier.price.amount)
101
+ : new Intl.NumberFormat(locale).format(tier.price.amount);
102
+ }
103
+ catch {
104
+ amount = `${value.currency} ${tier.price.amount}`.trim();
105
+ }
106
+ return tier.price.period ? `${amount} / ${tier.price.period}` : amount;
107
+ };
108
+ /** Tiers with a name, each with its formatted price and CTA href. */
109
+ export const pricingView = (value, locale) => value.items
110
+ .filter((t) => t.name !== '')
111
+ .map((t) => ({
112
+ ...t,
113
+ priceText: formatPrice(t, value, locale),
114
+ href: t.cta ? linkHref(t.cta) || null : null
115
+ }));
@@ -0,0 +1,19 @@
1
+ import { type ListValue } from './form.js';
2
+ export declare const SOCIAL_PLATFORMS: readonly ["instagram", "facebook", "tiktok", "youtube", "linkedin", "x", "threads", "pinterest", "whatsapp", "tripadvisor", "google", "yelp", "other"];
3
+ export type SocialPlatform = (typeof SOCIAL_PLATFORMS)[number];
4
+ export declare const SOCIAL_PLATFORM_NAMES: Readonly<Record<SocialPlatform, string>>;
5
+ export type SocialLink = {
6
+ id: string;
7
+ platform: SocialPlatform;
8
+ url: string;
9
+ label: string;
10
+ };
11
+ export type CmsSocialLinksValue = ListValue<SocialLink>;
12
+ export type SocialLinkView = SocialLink & {
13
+ name: string;
14
+ };
15
+ export declare const cmsSocialLinks: import("./form.js").FormShape<CmsSocialLinksValue>;
16
+ /** Profiles with a URL, each with a display `name` (the label, else the platform). */
17
+ export declare const socialLinksView: (value: CmsSocialLinksValue) => SocialLinkView[];
18
+ /** The `sameAs` list for an Organization or LocalBusiness JSON-LD node. */
19
+ export declare const toSameAs: (value: CmsSocialLinksValue) => string[];
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `CmsSocialLinks`: an ordered list of profiles. The template supplies the
3
+ * icon for `platform`; `label` defaults to the platform's name.
4
+ */
5
+ import { asEnum, asItems, asString, isPlainObject } from '../structured.js';
6
+ import { defineForm } from './form.js';
7
+ export const SOCIAL_PLATFORMS = [
8
+ 'instagram',
9
+ 'facebook',
10
+ 'tiktok',
11
+ 'youtube',
12
+ 'linkedin',
13
+ 'x',
14
+ 'threads',
15
+ 'pinterest',
16
+ 'whatsapp',
17
+ 'tripadvisor',
18
+ 'google',
19
+ 'yelp',
20
+ 'other'
21
+ ];
22
+ export const SOCIAL_PLATFORM_NAMES = {
23
+ instagram: 'Instagram',
24
+ facebook: 'Facebook',
25
+ tiktok: 'TikTok',
26
+ youtube: 'YouTube',
27
+ linkedin: 'LinkedIn',
28
+ x: 'X',
29
+ threads: 'Threads',
30
+ pinterest: 'Pinterest',
31
+ whatsapp: 'WhatsApp',
32
+ tripadvisor: 'Tripadvisor',
33
+ google: 'Google',
34
+ yelp: 'Yelp',
35
+ other: 'Website'
36
+ };
37
+ export const cmsSocialLinks = defineForm({
38
+ component: 'CmsSocialLinks',
39
+ label: 'Social links',
40
+ version: 1,
41
+ translatable: [],
42
+ items: { key: 'items', translatable: ['label'] },
43
+ normalize: (raw) => {
44
+ const items = Array.isArray(raw) ? raw : isPlainObject(raw) ? raw.items : [];
45
+ return {
46
+ v: 1,
47
+ items: asItems(items, (r) => ({
48
+ platform: asEnum(r.platform, SOCIAL_PLATFORMS, 'other'),
49
+ url: asString(r.url),
50
+ label: asString(r.label)
51
+ }))
52
+ };
53
+ },
54
+ empty: () => ({ v: 1, items: [] }),
55
+ fields: [
56
+ {
57
+ key: 'items',
58
+ label: 'Profiles',
59
+ type: 'list',
60
+ itemLabel: 'profile',
61
+ titleKey: 'platform',
62
+ blank: () => ({ platform: 'instagram', url: '', label: '' }),
63
+ fields: [
64
+ {
65
+ key: 'platform',
66
+ label: 'Platform',
67
+ type: 'enum',
68
+ values: SOCIAL_PLATFORMS,
69
+ names: SOCIAL_PLATFORM_NAMES,
70
+ half: true
71
+ },
72
+ { key: 'url', label: 'URL', type: 'url', placeholder: 'https://…' },
73
+ { key: 'label', label: 'Label', type: 'text', placeholder: 'Defaults to the platform name' }
74
+ ]
75
+ }
76
+ ]
77
+ });
78
+ /** Profiles with a URL, each with a display `name` (the label, else the platform). */
79
+ export const socialLinksView = (value) => value.items
80
+ .filter((i) => i.url !== '')
81
+ .map((i) => ({ ...i, name: i.label || SOCIAL_PLATFORM_NAMES[i.platform] }));
82
+ /** The `sameAs` list for an Organization or LocalBusiness JSON-LD node. */
83
+ export const toSameAs = (value) => value.items.map((i) => i.url).filter((u) => u !== '');
@@ -0,0 +1,22 @@
1
+ import { type ListValue } from './form.js';
2
+ import { type CmsImageValue } from './image.js';
3
+ import { type SocialPlatform } from './social-links.js';
4
+ export type TeamLink = {
5
+ id: string;
6
+ platform: SocialPlatform;
7
+ url: string;
8
+ };
9
+ export type TeamMember = {
10
+ id: string;
11
+ name: string;
12
+ role: string;
13
+ bio: string;
14
+ photo: CmsImageValue | null;
15
+ email: string;
16
+ phone: string;
17
+ links: TeamLink[];
18
+ };
19
+ export type CmsTeamValue = ListValue<TeamMember>;
20
+ export declare const cmsTeam: import("./form.js").FormShape<CmsTeamValue>;
21
+ /** Members with a name. */
22
+ export declare const teamView: (value: CmsTeamValue) => TeamMember[];
@@ -0,0 +1,70 @@
1
+ /** `CmsTeam`: people with a portrait, a role and a rich bio. */
2
+ import { asEnum, asItems, asString, isPlainObject } from '../structured.js';
3
+ import { defineForm } from './form.js';
4
+ import { asImage } from './image.js';
5
+ import { SOCIAL_PLATFORMS, SOCIAL_PLATFORM_NAMES } from './social-links.js';
6
+ export const cmsTeam = defineForm({
7
+ component: 'CmsTeam',
8
+ label: 'Team',
9
+ version: 1,
10
+ translatable: [],
11
+ items: { key: 'items', translatable: ['role', 'bio'] },
12
+ normalize: (raw) => {
13
+ const items = Array.isArray(raw) ? raw : isPlainObject(raw) ? raw.items : [];
14
+ return {
15
+ v: 1,
16
+ items: asItems(items, (r) => ({
17
+ name: asString(r.name),
18
+ role: asString(r.role),
19
+ bio: asString(r.bio),
20
+ photo: asImage(r.photo),
21
+ email: asString(r.email),
22
+ phone: asString(r.phone),
23
+ links: asItems(r.links, (l) => ({
24
+ platform: asEnum(l.platform, SOCIAL_PLATFORMS, 'other'),
25
+ url: asString(l.url)
26
+ }))
27
+ }))
28
+ };
29
+ },
30
+ empty: () => ({ v: 1, items: [] }),
31
+ fields: [
32
+ {
33
+ key: 'items',
34
+ label: 'People',
35
+ type: 'list',
36
+ itemLabel: 'person',
37
+ titleKey: 'name',
38
+ blank: () => ({ name: '', role: '', bio: '', photo: null, email: '', phone: '', links: [] }),
39
+ fields: [
40
+ { key: 'name', label: 'Name', type: 'text', half: true },
41
+ { key: 'role', label: 'Role', type: 'text' },
42
+ { key: 'photo', label: 'Portrait', type: 'image' },
43
+ { key: 'bio', label: 'Bio', type: 'html' },
44
+ { key: 'email', label: 'Email', type: 'text', half: true },
45
+ { key: 'phone', label: 'Phone', type: 'text' },
46
+ {
47
+ key: 'links',
48
+ label: 'Profiles',
49
+ type: 'list',
50
+ itemLabel: 'profile',
51
+ titleKey: 'platform',
52
+ blank: () => ({ platform: 'linkedin', url: '' }),
53
+ fields: [
54
+ {
55
+ key: 'platform',
56
+ label: 'Platform',
57
+ type: 'enum',
58
+ values: SOCIAL_PLATFORMS,
59
+ names: SOCIAL_PLATFORM_NAMES,
60
+ half: true
61
+ },
62
+ { key: 'url', label: 'URL', type: 'url' }
63
+ ]
64
+ }
65
+ ]
66
+ }
67
+ ]
68
+ });
69
+ /** Members with a name. */
70
+ export const teamView = (value) => value.items.filter((m) => m.name !== '');
@@ -0,0 +1,24 @@
1
+ import { type ListValue } from './form.js';
2
+ import { type CmsImageValue } from './image.js';
3
+ export type Testimonial = {
4
+ id: string;
5
+ quote: string;
6
+ author: string;
7
+ role: string;
8
+ company: string;
9
+ photo: CmsImageValue | null;
10
+ /** 1 to 5, or `null` when not rated. */
11
+ rating: number | null;
12
+ source: {
13
+ label: string;
14
+ url: string;
15
+ };
16
+ /** ISO date (`YYYY-MM-DD`), or empty. */
17
+ date: string;
18
+ };
19
+ export type CmsTestimonialsValue = ListValue<Testimonial>;
20
+ export declare const cmsTestimonials: import("./form.js").FormShape<CmsTestimonialsValue>;
21
+ /** Testimonials that have a quote. */
22
+ export declare const testimonialsView: (value: CmsTestimonialsValue) => Testimonial[];
23
+ /** A schema.org `Review` node. `itemReviewed` is the business or product node. */
24
+ export declare const toReview: (item: Testimonial, itemReviewed?: Record<string, unknown>) => Record<string, unknown>;