@getvitops/emdash 1.0.0-alpha.0 → 2.0.0-alpha.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.
- package/CHANGELOG.md +279 -3
- package/README.md +66 -21
- package/dist/index.d.mts +82 -18
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1626 -152
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -13
- package/src/admin/DesignSystemEditorPage.tsx +15 -42
- package/src/astro/blocks/Accordion.astro +39 -0
- package/src/astro/blocks/ActionLink.astro +4 -2
- package/src/astro/blocks/Callout.astro +73 -0
- package/src/astro/blocks/Cards.astro +96 -0
- package/src/astro/blocks/Carousel.astro +25 -8
- package/src/astro/blocks/Embed.astro +67 -0
- package/src/astro/blocks/Entries.astro +30 -0
- package/src/astro/blocks/Form.astro +194 -0
- package/src/astro/blocks/Gallery.astro +40 -0
- package/src/astro/blocks/Hero.astro +115 -0
- package/src/astro/blocks/ImageCompare.astro +5 -2
- package/src/astro/blocks/Logos.astro +48 -0
- package/src/astro/blocks/Marquee.astro +38 -0
- 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 +84 -0
- package/src/astro/blocks/Stats.astro +59 -0
- package/src/astro/blocks/Steps.astro +48 -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 +570 -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 +56 -0
- package/src/astro/blocks/value.ts +103 -1
- package/src/astro/index.ts +40 -0
- package/src/admin/core-editor.d.ts +0 -7
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { definePlugin, pluginResponse } from "emdash";
|
|
1
|
+
import { definePlugin, getMenus, 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";
|
|
5
5
|
import { commitFile } from "@getvitops/utils/git";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// src/icon-options.ts
|
|
8
8
|
/**
|
|
9
9
|
* Semantic icon names, as Block Kit combobox options.
|
|
10
10
|
*
|
|
@@ -25,8 +25,165 @@ const SEMANTIC_ICON_OPTIONS = Object.keys(iconMap.fa7).sort().map((name) => ({
|
|
|
25
25
|
value: name
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
|
|
134
|
+
// src/blocks.ts
|
|
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
|
+
/** Heading level for a block's own heading; `h1` is reserved for the page title. */
|
|
171
|
+
const levelField = (initial) => ({
|
|
172
|
+
type: "select",
|
|
173
|
+
action_id: "level",
|
|
174
|
+
label: "Heading level",
|
|
175
|
+
initial_value: initial,
|
|
176
|
+
options: [
|
|
177
|
+
"2",
|
|
178
|
+
"3",
|
|
179
|
+
"4",
|
|
180
|
+
"5",
|
|
181
|
+
"6"
|
|
182
|
+
].map((n) => ({
|
|
183
|
+
label: `h${n}`,
|
|
184
|
+
value: n
|
|
185
|
+
}))
|
|
186
|
+
});
|
|
30
187
|
const BLOCKS = [
|
|
31
188
|
{
|
|
32
189
|
/**
|
|
@@ -93,226 +250,1384 @@ const BLOCKS = [
|
|
|
93
250
|
},
|
|
94
251
|
{
|
|
95
252
|
type: "select",
|
|
96
|
-
action_id: "role",
|
|
97
|
-
label: "Colour role",
|
|
98
|
-
initial_value: "",
|
|
253
|
+
action_id: "role",
|
|
254
|
+
label: "Colour role",
|
|
255
|
+
initial_value: "",
|
|
256
|
+
options: [
|
|
257
|
+
{
|
|
258
|
+
label: "Default",
|
|
259
|
+
value: ""
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
label: "Success",
|
|
263
|
+
value: "success"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
label: "Danger",
|
|
267
|
+
value: "danger"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
label: "Warning",
|
|
271
|
+
value: "warning"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
label: "Info",
|
|
275
|
+
value: "info"
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
type: "toggle",
|
|
281
|
+
action_id: "newTab",
|
|
282
|
+
label: "Open in a new tab",
|
|
283
|
+
initial_value: false
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
type: "vitops.imageCompare",
|
|
289
|
+
label: "Image compare",
|
|
290
|
+
placeholder: "Before/after image slider",
|
|
291
|
+
fields: [
|
|
292
|
+
{
|
|
293
|
+
type: "text_input",
|
|
294
|
+
action_id: "before",
|
|
295
|
+
label: "Before image URL"
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
type: "text_input",
|
|
299
|
+
action_id: "beforeAlt",
|
|
300
|
+
label: "Before alt text"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
type: "text_input",
|
|
304
|
+
action_id: "beforeLabel",
|
|
305
|
+
label: "Before label",
|
|
306
|
+
initial_value: "Before"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
type: "text_input",
|
|
310
|
+
action_id: "after",
|
|
311
|
+
label: "After image URL"
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
type: "text_input",
|
|
315
|
+
action_id: "afterAlt",
|
|
316
|
+
label: "After alt text"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
type: "text_input",
|
|
320
|
+
action_id: "afterLabel",
|
|
321
|
+
label: "After label",
|
|
322
|
+
initial_value: "After"
|
|
323
|
+
}
|
|
324
|
+
]
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
type: "vitops.copyButton",
|
|
328
|
+
label: "Copy snippet",
|
|
329
|
+
icon: "code",
|
|
330
|
+
placeholder: "Text with a copy-to-clipboard button",
|
|
331
|
+
fields: [
|
|
332
|
+
{
|
|
333
|
+
type: "text_input",
|
|
334
|
+
action_id: "text",
|
|
335
|
+
label: "Text to copy",
|
|
336
|
+
multiline: true
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
type: "text_input",
|
|
340
|
+
action_id: "label",
|
|
341
|
+
label: "Button label",
|
|
342
|
+
initial_value: "Copy"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
type: "select",
|
|
346
|
+
action_id: "display",
|
|
347
|
+
label: "Display",
|
|
348
|
+
initial_value: "code-block",
|
|
349
|
+
options: [{
|
|
350
|
+
label: "Code block",
|
|
351
|
+
value: "code-block"
|
|
352
|
+
}, {
|
|
353
|
+
label: "Inline",
|
|
354
|
+
value: "inline"
|
|
355
|
+
}]
|
|
356
|
+
}
|
|
357
|
+
]
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
type: "vitops.banner",
|
|
361
|
+
label: "Banner",
|
|
362
|
+
placeholder: "Announcement or notice",
|
|
363
|
+
fields: [
|
|
364
|
+
{
|
|
365
|
+
type: "text_input",
|
|
366
|
+
action_id: "message",
|
|
367
|
+
label: "Message",
|
|
368
|
+
multiline: true
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
type: "select",
|
|
372
|
+
action_id: "tone",
|
|
373
|
+
label: "Tone",
|
|
374
|
+
initial_value: "info",
|
|
375
|
+
options: [
|
|
376
|
+
{
|
|
377
|
+
label: "Info",
|
|
378
|
+
value: "info"
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
label: "Success",
|
|
382
|
+
value: "success"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
label: "Warning",
|
|
386
|
+
value: "warning"
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
label: "Danger",
|
|
390
|
+
value: "danger"
|
|
391
|
+
}
|
|
392
|
+
]
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
type: "toggle",
|
|
396
|
+
action_id: "dismissible",
|
|
397
|
+
label: "Dismissible",
|
|
398
|
+
initial_value: true
|
|
399
|
+
}
|
|
400
|
+
]
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
type: "vitops.details",
|
|
404
|
+
label: "Disclosure",
|
|
405
|
+
placeholder: "Collapsible details/summary",
|
|
406
|
+
fields: [
|
|
407
|
+
{
|
|
408
|
+
type: "text_input",
|
|
409
|
+
action_id: "summary",
|
|
410
|
+
label: "Summary (always visible)"
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
type: "text_input",
|
|
414
|
+
action_id: "body",
|
|
415
|
+
label: "Body (revealed)",
|
|
416
|
+
multiline: true
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
type: "toggle",
|
|
420
|
+
action_id: "open",
|
|
421
|
+
label: "Open by default",
|
|
422
|
+
initial_value: false
|
|
423
|
+
}
|
|
424
|
+
]
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
type: "vitops.carousel",
|
|
428
|
+
label: "Carousel",
|
|
429
|
+
placeholder: "Image carousel",
|
|
430
|
+
category: "Media",
|
|
431
|
+
fields: [
|
|
432
|
+
{
|
|
433
|
+
type: "repeater",
|
|
434
|
+
action_id: "slides",
|
|
435
|
+
label: "Slides",
|
|
436
|
+
item_label: "slide",
|
|
437
|
+
min_items: 1,
|
|
438
|
+
fields: [{
|
|
439
|
+
type: "text_input",
|
|
440
|
+
action_id: "src",
|
|
441
|
+
label: "Image URL"
|
|
442
|
+
}, {
|
|
443
|
+
type: "text_input",
|
|
444
|
+
action_id: "alt",
|
|
445
|
+
label: "Alt text"
|
|
446
|
+
}]
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
type: "text_input",
|
|
450
|
+
action_id: "images",
|
|
451
|
+
label: "Image URLs, one per line (legacy — use Slides)",
|
|
452
|
+
multiline: true
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
type: "text_input",
|
|
456
|
+
action_id: "alts",
|
|
457
|
+
label: "Alt texts, one per line (legacy — use Slides)",
|
|
458
|
+
multiline: true
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
type: "text_input",
|
|
462
|
+
action_id: "label",
|
|
463
|
+
label: "Accessible name",
|
|
464
|
+
initial_value: "Carousel"
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
type: "number_input",
|
|
468
|
+
action_id: "autoplay",
|
|
469
|
+
label: "Autoplay interval (ms, 0 = off)",
|
|
470
|
+
initial_value: 0
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
type: "toggle",
|
|
474
|
+
action_id: "loop",
|
|
475
|
+
label: "Loop (clones the slides — autoplay implies it)",
|
|
476
|
+
initial_value: false
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
type: "text_input",
|
|
480
|
+
action_id: "aspect",
|
|
481
|
+
label: "Slide aspect ratio",
|
|
482
|
+
initial_value: "16 / 9"
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
type: "vitops.gallery",
|
|
488
|
+
label: "Gallery",
|
|
489
|
+
placeholder: "Thumbnail grid that opens a lightbox carousel",
|
|
490
|
+
category: "Media",
|
|
491
|
+
fields: [
|
|
492
|
+
{
|
|
493
|
+
type: "text_input",
|
|
494
|
+
action_id: "label",
|
|
495
|
+
label: "Accessible name",
|
|
496
|
+
initial_value: "Gallery"
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
type: "repeater",
|
|
500
|
+
action_id: "images",
|
|
501
|
+
label: "Images",
|
|
502
|
+
item_label: "image",
|
|
503
|
+
min_items: 1,
|
|
504
|
+
fields: [
|
|
505
|
+
{
|
|
506
|
+
type: "text_input",
|
|
507
|
+
action_id: "src",
|
|
508
|
+
label: "Image URL"
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
type: "text_input",
|
|
512
|
+
action_id: "alt",
|
|
513
|
+
label: "Alt text"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
type: "text_input",
|
|
517
|
+
action_id: "caption",
|
|
518
|
+
label: "Caption"
|
|
519
|
+
}
|
|
520
|
+
]
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
type: "text_input",
|
|
524
|
+
action_id: "aspect",
|
|
525
|
+
label: "Thumbnail aspect ratio",
|
|
526
|
+
initial_value: "1"
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
type: "toggle",
|
|
530
|
+
action_id: "loop",
|
|
531
|
+
label: "Wrap around at the ends",
|
|
532
|
+
initial_value: false
|
|
533
|
+
}
|
|
534
|
+
]
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
type: "vitops.cards",
|
|
538
|
+
label: "Cards",
|
|
539
|
+
placeholder: "A grid of cards",
|
|
540
|
+
category: "Sections",
|
|
541
|
+
fields: [
|
|
542
|
+
{
|
|
543
|
+
type: "repeater",
|
|
544
|
+
action_id: "cards",
|
|
545
|
+
label: "Cards",
|
|
546
|
+
item_label: "card",
|
|
547
|
+
min_items: 1,
|
|
548
|
+
fields: [
|
|
549
|
+
{
|
|
550
|
+
type: "text_input",
|
|
551
|
+
action_id: "title",
|
|
552
|
+
label: "Title"
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
type: "text_input",
|
|
556
|
+
action_id: "body",
|
|
557
|
+
label: "Text (write [link text](/url) for an inline link)",
|
|
558
|
+
multiline: true
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
type: "text_input",
|
|
562
|
+
action_id: "image",
|
|
563
|
+
label: "Image URL"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
type: "text_input",
|
|
567
|
+
action_id: "imageAlt",
|
|
568
|
+
label: "Image alt text"
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
type: "text_input",
|
|
572
|
+
action_id: "href",
|
|
573
|
+
label: "Link URL (makes the whole card clickable)"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
type: "text_input",
|
|
577
|
+
action_id: "linkLabel",
|
|
578
|
+
label: "Link text",
|
|
579
|
+
initial_value: "Read more"
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
type: "text_input",
|
|
583
|
+
action_id: "checklist",
|
|
584
|
+
label: "Checklist (panel style; one item per line)",
|
|
585
|
+
multiline: true
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
type: "text_input",
|
|
589
|
+
action_id: "ctaLabel",
|
|
590
|
+
label: "Button text (panel style)"
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
type: "text_input",
|
|
594
|
+
action_id: "ctaHref",
|
|
595
|
+
label: "Button URL (panel style)"
|
|
596
|
+
}
|
|
597
|
+
]
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
type: "select",
|
|
601
|
+
action_id: "variant",
|
|
602
|
+
label: "Style",
|
|
603
|
+
initial_value: "default",
|
|
604
|
+
options: [{
|
|
605
|
+
label: "Cards",
|
|
606
|
+
value: "default"
|
|
607
|
+
}, {
|
|
608
|
+
label: "Panels (tinted, with checklist and button)",
|
|
609
|
+
value: "panel"
|
|
610
|
+
}]
|
|
611
|
+
},
|
|
612
|
+
levelField("3"),
|
|
613
|
+
...toneFields(),
|
|
614
|
+
{
|
|
615
|
+
type: "select",
|
|
616
|
+
action_id: "columns",
|
|
617
|
+
label: "Columns",
|
|
618
|
+
initial_value: "3",
|
|
619
|
+
options: [
|
|
620
|
+
{
|
|
621
|
+
label: "1",
|
|
622
|
+
value: "1"
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
label: "2",
|
|
626
|
+
value: "2"
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
label: "3",
|
|
630
|
+
value: "3"
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
label: "4",
|
|
634
|
+
value: "4"
|
|
635
|
+
}
|
|
636
|
+
]
|
|
637
|
+
}
|
|
638
|
+
]
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
type: "vitops.accordion",
|
|
642
|
+
label: "Accordion",
|
|
643
|
+
placeholder: "A group of collapsible sections",
|
|
644
|
+
category: "Sections",
|
|
645
|
+
fields: [{
|
|
646
|
+
type: "repeater",
|
|
647
|
+
action_id: "items",
|
|
648
|
+
label: "Items",
|
|
649
|
+
item_label: "item",
|
|
650
|
+
min_items: 1,
|
|
651
|
+
fields: [
|
|
652
|
+
{
|
|
653
|
+
type: "text_input",
|
|
654
|
+
action_id: "summary",
|
|
655
|
+
label: "Summary (always visible)"
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
type: "text_input",
|
|
659
|
+
action_id: "body",
|
|
660
|
+
label: "Body (revealed)",
|
|
661
|
+
multiline: true
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
type: "toggle",
|
|
665
|
+
action_id: "open",
|
|
666
|
+
label: "Open by default",
|
|
667
|
+
initial_value: false
|
|
668
|
+
}
|
|
669
|
+
]
|
|
670
|
+
}, {
|
|
671
|
+
type: "toggle",
|
|
672
|
+
action_id: "exclusive",
|
|
673
|
+
label: "Only one open at a time",
|
|
674
|
+
initial_value: false
|
|
675
|
+
}]
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
type: "vitops.hero",
|
|
679
|
+
label: "Hero",
|
|
680
|
+
placeholder: "Page opener: heading, text, buttons and an optional image",
|
|
681
|
+
category: "Sections",
|
|
682
|
+
fields: [
|
|
683
|
+
{
|
|
684
|
+
type: "text_input",
|
|
685
|
+
action_id: "eyebrow",
|
|
686
|
+
label: "Eyebrow (small line above)"
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
type: "text_input",
|
|
690
|
+
action_id: "heading",
|
|
691
|
+
label: "Heading (wrap words in *stars* to accent them)"
|
|
692
|
+
},
|
|
693
|
+
levelField("2"),
|
|
694
|
+
{
|
|
695
|
+
type: "text_input",
|
|
696
|
+
action_id: "body",
|
|
697
|
+
label: "Text",
|
|
698
|
+
multiline: true
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
type: "text_input",
|
|
702
|
+
action_id: "primaryLabel",
|
|
703
|
+
label: "Main button text"
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
type: "text_input",
|
|
707
|
+
action_id: "primaryHref",
|
|
708
|
+
label: "Main button URL"
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
type: "text_input",
|
|
712
|
+
action_id: "secondaryLabel",
|
|
713
|
+
label: "Second link text"
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
type: "text_input",
|
|
717
|
+
action_id: "secondaryHref",
|
|
718
|
+
label: "Second link URL"
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
type: "text_input",
|
|
722
|
+
action_id: "image",
|
|
723
|
+
label: "Image URL (split layout)"
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
type: "text_input",
|
|
727
|
+
action_id: "imageAlt",
|
|
728
|
+
label: "Image alt text"
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
type: "select",
|
|
732
|
+
action_id: "layout",
|
|
733
|
+
label: "Layout",
|
|
734
|
+
initial_value: "centered",
|
|
735
|
+
options: [{
|
|
736
|
+
label: "Centered",
|
|
737
|
+
value: "centered"
|
|
738
|
+
}, {
|
|
739
|
+
label: "Text and image side by side",
|
|
740
|
+
value: "split"
|
|
741
|
+
}]
|
|
742
|
+
},
|
|
743
|
+
...toneFields(),
|
|
744
|
+
{
|
|
745
|
+
type: "repeater",
|
|
746
|
+
action_id: "cards",
|
|
747
|
+
label: "Option cards under the buttons (optional)",
|
|
748
|
+
item_label: "card",
|
|
749
|
+
fields: [
|
|
750
|
+
{
|
|
751
|
+
type: "text_input",
|
|
752
|
+
action_id: "title",
|
|
753
|
+
label: "Title"
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
type: "text_input",
|
|
757
|
+
action_id: "body",
|
|
758
|
+
label: "Text (write [link text](/url) for an inline link)",
|
|
759
|
+
multiline: true
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
type: "text_input",
|
|
763
|
+
action_id: "href",
|
|
764
|
+
label: "Link URL (makes the card clickable)"
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
type: "text_input",
|
|
768
|
+
action_id: "linkLabel",
|
|
769
|
+
label: "Link text",
|
|
770
|
+
initial_value: "Learn more"
|
|
771
|
+
}
|
|
772
|
+
]
|
|
773
|
+
}
|
|
774
|
+
]
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
type: "vitops.marquee",
|
|
778
|
+
label: "Marquee",
|
|
779
|
+
placeholder: "A continuously scrolling row (logos, quotes, badges)",
|
|
780
|
+
category: "Media",
|
|
781
|
+
fields: [
|
|
782
|
+
{
|
|
783
|
+
type: "repeater",
|
|
784
|
+
action_id: "items",
|
|
785
|
+
label: "Items",
|
|
786
|
+
item_label: "item",
|
|
787
|
+
min_items: 1,
|
|
788
|
+
fields: [
|
|
789
|
+
{
|
|
790
|
+
type: "text_input",
|
|
791
|
+
action_id: "text",
|
|
792
|
+
label: "Text"
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
type: "text_input",
|
|
796
|
+
action_id: "image",
|
|
797
|
+
label: "Image URL (shown instead of text)"
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
type: "text_input",
|
|
801
|
+
action_id: "alt",
|
|
802
|
+
label: "Image alt text"
|
|
803
|
+
}
|
|
804
|
+
]
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
type: "toggle",
|
|
808
|
+
action_id: "pauseOnHover",
|
|
809
|
+
label: "Pause on hover",
|
|
810
|
+
initial_value: true
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
type: "toggle",
|
|
814
|
+
action_id: "reverse",
|
|
815
|
+
label: "Scroll the other way",
|
|
816
|
+
initial_value: false
|
|
817
|
+
}
|
|
818
|
+
]
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
type: "vitops.logos",
|
|
822
|
+
label: "Logos",
|
|
823
|
+
placeholder: "A still row of partner or client logos",
|
|
824
|
+
category: "Media",
|
|
825
|
+
fields: [
|
|
826
|
+
{
|
|
827
|
+
type: "text_input",
|
|
828
|
+
action_id: "label",
|
|
829
|
+
label: "Accessible name",
|
|
830
|
+
initial_value: "Partners"
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
type: "repeater",
|
|
834
|
+
action_id: "logos",
|
|
835
|
+
label: "Logos",
|
|
836
|
+
item_label: "logo",
|
|
837
|
+
min_items: 1,
|
|
838
|
+
fields: [
|
|
839
|
+
{
|
|
840
|
+
type: "text_input",
|
|
841
|
+
action_id: "image",
|
|
842
|
+
label: "Image URL"
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
type: "text_input",
|
|
846
|
+
action_id: "alt",
|
|
847
|
+
label: "Name (alt text)"
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
type: "text_input",
|
|
851
|
+
action_id: "href",
|
|
852
|
+
label: "Link URL (optional)"
|
|
853
|
+
}
|
|
854
|
+
]
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
type: "toggle",
|
|
858
|
+
action_id: "grayscale",
|
|
859
|
+
label: "Grey until hovered",
|
|
860
|
+
initial_value: false
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
type: "select",
|
|
864
|
+
action_id: "size",
|
|
865
|
+
label: "Logo height",
|
|
866
|
+
initial_value: "md",
|
|
867
|
+
options: [
|
|
868
|
+
{
|
|
869
|
+
label: "Small",
|
|
870
|
+
value: "sm"
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
label: "Medium",
|
|
874
|
+
value: "md"
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
label: "Large",
|
|
878
|
+
value: "lg"
|
|
879
|
+
}
|
|
880
|
+
]
|
|
881
|
+
}
|
|
882
|
+
]
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
type: "vitops.steps",
|
|
886
|
+
label: "Steps",
|
|
887
|
+
placeholder: "A numbered sequence joined by a connector line",
|
|
888
|
+
category: "Sections",
|
|
889
|
+
fields: [
|
|
890
|
+
{
|
|
891
|
+
type: "repeater",
|
|
892
|
+
action_id: "steps",
|
|
893
|
+
label: "Steps",
|
|
894
|
+
item_label: "step",
|
|
895
|
+
min_items: 1,
|
|
896
|
+
fields: [{
|
|
897
|
+
type: "text_input",
|
|
898
|
+
action_id: "title",
|
|
899
|
+
label: "Title"
|
|
900
|
+
}, {
|
|
901
|
+
type: "text_input",
|
|
902
|
+
action_id: "text",
|
|
903
|
+
label: "Text (write [link text](/url) for an inline link)",
|
|
904
|
+
multiline: true
|
|
905
|
+
}]
|
|
906
|
+
},
|
|
907
|
+
levelField("3"),
|
|
908
|
+
{
|
|
909
|
+
type: "select",
|
|
910
|
+
action_id: "layout",
|
|
911
|
+
label: "Layout",
|
|
912
|
+
initial_value: "column",
|
|
913
|
+
options: [{
|
|
914
|
+
label: "Stacked",
|
|
915
|
+
value: "column"
|
|
916
|
+
}, {
|
|
917
|
+
label: "Side by side when wide",
|
|
918
|
+
value: "row"
|
|
919
|
+
}]
|
|
920
|
+
},
|
|
921
|
+
...toneFields()
|
|
922
|
+
]
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
type: "vitops.stats",
|
|
926
|
+
label: "Stats",
|
|
927
|
+
placeholder: "Headline figures that count up, or labelled tiles",
|
|
928
|
+
category: "Sections",
|
|
929
|
+
fields: [
|
|
930
|
+
{
|
|
931
|
+
type: "repeater",
|
|
932
|
+
action_id: "stats",
|
|
933
|
+
label: "Figures",
|
|
934
|
+
item_label: "figure",
|
|
935
|
+
min_items: 1,
|
|
936
|
+
fields: [
|
|
937
|
+
{
|
|
938
|
+
type: "text_input",
|
|
939
|
+
action_id: "value",
|
|
940
|
+
label: "Figure (e.g. 82% or 11 hrs)"
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
type: "text_input",
|
|
944
|
+
action_id: "label",
|
|
945
|
+
label: "Label"
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
type: "text_input",
|
|
949
|
+
action_id: "group",
|
|
950
|
+
label: "Group (tiles style; small line above)"
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
type: "text_input",
|
|
954
|
+
action_id: "delta",
|
|
955
|
+
label: "Change (tiles style; e.g. +3 this week)"
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
type: "select",
|
|
959
|
+
action_id: "state",
|
|
960
|
+
label: "Change is",
|
|
961
|
+
initial_value: "",
|
|
962
|
+
options: [
|
|
963
|
+
{
|
|
964
|
+
label: "Neutral",
|
|
965
|
+
value: ""
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
label: "Good (up)",
|
|
969
|
+
value: "up"
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
label: "Bad (down)",
|
|
973
|
+
value: "down"
|
|
974
|
+
}
|
|
975
|
+
]
|
|
976
|
+
}
|
|
977
|
+
]
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
type: "select",
|
|
981
|
+
action_id: "variant",
|
|
982
|
+
label: "Style",
|
|
983
|
+
initial_value: "figures",
|
|
984
|
+
options: [{
|
|
985
|
+
label: "Figures",
|
|
986
|
+
value: "figures"
|
|
987
|
+
}, {
|
|
988
|
+
label: "Tiles",
|
|
989
|
+
value: "tiles"
|
|
990
|
+
}]
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
type: "toggle",
|
|
994
|
+
action_id: "animate",
|
|
995
|
+
label: "Count up when scrolled into view",
|
|
996
|
+
initial_value: true
|
|
997
|
+
},
|
|
998
|
+
...toneFields()
|
|
999
|
+
]
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
type: "vitops.callout",
|
|
1003
|
+
label: "Callout",
|
|
1004
|
+
placeholder: "A call-to-action band (not a dismissible banner)",
|
|
1005
|
+
category: "Sections",
|
|
1006
|
+
fields: [
|
|
1007
|
+
{
|
|
1008
|
+
type: "text_input",
|
|
1009
|
+
action_id: "heading",
|
|
1010
|
+
label: "Heading"
|
|
1011
|
+
},
|
|
1012
|
+
levelField("2"),
|
|
1013
|
+
{
|
|
1014
|
+
type: "text_input",
|
|
1015
|
+
action_id: "body",
|
|
1016
|
+
label: "Text (write [link text](/url) for an inline link)",
|
|
1017
|
+
multiline: true
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
type: "text_input",
|
|
1021
|
+
action_id: "primaryLabel",
|
|
1022
|
+
label: "Main button text"
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
type: "text_input",
|
|
1026
|
+
action_id: "primaryHref",
|
|
1027
|
+
label: "Main button URL"
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
type: "text_input",
|
|
1031
|
+
action_id: "secondaryLabel",
|
|
1032
|
+
label: "Second link text"
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
type: "text_input",
|
|
1036
|
+
action_id: "secondaryHref",
|
|
1037
|
+
label: "Second link URL"
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
type: "select",
|
|
1041
|
+
action_id: "align",
|
|
1042
|
+
label: "Layout",
|
|
1043
|
+
initial_value: "start",
|
|
1044
|
+
options: [{
|
|
1045
|
+
label: "Message left, buttons right",
|
|
1046
|
+
value: "start"
|
|
1047
|
+
}, {
|
|
1048
|
+
label: "Centered",
|
|
1049
|
+
value: "center"
|
|
1050
|
+
}]
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
type: "toggle",
|
|
1054
|
+
action_id: "solid",
|
|
1055
|
+
label: "Fill with the solid colour",
|
|
1056
|
+
initial_value: false
|
|
1057
|
+
},
|
|
1058
|
+
...toneFields()
|
|
1059
|
+
]
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
type: "vitops.entries",
|
|
1063
|
+
label: "Entries table",
|
|
1064
|
+
placeholder: "Rows of term/value pairs: a stacked list that becomes a table when wide",
|
|
1065
|
+
category: "Sections",
|
|
1066
|
+
fields: [
|
|
1067
|
+
{
|
|
1068
|
+
type: "repeater",
|
|
1069
|
+
action_id: "entries",
|
|
1070
|
+
label: "Entries",
|
|
1071
|
+
item_label: "entry",
|
|
1072
|
+
min_items: 1,
|
|
1073
|
+
fields: [{
|
|
1074
|
+
type: "text_input",
|
|
1075
|
+
action_id: "heading",
|
|
1076
|
+
label: "Heading (row name)"
|
|
1077
|
+
}, {
|
|
1078
|
+
type: "text_input",
|
|
1079
|
+
action_id: "pairs",
|
|
1080
|
+
label: "Details (one per line: Term | Value)",
|
|
1081
|
+
multiline: true
|
|
1082
|
+
}]
|
|
1083
|
+
},
|
|
1084
|
+
levelField("3"),
|
|
1085
|
+
{
|
|
1086
|
+
type: "text_input",
|
|
1087
|
+
action_id: "breakpoint",
|
|
1088
|
+
label: "Becomes a table at this width",
|
|
1089
|
+
initial_value: "40rem"
|
|
1090
|
+
}
|
|
1091
|
+
]
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
type: "vitops.pricing",
|
|
1095
|
+
label: "Pricing",
|
|
1096
|
+
placeholder: "Plan cards with aligned feature rows",
|
|
1097
|
+
category: "Sections",
|
|
1098
|
+
fields: [{
|
|
1099
|
+
type: "repeater",
|
|
1100
|
+
action_id: "plans",
|
|
1101
|
+
label: "Plans",
|
|
1102
|
+
item_label: "plan",
|
|
1103
|
+
min_items: 1,
|
|
1104
|
+
fields: [
|
|
1105
|
+
{
|
|
1106
|
+
type: "text_input",
|
|
1107
|
+
action_id: "name",
|
|
1108
|
+
label: "Plan name"
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
type: "text_input",
|
|
1112
|
+
action_id: "price",
|
|
1113
|
+
label: "Price (e.g. $49)"
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
type: "text_input",
|
|
1117
|
+
action_id: "period",
|
|
1118
|
+
label: "Period (e.g. per month)"
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
type: "text_input",
|
|
1122
|
+
action_id: "features",
|
|
1123
|
+
label: "Features (one per line)",
|
|
1124
|
+
multiline: true
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
type: "text_input",
|
|
1128
|
+
action_id: "ctaLabel",
|
|
1129
|
+
label: "Button text"
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
type: "text_input",
|
|
1133
|
+
action_id: "ctaHref",
|
|
1134
|
+
label: "Button URL"
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
type: "toggle",
|
|
1138
|
+
action_id: "featured",
|
|
1139
|
+
label: "Highlight this plan",
|
|
1140
|
+
initial_value: false
|
|
1141
|
+
}
|
|
1142
|
+
]
|
|
1143
|
+
}, levelField("3")]
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
type: "vitops.embed",
|
|
1147
|
+
label: "Embed (video or map)",
|
|
1148
|
+
placeholder: "A video or map that loads only after the visitor allows it",
|
|
1149
|
+
category: "Embeds",
|
|
1150
|
+
fields: [
|
|
1151
|
+
{
|
|
1152
|
+
type: "select",
|
|
1153
|
+
action_id: "provider",
|
|
1154
|
+
label: "Provider",
|
|
1155
|
+
initial_value: "youtube",
|
|
1156
|
+
options: [
|
|
1157
|
+
{
|
|
1158
|
+
label: "YouTube",
|
|
1159
|
+
value: "youtube"
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
label: "Vimeo",
|
|
1163
|
+
value: "vimeo"
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
label: "Map (Google or OpenStreetMap embed address)",
|
|
1167
|
+
value: "map"
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
label: "Other (https address)",
|
|
1171
|
+
value: "custom"
|
|
1172
|
+
}
|
|
1173
|
+
]
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
type: "text_input",
|
|
1177
|
+
action_id: "url",
|
|
1178
|
+
label: "Address"
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
type: "text_input",
|
|
1182
|
+
action_id: "title",
|
|
1183
|
+
label: "Title (required: it names the frame for screen readers)"
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
type: "select",
|
|
1187
|
+
action_id: "category",
|
|
1188
|
+
label: "Consent category it waits on",
|
|
1189
|
+
initial_value: "marketing",
|
|
99
1190
|
options: [
|
|
100
1191
|
{
|
|
101
|
-
label: "
|
|
102
|
-
value: ""
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
label: "Success",
|
|
106
|
-
value: "success"
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
label: "Danger",
|
|
110
|
-
value: "danger"
|
|
1192
|
+
label: "Marketing",
|
|
1193
|
+
value: "marketing"
|
|
111
1194
|
},
|
|
112
1195
|
{
|
|
113
|
-
label: "
|
|
114
|
-
value: "
|
|
1196
|
+
label: "Preferences",
|
|
1197
|
+
value: "preferences"
|
|
115
1198
|
},
|
|
116
1199
|
{
|
|
117
|
-
label: "
|
|
118
|
-
value: "
|
|
1200
|
+
label: "Analytics",
|
|
1201
|
+
value: "analytics"
|
|
119
1202
|
}
|
|
120
1203
|
]
|
|
121
1204
|
},
|
|
122
1205
|
{
|
|
123
|
-
type: "
|
|
124
|
-
action_id: "
|
|
125
|
-
label: "
|
|
126
|
-
initial_value:
|
|
1206
|
+
type: "text_input",
|
|
1207
|
+
action_id: "ratio",
|
|
1208
|
+
label: "Aspect ratio",
|
|
1209
|
+
initial_value: "16 / 9"
|
|
127
1210
|
}
|
|
128
1211
|
]
|
|
129
1212
|
},
|
|
130
1213
|
{
|
|
131
|
-
type: "vitops.
|
|
132
|
-
label: "
|
|
133
|
-
placeholder: "
|
|
1214
|
+
type: "vitops.form",
|
|
1215
|
+
label: "Contact form",
|
|
1216
|
+
placeholder: "An enquiry form that emails you (and your CRM) and works without JavaScript",
|
|
1217
|
+
category: "Forms",
|
|
134
1218
|
fields: [
|
|
135
1219
|
{
|
|
136
1220
|
type: "text_input",
|
|
137
|
-
action_id: "
|
|
138
|
-
label: "
|
|
1221
|
+
action_id: "heading",
|
|
1222
|
+
label: "Heading"
|
|
139
1223
|
},
|
|
1224
|
+
levelField("2"),
|
|
140
1225
|
{
|
|
141
1226
|
type: "text_input",
|
|
142
|
-
action_id: "
|
|
143
|
-
label: "
|
|
1227
|
+
action_id: "intro",
|
|
1228
|
+
label: "Text above the form",
|
|
1229
|
+
multiline: true
|
|
144
1230
|
},
|
|
145
1231
|
{
|
|
146
|
-
type: "
|
|
147
|
-
action_id: "
|
|
148
|
-
label: "
|
|
149
|
-
|
|
1232
|
+
type: "repeater",
|
|
1233
|
+
action_id: "fields",
|
|
1234
|
+
label: "Fields",
|
|
1235
|
+
item_label: "field",
|
|
1236
|
+
min_items: 1,
|
|
1237
|
+
fields: [
|
|
1238
|
+
{
|
|
1239
|
+
type: "text_input",
|
|
1240
|
+
action_id: "label",
|
|
1241
|
+
label: "Label (what the visitor sees)"
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
type: "text_input",
|
|
1245
|
+
action_id: "name",
|
|
1246
|
+
label: "Name in the message (optional: letters, digits, underscores)"
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
type: "select",
|
|
1250
|
+
action_id: "type",
|
|
1251
|
+
label: "Kind",
|
|
1252
|
+
initial_value: "text",
|
|
1253
|
+
options: [
|
|
1254
|
+
{
|
|
1255
|
+
label: "Short text",
|
|
1256
|
+
value: "text"
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
label: "Email address",
|
|
1260
|
+
value: "email"
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
label: "Phone number",
|
|
1264
|
+
value: "tel"
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
label: "Long text",
|
|
1268
|
+
value: "textarea"
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
label: "Choose one",
|
|
1272
|
+
value: "select"
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
label: "Tick box",
|
|
1276
|
+
value: "checkbox"
|
|
1277
|
+
}
|
|
1278
|
+
]
|
|
1279
|
+
},
|
|
1280
|
+
{
|
|
1281
|
+
type: "toggle",
|
|
1282
|
+
action_id: "required",
|
|
1283
|
+
label: "Required",
|
|
1284
|
+
initial_value: false
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
type: "text_input",
|
|
1288
|
+
action_id: "placeholder",
|
|
1289
|
+
label: "Example text"
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
type: "text_input",
|
|
1293
|
+
action_id: "help",
|
|
1294
|
+
label: "Help text under the field"
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
type: "text_input",
|
|
1298
|
+
action_id: "options",
|
|
1299
|
+
label: "Choices (one per line; for \"Choose one\" only)",
|
|
1300
|
+
multiline: true
|
|
1301
|
+
}
|
|
1302
|
+
]
|
|
150
1303
|
},
|
|
151
1304
|
{
|
|
152
1305
|
type: "text_input",
|
|
153
|
-
action_id: "
|
|
154
|
-
label: "
|
|
1306
|
+
action_id: "submitLabel",
|
|
1307
|
+
label: "Button text",
|
|
1308
|
+
initial_value: "Send"
|
|
155
1309
|
},
|
|
156
1310
|
{
|
|
157
1311
|
type: "text_input",
|
|
158
|
-
action_id: "
|
|
159
|
-
label: "
|
|
1312
|
+
action_id: "successMessage",
|
|
1313
|
+
label: "Message after sending",
|
|
1314
|
+
initial_value: "Thanks. Your message has been sent."
|
|
160
1315
|
},
|
|
161
1316
|
{
|
|
162
1317
|
type: "text_input",
|
|
163
|
-
action_id: "
|
|
164
|
-
label: "
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
]
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
type: "vitops.copyButton",
|
|
171
|
-
label: "Copy snippet",
|
|
172
|
-
icon: "code",
|
|
173
|
-
placeholder: "Text with a copy-to-clipboard button",
|
|
174
|
-
fields: [
|
|
1318
|
+
action_id: "errorMessage",
|
|
1319
|
+
label: "Message if sending fails"
|
|
1320
|
+
},
|
|
175
1321
|
{
|
|
176
1322
|
type: "text_input",
|
|
177
|
-
action_id: "
|
|
178
|
-
label: "
|
|
179
|
-
multiline: true
|
|
1323
|
+
action_id: "noJsMessage",
|
|
1324
|
+
label: "Message when JavaScript is off (shown only if the site has a spam check)"
|
|
180
1325
|
},
|
|
181
1326
|
{
|
|
182
1327
|
type: "text_input",
|
|
183
|
-
action_id: "
|
|
184
|
-
label: "
|
|
185
|
-
initial_value: "Copy"
|
|
1328
|
+
action_id: "action",
|
|
1329
|
+
label: "Where the form posts (leave empty for /api/track)"
|
|
186
1330
|
},
|
|
187
|
-
|
|
188
|
-
type: "select",
|
|
189
|
-
action_id: "display",
|
|
190
|
-
label: "Display",
|
|
191
|
-
initial_value: "code-block",
|
|
192
|
-
options: [{
|
|
193
|
-
label: "Code block",
|
|
194
|
-
value: "code-block"
|
|
195
|
-
}, {
|
|
196
|
-
label: "Inline",
|
|
197
|
-
value: "inline"
|
|
198
|
-
}]
|
|
199
|
-
}
|
|
1331
|
+
...toneFields()
|
|
200
1332
|
]
|
|
201
1333
|
},
|
|
202
1334
|
{
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1335
|
+
/**
|
|
1336
|
+
* Google reviews from the committed `.vitops/reviews.json` cache (`vitops reviews`).
|
|
1337
|
+
*
|
|
1338
|
+
* The data is a BUILD-TIME import (`virtual:getvitops/reviews`, from the `vitops()` Astro
|
|
1339
|
+
* integration), never a filesystem read: the renderer runs in a Worker. So a new review
|
|
1340
|
+
* appears after the next `vitops reviews` run is committed and the site rebuilt, not when
|
|
1341
|
+
* an editor saves. Which reviews show is `selectReviews()` from `@getvitops/utils`, the
|
|
1342
|
+
* one place that is decided — these fields are exactly its `ReviewLocationConfig`.
|
|
1343
|
+
*/
|
|
1344
|
+
type: "vitops.reviews",
|
|
1345
|
+
label: "Google reviews",
|
|
1346
|
+
icon: "star",
|
|
1347
|
+
placeholder: "Customer reviews from the Google Business Profile cache",
|
|
1348
|
+
category: "Sections",
|
|
206
1349
|
fields: [
|
|
207
1350
|
{
|
|
208
1351
|
type: "text_input",
|
|
209
|
-
action_id: "
|
|
210
|
-
label: "
|
|
211
|
-
|
|
1352
|
+
action_id: "label",
|
|
1353
|
+
label: "Name for the group (read by screen readers)",
|
|
1354
|
+
initial_value: "Customer reviews"
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
type: "text_input",
|
|
1358
|
+
action_id: "location",
|
|
1359
|
+
label: "Location key (organization.locations key; blank = every location in the cache)"
|
|
212
1360
|
},
|
|
213
1361
|
{
|
|
214
1362
|
type: "select",
|
|
215
|
-
action_id: "
|
|
216
|
-
label: "
|
|
217
|
-
initial_value: "
|
|
1363
|
+
action_id: "layout",
|
|
1364
|
+
label: "Layout",
|
|
1365
|
+
initial_value: "grid",
|
|
218
1366
|
options: [
|
|
219
1367
|
{
|
|
220
|
-
label: "
|
|
221
|
-
value: "
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
label: "Success",
|
|
225
|
-
value: "success"
|
|
1368
|
+
label: "Grid",
|
|
1369
|
+
value: "grid"
|
|
226
1370
|
},
|
|
227
1371
|
{
|
|
228
|
-
label: "
|
|
229
|
-
value: "
|
|
1372
|
+
label: "Carousel",
|
|
1373
|
+
value: "carousel"
|
|
230
1374
|
},
|
|
231
1375
|
{
|
|
232
|
-
label: "
|
|
233
|
-
value: "
|
|
1376
|
+
label: "Marquee (scrolling row)",
|
|
1377
|
+
value: "marquee"
|
|
234
1378
|
}
|
|
235
1379
|
]
|
|
236
1380
|
},
|
|
237
1381
|
{
|
|
238
|
-
type: "
|
|
239
|
-
action_id: "
|
|
240
|
-
label: "
|
|
241
|
-
initial_value:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
1382
|
+
type: "select",
|
|
1383
|
+
action_id: "columns",
|
|
1384
|
+
label: "Columns (grid only)",
|
|
1385
|
+
initial_value: "3",
|
|
1386
|
+
options: [
|
|
1387
|
+
"1",
|
|
1388
|
+
"2",
|
|
1389
|
+
"3",
|
|
1390
|
+
"4"
|
|
1391
|
+
].map((n) => ({
|
|
1392
|
+
label: n,
|
|
1393
|
+
value: n
|
|
1394
|
+
}))
|
|
1395
|
+
},
|
|
250
1396
|
{
|
|
251
|
-
type: "
|
|
252
|
-
action_id: "
|
|
253
|
-
label: "
|
|
1397
|
+
type: "number_input",
|
|
1398
|
+
action_id: "limit",
|
|
1399
|
+
label: "Most reviews to show (0 = all)",
|
|
1400
|
+
initial_value: 6
|
|
1401
|
+
},
|
|
1402
|
+
{
|
|
1403
|
+
type: "select",
|
|
1404
|
+
action_id: "minRating",
|
|
1405
|
+
label: "Lowest rating to show",
|
|
1406
|
+
initial_value: "4",
|
|
1407
|
+
options: [
|
|
1408
|
+
"1",
|
|
1409
|
+
"2",
|
|
1410
|
+
"3",
|
|
1411
|
+
"4",
|
|
1412
|
+
"5"
|
|
1413
|
+
].map((n) => ({
|
|
1414
|
+
label: n === "1" ? "Any" : `${n} stars and up`,
|
|
1415
|
+
value: n
|
|
1416
|
+
}))
|
|
1417
|
+
},
|
|
1418
|
+
{
|
|
1419
|
+
type: "select",
|
|
1420
|
+
action_id: "sort",
|
|
1421
|
+
label: "Order",
|
|
1422
|
+
initial_value: "recent",
|
|
1423
|
+
options: [{
|
|
1424
|
+
label: "Newest first",
|
|
1425
|
+
value: "recent"
|
|
1426
|
+
}, {
|
|
1427
|
+
label: "Highest rated first",
|
|
1428
|
+
value: "rating"
|
|
1429
|
+
}]
|
|
254
1430
|
},
|
|
255
1431
|
{
|
|
256
1432
|
type: "text_input",
|
|
257
|
-
action_id: "
|
|
258
|
-
label: "
|
|
259
|
-
multiline: true
|
|
1433
|
+
action_id: "featured",
|
|
1434
|
+
label: "Review ids to show first (comma-separated; they ignore the rating filter)"
|
|
260
1435
|
},
|
|
261
1436
|
{
|
|
262
1437
|
type: "toggle",
|
|
263
|
-
action_id: "
|
|
264
|
-
label: "
|
|
1438
|
+
action_id: "avatars",
|
|
1439
|
+
label: "Show reviewers' Google photos (loads images from Google for every visitor)",
|
|
265
1440
|
initial_value: false
|
|
266
|
-
}
|
|
1441
|
+
},
|
|
1442
|
+
...toneFields()
|
|
267
1443
|
]
|
|
268
1444
|
},
|
|
269
1445
|
{
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
1446
|
+
/**
|
|
1447
|
+
* A named EmDash menu inside page content: a site map, a section's link list, a footer-style
|
|
1448
|
+
* set of columns. NOT the header — that is `<SiteNav>` in the layout, which owns the drawer,
|
|
1449
|
+
* dropdowns and breakpoints. `variant: sitenav` renders the same markup and pattern for the
|
|
1450
|
+
* rare page that wants a real navbar in the body.
|
|
1451
|
+
*
|
|
1452
|
+
* The menu is chosen from `menus/list` (the site's own menus) with the conventional names as
|
|
1453
|
+
* the static fallback, because a select needs a non-empty `options` list.
|
|
1454
|
+
*/
|
|
1455
|
+
type: "vitops.nav",
|
|
1456
|
+
label: "Menu",
|
|
1457
|
+
icon: "list",
|
|
1458
|
+
placeholder: "An EmDash menu as a list of links",
|
|
1459
|
+
category: "Navigation",
|
|
273
1460
|
fields: [
|
|
274
1461
|
{
|
|
275
|
-
type: "
|
|
276
|
-
action_id: "
|
|
277
|
-
label: "
|
|
278
|
-
|
|
1462
|
+
type: "select",
|
|
1463
|
+
action_id: "menu",
|
|
1464
|
+
label: "Menu",
|
|
1465
|
+
initial_value: "footer-sitemap",
|
|
1466
|
+
options: [
|
|
1467
|
+
{
|
|
1468
|
+
label: "Footer site map",
|
|
1469
|
+
value: "footer-sitemap"
|
|
1470
|
+
},
|
|
1471
|
+
{
|
|
1472
|
+
label: "Footer legal",
|
|
1473
|
+
value: "footer-legal"
|
|
1474
|
+
},
|
|
1475
|
+
{
|
|
1476
|
+
label: "Primary",
|
|
1477
|
+
value: "primary"
|
|
1478
|
+
}
|
|
1479
|
+
],
|
|
1480
|
+
optionsRoute: MENUS_ROUTE
|
|
279
1481
|
},
|
|
280
1482
|
{
|
|
281
1483
|
type: "text_input",
|
|
282
|
-
action_id: "
|
|
283
|
-
label: "
|
|
284
|
-
multiline: true
|
|
1484
|
+
action_id: "label",
|
|
1485
|
+
label: "Name for the navigation (read by screen readers; must differ from other menus)"
|
|
285
1486
|
},
|
|
286
1487
|
{
|
|
287
1488
|
type: "text_input",
|
|
288
|
-
action_id: "
|
|
289
|
-
label: "
|
|
290
|
-
initial_value: "Carousel"
|
|
1489
|
+
action_id: "heading",
|
|
1490
|
+
label: "Visible heading (optional)"
|
|
291
1491
|
},
|
|
1492
|
+
levelField("2"),
|
|
292
1493
|
{
|
|
293
|
-
type: "
|
|
294
|
-
action_id: "
|
|
295
|
-
label: "
|
|
296
|
-
initial_value:
|
|
1494
|
+
type: "select",
|
|
1495
|
+
action_id: "variant",
|
|
1496
|
+
label: "Layout",
|
|
1497
|
+
initial_value: "list",
|
|
1498
|
+
options: [
|
|
1499
|
+
{
|
|
1500
|
+
label: "Vertical list",
|
|
1501
|
+
value: "list"
|
|
1502
|
+
},
|
|
1503
|
+
{
|
|
1504
|
+
label: "Inline row",
|
|
1505
|
+
value: "inline"
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
label: "Columns (each top-level item with its sub-links)",
|
|
1509
|
+
value: "columns"
|
|
1510
|
+
},
|
|
1511
|
+
{
|
|
1512
|
+
label: "Navbar (the header pattern, in the page)",
|
|
1513
|
+
value: "sitenav"
|
|
1514
|
+
}
|
|
1515
|
+
]
|
|
297
1516
|
},
|
|
298
1517
|
{
|
|
299
|
-
type: "
|
|
300
|
-
action_id: "
|
|
301
|
-
label: "
|
|
302
|
-
initial_value:
|
|
1518
|
+
type: "select",
|
|
1519
|
+
action_id: "depth",
|
|
1520
|
+
label: "Levels to show",
|
|
1521
|
+
initial_value: "1",
|
|
1522
|
+
options: [
|
|
1523
|
+
"1",
|
|
1524
|
+
"2",
|
|
1525
|
+
"3"
|
|
1526
|
+
].map((n) => ({
|
|
1527
|
+
label: n,
|
|
1528
|
+
value: n
|
|
1529
|
+
}))
|
|
303
1530
|
},
|
|
304
|
-
|
|
305
|
-
type: "text_input",
|
|
306
|
-
action_id: "aspect",
|
|
307
|
-
label: "Slide aspect ratio",
|
|
308
|
-
initial_value: "16 / 9"
|
|
309
|
-
}
|
|
1531
|
+
...toneFields()
|
|
310
1532
|
]
|
|
311
1533
|
}
|
|
312
1534
|
];
|
|
1535
|
+
/**
|
|
1536
|
+
* Resolve {@link BlockSelection} against {@link BLOCKS}.
|
|
1537
|
+
*
|
|
1538
|
+
* Pure so a misconfiguration fails at plugin construction, with the offending name in the
|
|
1539
|
+
* message, rather than as an editor menu that is quietly missing something.
|
|
1540
|
+
*/
|
|
1541
|
+
function selectBlocks(selection = {}) {
|
|
1542
|
+
const known = new Set(BLOCKS.map((b) => b.type));
|
|
1543
|
+
const check = (names, option) => {
|
|
1544
|
+
for (const name of names ?? []) if (!known.has(name)) throw new Error(`[vitops] blocks.${option}: unknown block '${name}'. Known: ${[...known].join(", ")}.`);
|
|
1545
|
+
};
|
|
1546
|
+
check(selection.include, "include");
|
|
1547
|
+
check(selection.exclude, "exclude");
|
|
1548
|
+
const include = selection.include ? new Set(selection.include) : null;
|
|
1549
|
+
const exclude = new Set(selection.exclude);
|
|
1550
|
+
const base = BLOCKS.filter((b) => (!include || include.has(b.type)) && !exclude.has(b.type));
|
|
1551
|
+
const extra = selection.extra ?? [];
|
|
1552
|
+
const taken = new Set(BLOCKS.map((b) => b.type));
|
|
1553
|
+
for (const block of extra) {
|
|
1554
|
+
if (taken.has(block.type)) throw new Error(`[vitops] blocks.extra: type '${block.type}' is already defined.`);
|
|
1555
|
+
taken.add(block.type);
|
|
1556
|
+
}
|
|
1557
|
+
return [...base, ...extra];
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// src/menus-route.ts
|
|
1561
|
+
/** The items the select shows: one per menu, valued by its `name` (what `getMenu()` takes). */
|
|
1562
|
+
function menuItems(menus) {
|
|
1563
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1564
|
+
const out = [];
|
|
1565
|
+
for (const { name, label } of menus) {
|
|
1566
|
+
if (!name || seen.has(name)) continue;
|
|
1567
|
+
seen.add(name);
|
|
1568
|
+
out.push({
|
|
1569
|
+
id: name,
|
|
1570
|
+
name: label && label !== name ? `${label} (${name})` : name
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
return out;
|
|
1574
|
+
}
|
|
1575
|
+
function createMenusRoute(list) {
|
|
1576
|
+
return {
|
|
1577
|
+
permission: "content:read",
|
|
1578
|
+
handler: async () => {
|
|
1579
|
+
try {
|
|
1580
|
+
return { items: menuItems(await list()) };
|
|
1581
|
+
} catch {
|
|
1582
|
+
return { items: [] };
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// src/tones-route.ts
|
|
1589
|
+
/**
|
|
1590
|
+
* The `tones/list` plugin route: what fills every block's `tone` select with the site's own
|
|
1591
|
+
* colour roles (UPSTREAM 21 — "role names the admin never offered").
|
|
1592
|
+
*
|
|
1593
|
+
* EmDash's Block Kit `select` takes an `optionsRoute` for exactly this, and falls back to the
|
|
1594
|
+
* field's static options if the route fails or answers with no `items`, so a site that
|
|
1595
|
+
* configures nothing still gets the semantic names from `STATIC_TONE_OPTIONS`.
|
|
1596
|
+
*
|
|
1597
|
+
* **Where the roles come from.** Plugin code has no site config at runtime, so the site hands
|
|
1598
|
+
* it over: `tones: [...]` verbatim, or — if the design-system editor is already wired with the
|
|
1599
|
+
* site's `Config` — the `colors.roles` keys of that config, so there is nothing to keep in
|
|
1600
|
+
* step. A role is listed under its own name (`brand-primary`), because that is what
|
|
1601
|
+
* `resolveTone()` and the token names (`--color-bg-brand-primary-muted`) both use.
|
|
1602
|
+
*/
|
|
1603
|
+
/** Role names of a design system or full config, or `[]` if it cannot be resolved. */
|
|
1604
|
+
function rolesOf(config) {
|
|
1605
|
+
if (!config || typeof config !== "object") return [];
|
|
1606
|
+
try {
|
|
1607
|
+
const { designSystem } = resolveInput(config, { siteEnv: "production" });
|
|
1608
|
+
return Object.keys(designSystem.colors?.roles ?? {});
|
|
1609
|
+
} catch {
|
|
1610
|
+
return [];
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
/** The items the select shows: the site's roles first, then any semantic name it lacks. */
|
|
1614
|
+
function toneItems(roles) {
|
|
1615
|
+
return [{
|
|
1616
|
+
id: "",
|
|
1617
|
+
name: "None"
|
|
1618
|
+
}, ...[.../* @__PURE__ */ new Set([...roles, ...SEMANTIC_TONES])].map((id) => ({
|
|
1619
|
+
id,
|
|
1620
|
+
name: id.replace(/-/g, " ")
|
|
1621
|
+
}))];
|
|
1622
|
+
}
|
|
1623
|
+
function createTonesRoute(roles) {
|
|
1624
|
+
return {
|
|
1625
|
+
permission: "content:read",
|
|
1626
|
+
handler: async () => ({ items: toneItems(roles()) })
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
313
1629
|
|
|
314
|
-
|
|
315
|
-
//#region src/design-system-route.ts
|
|
1630
|
+
// src/design-system-route.ts
|
|
316
1631
|
/**
|
|
317
1632
|
* The `vitops.design-system` plugin route — what `<wc-theme-editor>` probes
|
|
318
1633
|
* and POSTs its patch to when mounted inside the EmDash admin (see this
|
|
@@ -453,8 +1768,164 @@ async function designSystemOverrideFragment(ctx, opts) {
|
|
|
453
1768
|
};
|
|
454
1769
|
}
|
|
455
1770
|
|
|
456
|
-
|
|
457
|
-
|
|
1771
|
+
// src/block-types.ts
|
|
1772
|
+
/** Block-type slugs may not contain a dot, so `vitops.section` is stored as `vitops_section`. */
|
|
1773
|
+
const SECTION_SLUG = "vitops_section";
|
|
1774
|
+
const select = (slug, label, options, defaultValue) => ({
|
|
1775
|
+
slug,
|
|
1776
|
+
label,
|
|
1777
|
+
type: "select",
|
|
1778
|
+
defaultValue,
|
|
1779
|
+
validation: { options }
|
|
1780
|
+
});
|
|
1781
|
+
const SECTION_FIELDS = [
|
|
1782
|
+
{
|
|
1783
|
+
slug: "kicker",
|
|
1784
|
+
label: "Kicker (small line above the heading)",
|
|
1785
|
+
type: "string"
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
slug: "heading",
|
|
1789
|
+
label: "Heading",
|
|
1790
|
+
type: "string"
|
|
1791
|
+
},
|
|
1792
|
+
select("level", "Heading level", [
|
|
1793
|
+
"2",
|
|
1794
|
+
"3",
|
|
1795
|
+
"4",
|
|
1796
|
+
"5",
|
|
1797
|
+
"6"
|
|
1798
|
+
], "2"),
|
|
1799
|
+
{
|
|
1800
|
+
slug: "sub",
|
|
1801
|
+
label: "Lead-in text under the heading",
|
|
1802
|
+
type: "text"
|
|
1803
|
+
},
|
|
1804
|
+
{
|
|
1805
|
+
slug: "body",
|
|
1806
|
+
label: "Content (any blocks)",
|
|
1807
|
+
type: "portableText"
|
|
1808
|
+
},
|
|
1809
|
+
select("layout", "Layout", [
|
|
1810
|
+
"stack",
|
|
1811
|
+
"center",
|
|
1812
|
+
"split"
|
|
1813
|
+
], "stack"),
|
|
1814
|
+
select("width", "Width", [
|
|
1815
|
+
"measure",
|
|
1816
|
+
"breakout",
|
|
1817
|
+
"spotlight",
|
|
1818
|
+
"fullbleed"
|
|
1819
|
+
], "measure"),
|
|
1820
|
+
{
|
|
1821
|
+
slug: "tone",
|
|
1822
|
+
label: "Backdrop tone (a colour role from the site, e.g. surface or brand-primary)",
|
|
1823
|
+
type: "string"
|
|
1824
|
+
},
|
|
1825
|
+
{
|
|
1826
|
+
slug: "color",
|
|
1827
|
+
label: "Palette colour instead of a tone (e.g. amber or amber-300)",
|
|
1828
|
+
type: "string"
|
|
1829
|
+
},
|
|
1830
|
+
{
|
|
1831
|
+
slug: "solid",
|
|
1832
|
+
label: "Fill with the solid colour",
|
|
1833
|
+
type: "boolean",
|
|
1834
|
+
defaultValue: false
|
|
1835
|
+
}
|
|
1836
|
+
];
|
|
1837
|
+
/** `vitops.section`: kicker, heading, lead-in, tone/backdrop, optional heading-left layout, and a body of blocks. */
|
|
1838
|
+
const SECTION_BLOCK_TYPE = {
|
|
1839
|
+
slug: SECTION_SLUG,
|
|
1840
|
+
label: "Section",
|
|
1841
|
+
description: "A titled page section whose body can hold any other blocks.",
|
|
1842
|
+
category: "Sections",
|
|
1843
|
+
icon: "layout",
|
|
1844
|
+
currentVersion: 1,
|
|
1845
|
+
versions: [{
|
|
1846
|
+
version: 1,
|
|
1847
|
+
fields: SECTION_FIELDS
|
|
1848
|
+
}]
|
|
1849
|
+
};
|
|
1850
|
+
/**
|
|
1851
|
+
* Every block type this plugin ships, in the shape `seed.json`'s `blockTypes` takes. Spread
|
|
1852
|
+
* it into the seed (and allow the slugs on the collection's `blocks` field), e.g.
|
|
1853
|
+
* `blockTypes: [...vitopsBlockTypes()]`.
|
|
1854
|
+
*/
|
|
1855
|
+
function vitopsBlockTypes() {
|
|
1856
|
+
return [SECTION_BLOCK_TYPE];
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// src/blocks-doc.ts
|
|
1860
|
+
const cell = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
1861
|
+
function describe(f) {
|
|
1862
|
+
const bits = [];
|
|
1863
|
+
if ("options" in f && Array.isArray(f.options) && f.options.length) {
|
|
1864
|
+
const values = f.options.map((o) => String(o.value));
|
|
1865
|
+
bits.push(values.length > 8 ? `one of ${values.length} values (e.g. ${values.slice(0, 4).join(", ")})` : `one of ${values.join(", ")}`);
|
|
1866
|
+
}
|
|
1867
|
+
if ("initial_value" in f && f.initial_value !== void 0) bits.push(`default \`${String(f.initial_value)}\``);
|
|
1868
|
+
if (f.type === "repeater" && "min_items" in f && f.min_items) bits.push(`min ${f.min_items} rows`);
|
|
1869
|
+
return bits.join("; ");
|
|
1870
|
+
}
|
|
1871
|
+
function fieldRows(fields, prefix = "") {
|
|
1872
|
+
return fields.flatMap((f) => {
|
|
1873
|
+
const own = `| \`${prefix}${f.action_id}\` | ${f.type} | ${cell(f.label)} | ${cell(describe(f))} |`;
|
|
1874
|
+
return f.type === "repeater" ? [own, ...fieldRows(f.fields, `${f.action_id}[].`)] : [own];
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* The block reference an agent reads via `vitops docs blocks`: every Portable Text `_type`,
|
|
1879
|
+
* its fields and what each saves. Rendered from the same `BLOCKS` the editor menu uses, so
|
|
1880
|
+
* it cannot describe a field the editor doesn't offer.
|
|
1881
|
+
*/
|
|
1882
|
+
function renderBlocksDoc(blocks = BLOCKS) {
|
|
1883
|
+
const out = [
|
|
1884
|
+
"---",
|
|
1885
|
+
"type: concept",
|
|
1886
|
+
"title: EmDash blocks",
|
|
1887
|
+
"description: Portable Text block types from @getvitops/emdash, with every field",
|
|
1888
|
+
"---",
|
|
1889
|
+
"",
|
|
1890
|
+
"# EmDash blocks",
|
|
1891
|
+
"",
|
|
1892
|
+
"Each block is a Portable Text object: `{ \"_type\": \"vitops.cards\", \"_key\": \"<unique>\", ...fields }`.",
|
|
1893
|
+
"Portable Text blocks are flat — they cannot contain other blocks, and a repeater row holds scalar fields only.",
|
|
1894
|
+
"To put blocks inside something, use the section block TYPE (end of this page), whose body is Portable Text.",
|
|
1895
|
+
"Give every block a unique `_key`; gallery and carousel derive their element id from it.",
|
|
1896
|
+
"",
|
|
1897
|
+
"## Colour: `tone` and `color`",
|
|
1898
|
+
"",
|
|
1899
|
+
"Every block that colours something takes the same two fields. `tone` is a colour role from the",
|
|
1900
|
+
"site's `colors.roles` (or a semantic name); an unknown role falls back through its semantic",
|
|
1901
|
+
"relatives to `neutral`. `color` is an escape hatch naming a palette hue (`amber`) or hue and step",
|
|
1902
|
+
"(`amber-300`) and wins over `tone`; its contrast is not checked. Renderers emit `--tone-*` custom",
|
|
1903
|
+
"properties, never a class built from the value. Multiline fields that accept links take",
|
|
1904
|
+
"`[text](/url)`; hero headings accent words wrapped in `*stars*`.",
|
|
1905
|
+
""
|
|
1906
|
+
];
|
|
1907
|
+
for (const b of blocks) {
|
|
1908
|
+
out.push(`## \`${b.type}\` — ${b.label}`, "");
|
|
1909
|
+
if (b.placeholder) out.push(`${b.placeholder}.`, "");
|
|
1910
|
+
if (b.fields?.length) out.push("| Field | Type | Label | Notes |", "| --- | --- | --- | --- |", ...fieldRows(b.fields), "");
|
|
1911
|
+
}
|
|
1912
|
+
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`.", "");
|
|
1913
|
+
for (const t of vitopsBlockTypes()) {
|
|
1914
|
+
out.push(`### \`${t.slug}\` — ${t.label}`, "", `${t.description ?? ""}`, "");
|
|
1915
|
+
const fields = t.versions.find((v) => v.version === t.currentVersion)?.fields ?? [];
|
|
1916
|
+
out.push("| Field | Type | Label | Notes |", "| --- | --- | --- | --- |", ...fields.map((f) => {
|
|
1917
|
+
const notes = [
|
|
1918
|
+
f.validation?.options ? `one of ${f.validation.options.join(", ")}` : "",
|
|
1919
|
+
f.defaultValue !== void 0 ? `default \`${String(f.defaultValue)}\`` : "",
|
|
1920
|
+
f.type === "portableText" ? "accepts every `vitops.*` block above" : ""
|
|
1921
|
+
].filter(Boolean);
|
|
1922
|
+
return `| \`${f.slug}\` | ${f.type} | ${cell(f.label)} | ${cell(notes.join("; "))} |`;
|
|
1923
|
+
}), "");
|
|
1924
|
+
}
|
|
1925
|
+
return out.join("\n");
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// src/hosting.ts
|
|
458
1929
|
const TARGETS = ["cloudflare", "node"];
|
|
459
1930
|
/**
|
|
460
1931
|
* Import an optional hosting dependency. The specifier is passed as a
|
|
@@ -483,7 +1954,8 @@ async function vitopsHosting(options = {}) {
|
|
|
483
1954
|
adapter: cloudflare(),
|
|
484
1955
|
database: d1({
|
|
485
1956
|
binding: cf.dbBinding ?? "DB",
|
|
486
|
-
session: cf.session ?? "auto"
|
|
1957
|
+
session: cf.session ?? "auto",
|
|
1958
|
+
coalesce: cf.coalesce ?? true
|
|
487
1959
|
}),
|
|
488
1960
|
storage: r2({ binding: cf.mediaBinding ?? "MEDIA" })
|
|
489
1961
|
};
|
|
@@ -513,12 +1985,10 @@ async function vitopsHosting(options = {}) {
|
|
|
513
1985
|
throw new Error(`[vitops] unknown hosting target '${target}' (from ${process.env.HOSTING ? "HOSTING env var" : "options.target"}). Valid targets: ${TARGETS.join(", ")}.`);
|
|
514
1986
|
}
|
|
515
1987
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
var version = "1.0.0-alpha.0";
|
|
1988
|
+
// package.json
|
|
1989
|
+
var version = "2.0.0-alpha.0";
|
|
519
1990
|
|
|
520
|
-
|
|
521
|
-
//#region src/index.ts
|
|
1991
|
+
// src/index.ts
|
|
522
1992
|
/**
|
|
523
1993
|
* @getvitops/emdash — EmDash CMS native plugin for the Vitops design system.
|
|
524
1994
|
*
|
|
@@ -578,6 +2048,7 @@ function createPlugin(options = {}) {
|
|
|
578
2048
|
const wcBase = options.wcBase ?? "/vitops";
|
|
579
2049
|
const editor = options.designSystemEditor || void 0;
|
|
580
2050
|
const fragmentsEnabled = scripts === "fragments" || !!editor;
|
|
2051
|
+
const roles = options.tones ?? rolesOf(editor?.config);
|
|
581
2052
|
const pageFragments = async (_event, ctx) => {
|
|
582
2053
|
const contributions = [];
|
|
583
2054
|
if (scripts === "fragments") contributions.push({
|
|
@@ -605,7 +2076,7 @@ function createPlugin(options = {}) {
|
|
|
605
2076
|
...editor ? { allowedHosts: ["api.github.com"] } : {},
|
|
606
2077
|
...fragmentsEnabled ? { hooks: { "page:fragments": pageFragments } } : {},
|
|
607
2078
|
admin: {
|
|
608
|
-
portableTextBlocks:
|
|
2079
|
+
portableTextBlocks: selectBlocks(options.blocks),
|
|
609
2080
|
...editor ? {
|
|
610
2081
|
entry: "@getvitops/emdash/admin",
|
|
611
2082
|
pages: [{
|
|
@@ -615,10 +2086,13 @@ function createPlugin(options = {}) {
|
|
|
615
2086
|
}]
|
|
616
2087
|
} : {}
|
|
617
2088
|
},
|
|
618
|
-
|
|
2089
|
+
routes: {
|
|
2090
|
+
[TONES_ROUTE]: createTonesRoute(() => roles),
|
|
2091
|
+
[MENUS_ROUTE]: createMenusRoute(() => getMenus()),
|
|
2092
|
+
...editor ? { "design-system": createDesignSystemRoute(editor) } : {}
|
|
2093
|
+
}
|
|
619
2094
|
});
|
|
620
2095
|
}
|
|
621
2096
|
|
|
622
|
-
|
|
623
|
-
export { BLOCKS, createPlugin, createPlugin as default, vitopsEmdash, vitopsHosting };
|
|
2097
|
+
export { BLOCKS, SECTION_BLOCK_TYPE, SECTION_SLUG, createPlugin, createPlugin as default, renderBlocksDoc, resolveTone, selectBlocks, toneChain, vitopsBlockTypes, vitopsEmdash, vitopsHosting };
|
|
624
2098
|
//# sourceMappingURL=index.mjs.map
|