@getvitops/emdash 1.0.0 → 2.0.0-alpha.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.
- package/CHANGELOG.md +102 -0
- package/dist/index.d.mts +34 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1450 -177
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -7
- package/src/admin/DesignSystemEditorPage.tsx +15 -42
- package/src/astro/Nothing.astro +3 -0
- package/src/astro/WidgetArea.astro +91 -0
- package/src/astro/blocks/Accordion.astro +2 -1
- package/src/astro/blocks/ActionLink.astro +2 -1
- package/src/astro/blocks/Badges.astro +67 -0
- package/src/astro/blocks/Banner.astro +3 -2
- package/src/astro/blocks/Callout.astro +68 -0
- package/src/astro/blocks/Cards.astro +54 -9
- package/src/astro/blocks/Carousel.astro +2 -0
- package/src/astro/blocks/CopyButton.astro +3 -2
- package/src/astro/blocks/Details.astro +2 -1
- package/src/astro/blocks/Embed.astro +68 -0
- package/src/astro/blocks/Entries.astro +31 -0
- package/src/astro/blocks/Form.astro +195 -0
- package/src/astro/blocks/Gallery.astro +2 -0
- package/src/astro/blocks/Hero.astro +78 -23
- package/src/astro/blocks/ImageCompare.astro +2 -1
- package/src/astro/blocks/Logos.astro +48 -0
- package/src/astro/blocks/Marquee.astro +2 -1
- package/src/astro/blocks/Nav.astro +85 -0
- package/src/astro/blocks/NavTree.astro +48 -0
- package/src/astro/blocks/Pricing.astro +66 -0
- package/src/astro/blocks/Reviews.astro +122 -0
- package/src/astro/blocks/RichText.astro +14 -0
- package/src/astro/blocks/Section.astro +82 -0
- package/src/astro/blocks/Stats.astro +59 -0
- package/src/astro/blocks/Steps.astro +48 -0
- package/src/astro/blocks/WidgetArea.astro +21 -0
- package/src/astro/blocks/attrs.test.ts +56 -0
- package/src/astro/blocks/attrs.ts +104 -0
- package/src/astro/blocks/embed.test.ts +43 -0
- package/src/astro/blocks/embed.ts +84 -0
- package/src/astro/blocks/form.sample.ts +33 -0
- package/src/astro/blocks/form.test.ts +55 -0
- package/src/astro/blocks/form.ts +158 -0
- package/src/astro/blocks/head.stub.ts +2 -0
- package/src/astro/blocks/nav.test.ts +51 -0
- package/src/astro/blocks/nav.ts +65 -0
- package/src/astro/blocks/render.container.test.ts +610 -0
- package/src/astro/blocks/tone.test.ts +53 -0
- package/src/astro/blocks/tone.ts +122 -0
- package/src/astro/blocks/value.test.ts +36 -1
- package/src/astro/blocks/value.ts +78 -1
- package/src/astro/index.ts +37 -0
- package/src/admin/core-editor.d.ts +0 -7
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { definePlugin, pluginResponse } from "emdash";
|
|
1
|
+
import { definePlugin, getMenus, getWidgetAreas, pluginResponse } from "emdash";
|
|
2
2
|
import { iconMap } from "@getvitops/utils";
|
|
3
3
|
import { designSystemPath, formatConfig, getAt, isConfig, mergePatch, resolveInput, setAt } from "@getvitops/generator/config";
|
|
4
4
|
import { buildReverseIndex, patchToOverrideCss, validateColorContract } from "@getvitops/generator/tokens";
|
|
@@ -25,8 +25,191 @@ const SEMANTIC_ICON_OPTIONS = Object.keys(iconMap.fa7).sort().map((name) => ({
|
|
|
25
25
|
value: name
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
|
+
// src/astro/blocks/tone.ts
|
|
29
|
+
/**
|
|
30
|
+
* The shared `tone` convention (UPSTREAM finding 21): every block that colours anything
|
|
31
|
+
* takes one `tone` and, optionally, one `color`.
|
|
32
|
+
*
|
|
33
|
+
* - **`tone`** — a role name from the site's `colors.roles` (`brand-primary`, `danger`, a
|
|
34
|
+
* consumer-named surface role…), or a semantic name. Role first, then its semantic
|
|
35
|
+
* fallbacks, then `neutral`.
|
|
36
|
+
* - **`color`** — the escape hatch: a palette hue (`amber`) or hue and step (`amber-300`).
|
|
37
|
+
* Wins over `tone` when set. Contrast is NOT checked for it — the generator's contrast
|
|
38
|
+
* contract covers roles, not arbitrary hue/step pairs — so it is for decoration.
|
|
39
|
+
*
|
|
40
|
+
* **Why CSS custom properties, not classes.** A class name built from a field value
|
|
41
|
+
* (`bg-${tone}`) is invisible to Tailwind's scanner, so it generates nothing in that
|
|
42
|
+
* format. The resolver instead returns an inline `style` of `--tone-*` properties that
|
|
43
|
+
* the framework's `.tone` pattern (patterns/tone.css) reads. Each property is a `var()`
|
|
44
|
+
* chain, so the fallback happens in CSS at render time and this module needs NO copy of
|
|
45
|
+
* the site's config — a tone the site never defined degrades to `neutral`, not to
|
|
46
|
+
* nothing.
|
|
47
|
+
*
|
|
48
|
+
* The inputs are editor-supplied strings that land inside a `style` attribute, so both are
|
|
49
|
+
* validated against a closed identifier grammar before use; anything else is dropped.
|
|
50
|
+
*/
|
|
51
|
+
/** Semantic names every site is expected to define (`REQUIRED_ROLES`), plus common aliases. */
|
|
52
|
+
const SEMANTIC_TONES = [
|
|
53
|
+
"neutral",
|
|
54
|
+
"info",
|
|
55
|
+
"success",
|
|
56
|
+
"warning",
|
|
57
|
+
"danger"
|
|
58
|
+
];
|
|
59
|
+
/**
|
|
60
|
+
* When a role is absent, which others to try before `neutral`. Covers the framework's own
|
|
61
|
+
* vocabulary (`ui-*`, `brand-*`) so a site that names only some of them still gets the
|
|
62
|
+
* nearest relative rather than a grey block.
|
|
63
|
+
*/
|
|
64
|
+
const FALLBACKS = {
|
|
65
|
+
"brand-primary": ["ui-primary", "info"],
|
|
66
|
+
"brand-secondary": ["ui-secondary", "info"],
|
|
67
|
+
"ui-primary": ["info"],
|
|
68
|
+
"ui-secondary": ["info"],
|
|
69
|
+
"ui-accent": ["warning"],
|
|
70
|
+
surface: ["neutral"],
|
|
71
|
+
error: ["danger"],
|
|
72
|
+
notice: ["warning"],
|
|
73
|
+
note: ["info"],
|
|
74
|
+
it: ["ui-primary", "info"],
|
|
75
|
+
mk: ["ui-accent", "warning"]
|
|
76
|
+
};
|
|
77
|
+
const ROLE = /^[a-z][a-z0-9-]{0,40}$/;
|
|
78
|
+
const HUE = /^([a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*?)(?:-(\d{2,3}))?$/;
|
|
79
|
+
/** The role chain for a tone: itself, its relatives, then `neutral`. Pure and exported for tests. */
|
|
80
|
+
function toneChain(tone) {
|
|
81
|
+
const name = typeof tone === "string" ? tone.trim().toLowerCase() : "";
|
|
82
|
+
if (!ROLE.test(name)) return ["neutral"];
|
|
83
|
+
return [.../* @__PURE__ */ new Set([
|
|
84
|
+
name,
|
|
85
|
+
...FALLBACKS[name] ?? [],
|
|
86
|
+
"neutral"
|
|
87
|
+
])];
|
|
88
|
+
}
|
|
89
|
+
/** `var(--a, var(--b, …))` over a chain of token names; the last link has no fallback. */
|
|
90
|
+
function chain(tokens) {
|
|
91
|
+
return tokens.reduceRight((inner, token) => inner ? `var(${token}, ${inner})` : `var(${token})`, "");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a block's `tone` / `color` fields.
|
|
95
|
+
*
|
|
96
|
+
* Returns an empty style when neither is set, so an unconfigured block carries no tone at
|
|
97
|
+
* all (rather than a neutral one) and stays transparent.
|
|
98
|
+
*/
|
|
99
|
+
function resolveTone(tone, color) {
|
|
100
|
+
const hasTone = typeof tone === "string" && tone.trim() !== "";
|
|
101
|
+
const hue = typeof color === "string" ? HUE.exec(color.trim().toLowerCase()) : null;
|
|
102
|
+
if (hue) {
|
|
103
|
+
const [, name, step] = hue;
|
|
104
|
+
const at = (s) => `var(--color-${name}-${s})`;
|
|
105
|
+
return {
|
|
106
|
+
className: "tone",
|
|
107
|
+
style: [
|
|
108
|
+
`--tone-bg: ${at("100")}`,
|
|
109
|
+
`--tone-fg: ${at("900")}`,
|
|
110
|
+
`--tone-border: ${at("300")}`,
|
|
111
|
+
`--tone-solid: ${at(step ?? "600")}`,
|
|
112
|
+
`--tone-on-solid: #fff`
|
|
113
|
+
].join("; ")
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (!hasTone) return {
|
|
117
|
+
className: "tone",
|
|
118
|
+
style: ""
|
|
119
|
+
};
|
|
120
|
+
const roles = toneChain(tone);
|
|
121
|
+
const of = (prefix, suffix = "") => chain(roles.map((r) => `--color-${prefix}-${r}${suffix}`));
|
|
122
|
+
return {
|
|
123
|
+
className: "tone",
|
|
124
|
+
style: [
|
|
125
|
+
`--tone-bg: ${of("bg", "-muted")}`,
|
|
126
|
+
`--tone-fg: ${of("text")}`,
|
|
127
|
+
`--tone-border: ${of("border")}`,
|
|
128
|
+
`--tone-solid: ${chain([...roles.map((r) => `--color-bg-${r}-solid`), ...roles.map((r) => `--color-bg-${r}`)])}`,
|
|
129
|
+
`--tone-on-solid: ${chain(roles.map((r) => `--color-text-on-${r}`))}`
|
|
130
|
+
].join("; ")
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
28
134
|
// src/blocks.ts
|
|
29
|
-
|
|
135
|
+
/** Route that fills every tone select with the site's own roles — see `index.ts`. */
|
|
136
|
+
const TONES_ROUTE = "tones/list";
|
|
137
|
+
/** Route that lists the site's EmDash menus for `vitops.nav`'s `menu` select — see `menus-route.ts`. */
|
|
138
|
+
const MENUS_ROUTE = "menus/list";
|
|
139
|
+
/**
|
|
140
|
+
* The tone options an editor can always pick, before the site adds its own roles.
|
|
141
|
+
* Static because Block Kit needs a non-empty `options` list even when `optionsRoute` is set
|
|
142
|
+
* (it is the fallback if the route fails, and what non-block-editor renderers read).
|
|
143
|
+
*/
|
|
144
|
+
const STATIC_TONE_OPTIONS = [{
|
|
145
|
+
label: "None",
|
|
146
|
+
value: ""
|
|
147
|
+
}, ...SEMANTIC_TONES.map((t) => ({
|
|
148
|
+
label: t[0].toUpperCase() + t.slice(1),
|
|
149
|
+
value: t
|
|
150
|
+
}))];
|
|
151
|
+
/**
|
|
152
|
+
* The shared tone pair (UPSTREAM 21; resolution lives in `astro/blocks/tone.ts`): a `tone`
|
|
153
|
+
* select that offers the site's roles, and a `color` escape hatch naming a palette hue.
|
|
154
|
+
* Spread into any block that colours something.
|
|
155
|
+
*/
|
|
156
|
+
function toneFields() {
|
|
157
|
+
return [{
|
|
158
|
+
type: "select",
|
|
159
|
+
action_id: "tone",
|
|
160
|
+
label: "Tone (a colour role from the site)",
|
|
161
|
+
initial_value: "",
|
|
162
|
+
options: STATIC_TONE_OPTIONS,
|
|
163
|
+
optionsRoute: TONES_ROUTE
|
|
164
|
+
}, {
|
|
165
|
+
type: "text_input",
|
|
166
|
+
action_id: "color",
|
|
167
|
+
label: "Palette colour instead of a tone (e.g. amber or amber-300)"
|
|
168
|
+
}];
|
|
169
|
+
}
|
|
170
|
+
/** Route that lists the site's widget areas for `vitops.widgetArea`'s `area` select. */
|
|
171
|
+
const WIDGET_AREAS_ROUTE = "widget-areas/list";
|
|
172
|
+
/**
|
|
173
|
+
* The escape hatches every block carries, appended last by `BLOCKS` (see `astro/blocks/attrs.ts`
|
|
174
|
+
* for how each is validated and why `style` takes custom properties only).
|
|
175
|
+
*/
|
|
176
|
+
function commonFields() {
|
|
177
|
+
return [
|
|
178
|
+
{
|
|
179
|
+
type: "text_input",
|
|
180
|
+
action_id: "anchor",
|
|
181
|
+
label: "Anchor id (so a link can point here with #id)"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: "text_input",
|
|
185
|
+
action_id: "className",
|
|
186
|
+
label: "Extra CSS classes (with the tailwind format, only classes the site already uses)"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
type: "text_input",
|
|
190
|
+
action_id: "style",
|
|
191
|
+
label: "CSS custom properties, e.g. --p-card: 2rem; --tone-bg: #fee"
|
|
192
|
+
}
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
/** Heading level for a block's own heading; `h1` is reserved for the page title. */
|
|
196
|
+
const levelField = (initial) => ({
|
|
197
|
+
type: "select",
|
|
198
|
+
action_id: "level",
|
|
199
|
+
label: "Heading level",
|
|
200
|
+
initial_value: initial,
|
|
201
|
+
options: [
|
|
202
|
+
"2",
|
|
203
|
+
"3",
|
|
204
|
+
"4",
|
|
205
|
+
"5",
|
|
206
|
+
"6"
|
|
207
|
+
].map((n) => ({
|
|
208
|
+
label: `h${n}`,
|
|
209
|
+
value: n
|
|
210
|
+
}))
|
|
211
|
+
});
|
|
212
|
+
const BASE_BLOCKS = [
|
|
30
213
|
{
|
|
31
214
|
/**
|
|
32
215
|
* A link or button, optionally with an icon at either end.
|
|
@@ -380,70 +563,104 @@ const BLOCKS = [
|
|
|
380
563
|
label: "Cards",
|
|
381
564
|
placeholder: "A grid of cards",
|
|
382
565
|
category: "Sections",
|
|
383
|
-
fields: [
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
566
|
+
fields: [
|
|
567
|
+
{
|
|
568
|
+
type: "repeater",
|
|
569
|
+
action_id: "cards",
|
|
570
|
+
label: "Cards",
|
|
571
|
+
item_label: "card",
|
|
572
|
+
min_items: 1,
|
|
573
|
+
fields: [
|
|
574
|
+
{
|
|
575
|
+
type: "text_input",
|
|
576
|
+
action_id: "title",
|
|
577
|
+
label: "Title"
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
type: "text_input",
|
|
581
|
+
action_id: "body",
|
|
582
|
+
label: "Text (write [link text](/url) for an inline link)",
|
|
583
|
+
multiline: true
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
type: "text_input",
|
|
587
|
+
action_id: "image",
|
|
588
|
+
label: "Image URL"
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
type: "text_input",
|
|
592
|
+
action_id: "imageAlt",
|
|
593
|
+
label: "Image alt text"
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
type: "text_input",
|
|
597
|
+
action_id: "href",
|
|
598
|
+
label: "Link URL (makes the whole card clickable)"
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
type: "text_input",
|
|
602
|
+
action_id: "linkLabel",
|
|
603
|
+
label: "Link text",
|
|
604
|
+
initial_value: "Read more"
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
type: "text_input",
|
|
608
|
+
action_id: "checklist",
|
|
609
|
+
label: "Checklist (panel style; one item per line)",
|
|
610
|
+
multiline: true
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
type: "text_input",
|
|
614
|
+
action_id: "ctaLabel",
|
|
615
|
+
label: "Button text (panel style)"
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
type: "text_input",
|
|
619
|
+
action_id: "ctaHref",
|
|
620
|
+
label: "Button URL (panel style)"
|
|
621
|
+
}
|
|
622
|
+
]
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
type: "select",
|
|
626
|
+
action_id: "variant",
|
|
627
|
+
label: "Style",
|
|
628
|
+
initial_value: "default",
|
|
629
|
+
options: [{
|
|
630
|
+
label: "Cards",
|
|
631
|
+
value: "default"
|
|
632
|
+
}, {
|
|
633
|
+
label: "Panels (tinted, with checklist and button)",
|
|
634
|
+
value: "panel"
|
|
635
|
+
}]
|
|
636
|
+
},
|
|
637
|
+
levelField("3"),
|
|
638
|
+
...toneFields(),
|
|
639
|
+
{
|
|
640
|
+
type: "select",
|
|
641
|
+
action_id: "columns",
|
|
642
|
+
label: "Columns",
|
|
643
|
+
initial_value: "3",
|
|
644
|
+
options: [
|
|
645
|
+
{
|
|
646
|
+
label: "1",
|
|
647
|
+
value: "1"
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
label: "2",
|
|
651
|
+
value: "2"
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
label: "3",
|
|
655
|
+
value: "3"
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
label: "4",
|
|
659
|
+
value: "4"
|
|
660
|
+
}
|
|
661
|
+
]
|
|
662
|
+
}
|
|
663
|
+
]
|
|
447
664
|
},
|
|
448
665
|
{
|
|
449
666
|
type: "vitops.accordion",
|
|
@@ -496,8 +713,9 @@ const BLOCKS = [
|
|
|
496
713
|
{
|
|
497
714
|
type: "text_input",
|
|
498
715
|
action_id: "heading",
|
|
499
|
-
label: "Heading"
|
|
716
|
+
label: "Heading (wrap words in *stars* to accent them)"
|
|
500
717
|
},
|
|
718
|
+
levelField("2"),
|
|
501
719
|
{
|
|
502
720
|
type: "text_input",
|
|
503
721
|
action_id: "body",
|
|
@@ -546,6 +764,37 @@ const BLOCKS = [
|
|
|
546
764
|
label: "Text and image side by side",
|
|
547
765
|
value: "split"
|
|
548
766
|
}]
|
|
767
|
+
},
|
|
768
|
+
...toneFields(),
|
|
769
|
+
{
|
|
770
|
+
type: "repeater",
|
|
771
|
+
action_id: "cards",
|
|
772
|
+
label: "Option cards under the buttons (optional)",
|
|
773
|
+
item_label: "card",
|
|
774
|
+
fields: [
|
|
775
|
+
{
|
|
776
|
+
type: "text_input",
|
|
777
|
+
action_id: "title",
|
|
778
|
+
label: "Title"
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
type: "text_input",
|
|
782
|
+
action_id: "body",
|
|
783
|
+
label: "Text (write [link text](/url) for an inline link)",
|
|
784
|
+
multiline: true
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
type: "text_input",
|
|
788
|
+
action_id: "href",
|
|
789
|
+
label: "Link URL (makes the card clickable)"
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
type: "text_input",
|
|
793
|
+
action_id: "linkLabel",
|
|
794
|
+
label: "Link text",
|
|
795
|
+
initial_value: "Learn more"
|
|
796
|
+
}
|
|
797
|
+
]
|
|
549
798
|
}
|
|
550
799
|
]
|
|
551
800
|
},
|
|
@@ -592,112 +841,983 @@ const BLOCKS = [
|
|
|
592
841
|
initial_value: false
|
|
593
842
|
}
|
|
594
843
|
]
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
type: "vitops.logos",
|
|
847
|
+
label: "Logos",
|
|
848
|
+
placeholder: "A still row of partner or client logos",
|
|
849
|
+
category: "Media",
|
|
850
|
+
fields: [
|
|
851
|
+
{
|
|
852
|
+
type: "text_input",
|
|
853
|
+
action_id: "label",
|
|
854
|
+
label: "Accessible name",
|
|
855
|
+
initial_value: "Partners"
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
type: "repeater",
|
|
859
|
+
action_id: "logos",
|
|
860
|
+
label: "Logos",
|
|
861
|
+
item_label: "logo",
|
|
862
|
+
min_items: 1,
|
|
863
|
+
fields: [
|
|
864
|
+
{
|
|
865
|
+
type: "text_input",
|
|
866
|
+
action_id: "image",
|
|
867
|
+
label: "Image URL"
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
type: "text_input",
|
|
871
|
+
action_id: "alt",
|
|
872
|
+
label: "Name (alt text)"
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
type: "text_input",
|
|
876
|
+
action_id: "href",
|
|
877
|
+
label: "Link URL (optional)"
|
|
878
|
+
}
|
|
879
|
+
]
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
type: "toggle",
|
|
883
|
+
action_id: "grayscale",
|
|
884
|
+
label: "Grey until hovered",
|
|
885
|
+
initial_value: false
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
type: "select",
|
|
889
|
+
action_id: "size",
|
|
890
|
+
label: "Logo height",
|
|
891
|
+
initial_value: "md",
|
|
892
|
+
options: [
|
|
893
|
+
{
|
|
894
|
+
label: "Small",
|
|
895
|
+
value: "sm"
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
label: "Medium",
|
|
899
|
+
value: "md"
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
label: "Large",
|
|
903
|
+
value: "lg"
|
|
904
|
+
}
|
|
905
|
+
]
|
|
906
|
+
}
|
|
907
|
+
]
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
type: "vitops.steps",
|
|
911
|
+
label: "Steps",
|
|
912
|
+
placeholder: "A numbered sequence joined by a connector line",
|
|
913
|
+
category: "Sections",
|
|
914
|
+
fields: [
|
|
915
|
+
{
|
|
916
|
+
type: "repeater",
|
|
917
|
+
action_id: "steps",
|
|
918
|
+
label: "Steps",
|
|
919
|
+
item_label: "step",
|
|
920
|
+
min_items: 1,
|
|
921
|
+
fields: [{
|
|
922
|
+
type: "text_input",
|
|
923
|
+
action_id: "title",
|
|
924
|
+
label: "Title"
|
|
925
|
+
}, {
|
|
926
|
+
type: "text_input",
|
|
927
|
+
action_id: "text",
|
|
928
|
+
label: "Text (write [link text](/url) for an inline link)",
|
|
929
|
+
multiline: true
|
|
930
|
+
}]
|
|
931
|
+
},
|
|
932
|
+
levelField("3"),
|
|
933
|
+
{
|
|
934
|
+
type: "select",
|
|
935
|
+
action_id: "layout",
|
|
936
|
+
label: "Layout",
|
|
937
|
+
initial_value: "column",
|
|
938
|
+
options: [{
|
|
939
|
+
label: "Stacked",
|
|
940
|
+
value: "column"
|
|
941
|
+
}, {
|
|
942
|
+
label: "Side by side when wide",
|
|
943
|
+
value: "row"
|
|
944
|
+
}]
|
|
945
|
+
},
|
|
946
|
+
...toneFields()
|
|
947
|
+
]
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
type: "vitops.stats",
|
|
951
|
+
label: "Stats",
|
|
952
|
+
placeholder: "Headline figures that count up, or labelled tiles",
|
|
953
|
+
category: "Sections",
|
|
954
|
+
fields: [
|
|
955
|
+
{
|
|
956
|
+
type: "repeater",
|
|
957
|
+
action_id: "stats",
|
|
958
|
+
label: "Figures",
|
|
959
|
+
item_label: "figure",
|
|
960
|
+
min_items: 1,
|
|
961
|
+
fields: [
|
|
962
|
+
{
|
|
963
|
+
type: "text_input",
|
|
964
|
+
action_id: "value",
|
|
965
|
+
label: "Figure (e.g. 82% or 11 hrs)"
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
type: "text_input",
|
|
969
|
+
action_id: "label",
|
|
970
|
+
label: "Label"
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
type: "text_input",
|
|
974
|
+
action_id: "group",
|
|
975
|
+
label: "Group (tiles style; small line above)"
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
type: "text_input",
|
|
979
|
+
action_id: "delta",
|
|
980
|
+
label: "Change (tiles style; e.g. +3 this week)"
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
type: "select",
|
|
984
|
+
action_id: "state",
|
|
985
|
+
label: "Change is",
|
|
986
|
+
initial_value: "",
|
|
987
|
+
options: [
|
|
988
|
+
{
|
|
989
|
+
label: "Neutral",
|
|
990
|
+
value: ""
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
label: "Good (up)",
|
|
994
|
+
value: "up"
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
label: "Bad (down)",
|
|
998
|
+
value: "down"
|
|
999
|
+
}
|
|
1000
|
+
]
|
|
1001
|
+
}
|
|
1002
|
+
]
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
type: "select",
|
|
1006
|
+
action_id: "variant",
|
|
1007
|
+
label: "Style",
|
|
1008
|
+
initial_value: "figures",
|
|
1009
|
+
options: [{
|
|
1010
|
+
label: "Figures",
|
|
1011
|
+
value: "figures"
|
|
1012
|
+
}, {
|
|
1013
|
+
label: "Tiles",
|
|
1014
|
+
value: "tiles"
|
|
1015
|
+
}]
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
type: "toggle",
|
|
1019
|
+
action_id: "animate",
|
|
1020
|
+
label: "Count up when scrolled into view",
|
|
1021
|
+
initial_value: true
|
|
1022
|
+
},
|
|
1023
|
+
...toneFields()
|
|
1024
|
+
]
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
type: "vitops.callout",
|
|
1028
|
+
label: "Callout",
|
|
1029
|
+
placeholder: "A call-to-action band (not a dismissible banner)",
|
|
1030
|
+
category: "Sections",
|
|
1031
|
+
fields: [
|
|
1032
|
+
{
|
|
1033
|
+
type: "text_input",
|
|
1034
|
+
action_id: "heading",
|
|
1035
|
+
label: "Heading"
|
|
1036
|
+
},
|
|
1037
|
+
levelField("2"),
|
|
1038
|
+
{
|
|
1039
|
+
type: "text_input",
|
|
1040
|
+
action_id: "body",
|
|
1041
|
+
label: "Text (write [link text](/url) for an inline link)",
|
|
1042
|
+
multiline: true
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
type: "text_input",
|
|
1046
|
+
action_id: "primaryLabel",
|
|
1047
|
+
label: "Main button text"
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
type: "text_input",
|
|
1051
|
+
action_id: "primaryHref",
|
|
1052
|
+
label: "Main button URL"
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
type: "text_input",
|
|
1056
|
+
action_id: "secondaryLabel",
|
|
1057
|
+
label: "Second link text"
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
type: "text_input",
|
|
1061
|
+
action_id: "secondaryHref",
|
|
1062
|
+
label: "Second link URL"
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
type: "select",
|
|
1066
|
+
action_id: "align",
|
|
1067
|
+
label: "Layout",
|
|
1068
|
+
initial_value: "start",
|
|
1069
|
+
options: [{
|
|
1070
|
+
label: "Message left, buttons right",
|
|
1071
|
+
value: "start"
|
|
1072
|
+
}, {
|
|
1073
|
+
label: "Centered",
|
|
1074
|
+
value: "center"
|
|
1075
|
+
}]
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
type: "toggle",
|
|
1079
|
+
action_id: "solid",
|
|
1080
|
+
label: "Fill with the solid colour",
|
|
1081
|
+
initial_value: false
|
|
1082
|
+
},
|
|
1083
|
+
...toneFields()
|
|
1084
|
+
]
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
type: "vitops.entries",
|
|
1088
|
+
label: "Entries table",
|
|
1089
|
+
placeholder: "Rows of term/value pairs: a stacked list that becomes a table when wide",
|
|
1090
|
+
category: "Sections",
|
|
1091
|
+
fields: [
|
|
1092
|
+
{
|
|
1093
|
+
type: "repeater",
|
|
1094
|
+
action_id: "entries",
|
|
1095
|
+
label: "Entries",
|
|
1096
|
+
item_label: "entry",
|
|
1097
|
+
min_items: 1,
|
|
1098
|
+
fields: [{
|
|
1099
|
+
type: "text_input",
|
|
1100
|
+
action_id: "heading",
|
|
1101
|
+
label: "Heading (row name)"
|
|
1102
|
+
}, {
|
|
1103
|
+
type: "text_input",
|
|
1104
|
+
action_id: "pairs",
|
|
1105
|
+
label: "Details (one per line: Term | Value)",
|
|
1106
|
+
multiline: true
|
|
1107
|
+
}]
|
|
1108
|
+
},
|
|
1109
|
+
levelField("3"),
|
|
1110
|
+
{
|
|
1111
|
+
type: "text_input",
|
|
1112
|
+
action_id: "breakpoint",
|
|
1113
|
+
label: "Becomes a table at this width",
|
|
1114
|
+
initial_value: "40rem"
|
|
1115
|
+
}
|
|
1116
|
+
]
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
type: "vitops.pricing",
|
|
1120
|
+
label: "Pricing",
|
|
1121
|
+
placeholder: "Plan cards with aligned feature rows",
|
|
1122
|
+
category: "Sections",
|
|
1123
|
+
fields: [{
|
|
1124
|
+
type: "repeater",
|
|
1125
|
+
action_id: "plans",
|
|
1126
|
+
label: "Plans",
|
|
1127
|
+
item_label: "plan",
|
|
1128
|
+
min_items: 1,
|
|
1129
|
+
fields: [
|
|
1130
|
+
{
|
|
1131
|
+
type: "text_input",
|
|
1132
|
+
action_id: "name",
|
|
1133
|
+
label: "Plan name"
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
type: "text_input",
|
|
1137
|
+
action_id: "price",
|
|
1138
|
+
label: "Price (e.g. $49)"
|
|
1139
|
+
},
|
|
1140
|
+
{
|
|
1141
|
+
type: "text_input",
|
|
1142
|
+
action_id: "period",
|
|
1143
|
+
label: "Period (e.g. per month)"
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
type: "text_input",
|
|
1147
|
+
action_id: "features",
|
|
1148
|
+
label: "Features (one per line)",
|
|
1149
|
+
multiline: true
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
type: "text_input",
|
|
1153
|
+
action_id: "ctaLabel",
|
|
1154
|
+
label: "Button text"
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
type: "text_input",
|
|
1158
|
+
action_id: "ctaHref",
|
|
1159
|
+
label: "Button URL"
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
type: "toggle",
|
|
1163
|
+
action_id: "featured",
|
|
1164
|
+
label: "Highlight this plan",
|
|
1165
|
+
initial_value: false
|
|
1166
|
+
}
|
|
1167
|
+
]
|
|
1168
|
+
}, levelField("3")]
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
type: "vitops.embed",
|
|
1172
|
+
label: "Embed (video or map)",
|
|
1173
|
+
placeholder: "A video or map that loads only after the visitor allows it",
|
|
1174
|
+
category: "Embeds",
|
|
1175
|
+
fields: [
|
|
1176
|
+
{
|
|
1177
|
+
type: "select",
|
|
1178
|
+
action_id: "provider",
|
|
1179
|
+
label: "Provider",
|
|
1180
|
+
initial_value: "youtube",
|
|
1181
|
+
options: [
|
|
1182
|
+
{
|
|
1183
|
+
label: "YouTube",
|
|
1184
|
+
value: "youtube"
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
label: "Vimeo",
|
|
1188
|
+
value: "vimeo"
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
label: "Map (Google or OpenStreetMap embed address)",
|
|
1192
|
+
value: "map"
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
label: "Other (https address)",
|
|
1196
|
+
value: "custom"
|
|
1197
|
+
}
|
|
1198
|
+
]
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
type: "text_input",
|
|
1202
|
+
action_id: "url",
|
|
1203
|
+
label: "Address"
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
type: "text_input",
|
|
1207
|
+
action_id: "title",
|
|
1208
|
+
label: "Title (required: it names the frame for screen readers)"
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
type: "select",
|
|
1212
|
+
action_id: "category",
|
|
1213
|
+
label: "Consent category it waits on",
|
|
1214
|
+
initial_value: "marketing",
|
|
1215
|
+
options: [
|
|
1216
|
+
{
|
|
1217
|
+
label: "Marketing",
|
|
1218
|
+
value: "marketing"
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
label: "Preferences",
|
|
1222
|
+
value: "preferences"
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
label: "Analytics",
|
|
1226
|
+
value: "analytics"
|
|
1227
|
+
}
|
|
1228
|
+
]
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
type: "text_input",
|
|
1232
|
+
action_id: "ratio",
|
|
1233
|
+
label: "Aspect ratio",
|
|
1234
|
+
initial_value: "16 / 9"
|
|
1235
|
+
}
|
|
1236
|
+
]
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
type: "vitops.form",
|
|
1240
|
+
label: "Contact form",
|
|
1241
|
+
placeholder: "An enquiry form that emails you (and your CRM) and works without JavaScript",
|
|
1242
|
+
category: "Forms",
|
|
1243
|
+
fields: [
|
|
1244
|
+
{
|
|
1245
|
+
type: "text_input",
|
|
1246
|
+
action_id: "heading",
|
|
1247
|
+
label: "Heading"
|
|
1248
|
+
},
|
|
1249
|
+
levelField("2"),
|
|
1250
|
+
{
|
|
1251
|
+
type: "text_input",
|
|
1252
|
+
action_id: "intro",
|
|
1253
|
+
label: "Text above the form",
|
|
1254
|
+
multiline: true
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
type: "repeater",
|
|
1258
|
+
action_id: "fields",
|
|
1259
|
+
label: "Fields",
|
|
1260
|
+
item_label: "field",
|
|
1261
|
+
min_items: 1,
|
|
1262
|
+
fields: [
|
|
1263
|
+
{
|
|
1264
|
+
type: "text_input",
|
|
1265
|
+
action_id: "label",
|
|
1266
|
+
label: "Label (what the visitor sees)"
|
|
1267
|
+
},
|
|
1268
|
+
{
|
|
1269
|
+
type: "text_input",
|
|
1270
|
+
action_id: "name",
|
|
1271
|
+
label: "Name in the message (optional: letters, digits, underscores)"
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
type: "select",
|
|
1275
|
+
action_id: "type",
|
|
1276
|
+
label: "Kind",
|
|
1277
|
+
initial_value: "text",
|
|
1278
|
+
options: [
|
|
1279
|
+
{
|
|
1280
|
+
label: "Short text",
|
|
1281
|
+
value: "text"
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
label: "Email address",
|
|
1285
|
+
value: "email"
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
label: "Phone number",
|
|
1289
|
+
value: "tel"
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
label: "Long text",
|
|
1293
|
+
value: "textarea"
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
label: "Choose one",
|
|
1297
|
+
value: "select"
|
|
1298
|
+
},
|
|
1299
|
+
{
|
|
1300
|
+
label: "Tick box",
|
|
1301
|
+
value: "checkbox"
|
|
1302
|
+
}
|
|
1303
|
+
]
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
type: "toggle",
|
|
1307
|
+
action_id: "required",
|
|
1308
|
+
label: "Required",
|
|
1309
|
+
initial_value: false
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
type: "text_input",
|
|
1313
|
+
action_id: "placeholder",
|
|
1314
|
+
label: "Example text"
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
type: "text_input",
|
|
1318
|
+
action_id: "help",
|
|
1319
|
+
label: "Help text under the field"
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
type: "text_input",
|
|
1323
|
+
action_id: "options",
|
|
1324
|
+
label: "Choices (one per line; for \"Choose one\" only)",
|
|
1325
|
+
multiline: true
|
|
1326
|
+
}
|
|
1327
|
+
]
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
type: "text_input",
|
|
1331
|
+
action_id: "submitLabel",
|
|
1332
|
+
label: "Button text",
|
|
1333
|
+
initial_value: "Send"
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
type: "text_input",
|
|
1337
|
+
action_id: "successMessage",
|
|
1338
|
+
label: "Message after sending",
|
|
1339
|
+
initial_value: "Thanks. Your message has been sent."
|
|
1340
|
+
},
|
|
1341
|
+
{
|
|
1342
|
+
type: "text_input",
|
|
1343
|
+
action_id: "errorMessage",
|
|
1344
|
+
label: "Message if sending fails"
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
type: "text_input",
|
|
1348
|
+
action_id: "noJsMessage",
|
|
1349
|
+
label: "Message when JavaScript is off (shown only if the site has a spam check)"
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
type: "text_input",
|
|
1353
|
+
action_id: "action",
|
|
1354
|
+
label: "Where the form posts (leave empty for /api/track)"
|
|
1355
|
+
},
|
|
1356
|
+
...toneFields()
|
|
1357
|
+
]
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
/**
|
|
1361
|
+
* Google reviews from the committed `.vitops/reviews.json` cache (`vitops reviews`).
|
|
1362
|
+
*
|
|
1363
|
+
* The data is a BUILD-TIME import (`virtual:getvitops/reviews`, from the `vitops()` Astro
|
|
1364
|
+
* integration), never a filesystem read: the renderer runs in a Worker. So a new review
|
|
1365
|
+
* appears after the next `vitops reviews` run is committed and the site rebuilt, not when
|
|
1366
|
+
* an editor saves. Which reviews show is `selectReviews()` from `@getvitops/utils`, the
|
|
1367
|
+
* one place that is decided — these fields are exactly its `ReviewLocationConfig`.
|
|
1368
|
+
*/
|
|
1369
|
+
type: "vitops.reviews",
|
|
1370
|
+
label: "Google reviews",
|
|
1371
|
+
icon: "star",
|
|
1372
|
+
placeholder: "Customer reviews from the Google Business Profile cache",
|
|
1373
|
+
category: "Sections",
|
|
1374
|
+
fields: [
|
|
1375
|
+
{
|
|
1376
|
+
type: "text_input",
|
|
1377
|
+
action_id: "label",
|
|
1378
|
+
label: "Name for the group (read by screen readers)",
|
|
1379
|
+
initial_value: "Customer reviews"
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
type: "text_input",
|
|
1383
|
+
action_id: "location",
|
|
1384
|
+
label: "Location key (organization.locations key; blank = every location in the cache)"
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
type: "select",
|
|
1388
|
+
action_id: "layout",
|
|
1389
|
+
label: "Layout",
|
|
1390
|
+
initial_value: "grid",
|
|
1391
|
+
options: [
|
|
1392
|
+
{
|
|
1393
|
+
label: "Grid",
|
|
1394
|
+
value: "grid"
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
label: "Carousel",
|
|
1398
|
+
value: "carousel"
|
|
1399
|
+
},
|
|
1400
|
+
{
|
|
1401
|
+
label: "Marquee (scrolling row)",
|
|
1402
|
+
value: "marquee"
|
|
1403
|
+
}
|
|
1404
|
+
]
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
type: "select",
|
|
1408
|
+
action_id: "columns",
|
|
1409
|
+
label: "Columns (grid only)",
|
|
1410
|
+
initial_value: "3",
|
|
1411
|
+
options: [
|
|
1412
|
+
"1",
|
|
1413
|
+
"2",
|
|
1414
|
+
"3",
|
|
1415
|
+
"4"
|
|
1416
|
+
].map((n) => ({
|
|
1417
|
+
label: n,
|
|
1418
|
+
value: n
|
|
1419
|
+
}))
|
|
1420
|
+
},
|
|
1421
|
+
{
|
|
1422
|
+
type: "number_input",
|
|
1423
|
+
action_id: "limit",
|
|
1424
|
+
label: "Most reviews to show (0 = all)",
|
|
1425
|
+
initial_value: 6
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
type: "select",
|
|
1429
|
+
action_id: "minRating",
|
|
1430
|
+
label: "Lowest rating to show",
|
|
1431
|
+
initial_value: "4",
|
|
1432
|
+
options: [
|
|
1433
|
+
"1",
|
|
1434
|
+
"2",
|
|
1435
|
+
"3",
|
|
1436
|
+
"4",
|
|
1437
|
+
"5"
|
|
1438
|
+
].map((n) => ({
|
|
1439
|
+
label: n === "1" ? "Any" : `${n} stars and up`,
|
|
1440
|
+
value: n
|
|
1441
|
+
}))
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
type: "select",
|
|
1445
|
+
action_id: "sort",
|
|
1446
|
+
label: "Order",
|
|
1447
|
+
initial_value: "recent",
|
|
1448
|
+
options: [{
|
|
1449
|
+
label: "Newest first",
|
|
1450
|
+
value: "recent"
|
|
1451
|
+
}, {
|
|
1452
|
+
label: "Highest rated first",
|
|
1453
|
+
value: "rating"
|
|
1454
|
+
}]
|
|
1455
|
+
},
|
|
1456
|
+
{
|
|
1457
|
+
type: "text_input",
|
|
1458
|
+
action_id: "featured",
|
|
1459
|
+
label: "Review ids to show first (comma-separated; they ignore the rating filter)"
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
type: "toggle",
|
|
1463
|
+
action_id: "avatars",
|
|
1464
|
+
label: "Show reviewers' Google photos (loads images from Google for every visitor)",
|
|
1465
|
+
initial_value: false
|
|
1466
|
+
},
|
|
1467
|
+
...toneFields()
|
|
1468
|
+
]
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
/**
|
|
1472
|
+
* A named EmDash menu inside page content: a site map, a section's link list, a footer-style
|
|
1473
|
+
* set of columns. NOT the header — that is `<SiteNav>` in the layout, which owns the drawer,
|
|
1474
|
+
* dropdowns and breakpoints. `variant: sitenav` renders the same markup and pattern for the
|
|
1475
|
+
* rare page that wants a real navbar in the body.
|
|
1476
|
+
*
|
|
1477
|
+
* The menu is chosen from `menus/list` (the site's own menus) with the conventional names as
|
|
1478
|
+
* the static fallback, because a select needs a non-empty `options` list.
|
|
1479
|
+
*/
|
|
1480
|
+
type: "vitops.nav",
|
|
1481
|
+
label: "Menu",
|
|
1482
|
+
icon: "list",
|
|
1483
|
+
placeholder: "An EmDash menu as a list of links",
|
|
1484
|
+
category: "Navigation",
|
|
1485
|
+
fields: [
|
|
1486
|
+
{
|
|
1487
|
+
type: "select",
|
|
1488
|
+
action_id: "menu",
|
|
1489
|
+
label: "Menu",
|
|
1490
|
+
initial_value: "footer-sitemap",
|
|
1491
|
+
options: [
|
|
1492
|
+
{
|
|
1493
|
+
label: "Footer site map",
|
|
1494
|
+
value: "footer-sitemap"
|
|
1495
|
+
},
|
|
1496
|
+
{
|
|
1497
|
+
label: "Footer legal",
|
|
1498
|
+
value: "footer-legal"
|
|
1499
|
+
},
|
|
1500
|
+
{
|
|
1501
|
+
label: "Primary",
|
|
1502
|
+
value: "primary"
|
|
1503
|
+
}
|
|
1504
|
+
],
|
|
1505
|
+
optionsRoute: MENUS_ROUTE
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
type: "text_input",
|
|
1509
|
+
action_id: "label",
|
|
1510
|
+
label: "Name for the navigation (read by screen readers; must differ from other menus)"
|
|
1511
|
+
},
|
|
1512
|
+
{
|
|
1513
|
+
type: "text_input",
|
|
1514
|
+
action_id: "heading",
|
|
1515
|
+
label: "Visible heading (optional)"
|
|
1516
|
+
},
|
|
1517
|
+
levelField("2"),
|
|
1518
|
+
{
|
|
1519
|
+
type: "select",
|
|
1520
|
+
action_id: "variant",
|
|
1521
|
+
label: "Layout",
|
|
1522
|
+
initial_value: "list",
|
|
1523
|
+
options: [
|
|
1524
|
+
{
|
|
1525
|
+
label: "Vertical list",
|
|
1526
|
+
value: "list"
|
|
1527
|
+
},
|
|
1528
|
+
{
|
|
1529
|
+
label: "Inline row",
|
|
1530
|
+
value: "inline"
|
|
1531
|
+
},
|
|
1532
|
+
{
|
|
1533
|
+
label: "Columns (each top-level item with its sub-links)",
|
|
1534
|
+
value: "columns"
|
|
1535
|
+
},
|
|
1536
|
+
{
|
|
1537
|
+
label: "Navbar (the header pattern, in the page)",
|
|
1538
|
+
value: "sitenav"
|
|
1539
|
+
}
|
|
1540
|
+
]
|
|
1541
|
+
},
|
|
1542
|
+
{
|
|
1543
|
+
type: "select",
|
|
1544
|
+
action_id: "depth",
|
|
1545
|
+
label: "Levels to show",
|
|
1546
|
+
initial_value: "1",
|
|
1547
|
+
options: [
|
|
1548
|
+
"1",
|
|
1549
|
+
"2",
|
|
1550
|
+
"3"
|
|
1551
|
+
].map((n) => ({
|
|
1552
|
+
label: n,
|
|
1553
|
+
value: n
|
|
1554
|
+
}))
|
|
1555
|
+
},
|
|
1556
|
+
...toneFields()
|
|
1557
|
+
]
|
|
1558
|
+
},
|
|
1559
|
+
{
|
|
1560
|
+
/** A row of static labels (`badge`) or editable/dismissable ones (`tag`). */
|
|
1561
|
+
type: "vitops.badges",
|
|
1562
|
+
label: "Badges",
|
|
1563
|
+
icon: "tag",
|
|
1564
|
+
placeholder: "A row of short labels",
|
|
1565
|
+
fields: [
|
|
1566
|
+
{
|
|
1567
|
+
type: "repeater",
|
|
1568
|
+
action_id: "items",
|
|
1569
|
+
label: "Labels",
|
|
1570
|
+
fields: [
|
|
1571
|
+
{
|
|
1572
|
+
type: "text_input",
|
|
1573
|
+
action_id: "label",
|
|
1574
|
+
label: "Label"
|
|
1575
|
+
},
|
|
1576
|
+
{
|
|
1577
|
+
type: "select",
|
|
1578
|
+
action_id: "tone",
|
|
1579
|
+
label: "Tone (blank uses the block tone)",
|
|
1580
|
+
initial_value: "",
|
|
1581
|
+
options: STATIC_TONE_OPTIONS,
|
|
1582
|
+
optionsRoute: TONES_ROUTE
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
type: "text_input",
|
|
1586
|
+
action_id: "color",
|
|
1587
|
+
label: "Palette colour instead (e.g. amber)"
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
type: "combobox",
|
|
1591
|
+
action_id: "icon",
|
|
1592
|
+
label: "Icon before the label",
|
|
1593
|
+
placeholder: "None",
|
|
1594
|
+
options: SEMANTIC_ICON_OPTIONS
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
type: "text_input",
|
|
1598
|
+
action_id: "href",
|
|
1599
|
+
label: "Link URL (optional)"
|
|
1600
|
+
}
|
|
1601
|
+
]
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
type: "select",
|
|
1605
|
+
action_id: "variant",
|
|
1606
|
+
label: "Style",
|
|
1607
|
+
initial_value: "badge",
|
|
1608
|
+
options: [{
|
|
1609
|
+
label: "Badge (static label)",
|
|
1610
|
+
value: "badge"
|
|
1611
|
+
}, {
|
|
1612
|
+
label: "Tag",
|
|
1613
|
+
value: "tag"
|
|
1614
|
+
}]
|
|
1615
|
+
},
|
|
1616
|
+
...toneFields()
|
|
1617
|
+
]
|
|
1618
|
+
},
|
|
1619
|
+
{
|
|
1620
|
+
/**
|
|
1621
|
+
* A widget area: content defined once under Widgets in the admin and shown wherever this
|
|
1622
|
+
* block is placed. Edits reach every page it sits on (after the next build, on a
|
|
1623
|
+
* prerendered site) — unlike a Section, which is copied.
|
|
1624
|
+
*/
|
|
1625
|
+
type: "vitops.widgetArea",
|
|
1626
|
+
label: "Widget area",
|
|
1627
|
+
icon: "layout",
|
|
1628
|
+
placeholder: "Shared content managed under Widgets",
|
|
1629
|
+
fields: [{
|
|
1630
|
+
type: "select",
|
|
1631
|
+
action_id: "area",
|
|
1632
|
+
label: "Widget area",
|
|
1633
|
+
initial_value: "",
|
|
1634
|
+
options: [{
|
|
1635
|
+
label: "Choose…",
|
|
1636
|
+
value: ""
|
|
1637
|
+
}],
|
|
1638
|
+
optionsRoute: WIDGET_AREAS_ROUTE
|
|
1639
|
+
}, levelField("3")]
|
|
1640
|
+
}
|
|
1641
|
+
];
|
|
1642
|
+
/** Every block gets the escape hatches last, so the editor reads content first. */
|
|
1643
|
+
const BLOCKS = BASE_BLOCKS.map((b) => ({
|
|
1644
|
+
...b,
|
|
1645
|
+
fields: [...b.fields ?? [], ...commonFields()]
|
|
1646
|
+
}));
|
|
1647
|
+
/**
|
|
1648
|
+
* Resolve {@link BlockSelection} against {@link BLOCKS}.
|
|
1649
|
+
*
|
|
1650
|
+
* Pure so a misconfiguration fails at plugin construction, with the offending name in the
|
|
1651
|
+
* message, rather than as an editor menu that is quietly missing something.
|
|
1652
|
+
*/
|
|
1653
|
+
function selectBlocks(selection = {}) {
|
|
1654
|
+
const known = new Set(BLOCKS.map((b) => b.type));
|
|
1655
|
+
const check = (names, option) => {
|
|
1656
|
+
for (const name of names ?? []) if (!known.has(name)) throw new Error(`[vitops] blocks.${option}: unknown block '${name}'. Known: ${[...known].join(", ")}.`);
|
|
1657
|
+
};
|
|
1658
|
+
check(selection.include, "include");
|
|
1659
|
+
check(selection.exclude, "exclude");
|
|
1660
|
+
const include = selection.include ? new Set(selection.include) : null;
|
|
1661
|
+
const exclude = new Set(selection.exclude);
|
|
1662
|
+
const base = BLOCKS.filter((b) => (!include || include.has(b.type)) && !exclude.has(b.type));
|
|
1663
|
+
const extra = selection.extra ?? [];
|
|
1664
|
+
const taken = new Set(BLOCKS.map((b) => b.type));
|
|
1665
|
+
for (const block of extra) {
|
|
1666
|
+
if (taken.has(block.type)) throw new Error(`[vitops] blocks.extra: type '${block.type}' is already defined.`);
|
|
1667
|
+
taken.add(block.type);
|
|
1668
|
+
}
|
|
1669
|
+
return [...base, ...extra];
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// src/menus-route.ts
|
|
1673
|
+
/** The items the select shows: one per menu, valued by its `name` (what `getMenu()` takes). */
|
|
1674
|
+
function menuItems(menus) {
|
|
1675
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1676
|
+
const out = [];
|
|
1677
|
+
for (const { name, label } of menus) {
|
|
1678
|
+
if (!name || seen.has(name)) continue;
|
|
1679
|
+
seen.add(name);
|
|
1680
|
+
out.push({
|
|
1681
|
+
id: name,
|
|
1682
|
+
name: label && label !== name ? `${label} (${name})` : name
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
return out;
|
|
1686
|
+
}
|
|
1687
|
+
function createMenusRoute(list) {
|
|
1688
|
+
return {
|
|
1689
|
+
permission: "content:read",
|
|
1690
|
+
handler: async () => {
|
|
1691
|
+
try {
|
|
1692
|
+
return { items: menuItems(await list()) };
|
|
1693
|
+
} catch {
|
|
1694
|
+
return { items: [] };
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/tones-route.ts
|
|
1701
|
+
/**
|
|
1702
|
+
* The `tones/list` plugin route: what fills every block's `tone` select with the site's own
|
|
1703
|
+
* colour roles (UPSTREAM 21 — "role names the admin never offered").
|
|
1704
|
+
*
|
|
1705
|
+
* EmDash's Block Kit `select` takes an `optionsRoute` for exactly this, and falls back to the
|
|
1706
|
+
* field's static options if the route fails or answers with no `items`, so a site that
|
|
1707
|
+
* configures nothing still gets the semantic names from `STATIC_TONE_OPTIONS`.
|
|
1708
|
+
*
|
|
1709
|
+
* **Where the roles come from.** Plugin code has no site config at runtime, so the site hands
|
|
1710
|
+
* it over: `tones: [...]` verbatim, or — if the design-system editor is already wired with the
|
|
1711
|
+
* site's `Config` — the `colors.roles` keys of that config, so there is nothing to keep in
|
|
1712
|
+
* step. A role is listed under its own name (`brand-primary`), because that is what
|
|
1713
|
+
* `resolveTone()` and the token names (`--color-bg-brand-primary-muted`) both use.
|
|
1714
|
+
*/
|
|
1715
|
+
/** Role names of a design system or full config, or `[]` if it cannot be resolved. */
|
|
1716
|
+
function rolesOf(config) {
|
|
1717
|
+
if (!config || typeof config !== "object") return [];
|
|
1718
|
+
try {
|
|
1719
|
+
const { designSystem } = resolveInput(config, { siteEnv: "production" });
|
|
1720
|
+
return Object.keys(designSystem.colors?.roles ?? {});
|
|
1721
|
+
} catch {
|
|
1722
|
+
return [];
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
/** The items the select shows: the site's roles first, then any semantic name it lacks. */
|
|
1726
|
+
function toneItems(roles) {
|
|
1727
|
+
return [{
|
|
1728
|
+
id: "",
|
|
1729
|
+
name: "None"
|
|
1730
|
+
}, ...[.../* @__PURE__ */ new Set([...roles, ...SEMANTIC_TONES])].map((id) => ({
|
|
1731
|
+
id,
|
|
1732
|
+
name: id.replace(/-/g, " ")
|
|
1733
|
+
}))];
|
|
1734
|
+
}
|
|
1735
|
+
function createTonesRoute(roles) {
|
|
1736
|
+
return {
|
|
1737
|
+
permission: "content:read",
|
|
1738
|
+
handler: async () => ({ items: toneItems(roles()) })
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// src/design-system-route.ts
|
|
1743
|
+
/**
|
|
1744
|
+
* The `vitops.design-system` plugin route — what `<wc-theme-editor>` probes
|
|
1745
|
+
* and POSTs its patch to when mounted inside the EmDash admin (see this
|
|
1746
|
+
* package's README and AGENTS.md's "EmDash plugin as the design-system
|
|
1747
|
+
* editing surface" section). Mounted at
|
|
1748
|
+
* `/_emdash/api/plugins/vitops/design-system`.
|
|
1749
|
+
*
|
|
1750
|
+
* Two responsibilities, matching the dev-only `@getvitops/vite` endpoint's
|
|
1751
|
+
* contract byte-for-byte (`GET → { writable }`, `POST { patch } → { ok } |
|
|
1752
|
+
* { errors }`) so `<wc-theme-editor>` needs no branch for which one it's
|
|
1753
|
+
* talking to:
|
|
1754
|
+
*
|
|
1755
|
+
* 1. **Immediate**: persist the patch to `ctx.settings` — read back by the
|
|
1756
|
+
* `page:fragments` hook in `index.ts`, which turns it into a site-wide
|
|
1757
|
+
* `:root` override `<style>` on every public page. This is what makes an
|
|
1758
|
+
* edit visible without a rebuild.
|
|
1759
|
+
* 2. **Canonical**: auto-commit the merged file to `site.deployment.branch`
|
|
1760
|
+
* (default `dev`) via `@getvitops/utils/git`'s `commitFile()`, so the edit
|
|
1761
|
+
* reaches the git-tracked `site.json` through this repo's own normal
|
|
1762
|
+
* `dev → promote → main` review path — never a silent second source of
|
|
1763
|
+
* truth. A commit failure (missing token, misconfigured repo, a transient
|
|
1764
|
+
* GitHub error) is logged and surfaces as a warning, but never fails the
|
|
1765
|
+
* save: the immediate override already works regardless.
|
|
1766
|
+
*
|
|
1767
|
+
* **Assumes `config` is a full site `Config`** (has `organization`/`site`),
|
|
1768
|
+
* not a bare `design-system.json` — true for any real EmDash site (it always
|
|
1769
|
+
* has a `site.json`), and the only case this plugin already handles anywhere
|
|
1770
|
+
* else (`vitopsHosting()`, `BLOCKS`).
|
|
1771
|
+
*
|
|
1772
|
+
* The GitHub token is read as `process.env.VITOPS_GITHUB_TOKEN` — matching
|
|
1773
|
+
* `hosting.ts`'s own `process.env.HOSTING` read in this same package, rather
|
|
1774
|
+
* than a `cloudflare:workers`-specific import: a native EmDash plugin runs
|
|
1775
|
+
* in-process with no isolation boundary (EmDash's own docs say plugins "can
|
|
1776
|
+
* access environment variables" directly), and this package already targets
|
|
1777
|
+
* both the Cloudflare and Node hosting targets through one `process.env`
|
|
1778
|
+
* convention (`nodejs_compat` on Workers maps bindings into `process.env`).
|
|
1779
|
+
*/
|
|
1780
|
+
const SETTINGS_KEY = "designSystemPatch";
|
|
1781
|
+
function json(status, body) {
|
|
1782
|
+
return pluginResponse({
|
|
1783
|
+
status,
|
|
1784
|
+
headers: { "content-type": "application/json" },
|
|
1785
|
+
body: {
|
|
1786
|
+
kind: "text",
|
|
1787
|
+
value: JSON.stringify(body)
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
/** Fetches the currently-stored patch, clearing it if the bundled `config`
|
|
1792
|
+
* (the last real build) already reflects every value in it — see the
|
|
1793
|
+
* module docblock's "self-clearing" note in AGENTS.md. */
|
|
1794
|
+
async function currentPatch(ctx, ds) {
|
|
1795
|
+
const stored = await ctx.settings.get(SETTINGS_KEY);
|
|
1796
|
+
if (!stored) return null;
|
|
1797
|
+
const patchedColors = stored.patch["colors"];
|
|
1798
|
+
if (patchedColors != null && JSON.stringify(mergePatch(ds.colors, patchedColors)) === JSON.stringify(ds.colors)) {
|
|
1799
|
+
await ctx.settings.delete(SETTINGS_KEY);
|
|
1800
|
+
return null;
|
|
1801
|
+
}
|
|
1802
|
+
return stored;
|
|
1803
|
+
}
|
|
1804
|
+
function createDesignSystemRoute(opts) {
|
|
1805
|
+
const theme = opts.theme ?? "default";
|
|
1806
|
+
return {
|
|
1807
|
+
methods: ["GET", "POST"],
|
|
1808
|
+
response: "raw",
|
|
1809
|
+
permission: "plugins:manage",
|
|
1810
|
+
handler: async (ctx) => {
|
|
1811
|
+
const { designSystem: ds, config } = resolveInput(opts.config, {
|
|
1812
|
+
theme,
|
|
1813
|
+
siteEnv: "production"
|
|
1814
|
+
});
|
|
1815
|
+
if (ctx.request.method === "GET") {
|
|
1816
|
+
await currentPatch(ctx, ds);
|
|
1817
|
+
return json(200, { writable: true });
|
|
1818
|
+
}
|
|
1819
|
+
const patch = ctx.input?.patch;
|
|
1820
|
+
if (patch == null || typeof patch !== "object" || Array.isArray(patch)) return json(400, { errors: ["body must be { patch: object }"] });
|
|
701
1821
|
const mergedColors = mergePatch(ds.colors, patch["colors"] ?? {});
|
|
702
1822
|
const contrastErrors = validateColorContract(mergedColors);
|
|
703
1823
|
if (contrastErrors.length) return json(400, { errors: contrastErrors });
|
|
@@ -760,6 +1880,130 @@ async function designSystemOverrideFragment(ctx, opts) {
|
|
|
760
1880
|
};
|
|
761
1881
|
}
|
|
762
1882
|
|
|
1883
|
+
// src/block-types.ts
|
|
1884
|
+
/** Block-type slugs may not contain a dot, so `vitops.section` is stored as `vitops_section`. */
|
|
1885
|
+
const SECTION_SLUG = "vitops_section";
|
|
1886
|
+
const select = (slug, label, options, defaultValue) => ({
|
|
1887
|
+
slug,
|
|
1888
|
+
label,
|
|
1889
|
+
type: "select",
|
|
1890
|
+
defaultValue,
|
|
1891
|
+
validation: { options }
|
|
1892
|
+
});
|
|
1893
|
+
/**
|
|
1894
|
+
* The `theme` select's values: `none`, a role, or `<role>:solid`. Block-type selects are
|
|
1895
|
+
* static, so this lists the roles every site defines; any other site role still works through
|
|
1896
|
+
* the `tone` field. `<role>:solid` is the filled backdrop with the contrast-computed text.
|
|
1897
|
+
*/
|
|
1898
|
+
const THEME_ROLES = [
|
|
1899
|
+
"neutral",
|
|
1900
|
+
"surface",
|
|
1901
|
+
"brand-primary",
|
|
1902
|
+
"brand-secondary",
|
|
1903
|
+
"info",
|
|
1904
|
+
"success",
|
|
1905
|
+
"warning",
|
|
1906
|
+
"danger"
|
|
1907
|
+
];
|
|
1908
|
+
const THEMES = [
|
|
1909
|
+
"none",
|
|
1910
|
+
...THEME_ROLES,
|
|
1911
|
+
...THEME_ROLES.map((r) => `${r}:solid`)
|
|
1912
|
+
];
|
|
1913
|
+
const SECTION_FIELDS = [
|
|
1914
|
+
{
|
|
1915
|
+
slug: "kicker",
|
|
1916
|
+
label: "Kicker (small line above the heading)",
|
|
1917
|
+
type: "string"
|
|
1918
|
+
},
|
|
1919
|
+
{
|
|
1920
|
+
slug: "heading",
|
|
1921
|
+
label: "Heading",
|
|
1922
|
+
type: "string"
|
|
1923
|
+
},
|
|
1924
|
+
select("level", "Heading level", [
|
|
1925
|
+
"2",
|
|
1926
|
+
"3",
|
|
1927
|
+
"4",
|
|
1928
|
+
"5",
|
|
1929
|
+
"6"
|
|
1930
|
+
], "2"),
|
|
1931
|
+
{
|
|
1932
|
+
slug: "sub",
|
|
1933
|
+
label: "Lead-in text under the heading",
|
|
1934
|
+
type: "text"
|
|
1935
|
+
},
|
|
1936
|
+
{
|
|
1937
|
+
slug: "body",
|
|
1938
|
+
label: "Content (any blocks)",
|
|
1939
|
+
type: "portableText"
|
|
1940
|
+
},
|
|
1941
|
+
select("layout", "Layout", [
|
|
1942
|
+
"stack",
|
|
1943
|
+
"center",
|
|
1944
|
+
"split"
|
|
1945
|
+
], "stack"),
|
|
1946
|
+
select("width", "Width", [
|
|
1947
|
+
"measure",
|
|
1948
|
+
"breakout",
|
|
1949
|
+
"spotlight",
|
|
1950
|
+
"fullbleed"
|
|
1951
|
+
], "measure"),
|
|
1952
|
+
{
|
|
1953
|
+
slug: "tone",
|
|
1954
|
+
label: "Backdrop tone (a colour role from the site, e.g. surface or brand-primary)",
|
|
1955
|
+
type: "string"
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
slug: "color",
|
|
1959
|
+
label: "Palette colour instead of a tone (e.g. amber or amber-300)",
|
|
1960
|
+
type: "string"
|
|
1961
|
+
},
|
|
1962
|
+
{
|
|
1963
|
+
slug: "solid",
|
|
1964
|
+
label: "Fill with the solid colour",
|
|
1965
|
+
type: "boolean",
|
|
1966
|
+
defaultValue: false
|
|
1967
|
+
},
|
|
1968
|
+
select("theme", "Theme (backdrop and readable text in one pick; overrides the tone fields)", THEMES, "none"),
|
|
1969
|
+
{
|
|
1970
|
+
slug: "anchor",
|
|
1971
|
+
label: "Anchor id (so a link can point here with #id)",
|
|
1972
|
+
type: "string"
|
|
1973
|
+
},
|
|
1974
|
+
{
|
|
1975
|
+
slug: "class_name",
|
|
1976
|
+
label: "Extra CSS classes",
|
|
1977
|
+
type: "string"
|
|
1978
|
+
},
|
|
1979
|
+
{
|
|
1980
|
+
slug: "style",
|
|
1981
|
+
label: "CSS custom properties, e.g. --p-card: 2rem",
|
|
1982
|
+
type: "string"
|
|
1983
|
+
}
|
|
1984
|
+
];
|
|
1985
|
+
/** `vitops.section`: kicker, heading, lead-in, tone/backdrop, optional heading-left layout, and a body of blocks. */
|
|
1986
|
+
const SECTION_BLOCK_TYPE = {
|
|
1987
|
+
slug: SECTION_SLUG,
|
|
1988
|
+
label: "Section",
|
|
1989
|
+
description: "A titled page section whose body can hold any other blocks.",
|
|
1990
|
+
category: "Sections",
|
|
1991
|
+
icon: "layout",
|
|
1992
|
+
currentVersion: 1,
|
|
1993
|
+
versions: [{
|
|
1994
|
+
version: 1,
|
|
1995
|
+
fields: SECTION_FIELDS
|
|
1996
|
+
}]
|
|
1997
|
+
};
|
|
1998
|
+
/**
|
|
1999
|
+
* Every block type this plugin ships, in the shape `seed.json`'s `blockTypes` takes. Spread
|
|
2000
|
+
* it into the seed (and allow the slugs on the collection's `blocks` field), e.g.
|
|
2001
|
+
* `blockTypes: [...vitopsBlockTypes()]`.
|
|
2002
|
+
*/
|
|
2003
|
+
function vitopsBlockTypes() {
|
|
2004
|
+
return [SECTION_BLOCK_TYPE];
|
|
2005
|
+
}
|
|
2006
|
+
|
|
763
2007
|
// src/blocks-doc.ts
|
|
764
2008
|
const cell = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
765
2009
|
function describe(f) {
|
|
@@ -794,8 +2038,18 @@ function renderBlocksDoc(blocks = BLOCKS) {
|
|
|
794
2038
|
"# EmDash blocks",
|
|
795
2039
|
"",
|
|
796
2040
|
"Each block is a Portable Text object: `{ \"_type\": \"vitops.cards\", \"_key\": \"<unique>\", ...fields }`.",
|
|
797
|
-
"
|
|
2041
|
+
"Portable Text blocks are flat — they cannot contain other blocks, and a repeater row holds scalar fields only.",
|
|
2042
|
+
"To put blocks inside something, use the section block TYPE (end of this page), whose body is Portable Text.",
|
|
798
2043
|
"Give every block a unique `_key`; gallery and carousel derive their element id from it.",
|
|
2044
|
+
"",
|
|
2045
|
+
"## Colour: `tone` and `color`",
|
|
2046
|
+
"",
|
|
2047
|
+
"Every block that colours something takes the same two fields. `tone` is a colour role from the",
|
|
2048
|
+
"site's `colors.roles` (or a semantic name); an unknown role falls back through its semantic",
|
|
2049
|
+
"relatives to `neutral`. `color` is an escape hatch naming a palette hue (`amber`) or hue and step",
|
|
2050
|
+
"(`amber-300`) and wins over `tone`; its contrast is not checked. Renderers emit `--tone-*` custom",
|
|
2051
|
+
"properties, never a class built from the value. Multiline fields that accept links take",
|
|
2052
|
+
"`[text](/url)`; hero headings accent words wrapped in `*stars*`.",
|
|
799
2053
|
""
|
|
800
2054
|
];
|
|
801
2055
|
for (const b of blocks) {
|
|
@@ -803,6 +2057,19 @@ function renderBlocksDoc(blocks = BLOCKS) {
|
|
|
803
2057
|
if (b.placeholder) out.push(`${b.placeholder}.`, "");
|
|
804
2058
|
if (b.fields?.length) out.push("| Field | Type | Label | Notes |", "| --- | --- | --- | --- |", ...fieldRows(b.fields), "");
|
|
805
2059
|
}
|
|
2060
|
+
out.push("## `vitops.form` needs an endpoint", "", "The form posts to `/api/track` (the `action` field overrides it), which must be an on-demand route", "built with `createConversionRoute()` from `@getvitops/astro/routes`; the emdash template ships one.", "Field names are submitted as written, except that `phone` becomes `tel` and a few names the route", "reserves (`event`, `website`, `honeypot`, `_honeypot`, `cf-turnstile-response`) are renamed.", "It emails `site.notifications.email` and writes `site.crm` when set; with neither, the route refuses", "the post rather than accept a message nobody receives. Add the spam check with", "`vitops setup turnstile`: once `site.security.turnstile.siteKey` is set the block renders", "`<wc-turnstile>`, which loads Cloudflare Turnstile only when the visitor first uses the form", "(category `necessary`, so it is not behind the consent banner). The secret lives in the", "`VITOPS_TURNSTILE_SECRET` Worker secret, never in `site.json`. Use one form per page.", "", "## Block types (the `blocks` field)", "", "Schema-level block types, not Portable Text blocks: add them to the seed (`blockTypes: [...vitopsBlockTypes()]`),", "allow the slug on the collection's `blocks` field, and render with `blockTypeComponents` from `@getvitops/emdash/astro`.", "");
|
|
2061
|
+
for (const t of vitopsBlockTypes()) {
|
|
2062
|
+
out.push(`### \`${t.slug}\` — ${t.label}`, "", `${t.description ?? ""}`, "");
|
|
2063
|
+
const fields = t.versions.find((v) => v.version === t.currentVersion)?.fields ?? [];
|
|
2064
|
+
out.push("| Field | Type | Label | Notes |", "| --- | --- | --- | --- |", ...fields.map((f) => {
|
|
2065
|
+
const notes = [
|
|
2066
|
+
f.validation?.options ? `one of ${f.validation.options.join(", ")}` : "",
|
|
2067
|
+
f.defaultValue !== void 0 ? `default \`${String(f.defaultValue)}\`` : "",
|
|
2068
|
+
f.type === "portableText" ? "accepts every `vitops.*` block above" : ""
|
|
2069
|
+
].filter(Boolean);
|
|
2070
|
+
return `| \`${f.slug}\` | ${f.type} | ${cell(f.label)} | ${cell(notes.join("; "))} |`;
|
|
2071
|
+
}), "");
|
|
2072
|
+
}
|
|
806
2073
|
return out.join("\n");
|
|
807
2074
|
}
|
|
808
2075
|
|
|
@@ -867,7 +2134,7 @@ async function vitopsHosting(options = {}) {
|
|
|
867
2134
|
}
|
|
868
2135
|
|
|
869
2136
|
// package.json
|
|
870
|
-
var version = "
|
|
2137
|
+
var version = "2.0.0-alpha.1";
|
|
871
2138
|
|
|
872
2139
|
// src/index.ts
|
|
873
2140
|
/**
|
|
@@ -929,6 +2196,7 @@ function createPlugin(options = {}) {
|
|
|
929
2196
|
const wcBase = options.wcBase ?? "/vitops";
|
|
930
2197
|
const editor = options.designSystemEditor || void 0;
|
|
931
2198
|
const fragmentsEnabled = scripts === "fragments" || !!editor;
|
|
2199
|
+
const roles = options.tones ?? rolesOf(editor?.config);
|
|
932
2200
|
const pageFragments = async (_event, ctx) => {
|
|
933
2201
|
const contributions = [];
|
|
934
2202
|
if (scripts === "fragments") contributions.push({
|
|
@@ -966,9 +2234,14 @@ function createPlugin(options = {}) {
|
|
|
966
2234
|
}]
|
|
967
2235
|
} : {}
|
|
968
2236
|
},
|
|
969
|
-
|
|
2237
|
+
routes: {
|
|
2238
|
+
[TONES_ROUTE]: createTonesRoute(() => roles),
|
|
2239
|
+
[MENUS_ROUTE]: createMenusRoute(() => getMenus()),
|
|
2240
|
+
[WIDGET_AREAS_ROUTE]: createMenusRoute(() => getWidgetAreas()),
|
|
2241
|
+
...editor ? { "design-system": createDesignSystemRoute(editor) } : {}
|
|
2242
|
+
}
|
|
970
2243
|
});
|
|
971
2244
|
}
|
|
972
2245
|
|
|
973
|
-
export { BLOCKS, createPlugin, createPlugin as default, renderBlocksDoc, selectBlocks, vitopsEmdash, vitopsHosting };
|
|
2246
|
+
export { BLOCKS, SECTION_BLOCK_TYPE, SECTION_SLUG, createPlugin, createPlugin as default, renderBlocksDoc, resolveTone, selectBlocks, toneChain, vitopsBlockTypes, vitopsEmdash, vitopsHosting };
|
|
974
2247
|
//# sourceMappingURL=index.mjs.map
|