@getvitops/emdash 2.0.0-alpha.1 → 2.0.0-alpha.2
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 +45 -0
- package/README.md +18 -16
- package/dist/index.d.mts +26 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +467 -167
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/astro/WidgetArea.astro +31 -9
- package/src/astro/blocks/Badges.astro +1 -1
- package/src/astro/blocks/Callout.astro +1 -1
- package/src/astro/blocks/Cards.astro +4 -3
- package/src/astro/blocks/Form.astro +18 -0
- package/src/astro/blocks/Hero.astro +25 -10
- package/src/astro/blocks/Logos.astro +8 -5
- package/src/astro/blocks/NavTree.astro +1 -1
- package/src/astro/blocks/Reviews.astro +1 -1
- package/src/astro/blocks/Section.astro +15 -9
- package/src/astro/blocks/Stats.astro +3 -2
- package/src/astro/blocks/Steps.astro +3 -3
- package/src/astro/blocks/WidgetArea.astro +1 -1
- package/src/astro/blocks/attrs.test.ts +19 -1
- package/src/astro/blocks/attrs.ts +14 -2
- package/src/astro/blocks/cycle.container.test.ts +73 -0
- package/src/astro/blocks/form.sample.ts +8 -2
- package/src/astro/blocks/form.ts +16 -5
- package/src/astro/blocks/nav.ts +1 -1
- package/src/astro/blocks/render.container.test.ts +91 -19
- package/src/astro/blocks/tone.test.ts +10 -0
- package/src/astro/blocks/tone.ts +48 -6
- package/src/astro/blocks/unrendered.ts +23 -0
- package/src/astro/blocks/value.test.ts +27 -1
- package/src/astro/blocks/value.ts +16 -0
- package/src/astro/index.ts +34 -24
package/dist/index.mjs
CHANGED
|
@@ -35,7 +35,10 @@ const SEMANTIC_ICON_OPTIONS = Object.keys(iconMap.fa7).sort().map((name) => ({
|
|
|
35
35
|
* fallbacks, then `neutral`.
|
|
36
36
|
* - **`color`** — the escape hatch: a palette hue (`amber`) or hue and step (`amber-300`).
|
|
37
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.
|
|
38
|
+
* contract covers roles, not arbitrary hue/step pairs — so it is for decoration. A solid fill
|
|
39
|
+
* reads from the site's `--color-<hue>-<step>-on` and the tint flips with the colour scheme
|
|
40
|
+
* (`light-dark()`); mid steps (300–500) cannot reach APCA Lc 75 with either text colour, so use
|
|
41
|
+
* a role where contrast matters.
|
|
39
42
|
*
|
|
40
43
|
* **Why CSS custom properties, not classes.** A class name built from a field value
|
|
41
44
|
* (`bg-${tone}`) is invisible to Tailwind's scanner, so it generates nothing in that
|
|
@@ -102,14 +105,16 @@ function resolveTone(tone, color) {
|
|
|
102
105
|
if (hue) {
|
|
103
106
|
const [, name, step] = hue;
|
|
104
107
|
const at = (s) => `var(--color-${name}-${s})`;
|
|
108
|
+
const fill = step ?? "600";
|
|
109
|
+
const flip = (light, dark) => `light-dark(${at(light)}, ${at(dark)})`;
|
|
105
110
|
return {
|
|
106
111
|
className: "tone",
|
|
107
112
|
style: [
|
|
108
|
-
`--tone-bg: ${
|
|
109
|
-
`--tone-fg: ${
|
|
110
|
-
`--tone-border: ${
|
|
111
|
-
`--tone-solid: ${at(
|
|
112
|
-
`--tone-on-solid:
|
|
113
|
+
`--tone-bg: ${flip("100", "900")}`,
|
|
114
|
+
`--tone-fg: ${flip("900", "100")}`,
|
|
115
|
+
`--tone-border: ${flip("300", "700")}`,
|
|
116
|
+
`--tone-solid: ${at(fill)}`,
|
|
117
|
+
`--tone-on-solid: ${at(`${fill}-on`)}`
|
|
113
118
|
].join("; ")
|
|
114
119
|
};
|
|
115
120
|
}
|
|
@@ -131,10 +136,138 @@ function resolveTone(tone, color) {
|
|
|
131
136
|
};
|
|
132
137
|
}
|
|
133
138
|
|
|
139
|
+
// src/block-types.ts
|
|
140
|
+
/** Block-type slugs may not contain a dot, so `vitops_section` is stored as `vitops_section`. */
|
|
141
|
+
const SECTION_SLUG = "vitops_section";
|
|
142
|
+
const select = (slug, label, options, defaultValue) => ({
|
|
143
|
+
slug,
|
|
144
|
+
label,
|
|
145
|
+
type: "select",
|
|
146
|
+
defaultValue,
|
|
147
|
+
validation: { options }
|
|
148
|
+
});
|
|
149
|
+
/**
|
|
150
|
+
* The `theme` select's values: `none`, a role, or `<role>:solid`. Block-type selects are
|
|
151
|
+
* static, so this lists the roles every site defines; any other site role still works through
|
|
152
|
+
* the `tone` field. `<role>:solid` is the filled backdrop with the contrast-computed text.
|
|
153
|
+
*/
|
|
154
|
+
const THEME_ROLES = [
|
|
155
|
+
"neutral",
|
|
156
|
+
"surface",
|
|
157
|
+
"brand-primary",
|
|
158
|
+
"brand-secondary",
|
|
159
|
+
"info",
|
|
160
|
+
"success",
|
|
161
|
+
"warning",
|
|
162
|
+
"danger"
|
|
163
|
+
];
|
|
164
|
+
const THEMES = [
|
|
165
|
+
"none",
|
|
166
|
+
...THEME_ROLES,
|
|
167
|
+
...THEME_ROLES.map((r) => `${r}:solid`)
|
|
168
|
+
];
|
|
169
|
+
const SECTION_FIELDS = [
|
|
170
|
+
{
|
|
171
|
+
slug: "kicker",
|
|
172
|
+
label: "Kicker (small line above the heading)",
|
|
173
|
+
type: "string"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
slug: "heading",
|
|
177
|
+
label: "Heading",
|
|
178
|
+
type: "string"
|
|
179
|
+
},
|
|
180
|
+
select("level", "Heading level (p: styled like a heading, not in the outline)", [
|
|
181
|
+
"1",
|
|
182
|
+
"2",
|
|
183
|
+
"3",
|
|
184
|
+
"4",
|
|
185
|
+
"5",
|
|
186
|
+
"6",
|
|
187
|
+
"p"
|
|
188
|
+
], "2"),
|
|
189
|
+
{
|
|
190
|
+
slug: "sub",
|
|
191
|
+
label: "Lead-in text under the heading",
|
|
192
|
+
type: "text"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
slug: "body",
|
|
196
|
+
label: "Content (any blocks)",
|
|
197
|
+
type: "portableText"
|
|
198
|
+
},
|
|
199
|
+
select("layout", "Layout", [
|
|
200
|
+
"stack",
|
|
201
|
+
"center",
|
|
202
|
+
"split"
|
|
203
|
+
], "stack"),
|
|
204
|
+
select("width", "Width", [
|
|
205
|
+
"measure",
|
|
206
|
+
"breakout",
|
|
207
|
+
"spotlight",
|
|
208
|
+
"fullbleed"
|
|
209
|
+
], "measure"),
|
|
210
|
+
{
|
|
211
|
+
slug: "tone",
|
|
212
|
+
label: "Backdrop tone (a colour role from the site, e.g. surface or brand-primary)",
|
|
213
|
+
type: "string"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
slug: "color",
|
|
217
|
+
label: "Palette colour instead of a tone (e.g. amber or amber-300)",
|
|
218
|
+
type: "string"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
slug: "solid",
|
|
222
|
+
label: "Fill with the solid colour",
|
|
223
|
+
type: "boolean",
|
|
224
|
+
defaultValue: false
|
|
225
|
+
},
|
|
226
|
+
select("theme", "Theme (backdrop and readable text in one pick; overrides the tone fields)", THEMES, "none"),
|
|
227
|
+
{
|
|
228
|
+
slug: "anchor",
|
|
229
|
+
label: "Anchor id (so a link can point here with #id)",
|
|
230
|
+
type: "string"
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
slug: "class_name",
|
|
234
|
+
label: "Extra CSS classes",
|
|
235
|
+
type: "string"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
slug: "style",
|
|
239
|
+
label: "CSS custom properties, e.g. --p-card: 2rem",
|
|
240
|
+
type: "string"
|
|
241
|
+
}
|
|
242
|
+
];
|
|
243
|
+
/** `vitops_section`: kicker, heading, lead-in, tone/backdrop, optional heading-left layout, and a body of blocks. */
|
|
244
|
+
const SECTION_BLOCK_TYPE = {
|
|
245
|
+
slug: SECTION_SLUG,
|
|
246
|
+
label: "Section",
|
|
247
|
+
description: "A titled page section whose body can hold any other blocks.",
|
|
248
|
+
category: "Sections",
|
|
249
|
+
icon: "layout",
|
|
250
|
+
currentVersion: 1,
|
|
251
|
+
versions: [{
|
|
252
|
+
version: 1,
|
|
253
|
+
fields: SECTION_FIELDS
|
|
254
|
+
}]
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* Every block type this plugin ships, in the shape `seed.json`'s `blockTypes` takes. Spread
|
|
258
|
+
* it into the seed (and allow the slugs on the collection's `blocks` field), e.g.
|
|
259
|
+
* `blockTypes: [...vitopsBlockTypes()]`.
|
|
260
|
+
*/
|
|
261
|
+
function vitopsBlockTypes() {
|
|
262
|
+
return [SECTION_BLOCK_TYPE];
|
|
263
|
+
}
|
|
264
|
+
|
|
134
265
|
// src/blocks.ts
|
|
135
266
|
/** Route that fills every tone select with the site's own roles — see `index.ts`. */
|
|
136
267
|
const TONES_ROUTE = "tones/list";
|
|
137
|
-
/**
|
|
268
|
+
/** Lists each colour role twice: tinted (`role`) and filled (`role:solid`), for every block's `theme`. */
|
|
269
|
+
const THEMES_ROUTE = "themes/list";
|
|
270
|
+
/** Route that lists the site's EmDash menus for `vitops_nav`'s `menu` select — see `menus-route.ts`. */
|
|
138
271
|
const MENUS_ROUTE = "menus/list";
|
|
139
272
|
/**
|
|
140
273
|
* The tone options an editor can always pick, before the site adds its own roles.
|
|
@@ -167,7 +300,7 @@ function toneFields() {
|
|
|
167
300
|
label: "Palette colour instead of a tone (e.g. amber or amber-300)"
|
|
168
301
|
}];
|
|
169
302
|
}
|
|
170
|
-
/** Route that lists the site's widget areas for `
|
|
303
|
+
/** Route that lists the site's widget areas for `vitops_widgetArea`'s `area` select. */
|
|
171
304
|
const WIDGET_AREAS_ROUTE = "widget-areas/list";
|
|
172
305
|
/**
|
|
173
306
|
* The escape hatches every block carries, appended last by `BLOCKS` (see `astro/blocks/attrs.ts`
|
|
@@ -175,6 +308,23 @@ const WIDGET_AREAS_ROUTE = "widget-areas/list";
|
|
|
175
308
|
*/
|
|
176
309
|
function commonFields() {
|
|
177
310
|
return [
|
|
311
|
+
{
|
|
312
|
+
type: "select",
|
|
313
|
+
action_id: "theme",
|
|
314
|
+
label: "Theme (a backdrop with readable text, from the site colours)",
|
|
315
|
+
initial_value: "",
|
|
316
|
+
options: [{
|
|
317
|
+
label: "None",
|
|
318
|
+
value: ""
|
|
319
|
+
}, ...THEME_ROLES.flatMap((r) => [{
|
|
320
|
+
label: r.replace(/-/g, " "),
|
|
321
|
+
value: r
|
|
322
|
+
}, {
|
|
323
|
+
label: `${r.replace(/-/g, " ")} (filled)`,
|
|
324
|
+
value: `${r}:solid`
|
|
325
|
+
}])],
|
|
326
|
+
optionsRoute: THEMES_ROUTE
|
|
327
|
+
},
|
|
178
328
|
{
|
|
179
329
|
type: "text_input",
|
|
180
330
|
action_id: "anchor",
|
|
@@ -209,6 +359,30 @@ const levelField = (initial) => ({
|
|
|
209
359
|
value: n
|
|
210
360
|
}))
|
|
211
361
|
});
|
|
362
|
+
/**
|
|
363
|
+
* Heading level for a block that can own the page title (hero): `h1`, or `p` for a heading that is
|
|
364
|
+
* visual only and should not appear in the outline.
|
|
365
|
+
*/
|
|
366
|
+
const titleLevelField = (initial) => ({
|
|
367
|
+
type: "select",
|
|
368
|
+
action_id: "level",
|
|
369
|
+
label: "Heading level (h1 is the page title; p is styled like a heading but not in the outline)",
|
|
370
|
+
initial_value: initial,
|
|
371
|
+
options: [...[
|
|
372
|
+
"1",
|
|
373
|
+
"2",
|
|
374
|
+
"3",
|
|
375
|
+
"4",
|
|
376
|
+
"5",
|
|
377
|
+
"6"
|
|
378
|
+
].map((n) => ({
|
|
379
|
+
label: `h${n}`,
|
|
380
|
+
value: n
|
|
381
|
+
})), {
|
|
382
|
+
label: "p (not a heading)",
|
|
383
|
+
value: "p"
|
|
384
|
+
}]
|
|
385
|
+
});
|
|
212
386
|
const BASE_BLOCKS = [
|
|
213
387
|
{
|
|
214
388
|
/**
|
|
@@ -224,7 +398,7 @@ const BASE_BLOCKS = [
|
|
|
224
398
|
* raw HTML), so `<wc-icon-picker>` cannot be mounted inside a block modal
|
|
225
399
|
* however much nicer it would be. Revisit if EmDash opens that union.
|
|
226
400
|
*/
|
|
227
|
-
type: "
|
|
401
|
+
type: "vitops_actionLink",
|
|
228
402
|
label: "Link or button",
|
|
229
403
|
icon: "link",
|
|
230
404
|
placeholder: "A link or button, with optional icons",
|
|
@@ -310,7 +484,7 @@ const BASE_BLOCKS = [
|
|
|
310
484
|
]
|
|
311
485
|
},
|
|
312
486
|
{
|
|
313
|
-
type: "
|
|
487
|
+
type: "vitops_imageCompare",
|
|
314
488
|
label: "Image compare",
|
|
315
489
|
placeholder: "Before/after image slider",
|
|
316
490
|
fields: [
|
|
@@ -349,7 +523,7 @@ const BASE_BLOCKS = [
|
|
|
349
523
|
]
|
|
350
524
|
},
|
|
351
525
|
{
|
|
352
|
-
type: "
|
|
526
|
+
type: "vitops_copyButton",
|
|
353
527
|
label: "Copy snippet",
|
|
354
528
|
icon: "code",
|
|
355
529
|
placeholder: "Text with a copy-to-clipboard button",
|
|
@@ -382,7 +556,7 @@ const BASE_BLOCKS = [
|
|
|
382
556
|
]
|
|
383
557
|
},
|
|
384
558
|
{
|
|
385
|
-
type: "
|
|
559
|
+
type: "vitops_banner",
|
|
386
560
|
label: "Banner",
|
|
387
561
|
placeholder: "Announcement or notice",
|
|
388
562
|
fields: [
|
|
@@ -425,7 +599,7 @@ const BASE_BLOCKS = [
|
|
|
425
599
|
]
|
|
426
600
|
},
|
|
427
601
|
{
|
|
428
|
-
type: "
|
|
602
|
+
type: "vitops_details",
|
|
429
603
|
label: "Disclosure",
|
|
430
604
|
placeholder: "Collapsible details/summary",
|
|
431
605
|
fields: [
|
|
@@ -449,7 +623,7 @@ const BASE_BLOCKS = [
|
|
|
449
623
|
]
|
|
450
624
|
},
|
|
451
625
|
{
|
|
452
|
-
type: "
|
|
626
|
+
type: "vitops_carousel",
|
|
453
627
|
label: "Carousel",
|
|
454
628
|
placeholder: "Image carousel",
|
|
455
629
|
category: "Media",
|
|
@@ -509,7 +683,7 @@ const BASE_BLOCKS = [
|
|
|
509
683
|
]
|
|
510
684
|
},
|
|
511
685
|
{
|
|
512
|
-
type: "
|
|
686
|
+
type: "vitops_gallery",
|
|
513
687
|
label: "Gallery",
|
|
514
688
|
placeholder: "Thumbnail grid that opens a lightbox carousel",
|
|
515
689
|
category: "Media",
|
|
@@ -559,7 +733,7 @@ const BASE_BLOCKS = [
|
|
|
559
733
|
]
|
|
560
734
|
},
|
|
561
735
|
{
|
|
562
|
-
type: "
|
|
736
|
+
type: "vitops_cards",
|
|
563
737
|
label: "Cards",
|
|
564
738
|
placeholder: "A grid of cards",
|
|
565
739
|
category: "Sections",
|
|
@@ -663,7 +837,7 @@ const BASE_BLOCKS = [
|
|
|
663
837
|
]
|
|
664
838
|
},
|
|
665
839
|
{
|
|
666
|
-
type: "
|
|
840
|
+
type: "vitops_accordion",
|
|
667
841
|
label: "Accordion",
|
|
668
842
|
placeholder: "A group of collapsible sections",
|
|
669
843
|
category: "Sections",
|
|
@@ -700,7 +874,7 @@ const BASE_BLOCKS = [
|
|
|
700
874
|
}]
|
|
701
875
|
},
|
|
702
876
|
{
|
|
703
|
-
type: "
|
|
877
|
+
type: "vitops_hero",
|
|
704
878
|
label: "Hero",
|
|
705
879
|
placeholder: "Page opener: heading, text, buttons and an optional image",
|
|
706
880
|
category: "Sections",
|
|
@@ -715,7 +889,7 @@ const BASE_BLOCKS = [
|
|
|
715
889
|
action_id: "heading",
|
|
716
890
|
label: "Heading (wrap words in *stars* to accent them)"
|
|
717
891
|
},
|
|
718
|
-
|
|
892
|
+
titleLevelField("1"),
|
|
719
893
|
{
|
|
720
894
|
type: "text_input",
|
|
721
895
|
action_id: "body",
|
|
@@ -799,7 +973,7 @@ const BASE_BLOCKS = [
|
|
|
799
973
|
]
|
|
800
974
|
},
|
|
801
975
|
{
|
|
802
|
-
type: "
|
|
976
|
+
type: "vitops_marquee",
|
|
803
977
|
label: "Marquee",
|
|
804
978
|
placeholder: "A continuously scrolling row (logos, quotes, badges)",
|
|
805
979
|
category: "Media",
|
|
@@ -843,7 +1017,7 @@ const BASE_BLOCKS = [
|
|
|
843
1017
|
]
|
|
844
1018
|
},
|
|
845
1019
|
{
|
|
846
|
-
type: "
|
|
1020
|
+
type: "vitops_logos",
|
|
847
1021
|
label: "Logos",
|
|
848
1022
|
placeholder: "A still row of partner or client logos",
|
|
849
1023
|
category: "Media",
|
|
@@ -864,12 +1038,12 @@ const BASE_BLOCKS = [
|
|
|
864
1038
|
{
|
|
865
1039
|
type: "text_input",
|
|
866
1040
|
action_id: "image",
|
|
867
|
-
label: "Image URL"
|
|
1041
|
+
label: "Image URL (leave empty to show the name as text)"
|
|
868
1042
|
},
|
|
869
1043
|
{
|
|
870
1044
|
type: "text_input",
|
|
871
1045
|
action_id: "alt",
|
|
872
|
-
label: "Name (alt text)"
|
|
1046
|
+
label: "Name (alt text, or the text shown with no image)"
|
|
873
1047
|
},
|
|
874
1048
|
{
|
|
875
1049
|
type: "text_input",
|
|
@@ -907,7 +1081,7 @@ const BASE_BLOCKS = [
|
|
|
907
1081
|
]
|
|
908
1082
|
},
|
|
909
1083
|
{
|
|
910
|
-
type: "
|
|
1084
|
+
type: "vitops_steps",
|
|
911
1085
|
label: "Steps",
|
|
912
1086
|
placeholder: "A numbered sequence joined by a connector line",
|
|
913
1087
|
category: "Sections",
|
|
@@ -947,7 +1121,7 @@ const BASE_BLOCKS = [
|
|
|
947
1121
|
]
|
|
948
1122
|
},
|
|
949
1123
|
{
|
|
950
|
-
type: "
|
|
1124
|
+
type: "vitops_stats",
|
|
951
1125
|
label: "Stats",
|
|
952
1126
|
placeholder: "Headline figures that count up, or labelled tiles",
|
|
953
1127
|
category: "Sections",
|
|
@@ -1024,7 +1198,7 @@ const BASE_BLOCKS = [
|
|
|
1024
1198
|
]
|
|
1025
1199
|
},
|
|
1026
1200
|
{
|
|
1027
|
-
type: "
|
|
1201
|
+
type: "vitops_callout",
|
|
1028
1202
|
label: "Callout",
|
|
1029
1203
|
placeholder: "A call-to-action band (not a dismissible banner)",
|
|
1030
1204
|
category: "Sections",
|
|
@@ -1084,7 +1258,7 @@ const BASE_BLOCKS = [
|
|
|
1084
1258
|
]
|
|
1085
1259
|
},
|
|
1086
1260
|
{
|
|
1087
|
-
type: "
|
|
1261
|
+
type: "vitops_entries",
|
|
1088
1262
|
label: "Entries table",
|
|
1089
1263
|
placeholder: "Rows of term/value pairs: a stacked list that becomes a table when wide",
|
|
1090
1264
|
category: "Sections",
|
|
@@ -1116,7 +1290,7 @@ const BASE_BLOCKS = [
|
|
|
1116
1290
|
]
|
|
1117
1291
|
},
|
|
1118
1292
|
{
|
|
1119
|
-
type: "
|
|
1293
|
+
type: "vitops_pricing",
|
|
1120
1294
|
label: "Pricing",
|
|
1121
1295
|
placeholder: "Plan cards with aligned feature rows",
|
|
1122
1296
|
category: "Sections",
|
|
@@ -1168,7 +1342,7 @@ const BASE_BLOCKS = [
|
|
|
1168
1342
|
}, levelField("3")]
|
|
1169
1343
|
},
|
|
1170
1344
|
{
|
|
1171
|
-
type: "
|
|
1345
|
+
type: "vitops_embed",
|
|
1172
1346
|
label: "Embed (video or map)",
|
|
1173
1347
|
placeholder: "A video or map that loads only after the visitor allows it",
|
|
1174
1348
|
category: "Embeds",
|
|
@@ -1236,7 +1410,7 @@ const BASE_BLOCKS = [
|
|
|
1236
1410
|
]
|
|
1237
1411
|
},
|
|
1238
1412
|
{
|
|
1239
|
-
type: "
|
|
1413
|
+
type: "vitops_form",
|
|
1240
1414
|
label: "Contact form",
|
|
1241
1415
|
placeholder: "An enquiry form that emails you (and your CRM) and works without JavaScript",
|
|
1242
1416
|
category: "Forms",
|
|
@@ -1299,6 +1473,10 @@ const BASE_BLOCKS = [
|
|
|
1299
1473
|
{
|
|
1300
1474
|
label: "Tick box",
|
|
1301
1475
|
value: "checkbox"
|
|
1476
|
+
},
|
|
1477
|
+
{
|
|
1478
|
+
label: "Tick any number",
|
|
1479
|
+
value: "checkboxes"
|
|
1302
1480
|
}
|
|
1303
1481
|
]
|
|
1304
1482
|
},
|
|
@@ -1321,7 +1499,7 @@ const BASE_BLOCKS = [
|
|
|
1321
1499
|
{
|
|
1322
1500
|
type: "text_input",
|
|
1323
1501
|
action_id: "options",
|
|
1324
|
-
label: "Choices (one per line; for \"Choose one\"
|
|
1502
|
+
label: "Choices (one per line; for \"Choose one\" and \"Tick any number\")",
|
|
1325
1503
|
multiline: true
|
|
1326
1504
|
}
|
|
1327
1505
|
]
|
|
@@ -1366,7 +1544,7 @@ const BASE_BLOCKS = [
|
|
|
1366
1544
|
* an editor saves. Which reviews show is `selectReviews()` from `@getvitops/utils`, the
|
|
1367
1545
|
* one place that is decided — these fields are exactly its `ReviewLocationConfig`.
|
|
1368
1546
|
*/
|
|
1369
|
-
type: "
|
|
1547
|
+
type: "vitops_reviews",
|
|
1370
1548
|
label: "Google reviews",
|
|
1371
1549
|
icon: "star",
|
|
1372
1550
|
placeholder: "Customer reviews from the Google Business Profile cache",
|
|
@@ -1477,7 +1655,7 @@ const BASE_BLOCKS = [
|
|
|
1477
1655
|
* The menu is chosen from `menus/list` (the site's own menus) with the conventional names as
|
|
1478
1656
|
* the static fallback, because a select needs a non-empty `options` list.
|
|
1479
1657
|
*/
|
|
1480
|
-
type: "
|
|
1658
|
+
type: "vitops_nav",
|
|
1481
1659
|
label: "Menu",
|
|
1482
1660
|
icon: "list",
|
|
1483
1661
|
placeholder: "An EmDash menu as a list of links",
|
|
@@ -1558,7 +1736,7 @@ const BASE_BLOCKS = [
|
|
|
1558
1736
|
},
|
|
1559
1737
|
{
|
|
1560
1738
|
/** A row of static labels (`badge`) or editable/dismissable ones (`tag`). */
|
|
1561
|
-
type: "
|
|
1739
|
+
type: "vitops_badges",
|
|
1562
1740
|
label: "Badges",
|
|
1563
1741
|
icon: "tag",
|
|
1564
1742
|
placeholder: "A row of short labels",
|
|
@@ -1622,7 +1800,7 @@ const BASE_BLOCKS = [
|
|
|
1622
1800
|
* block is placed. Edits reach every page it sits on (after the next build, on a
|
|
1623
1801
|
* prerendered site) — unlike a Section, which is copied.
|
|
1624
1802
|
*/
|
|
1625
|
-
type: "
|
|
1803
|
+
type: "vitops_widgetArea",
|
|
1626
1804
|
label: "Widget area",
|
|
1627
1805
|
icon: "layout",
|
|
1628
1806
|
placeholder: "Shared content managed under Widgets",
|
|
@@ -1640,9 +1818,61 @@ const BASE_BLOCKS = [
|
|
|
1640
1818
|
}
|
|
1641
1819
|
];
|
|
1642
1820
|
/** Every block gets the escape hatches last, so the editor reads content first. */
|
|
1821
|
+
/**
|
|
1822
|
+
* Repeaters whose rows can carry their own colour. A row's `tone` / `color` win over the block's,
|
|
1823
|
+
* and blank falls back to it, so one block can hold a pine card and an amber one without
|
|
1824
|
+
* becoming two blocks. (If you want a single colour for the lot, set it on the block.)
|
|
1825
|
+
*/
|
|
1826
|
+
const ROW_TONE = {
|
|
1827
|
+
vitops_cards: "cards",
|
|
1828
|
+
vitops_stats: "stats",
|
|
1829
|
+
vitops_steps: "steps",
|
|
1830
|
+
vitops_hero: "cards"
|
|
1831
|
+
};
|
|
1832
|
+
/** Extra row fields for one repeater, beyond the shared tone pair. */
|
|
1833
|
+
const ROW_EXTRA = { vitops_hero: { cards: [
|
|
1834
|
+
{
|
|
1835
|
+
type: "text_input",
|
|
1836
|
+
action_id: "eyebrow",
|
|
1837
|
+
label: "Eyebrow (small line above the title)"
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
type: "text_input",
|
|
1841
|
+
action_id: "ctaLabel",
|
|
1842
|
+
label: "Button text"
|
|
1843
|
+
},
|
|
1844
|
+
{
|
|
1845
|
+
type: "text_input",
|
|
1846
|
+
action_id: "ctaHref",
|
|
1847
|
+
label: "Button URL"
|
|
1848
|
+
}
|
|
1849
|
+
] } };
|
|
1850
|
+
const rowToneFields = () => [{
|
|
1851
|
+
type: "select",
|
|
1852
|
+
action_id: "tone",
|
|
1853
|
+
label: "Tone for this item (blank uses the block tone)",
|
|
1854
|
+
initial_value: "",
|
|
1855
|
+
options: STATIC_TONE_OPTIONS,
|
|
1856
|
+
optionsRoute: TONES_ROUTE
|
|
1857
|
+
}, {
|
|
1858
|
+
type: "text_input",
|
|
1859
|
+
action_id: "color",
|
|
1860
|
+
label: "Palette colour instead (e.g. amber)"
|
|
1861
|
+
}];
|
|
1862
|
+
function withRowFields(block) {
|
|
1863
|
+
const repeater = ROW_TONE[block.type];
|
|
1864
|
+
return (block.fields ?? []).map((f) => f.type === "repeater" && f.action_id === repeater ? {
|
|
1865
|
+
...f,
|
|
1866
|
+
fields: [
|
|
1867
|
+
...f.fields,
|
|
1868
|
+
...ROW_EXTRA[block.type]?.[repeater] ?? [],
|
|
1869
|
+
...rowToneFields()
|
|
1870
|
+
]
|
|
1871
|
+
} : f);
|
|
1872
|
+
}
|
|
1643
1873
|
const BLOCKS = BASE_BLOCKS.map((b) => ({
|
|
1644
1874
|
...b,
|
|
1645
|
-
fields: [...b
|
|
1875
|
+
fields: [...withRowFields(b), ...commonFields()]
|
|
1646
1876
|
}));
|
|
1647
1877
|
/**
|
|
1648
1878
|
* Resolve {@link BlockSelection} against {@link BLOCKS}.
|
|
@@ -1738,6 +1968,27 @@ function createTonesRoute(roles) {
|
|
|
1738
1968
|
handler: async () => ({ items: toneItems(roles()) })
|
|
1739
1969
|
};
|
|
1740
1970
|
}
|
|
1971
|
+
/** The `theme` select's items: `None`, then each role tinted and filled (`role`, `role:solid`). */
|
|
1972
|
+
function themeItems(roles) {
|
|
1973
|
+
const names = [.../* @__PURE__ */ new Set([...roles, ...SEMANTIC_TONES])];
|
|
1974
|
+
const label = (id) => id.replace(/-/g, " ");
|
|
1975
|
+
return [{
|
|
1976
|
+
id: "",
|
|
1977
|
+
name: "None"
|
|
1978
|
+
}, ...names.flatMap((id) => [{
|
|
1979
|
+
id,
|
|
1980
|
+
name: label(id)
|
|
1981
|
+
}, {
|
|
1982
|
+
id: `${id}:solid`,
|
|
1983
|
+
name: `${label(id)} (filled)`
|
|
1984
|
+
}])];
|
|
1985
|
+
}
|
|
1986
|
+
function createThemesRoute(roles) {
|
|
1987
|
+
return {
|
|
1988
|
+
permission: "content:read",
|
|
1989
|
+
handler: async () => ({ items: themeItems(roles()) })
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1741
1992
|
|
|
1742
1993
|
// src/design-system-route.ts
|
|
1743
1994
|
/**
|
|
@@ -1880,130 +2131,6 @@ async function designSystemOverrideFragment(ctx, opts) {
|
|
|
1880
2131
|
};
|
|
1881
2132
|
}
|
|
1882
2133
|
|
|
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
|
-
|
|
2007
2134
|
// src/blocks-doc.ts
|
|
2008
2135
|
const cell = (s) => (s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2009
2136
|
function describe(f) {
|
|
@@ -2037,7 +2164,7 @@ function renderBlocksDoc(blocks = BLOCKS) {
|
|
|
2037
2164
|
"",
|
|
2038
2165
|
"# EmDash blocks",
|
|
2039
2166
|
"",
|
|
2040
|
-
"Each block is a Portable Text object: `{ \"_type\": \"
|
|
2167
|
+
"Each block is a Portable Text object: `{ \"_type\": \"vitops_cards\", \"_key\": \"<unique>\", ...fields }`.",
|
|
2041
2168
|
"Portable Text blocks are flat — they cannot contain other blocks, and a repeater row holds scalar fields only.",
|
|
2042
2169
|
"To put blocks inside something, use the section block TYPE (end of this page), whose body is Portable Text.",
|
|
2043
2170
|
"Give every block a unique `_key`; gallery and carousel derive their element id from it.",
|
|
@@ -2047,9 +2174,27 @@ function renderBlocksDoc(blocks = BLOCKS) {
|
|
|
2047
2174
|
"Every block that colours something takes the same two fields. `tone` is a colour role from the",
|
|
2048
2175
|
"site's `colors.roles` (or a semantic name); an unknown role falls back through its semantic",
|
|
2049
2176
|
"relatives to `neutral`. `color` is an escape hatch naming a palette hue (`amber`) or hue and step",
|
|
2050
|
-
"(`amber-300`) and wins over `tone`;
|
|
2051
|
-
"
|
|
2177
|
+
"(`amber-300`) and wins over `tone`; text on a hue solid is the site's `--color-<hue>-<step>-on`,",
|
|
2178
|
+
"but contrast is only guaranteed for roles. Renderers emit `--tone-*` custom properties, never a",
|
|
2179
|
+
"class built from the value. Rows of `hero.cards`, `cards`, `stats` and `steps` (and `badges.items`)",
|
|
2180
|
+
"take their own `tone`/`color`; blank inherits the block's, and a row's colour never reaches its",
|
|
2181
|
+
"siblings, so mixed colours need no extra block. Multiline fields that accept links take",
|
|
2052
2182
|
"`[text](/url)`; hero headings accent words wrapped in `*stars*`.",
|
|
2183
|
+
"",
|
|
2184
|
+
"## Theme",
|
|
2185
|
+
"",
|
|
2186
|
+
"Every block carries `theme`: a colour role, tinted (`surface`) or filled (`brand-primary:solid`), applied to",
|
|
2187
|
+
"the block's own root with the role's computed readable text. It wins over the block's `tone`.",
|
|
2188
|
+
"",
|
|
2189
|
+
"## Type size",
|
|
2190
|
+
"",
|
|
2191
|
+
"Titles use the type roles (`font-heading`, `font-display`, `font-eyebrow`). Every role reads its size",
|
|
2192
|
+
"from a custom property, `--<role>-fs` (`--heading-fs`, `--display-fs`), falling back to the role's",
|
|
2193
|
+
"configured size, so one block can resize its titles through its `style` field",
|
|
2194
|
+
"(`--heading-fs: 1.125rem`) with no site CSS; set it on an ancestor to resize a region. It is inherited,",
|
|
2195
|
+
"so it reaches every use of that role inside the block. `style` accepts custom properties only.",
|
|
2196
|
+
"Steps, stats and cards also have per-part hooks: `--steps-title-size` (default one step below the",
|
|
2197
|
+
"heading role), `--stats-figure-size` and `--card-title-size`.",
|
|
2053
2198
|
""
|
|
2054
2199
|
];
|
|
2055
2200
|
for (const b of blocks) {
|
|
@@ -2057,7 +2202,7 @@ function renderBlocksDoc(blocks = BLOCKS) {
|
|
|
2057
2202
|
if (b.placeholder) out.push(`${b.placeholder}.`, "");
|
|
2058
2203
|
if (b.fields?.length) out.push("| Field | Type | Label | Notes |", "| --- | --- | --- | --- |", ...fieldRows(b.fields), "");
|
|
2059
2204
|
}
|
|
2060
|
-
out.push("## `
|
|
2205
|
+
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.", "`vitops emdash check-prerender` warns (fails under `--strict`) when a built page has a form", "posting to `/api/track` and `site.json` configures nowhere to deliver it, or lists none of its fields", "under `site.templates`.", "", "A `checkboxes` field (\"tick any number\") posts one entry per ticked box under the same name; the route", "joins them with `\", \"`. HTML cannot require \"at least one\", so `required` is not applied to the group.", "", "The generated privacy policy reads `site.templates` form entries, not this block. Convert the saved", "block with `formTemplateFromBlock(block)` from `@getvitops/emdash` and paste the result under", "`site.templates` in `site.json`, so the policy lists what the form collects.", "", "## 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
2206
|
for (const t of vitopsBlockTypes()) {
|
|
2062
2207
|
out.push(`### \`${t.slug}\` — ${t.label}`, "", `${t.description ?? ""}`, "");
|
|
2063
2208
|
const fields = t.versions.find((v) => v.version === t.currentVersion)?.fields ?? [];
|
|
@@ -2065,7 +2210,7 @@ function renderBlocksDoc(blocks = BLOCKS) {
|
|
|
2065
2210
|
const notes = [
|
|
2066
2211
|
f.validation?.options ? `one of ${f.validation.options.join(", ")}` : "",
|
|
2067
2212
|
f.defaultValue !== void 0 ? `default \`${String(f.defaultValue)}\`` : "",
|
|
2068
|
-
f.type === "portableText" ? "accepts every `
|
|
2213
|
+
f.type === "portableText" ? "accepts every `vitops_*` block above" : ""
|
|
2069
2214
|
].filter(Boolean);
|
|
2070
2215
|
return `| \`${f.slug}\` | ${f.type} | ${cell(f.label)} | ${cell(notes.join("; "))} |`;
|
|
2071
2216
|
}), "");
|
|
@@ -2073,6 +2218,160 @@ function renderBlocksDoc(blocks = BLOCKS) {
|
|
|
2073
2218
|
return out.join("\n");
|
|
2074
2219
|
}
|
|
2075
2220
|
|
|
2221
|
+
// src/astro/blocks/form.ts
|
|
2222
|
+
/**
|
|
2223
|
+
* Pure decisions for the `vitops_form` block: which of an editor's field rows become real
|
|
2224
|
+
* controls, and under what submitted names.
|
|
2225
|
+
*
|
|
2226
|
+
* Names matter more than they look. `createConversionRoute` (`@getvitops/astro/routes`) is
|
|
2227
|
+
* what receives the post, and it reserves a few: `phone` and `event` are the tel:/mailto:
|
|
2228
|
+
* click-beacon payload and are dropped from a form submission, `cf-turnstile-response` is
|
|
2229
|
+
* the Turnstile token, and `website`/`honeypot`/`_honeypot` are the honeypots (a filled one
|
|
2230
|
+
* makes the route answer success and deliver nothing). A visitor-facing field called
|
|
2231
|
+
* "Phone" must therefore not submit as `phone`, or the number silently vanishes from the
|
|
2232
|
+
* notification and the CRM record. `identifySubmitter` reads `tel` for a phone number, so
|
|
2233
|
+
* that is where it goes.
|
|
2234
|
+
*/
|
|
2235
|
+
const FIELD_TYPES = [
|
|
2236
|
+
"text",
|
|
2237
|
+
"email",
|
|
2238
|
+
"tel",
|
|
2239
|
+
"textarea",
|
|
2240
|
+
"select",
|
|
2241
|
+
"checkbox",
|
|
2242
|
+
"checkboxes"
|
|
2243
|
+
];
|
|
2244
|
+
/** Names the route treats specially. A field must never submit under one of these. */
|
|
2245
|
+
const RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
2246
|
+
"event",
|
|
2247
|
+
"phone",
|
|
2248
|
+
"cf-turnstile-response",
|
|
2249
|
+
"website",
|
|
2250
|
+
"honeypot",
|
|
2251
|
+
"_honeypot",
|
|
2252
|
+
"_next"
|
|
2253
|
+
]);
|
|
2254
|
+
const MAX_FIELDS = 20;
|
|
2255
|
+
const str = (v) => typeof v === "string" ? v.trim() : "";
|
|
2256
|
+
/** `Your full name` -> `your_full_name`. Empty when the text holds nothing usable. */
|
|
2257
|
+
function slugName(input) {
|
|
2258
|
+
return input.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40);
|
|
2259
|
+
}
|
|
2260
|
+
/** A submitted name for a row: the editor's `name` if usable, else one derived from the label. */
|
|
2261
|
+
function fieldName(raw, label, type, taken) {
|
|
2262
|
+
let base = slugName(str(raw)) || slugName(label) || type;
|
|
2263
|
+
if (!/^[a-z]/.test(base)) base = `f_${base}`;
|
|
2264
|
+
if (base === "phone") base = "tel";
|
|
2265
|
+
if (RESERVED_NAMES.has(base)) base = `${base}_field`;
|
|
2266
|
+
let name = base;
|
|
2267
|
+
for (let n = 2; taken.has(name); n++) name = `${base}_${n}`;
|
|
2268
|
+
taken.add(name);
|
|
2269
|
+
return name;
|
|
2270
|
+
}
|
|
2271
|
+
/** The HTML `autocomplete` token for a field, or undefined when there is no honest one. */
|
|
2272
|
+
function autocompleteFor(name, type) {
|
|
2273
|
+
if (type === "email") return "email";
|
|
2274
|
+
if (type === "tel") return "tel";
|
|
2275
|
+
if (type !== "text") return void 0;
|
|
2276
|
+
return {
|
|
2277
|
+
name: "name",
|
|
2278
|
+
full_name: "name",
|
|
2279
|
+
your_name: "name",
|
|
2280
|
+
first_name: "given-name",
|
|
2281
|
+
given_name: "given-name",
|
|
2282
|
+
last_name: "family-name",
|
|
2283
|
+
family_name: "family-name",
|
|
2284
|
+
company: "organization",
|
|
2285
|
+
organization: "organization",
|
|
2286
|
+
organisation: "organization",
|
|
2287
|
+
postal_code: "postal-code",
|
|
2288
|
+
postcode: "postal-code",
|
|
2289
|
+
zip: "postal-code",
|
|
2290
|
+
city: "address-level2",
|
|
2291
|
+
address: "street-address"
|
|
2292
|
+
}[name];
|
|
2293
|
+
}
|
|
2294
|
+
/** Editor rows to renderable fields. Rows with no label are dropped; at most {@link MAX_FIELDS}. */
|
|
2295
|
+
function resolveFields(rows) {
|
|
2296
|
+
const taken = /* @__PURE__ */ new Set();
|
|
2297
|
+
const out = [];
|
|
2298
|
+
for (const row of rows) {
|
|
2299
|
+
const label = str(row["label"]);
|
|
2300
|
+
if (!label) continue;
|
|
2301
|
+
if (out.length >= 20) break;
|
|
2302
|
+
const rawType = str(row["type"]);
|
|
2303
|
+
const type = FIELD_TYPES.includes(rawType) ? rawType : "text";
|
|
2304
|
+
const name = fieldName(row["name"], label, type, taken);
|
|
2305
|
+
const options = type === "select" || type === "checkboxes" ? str(row["options"]).split("\n").map((o) => o.trim()).filter(Boolean) : [];
|
|
2306
|
+
const effective = (type === "select" || type === "checkboxes") && options.length === 0 ? "text" : type;
|
|
2307
|
+
out.push({
|
|
2308
|
+
name,
|
|
2309
|
+
id: `form-${out.length + 1}-${name}`,
|
|
2310
|
+
label,
|
|
2311
|
+
type: effective,
|
|
2312
|
+
required: row["required"] === true,
|
|
2313
|
+
placeholder: effective === "checkbox" || effective === "select" || effective === "checkboxes" ? "" : str(row["placeholder"]),
|
|
2314
|
+
help: str(row["help"]),
|
|
2315
|
+
options,
|
|
2316
|
+
autocomplete: autocompleteFor(name, effective)
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
return out;
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* Where the endpoint may send a visitor back to: a same-site path, nothing else. The form
|
|
2323
|
+
* posts its own page path as `_next`; the route that reads it must apply this too, or the
|
|
2324
|
+
* field becomes an open redirect.
|
|
2325
|
+
*/
|
|
2326
|
+
function safeNext(input) {
|
|
2327
|
+
const v = typeof input === "string" ? input : "";
|
|
2328
|
+
if (!v.startsWith("/") || v.startsWith("//") || v.includes("\\") || v.length > 200) return null;
|
|
2329
|
+
if (/[\u0000-\u001f\u007f#?]/.test(v)) return null;
|
|
2330
|
+
return v;
|
|
2331
|
+
}
|
|
2332
|
+
/** The endpoint a form posts to: a same-site path, defaulting to the conversion route. */
|
|
2333
|
+
function safeAction(input) {
|
|
2334
|
+
return safeNext(input) ?? "/api/track";
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/form-template.ts
|
|
2338
|
+
/**
|
|
2339
|
+
* A saved `vitops_form` block as a `site.templates` form entry.
|
|
2340
|
+
*
|
|
2341
|
+
* `vitops legal` derives "what personal information we collect" from `site.templates` entries
|
|
2342
|
+
* of type `form`, and a build cannot read the CMS database the editor's block lives in, so a
|
|
2343
|
+
* form added in the admin is invisible to the generated privacy policy. This turns a block's
|
|
2344
|
+
* saved value (copy it from the admin's JSON view, or from `emdash` content export) into the
|
|
2345
|
+
* entry to paste under `site.templates` in `site.json`.
|
|
2346
|
+
*
|
|
2347
|
+
* It resolves fields through the SAME `resolveFields` the renderer uses, so the names are the
|
|
2348
|
+
* ones the form actually submits (`phone` becomes `tel`, reserved names are renamed) and rows
|
|
2349
|
+
* the renderer drops (no label) are dropped here too. A `checkboxes` becomes a `checkbox`
|
|
2350
|
+
* with its choices as `options`: the policy's schema has no group type and both read as
|
|
2351
|
+
* "your answers to the questions we ask".
|
|
2352
|
+
*/
|
|
2353
|
+
/** `block` is a saved `vitops_form` value (the object with `fields`, `action`, …). */
|
|
2354
|
+
function formTemplateFromBlock(block) {
|
|
2355
|
+
const value = block && typeof block === "object" ? block : {};
|
|
2356
|
+
const rows = (Array.isArray(value["fields"]) ? value["fields"] : []).filter((r) => !!r && typeof r === "object" && !Array.isArray(r));
|
|
2357
|
+
return {
|
|
2358
|
+
type: "form",
|
|
2359
|
+
action: safeAction(value["action"]),
|
|
2360
|
+
method: "post",
|
|
2361
|
+
honeypot: true,
|
|
2362
|
+
fields: resolveFields(rows).map((f) => ({
|
|
2363
|
+
name: f.name,
|
|
2364
|
+
type: f.type === "checkboxes" ? "checkbox" : f.type,
|
|
2365
|
+
label: f.label,
|
|
2366
|
+
...f.required ? { required: true } : {},
|
|
2367
|
+
...f.options.length ? { options: f.options.map((o) => ({
|
|
2368
|
+
label: o,
|
|
2369
|
+
value: o
|
|
2370
|
+
})) } : {}
|
|
2371
|
+
}))
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2076
2375
|
// src/hosting.ts
|
|
2077
2376
|
const TARGETS = ["cloudflare", "node"];
|
|
2078
2377
|
/**
|
|
@@ -2134,7 +2433,7 @@ async function vitopsHosting(options = {}) {
|
|
|
2134
2433
|
}
|
|
2135
2434
|
|
|
2136
2435
|
// package.json
|
|
2137
|
-
var version = "2.0.0-alpha.
|
|
2436
|
+
var version = "2.0.0-alpha.2";
|
|
2138
2437
|
|
|
2139
2438
|
// src/index.ts
|
|
2140
2439
|
/**
|
|
@@ -2236,6 +2535,7 @@ function createPlugin(options = {}) {
|
|
|
2236
2535
|
},
|
|
2237
2536
|
routes: {
|
|
2238
2537
|
[TONES_ROUTE]: createTonesRoute(() => roles),
|
|
2538
|
+
[THEMES_ROUTE]: createThemesRoute(() => roles),
|
|
2239
2539
|
[MENUS_ROUTE]: createMenusRoute(() => getMenus()),
|
|
2240
2540
|
[WIDGET_AREAS_ROUTE]: createMenusRoute(() => getWidgetAreas()),
|
|
2241
2541
|
...editor ? { "design-system": createDesignSystemRoute(editor) } : {}
|
|
@@ -2243,5 +2543,5 @@ function createPlugin(options = {}) {
|
|
|
2243
2543
|
});
|
|
2244
2544
|
}
|
|
2245
2545
|
|
|
2246
|
-
export { BLOCKS, SECTION_BLOCK_TYPE, SECTION_SLUG, createPlugin, createPlugin as default, renderBlocksDoc, resolveTone, selectBlocks, toneChain, vitopsBlockTypes, vitopsEmdash, vitopsHosting };
|
|
2546
|
+
export { BLOCKS, SECTION_BLOCK_TYPE, SECTION_SLUG, createPlugin, createPlugin as default, formTemplateFromBlock, renderBlocksDoc, resolveTone, selectBlocks, toneChain, vitopsBlockTypes, vitopsEmdash, vitopsHosting };
|
|
2247
2547
|
//# sourceMappingURL=index.mjs.map
|