@velastack/cms 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +104 -47
  2. package/dist/backend/index.d.ts +7 -5
  3. package/dist/backend/index.js +7 -5
  4. package/dist/backend/types.d.ts +1 -1
  5. package/dist/components/admin-bar/admin-bar-internal.svelte +54 -59
  6. package/dist/components/admin-bar/admin-bar.css +1 -1
  7. package/dist/components/admin-bar/admin-bar.svelte +11 -7
  8. package/dist/components/admin-bar/field-input.svelte +20 -1
  9. package/dist/components/admin-bar/field-input.svelte.d.ts +2 -1
  10. package/dist/components/admin-bar/fields-form.svelte +218 -0
  11. package/dist/components/admin-bar/fields-form.svelte.d.ts +16 -0
  12. package/dist/components/admin-bar/history-panel.svelte +4 -4
  13. package/dist/components/admin-bar/link-input.svelte +102 -0
  14. package/dist/components/admin-bar/link-input.svelte.d.ts +9 -0
  15. package/dist/components/admin-bar/list-field.svelte +93 -0
  16. package/dist/components/admin-bar/list-field.svelte.d.ts +20 -0
  17. package/dist/components/admin-bar/locales-panel.svelte +1 -1
  18. package/dist/components/admin-bar/page-options.d.ts +24 -0
  19. package/dist/components/admin-bar/page-options.js +53 -0
  20. package/dist/components/admin-bar/pages-panel.svelte +1 -1
  21. package/dist/components/admin-bar/rich-text-input.svelte +92 -0
  22. package/dist/components/admin-bar/rich-text-input.svelte.d.ts +9 -0
  23. package/dist/components/admin-bar/structured-editor-popover.svelte +15 -6
  24. package/dist/components/admin-bar/theme.svelte.js +1 -1
  25. package/dist/components/cms/cms-collection.svelte +46 -0
  26. package/dist/components/cms/cms-collection.svelte.d.ts +25 -0
  27. package/dist/components/cms/cms-contact.svelte +26 -0
  28. package/dist/components/cms/cms-contact.svelte.d.ts +14 -0
  29. package/dist/components/cms/cms-faq.svelte +22 -0
  30. package/dist/components/cms/cms-faq.svelte.d.ts +15 -0
  31. package/dist/components/cms/cms-gallery.svelte +31 -0
  32. package/dist/components/cms/cms-gallery.svelte.d.ts +16 -0
  33. package/dist/components/cms/cms-hours-editable.svelte +160 -0
  34. package/dist/components/cms/cms-hours-editable.svelte.d.ts +17 -0
  35. package/dist/components/cms/cms-hours.svelte +42 -0
  36. package/dist/components/cms/cms-hours.svelte.d.ts +19 -0
  37. package/dist/components/cms/cms-image.svelte +3 -25
  38. package/dist/components/cms/cms-image.svelte.d.ts +3 -11
  39. package/dist/components/cms/cms-link-editable.svelte +4 -60
  40. package/dist/components/cms/cms-link.svelte +3 -23
  41. package/dist/components/cms/cms-link.svelte.d.ts +3 -10
  42. package/dist/components/cms/cms-logos.svelte +29 -0
  43. package/dist/components/cms/cms-logos.svelte.d.ts +16 -0
  44. package/dist/components/cms/cms-nav.svelte +22 -0
  45. package/dist/components/cms/cms-nav.svelte.d.ts +15 -0
  46. package/dist/components/cms/cms-pricing.svelte +36 -0
  47. package/dist/components/cms/cms-pricing.svelte.d.ts +14 -0
  48. package/dist/components/cms/cms-schedule.svelte +35 -0
  49. package/dist/components/cms/cms-schedule.svelte.d.ts +16 -0
  50. package/dist/components/cms/cms-social-links.svelte +35 -0
  51. package/dist/components/cms/cms-social-links.svelte.d.ts +15 -0
  52. package/dist/components/cms/cms-stats.svelte +31 -0
  53. package/dist/components/cms/cms-stats.svelte.d.ts +16 -0
  54. package/dist/components/cms/cms-steps.svelte +31 -0
  55. package/dist/components/cms/cms-steps.svelte.d.ts +16 -0
  56. package/dist/components/cms/cms-store.svelte.js +8 -5
  57. package/dist/components/cms/cms-structured-editable.svelte +29 -0
  58. package/dist/components/cms/{cms-repeater-editable.svelte.d.ts → cms-structured-editable.svelte.d.ts} +12 -8
  59. package/dist/components/cms/cms-structured.svelte +50 -0
  60. package/dist/components/cms/cms-structured.svelte.d.ts +49 -0
  61. package/dist/components/cms/cms-team.svelte +22 -0
  62. package/dist/components/cms/cms-team.svelte.d.ts +15 -0
  63. package/dist/components/cms/cms-testimonials.svelte +36 -0
  64. package/dist/components/cms/cms-testimonials.svelte.d.ts +15 -0
  65. package/dist/components/cms/cms-timeline.svelte +35 -0
  66. package/dist/components/cms/cms-timeline.svelte.d.ts +16 -0
  67. package/dist/components/cms/index.d.ts +15 -1
  68. package/dist/components/cms/index.js +15 -1
  69. package/dist/core/cms-fetch.d.ts +16 -0
  70. package/dist/core/cms-fetch.js +24 -0
  71. package/dist/core/shapes/collection.d.ts +60 -0
  72. package/dist/core/shapes/collection.js +194 -0
  73. package/dist/core/shapes/contact.d.ts +70 -0
  74. package/dist/core/shapes/contact.js +214 -0
  75. package/dist/core/shapes/faq.d.ts +12 -0
  76. package/dist/core/shapes/faq.js +48 -0
  77. package/dist/core/shapes/form.d.ts +46 -0
  78. package/dist/core/shapes/form.js +7 -0
  79. package/dist/core/shapes/hours.d.ts +91 -0
  80. package/dist/core/shapes/hours.js +325 -0
  81. package/dist/core/shapes/image.d.ts +15 -0
  82. package/dist/core/shapes/image.js +27 -0
  83. package/dist/core/shapes/index.d.ts +31 -0
  84. package/dist/core/shapes/index.js +13 -0
  85. package/dist/core/shapes/link.d.ts +15 -0
  86. package/dist/core/shapes/link.js +42 -0
  87. package/dist/core/shapes/nav.d.ts +21 -0
  88. package/dist/core/shapes/nav.js +71 -0
  89. package/dist/core/shapes/presets.d.ts +65 -0
  90. package/dist/core/shapes/presets.js +152 -0
  91. package/dist/core/shapes/pricing.d.ts +40 -0
  92. package/dist/core/shapes/pricing.js +115 -0
  93. package/dist/core/shapes/social-links.d.ts +19 -0
  94. package/dist/core/shapes/social-links.js +83 -0
  95. package/dist/core/shapes/team.d.ts +22 -0
  96. package/dist/core/shapes/team.js +70 -0
  97. package/dist/core/shapes/testimonials.d.ts +24 -0
  98. package/dist/core/shapes/testimonials.js +86 -0
  99. package/dist/{components/cms → core}/structured-registry.d.ts +2 -2
  100. package/dist/core/structured.d.ts +33 -10
  101. package/dist/core/structured.js +98 -47
  102. package/dist/editor.d.ts +4 -0
  103. package/dist/editor.js +4 -0
  104. package/dist/index.d.ts +22 -8
  105. package/dist/index.js +24 -5
  106. package/dist/server/README.md +1 -1
  107. package/dist/server/api-adapter.js +1 -1
  108. package/dist/server/cms.d.ts +1 -1
  109. package/dist/server/cms.js +1 -1
  110. package/dist/server/load-cms.js +1 -1
  111. package/package.json +15 -11
  112. package/plugin/README.md +34 -4
  113. package/plugin/dist/adapter.d.ts +5 -5
  114. package/plugin/dist/adapter.js +21 -7
  115. package/plugin/dist/adapter.js.map +1 -1
  116. package/plugin/dist/build-state.d.ts +1 -0
  117. package/plugin/dist/build-state.js +10 -1
  118. package/plugin/dist/build-state.js.map +1 -1
  119. package/plugin/dist/build.d.ts +2 -2
  120. package/plugin/dist/build.js +8 -7
  121. package/plugin/dist/build.js.map +1 -1
  122. package/plugin/dist/index.js +5 -1
  123. package/plugin/dist/index.js.map +1 -1
  124. package/plugin/dist/manifest.d.ts +18 -0
  125. package/plugin/dist/manifest.js +78 -10
  126. package/plugin/dist/manifest.js.map +1 -1
  127. package/plugin/dist/transforms.d.ts +4 -4
  128. package/plugin/dist/transforms.js +4 -4
  129. package/dist/cms.d.ts +0 -6
  130. package/dist/cms.js +0 -27
  131. package/dist/components/cms/cms-repeater-editable.svelte +0 -137
  132. package/dist/components/cms/cms-repeater.svelte +0 -52
  133. package/dist/components/cms/cms-repeater.svelte.d.ts +0 -31
  134. /package/dist/{components/cms → core}/structured-registry.js +0 -0
@@ -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>;
@@ -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;