@vectoriox/iox-ui 5.28.0 → 5.29.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/design-core/index.d.ts +42 -3
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +48 -6
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs +80 -1
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs +591 -181
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +117 -2
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/forms-core/index.d.ts +42 -2
- package/generation-core/index.d.ts +206 -74
- package/index.d.ts +17 -1
- package/package.json +1 -1
- package/src/design-core/contrast-audit.ts +7 -6
- package/src/design-core/flavor-spec.generated.ts +17 -0
- package/src/design-core/node-links.ts +32 -0
- package/src/design-core/public-api.ts +1 -0
- package/src/forms-core/form-presets.ts +105 -0
- package/src/forms-core/public-api.ts +1 -0
- package/src/generation-core/baseline-roles.ts +75 -0
- package/src/generation-core/compose-site.ts +256 -95
- package/src/generation-core/node-baselines.generated.ts +90 -0
- package/src/generation-core/patterns.ts +31 -9
- package/src/generation-core/public-api.ts +1 -0
- package/src/generation-core/serialized-node.ts +13 -2
- package/src/generation-core/site-plan.model.ts +4 -3
- package/src/generation-core/site-spec.generated.ts +110 -57
- package/src/generation-core/validate-site-plan.ts +52 -20
|
@@ -1,18 +1,104 @@
|
|
|
1
|
-
import { NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, elementStyleById, ownedTraitsOf, applyElementStyle, flavorPackById, parseHexColor, SPEC_AXES, resolveDesignChoices } from '@vectoriox/iox-ui/design-core';
|
|
1
|
+
import { NEW_NODE_STYLE_DEFAULTS, defaultElementStyleFor, elementStyleById, ownedTraitsOf, applyElementStyle, flavorPackById, parseHexColor, SPEC_AXES, LinkType, resolveDesignChoices } from '@vectoriox/iox-ui/design-core';
|
|
2
|
+
import { formPresetById } from '@vectoriox/iox-ui/forms-core';
|
|
2
3
|
|
|
3
4
|
// GENERATED from iox-ai-guidance flavor-spec.v1.json — do not edit; re-run scripts/gen-flavor-spec.mjs
|
|
4
5
|
// (npm run gen:flavor-spec). The JSON is itself generated by iox-ai-guidance
|
|
5
6
|
// architecture/builder/design-system/build-spec.mjs: change a value THERE, then regenerate both.
|
|
6
7
|
/* eslint-disable */
|
|
7
8
|
const SPEC_SITE_PLAN_SHAPE = {
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
"plan": {
|
|
10
|
+
"version": "'1'",
|
|
11
|
+
"source": "sourceTemplate | sourceAi",
|
|
12
|
+
"business": "business",
|
|
13
|
+
"design": "design",
|
|
14
|
+
"contentTypes": "contentType[]",
|
|
15
|
+
"dataSources": "(cmsDataSource | relativeDataSource)[] — merged by alias into the org library",
|
|
16
|
+
"pages": "page[]",
|
|
17
|
+
"globals": "globals"
|
|
18
|
+
},
|
|
19
|
+
"sourceTemplate": {
|
|
20
|
+
"kind": "'template'",
|
|
21
|
+
"templateId": "string",
|
|
22
|
+
"templateVersion": "integer"
|
|
23
|
+
},
|
|
24
|
+
"sourceAi": {
|
|
25
|
+
"kind": "'ai'",
|
|
26
|
+
"runId": "string"
|
|
27
|
+
},
|
|
28
|
+
"business": {
|
|
29
|
+
"name": "string — the brand: the header, the hero, the copyright line",
|
|
30
|
+
"tagline?": "string — the hero lead and the footer line",
|
|
31
|
+
"facts?": "{ [fact]: string } — facts (email, phone, city): only the customer's own words; a blank fact is a publish-blocking placeholder, never invented"
|
|
32
|
+
},
|
|
33
|
+
"design": {
|
|
34
|
+
"flavorId": "string — a v2 flavor pack id",
|
|
35
|
+
"seed": "string — #rrggbb brand colour",
|
|
36
|
+
"pairId": "string — one of the pack's font pairings",
|
|
37
|
+
"shape": "string — axes.shapes",
|
|
38
|
+
"density": "string — axes.densities",
|
|
39
|
+
"motion": "string — axes.motions",
|
|
40
|
+
"pillButtons": "boolean",
|
|
41
|
+
"mode": "'light' | 'dark' | 'auto'",
|
|
42
|
+
"tint?": "string — axes.tints (DS9 neutral tint)",
|
|
43
|
+
"fonts?": "object — uploaded font families per role (DS5)"
|
|
44
|
+
},
|
|
45
|
+
"contentType": {
|
|
46
|
+
"name": "string — unique",
|
|
47
|
+
"label": "string",
|
|
48
|
+
"previewTextField?": "string — a field of the type: the item's name on cards and the alt text of its bound images",
|
|
49
|
+
"previewImageField?": "string — a field of the type: the item's picture",
|
|
50
|
+
"fields": "field[]",
|
|
51
|
+
"seed?": "seed"
|
|
52
|
+
},
|
|
53
|
+
"field": {
|
|
54
|
+
"name": "string — unique in the type",
|
|
55
|
+
"type": "string",
|
|
56
|
+
"controller": "string — an IOX input controller",
|
|
57
|
+
"required?": "boolean",
|
|
58
|
+
"note?": "string"
|
|
59
|
+
},
|
|
60
|
+
"seed": {
|
|
61
|
+
"count": "integer ≥ 0",
|
|
62
|
+
"sample": "boolean — items are flagged isSample",
|
|
63
|
+
"isPublished": "boolean",
|
|
64
|
+
"[field]?": "the field's value — strings are templates: {n} = the item's 1-based number, {sampleImage:n} / {sampleImage:n+k} = a bundled sample image"
|
|
65
|
+
},
|
|
66
|
+
"cmsDataSource": {
|
|
67
|
+
"alias": "string — unique; never $… (platform scopes)",
|
|
68
|
+
"type": "'cms'",
|
|
69
|
+
"mode": "'query' | 'single'",
|
|
70
|
+
"contentType": "string — a content type of the plan",
|
|
71
|
+
"limit?": "integer ≥ 1",
|
|
72
|
+
"filterByRouteParam?": "{ field, routeParam } — single only: the route param that picks the item"
|
|
73
|
+
},
|
|
74
|
+
"relativeDataSource": {
|
|
75
|
+
"alias": "string — unique",
|
|
76
|
+
"type": "'relative'",
|
|
77
|
+
"of": "string — a single cms source (the subject)",
|
|
78
|
+
"op": "'$siblings.next' | '$siblings.prev'",
|
|
79
|
+
"within": "string — a query cms source of the same type"
|
|
80
|
+
},
|
|
81
|
+
"page": {
|
|
82
|
+
"key": "string — unique",
|
|
83
|
+
"name": "string — the nav label",
|
|
84
|
+
"route": "string — /lowercase-words, :params; unique",
|
|
85
|
+
"isHome?": "boolean — exactly one page, at /",
|
|
86
|
+
"required?": "boolean — a template page the wizard's Pages step cannot leave out",
|
|
87
|
+
"inNav?": "boolean — a Link in the header and footer navigation, in plan order",
|
|
88
|
+
"template?": "string — the content type a template page renders one item of",
|
|
89
|
+
"routeSource?": "string — the single source read by the route param (route.* binds it)",
|
|
90
|
+
"sections": "section[]"
|
|
91
|
+
},
|
|
92
|
+
"section": {
|
|
93
|
+
"pattern": "string — a non-global pattern id",
|
|
94
|
+
"slots?": "{ [slot]: string } — the pattern's declared slots",
|
|
95
|
+
"binding?": "string — the list source a pattern's $binding repeater iterates (exactly when it has one)",
|
|
96
|
+
"without?": "string[] — the pattern's optional parts to leave out"
|
|
97
|
+
},
|
|
98
|
+
"globals": {
|
|
99
|
+
"header": "string — a header pattern (global: before)",
|
|
100
|
+
"footer": "string — a footer pattern (global: after)"
|
|
101
|
+
}
|
|
16
102
|
};
|
|
17
103
|
const SPEC_SITE_FACTS = {
|
|
18
104
|
"email": "Email address",
|
|
@@ -30,6 +116,7 @@ const SPEC_SITE_PATTERNS = {
|
|
|
30
116
|
"tag": "nav",
|
|
31
117
|
"lay": {
|
|
32
118
|
"display": "flex",
|
|
119
|
+
"flexWrap": "wrap",
|
|
33
120
|
"justifyContent": "space-between",
|
|
34
121
|
"alignItems": "center",
|
|
35
122
|
"gap": "var(--iox-space-gap)"
|
|
@@ -55,21 +142,17 @@ const SPEC_SITE_PATTERNS = {
|
|
|
55
142
|
"lay": {
|
|
56
143
|
"display": "flex",
|
|
57
144
|
"flexWrap": "wrap",
|
|
58
|
-
"gap": "
|
|
59
|
-
"alignItems": "center"
|
|
145
|
+
"gap": "var(--iox-space-gap)",
|
|
146
|
+
"alignItems": "center",
|
|
147
|
+
"width": "auto"
|
|
60
148
|
},
|
|
61
149
|
"c": [
|
|
62
150
|
{
|
|
63
|
-
"t": "
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"es": "link",
|
|
69
|
-
"bind": "page.name",
|
|
70
|
-
"href": "$page.route"
|
|
71
|
-
}
|
|
72
|
-
]
|
|
151
|
+
"t": "Link",
|
|
152
|
+
"es": "link",
|
|
153
|
+
"each": "$navPages",
|
|
154
|
+
"bind": "page.name",
|
|
155
|
+
"href": "$page.route"
|
|
73
156
|
},
|
|
74
157
|
{
|
|
75
158
|
"t": "ButtonBlock",
|
|
@@ -147,16 +230,11 @@ const SPEC_SITE_PATTERNS = {
|
|
|
147
230
|
"text": "Pages"
|
|
148
231
|
},
|
|
149
232
|
{
|
|
150
|
-
"t": "
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
"es": "link",
|
|
156
|
-
"bind": "page.name",
|
|
157
|
-
"href": "$page.route"
|
|
158
|
-
}
|
|
159
|
-
]
|
|
233
|
+
"t": "Link",
|
|
234
|
+
"es": "link",
|
|
235
|
+
"each": "$navPages",
|
|
236
|
+
"bind": "page.name",
|
|
237
|
+
"href": "$page.route"
|
|
160
238
|
}
|
|
161
239
|
]
|
|
162
240
|
},
|
|
@@ -202,7 +280,7 @@ const SPEC_SITE_PATTERNS = {
|
|
|
202
280
|
"t": "Text",
|
|
203
281
|
"es": "small",
|
|
204
282
|
"slot": "copyright",
|
|
205
|
-
"
|
|
283
|
+
"text": "© {year} {business.name}"
|
|
206
284
|
}
|
|
207
285
|
],
|
|
208
286
|
"lay": {
|
|
@@ -680,6 +758,7 @@ const SPEC_SITE_PATTERNS = {
|
|
|
680
758
|
"t": "Image",
|
|
681
759
|
"es": "media",
|
|
682
760
|
"bind": "item",
|
|
761
|
+
"alt": "",
|
|
683
762
|
"aspect": "4 / 3"
|
|
684
763
|
}
|
|
685
764
|
]
|
|
@@ -854,32 +933,7 @@ const SPEC_SITE_PATTERNS = {
|
|
|
854
933
|
"c": [
|
|
855
934
|
{
|
|
856
935
|
"t": "Form",
|
|
857
|
-
"preset": "contact"
|
|
858
|
-
"c": [
|
|
859
|
-
{
|
|
860
|
-
"t": "Field",
|
|
861
|
-
"control": "text",
|
|
862
|
-
"label": "Name",
|
|
863
|
-
"required": true
|
|
864
|
-
},
|
|
865
|
-
{
|
|
866
|
-
"t": "Field",
|
|
867
|
-
"control": "email",
|
|
868
|
-
"label": "Email",
|
|
869
|
-
"required": true
|
|
870
|
-
},
|
|
871
|
-
{
|
|
872
|
-
"t": "Field",
|
|
873
|
-
"control": "textarea",
|
|
874
|
-
"label": "Message",
|
|
875
|
-
"required": true
|
|
876
|
-
},
|
|
877
|
-
{
|
|
878
|
-
"t": "SubmitButton",
|
|
879
|
-
"es": "button-primary",
|
|
880
|
-
"text": "Send"
|
|
881
|
-
}
|
|
882
|
-
]
|
|
936
|
+
"preset": "contact"
|
|
883
937
|
}
|
|
884
938
|
]
|
|
885
939
|
}
|
|
@@ -1301,7 +1355,9 @@ const NODE_BASELINES = {
|
|
|
1301
1355
|
"ButtonBlock": {
|
|
1302
1356
|
"props": {
|
|
1303
1357
|
"tag": "button",
|
|
1358
|
+
"linkType": "external",
|
|
1304
1359
|
"href": "",
|
|
1360
|
+
"pageRoute": "",
|
|
1305
1361
|
"target": "_self",
|
|
1306
1362
|
"type": "button"
|
|
1307
1363
|
},
|
|
@@ -1391,7 +1447,9 @@ const NODE_BASELINES = {
|
|
|
1391
1447
|
},
|
|
1392
1448
|
"traits": [
|
|
1393
1449
|
"tag",
|
|
1450
|
+
"linkType",
|
|
1394
1451
|
"href",
|
|
1452
|
+
"pageRoute",
|
|
1395
1453
|
"target",
|
|
1396
1454
|
"type"
|
|
1397
1455
|
],
|
|
@@ -5944,9 +6002,168 @@ const NODE_BASELINES = {
|
|
|
5944
6002
|
"container": true
|
|
5945
6003
|
}
|
|
5946
6004
|
};
|
|
6005
|
+
/** What every style trait saves as when no constructor sets it (buildFullStyleTraits' defaults). */
|
|
6006
|
+
const NEUTRAL_STYLE_PROPS = {
|
|
6007
|
+
"position": "static",
|
|
6008
|
+
"top": "auto",
|
|
6009
|
+
"right": "auto",
|
|
6010
|
+
"bottom": "auto",
|
|
6011
|
+
"left": "auto",
|
|
6012
|
+
"zIndex": "0",
|
|
6013
|
+
"display": "block",
|
|
6014
|
+
"flexDirection": "row",
|
|
6015
|
+
"flexWrap": "nowrap",
|
|
6016
|
+
"justifyContent": "flex-start",
|
|
6017
|
+
"alignItems": "stretch",
|
|
6018
|
+
"gap": "0px",
|
|
6019
|
+
"gridTemplateColumns": "1fr",
|
|
6020
|
+
"gridTemplateRows": "auto",
|
|
6021
|
+
"justifyItems": "stretch",
|
|
6022
|
+
"alignContent": "stretch",
|
|
6023
|
+
"direction": "ltr",
|
|
6024
|
+
"alignSelf": "auto",
|
|
6025
|
+
"order": "0",
|
|
6026
|
+
"flexGrow": "0",
|
|
6027
|
+
"flexShrink": "1",
|
|
6028
|
+
"flexBasis": "auto",
|
|
6029
|
+
"justifySelf": "auto",
|
|
6030
|
+
"gridColumn": "auto",
|
|
6031
|
+
"gridRow": "auto",
|
|
6032
|
+
"width": "auto",
|
|
6033
|
+
"height": "auto",
|
|
6034
|
+
"minWidth": "auto",
|
|
6035
|
+
"minHeight": "auto",
|
|
6036
|
+
"maxWidth": "auto",
|
|
6037
|
+
"maxHeight": "auto",
|
|
6038
|
+
"aspectRatio": "auto",
|
|
6039
|
+
"objectFit": "cover",
|
|
6040
|
+
"overflow": "visible",
|
|
6041
|
+
"fontFamily": "Poppins, sans-serif",
|
|
6042
|
+
"fontSize": "16px",
|
|
6043
|
+
"fontWeight": "400",
|
|
6044
|
+
"color": "#000000",
|
|
6045
|
+
"lineHeight": "1.5",
|
|
6046
|
+
"letterSpacing": "0px",
|
|
6047
|
+
"textTransform": "none",
|
|
6048
|
+
"textAlign": "left",
|
|
6049
|
+
"fontStyle": "normal",
|
|
6050
|
+
"textDecoration": "none",
|
|
6051
|
+
"WebkitTextStrokeWidth": "0px",
|
|
6052
|
+
"WebkitTextStrokeColor": "transparent",
|
|
6053
|
+
"margin": "0px 0px 0px 0px",
|
|
6054
|
+
"padding": "0px 0px 0px 0px",
|
|
6055
|
+
"borderWidth": "0px",
|
|
6056
|
+
"borderStyle": "solid",
|
|
6057
|
+
"borderColor": "#CCCCCC",
|
|
6058
|
+
"borderRadius": "0px",
|
|
6059
|
+
"backgroundColor": "transparent",
|
|
6060
|
+
"opacity": "1",
|
|
6061
|
+
"mixBlendMode": "normal",
|
|
6062
|
+
"cursor": "auto",
|
|
6063
|
+
"boxShadow": "",
|
|
6064
|
+
"textShadow": "",
|
|
6065
|
+
"translateX": "0px",
|
|
6066
|
+
"translateY": "0px",
|
|
6067
|
+
"scaleX": "1",
|
|
6068
|
+
"scaleY": "1",
|
|
6069
|
+
"rotate": "0deg",
|
|
6070
|
+
"skewX": "0deg",
|
|
6071
|
+
"skewY": "0deg",
|
|
6072
|
+
"filterBlur": "0px",
|
|
6073
|
+
"filterHueRotate": "0deg",
|
|
6074
|
+
"filterBrightness": "1",
|
|
6075
|
+
"filterContrast": "1",
|
|
6076
|
+
"filterSaturate": "1",
|
|
6077
|
+
"filterGrayscale": "0",
|
|
6078
|
+
"filterSepia": "0",
|
|
6079
|
+
"filterInvert": "0",
|
|
6080
|
+
"backdropFilter": "none",
|
|
6081
|
+
"backdropBlur": "0px",
|
|
6082
|
+
"backdropBrightness": "1",
|
|
6083
|
+
"backdropContrast": "1",
|
|
6084
|
+
"backdropSaturate": "1",
|
|
6085
|
+
"clipPath": "none",
|
|
6086
|
+
"transitionDuration": "0ms",
|
|
6087
|
+
"transitionDelay": "0ms",
|
|
6088
|
+
"transitionTimingFunction": "ease"
|
|
6089
|
+
};
|
|
6090
|
+
|
|
6091
|
+
/**
|
|
6092
|
+
* Why each type's constructor sets the style values it sets (W1.1, Q1). Every value a type's config
|
|
6093
|
+
* gives that differs from what the trait saves when nobody sets it (`NEUTRAL_STYLE_PROPS`) is exactly
|
|
6094
|
+
* one of:
|
|
6095
|
+
*
|
|
6096
|
+
* - **Layout** — a size or spacing that exists only so an EMPTY node dropped from the palette is visible
|
|
6097
|
+
* and grabbable (a Container's 200px, a Text's 10px padding). A composed node is never empty and its
|
|
6098
|
+
* pattern owns its box, so the composer RELEASES these to the neutral value unless the pattern
|
|
6099
|
+
* declares one: the nav bar was 200px tall and every lead line indented 10px (W1's screenshot).
|
|
6100
|
+
* - **Behaviour** — what makes the type work: a button's inline-flex centring, a form's column, the 44px
|
|
6101
|
+
* touch target, a slide's width, a spacer's height, a list's marker room. Kept.
|
|
6102
|
+
* - **Look** — the type's pre-design-system paint, which its element style replaces. Kept (the style
|
|
6103
|
+
* writes over it; a type with no style keeps its pre-DS2 look, D5).
|
|
6104
|
+
*
|
|
6105
|
+
* Saved pages are untouched: this is the composer's rule, never the builder's (a palette drop still
|
|
6106
|
+
* gets its 200px). `baseline-roles.spec.ts` fails the moment a config gains a default this table does
|
|
6107
|
+
* not classify — so a new default is a decision, not an accident.
|
|
6108
|
+
*/
|
|
6109
|
+
var BaselineRole;
|
|
6110
|
+
(function (BaselineRole) {
|
|
6111
|
+
BaselineRole["Layout"] = "layout";
|
|
6112
|
+
BaselineRole["Behaviour"] = "behaviour";
|
|
6113
|
+
BaselineRole["Look"] = "look";
|
|
6114
|
+
})(BaselineRole || (BaselineRole = {}));
|
|
6115
|
+
const { Layout, Behaviour, Look } = BaselineRole;
|
|
6116
|
+
const BASELINE_ROLES = {
|
|
6117
|
+
Button: {},
|
|
6118
|
+
ButtonBlock: {
|
|
6119
|
+
display: Behaviour, justifyContent: Behaviour, alignItems: Behaviour, gap: Behaviour, cursor: Behaviour,
|
|
6120
|
+
padding: Look, borderRadius: Look,
|
|
6121
|
+
},
|
|
6122
|
+
Card: { width: Behaviour, maxWidth: Layout, padding: Look, borderWidth: Look, borderColor: Look, borderRadius: Look, backgroundColor: Look },
|
|
6123
|
+
// `width: 100%` (DEFAULT_WIDTH_BY_TYPE) is what lets a block fill its row — a flex child collapses without it.
|
|
6124
|
+
Container: { width: Behaviour, minHeight: Layout },
|
|
6125
|
+
Heading: { fontSize: Look, fontWeight: Look },
|
|
6126
|
+
Text: { width: Behaviour, minHeight: Layout, padding: Layout },
|
|
6127
|
+
Image: { width: Behaviour, maxWidth: Layout, aspectRatio: Layout },
|
|
6128
|
+
Link: {},
|
|
6129
|
+
LinkedContainer: { minHeight: Layout },
|
|
6130
|
+
Section: { minHeight: Layout },
|
|
6131
|
+
Icon: { lineHeight: Behaviour, fontSize: Behaviour, color: Look },
|
|
6132
|
+
Divider: { width: Behaviour, height: Look, margin: Layout, backgroundColor: Look },
|
|
6133
|
+
Spacer: { width: Behaviour, height: Behaviour },
|
|
6134
|
+
List: { width: Behaviour, padding: Behaviour },
|
|
6135
|
+
ListItem: { width: Behaviour },
|
|
6136
|
+
Repeater: { width: Behaviour },
|
|
6137
|
+
SliderContainer: { width: Behaviour },
|
|
6138
|
+
SliderSlide: { width: Behaviour, aspectRatio: Behaviour },
|
|
6139
|
+
// The crossfade stacks its frames absolutely: without a box it has no height at all.
|
|
6140
|
+
Gallery: { minHeight: Behaviour, backgroundColor: Look },
|
|
6141
|
+
Form: { display: Behaviour, flexDirection: Behaviour, gap: Behaviour, width: Behaviour },
|
|
6142
|
+
Field: { width: Behaviour },
|
|
6143
|
+
FieldGroup: { display: Behaviour, gap: Behaviour, width: Behaviour },
|
|
6144
|
+
SubmitButton: {
|
|
6145
|
+
display: Behaviour, justifyContent: Behaviour, alignItems: Behaviour, minHeight: Behaviour, cursor: Behaviour,
|
|
6146
|
+
color: Look, padding: Look, borderRadius: Look, backgroundColor: Look,
|
|
6147
|
+
},
|
|
6148
|
+
FormSuccess: { width: Behaviour, color: Look, padding: Look, borderRadius: Look, backgroundColor: Look },
|
|
6149
|
+
FormError: { width: Behaviour, color: Look, padding: Look, borderRadius: Look, backgroundColor: Look },
|
|
6150
|
+
Dialog: { width: Behaviour, padding: Look, borderRadius: Look, backgroundColor: Look, boxShadow: Look },
|
|
6151
|
+
};
|
|
6152
|
+
/** The style values a type's constructor sets for an empty palette drop, each mapped to the value the
|
|
6153
|
+
* trait saves when nobody sets it — what a composed node of that type starts from. */
|
|
6154
|
+
function releasedLayoutDefaults(type) {
|
|
6155
|
+
const roles = BASELINE_ROLES[type] ?? {};
|
|
6156
|
+
const out = {};
|
|
6157
|
+
for (const [trait, role] of Object.entries(roles)) {
|
|
6158
|
+
if (role === BaselineRole.Layout && trait in (NODE_BASELINES[type]?.styleProps ?? {}))
|
|
6159
|
+
out[trait] = NEUTRAL_STYLE_PROPS[trait];
|
|
6160
|
+
}
|
|
6161
|
+
return out;
|
|
6162
|
+
}
|
|
5947
6163
|
|
|
5948
6164
|
/**
|
|
5949
|
-
* The Site Plan (contract `site.planShape`
|
|
6165
|
+
* The Site Plan (contract `site.planShape`, `SPEC_SITE_PLAN_SHAPE`: every key typed and explained, and the
|
|
6166
|
+
* validator reads its key lists from it) — the ONE document the website wizard writes
|
|
5950
6167
|
* (`source.kind: template`) and the AI agent will write (`source.kind: ai`). One validator, one
|
|
5951
6168
|
* composer: everything the wizard proves is proven for the agent.
|
|
5952
6169
|
*/
|
|
@@ -5998,9 +6215,10 @@ var PatternDirective;
|
|
|
5998
6215
|
(function (PatternDirective) {
|
|
5999
6216
|
/** A Repeater's source: the section's `binding`. */
|
|
6000
6217
|
PatternDirective["Binding"] = "$binding";
|
|
6001
|
-
/**
|
|
6218
|
+
/** `each`: one plain node per page of the plan that is in the navigation (`inNav`), in plan order —
|
|
6219
|
+
* words and routes written in, editable on the canvas (W1.1). */
|
|
6002
6220
|
PatternDirective["NavPages"] = "$navPages";
|
|
6003
|
-
/** A Link's route: the
|
|
6221
|
+
/** A Link's route: the route of the page an `each` node is stamped for. */
|
|
6004
6222
|
PatternDirective["PageRoute"] = "$page.route";
|
|
6005
6223
|
})(PatternDirective || (PatternDirective = {}));
|
|
6006
6224
|
/** The scopes a `bind` / `param` / `src` path starts with. */
|
|
@@ -6008,7 +6226,7 @@ var BindScope;
|
|
|
6008
6226
|
(function (BindScope) {
|
|
6009
6227
|
/** The nearest Repeater's row. */
|
|
6010
6228
|
BindScope["Item"] = "item";
|
|
6011
|
-
/** The
|
|
6229
|
+
/** The page an `each: $navPages` node is stamped for — resolved to plain words, never a binding. */
|
|
6012
6230
|
BindScope["Page"] = "page";
|
|
6013
6231
|
/** The page's route item (`routeSource`). */
|
|
6014
6232
|
BindScope["Route"] = "route";
|
|
@@ -6041,11 +6259,26 @@ function patternOptionalParts(pattern) {
|
|
|
6041
6259
|
function patternNeedsBinding(pattern) {
|
|
6042
6260
|
return patternNodes(pattern).some(n => n.src === PatternDirective.Binding);
|
|
6043
6261
|
}
|
|
6262
|
+
const IMAGE_TYPE = 'Image';
|
|
6263
|
+
/** The scopes whose ITEM a pattern's bound Images take their alt text from (an Image that names no `alt`
|
|
6264
|
+
* of its own): `item` (the row of a `$binding` Repeater — the section's list), `route` or `next`. Each
|
|
6265
|
+
* needs a content type with a `previewTextField` (W1.1, Q7). */
|
|
6266
|
+
function altSourceScopes(pattern) {
|
|
6267
|
+
const scopes = new Set();
|
|
6268
|
+
const visit = (n) => {
|
|
6269
|
+
if (n.t === IMAGE_TYPE && n.bind && n.alt === undefined)
|
|
6270
|
+
scopes.add(n.bind.split('.')[0]);
|
|
6271
|
+
for (const child of n.c ?? [])
|
|
6272
|
+
visit(child);
|
|
6273
|
+
};
|
|
6274
|
+
visit(pattern.node);
|
|
6275
|
+
return scopes;
|
|
6276
|
+
}
|
|
6044
6277
|
/** The bind scopes a pattern reads (`route`, `next`, …) — what its page must provide. */
|
|
6045
6278
|
function patternScopes(pattern) {
|
|
6046
6279
|
const scopes = new Set();
|
|
6047
6280
|
for (const n of patternNodes(pattern)) {
|
|
6048
|
-
for (const path of [n.bind, n.param, n.src]) {
|
|
6281
|
+
for (const path of [n.bind, n.alt, n.param, n.src]) {
|
|
6049
6282
|
if (path && !path.startsWith('$'))
|
|
6050
6283
|
scopes.add(path.split('.')[0]);
|
|
6051
6284
|
}
|
|
@@ -6097,9 +6330,10 @@ const clone$1 = (value) => JSON.parse(JSON.stringify(value));
|
|
|
6097
6330
|
* A NEW node of `type`, as the builder creates one (ComponentRegistryService.createConfig, serialized):
|
|
6098
6331
|
* the type's constructor defaults, its text colour released to `inherit` (NEW_NODE_STYLE_DEFAULTS), then
|
|
6099
6332
|
* ONE element style — `elementStyle` when given, else the type's default for its props (a Heading
|
|
6100
|
-
* follows its level). `props` are applied before the style is chosen.
|
|
6333
|
+
* follows its level). `props` are applied before the style is chosen. With `releaseLayoutDefaults` the
|
|
6334
|
+
* constructor's layout sizes are released first, so an element style that owns one still writes it.
|
|
6101
6335
|
*/
|
|
6102
|
-
function createSerializedNode(id, type, props = {}, elementStyle) {
|
|
6336
|
+
function createSerializedNode(id, type, props = {}, elementStyle, options = {}) {
|
|
6103
6337
|
const baseline = NODE_BASELINES[type];
|
|
6104
6338
|
if (!baseline)
|
|
6105
6339
|
throw new ComposeError(`"${type}" is not a registered component type`);
|
|
@@ -6111,6 +6345,8 @@ function createSerializedNode(id, type, props = {}, elementStyle) {
|
|
|
6111
6345
|
bindings: [],
|
|
6112
6346
|
interactions: [],
|
|
6113
6347
|
};
|
|
6348
|
+
if (options.releaseLayoutDefaults)
|
|
6349
|
+
Object.assign(node.styleProps, releasedLayoutDefaults(type));
|
|
6114
6350
|
for (const [name, value] of Object.entries(NEW_NODE_STYLE_DEFAULTS))
|
|
6115
6351
|
if (name in node.styleProps)
|
|
6116
6352
|
node.styleProps[name] = value;
|
|
@@ -6182,6 +6418,9 @@ var SitePlanIssueCode;
|
|
|
6182
6418
|
SitePlanIssueCode["UnknownOptionalPart"] = "unknown-optional-part";
|
|
6183
6419
|
SitePlanIssueCode["Binding"] = "binding";
|
|
6184
6420
|
SitePlanIssueCode["MissingScope"] = "missing-scope";
|
|
6421
|
+
/** A pattern binds an item's image, and the item's content type names no `previewTextField` to be
|
|
6422
|
+
* its alt text. */
|
|
6423
|
+
SitePlanIssueCode["MissingPreviewField"] = "missing-preview-field";
|
|
6185
6424
|
})(SitePlanIssueCode || (SitePlanIssueCode = {}));
|
|
6186
6425
|
class SitePlanError extends Error {
|
|
6187
6426
|
constructor(issues) {
|
|
@@ -6189,23 +6428,34 @@ class SitePlanError extends Error {
|
|
|
6189
6428
|
this.issues = issues;
|
|
6190
6429
|
}
|
|
6191
6430
|
}
|
|
6192
|
-
/** The alias of the static source the composer writes for the navigation (`$navPages`). A plan source
|
|
6193
|
-
* may not take it. */
|
|
6194
|
-
const NAV_PAGES_ALIAS = 'navPages';
|
|
6195
6431
|
/** Route-scope aliases (`$route`, `$current`, …) are platform-owned. */
|
|
6196
6432
|
const RESERVED_PREFIX = '$';
|
|
6197
6433
|
const ID_FIELD = '_id';
|
|
6198
|
-
const
|
|
6199
|
-
const
|
|
6200
|
-
const
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6434
|
+
const OPTIONAL_MARK = '?';
|
|
6435
|
+
const DYNAMIC_KEY = /^\[[^\]]+\]\??$/;
|
|
6436
|
+
const isDynamic = (key) => DYNAMIC_KEY.test(key);
|
|
6437
|
+
/** Every fixed key an object of the shape may carry. */
|
|
6438
|
+
function shapeKeys(entry) {
|
|
6439
|
+
return Object.keys(entry).filter(k => !isDynamic(k)).map(k => (k.endsWith(OPTIONAL_MARK) ? k.slice(0, -1) : k));
|
|
6440
|
+
}
|
|
6441
|
+
/** The keys an object of the shape must carry. */
|
|
6442
|
+
function requiredShapeKeys(entry) {
|
|
6443
|
+
return Object.keys(entry).filter(k => !isDynamic(k) && !k.endsWith(OPTIONAL_MARK));
|
|
6444
|
+
}
|
|
6445
|
+
const SHAPE = SPEC_SITE_PLAN_SHAPE;
|
|
6446
|
+
const PLAN_KEYS = shapeKeys(SHAPE['plan']);
|
|
6447
|
+
const TEMPLATE_SOURCE_KEYS = shapeKeys(SHAPE['sourceTemplate']);
|
|
6448
|
+
const AI_SOURCE_KEYS = shapeKeys(SHAPE['sourceAi']);
|
|
6449
|
+
const BUSINESS_KEYS = shapeKeys(SHAPE['business']);
|
|
6450
|
+
const DESIGN_REQUIRED = requiredShapeKeys(SHAPE['design']);
|
|
6451
|
+
const DESIGN_KEYS = shapeKeys(SHAPE['design']);
|
|
6452
|
+
const CONTENT_TYPE_KEYS = shapeKeys(SHAPE['contentType']);
|
|
6453
|
+
const FIELD_KEYS = shapeKeys(SHAPE['field']);
|
|
6454
|
+
const SEED_FIXED_KEYS = shapeKeys(SHAPE['seed']);
|
|
6455
|
+
const CMS_SOURCE_KEYS = shapeKeys(SHAPE['cmsDataSource']);
|
|
6456
|
+
const RELATIVE_SOURCE_KEYS = shapeKeys(SHAPE['relativeDataSource']);
|
|
6457
|
+
const PAGE_KEYS = shapeKeys(SHAPE['page']);
|
|
6458
|
+
const SECTION_KEYS = shapeKeys(SHAPE['section']);
|
|
6209
6459
|
const GLOBAL_POSITION = {
|
|
6210
6460
|
[GlobalSlot.Header]: PatternGlobalPosition.Before,
|
|
6211
6461
|
[GlobalSlot.Footer]: PatternGlobalPosition.After,
|
|
@@ -6269,13 +6519,13 @@ function validateSource(c, source) {
|
|
|
6269
6519
|
if (!isObject(source))
|
|
6270
6520
|
return c.add(SitePlanIssueCode.Shape, 'source', 'must be an object');
|
|
6271
6521
|
if (source['kind'] === SitePlanSourceKind.Template) {
|
|
6272
|
-
c.keys(source,
|
|
6522
|
+
c.keys(source, TEMPLATE_SOURCE_KEYS, 'source');
|
|
6273
6523
|
c.string(source, 'templateId', 'source');
|
|
6274
6524
|
if (!Number.isInteger(source['templateVersion']))
|
|
6275
6525
|
c.add(SitePlanIssueCode.Shape, 'source.templateVersion', 'must be an integer');
|
|
6276
6526
|
}
|
|
6277
6527
|
else if (source['kind'] === SitePlanSourceKind.Ai) {
|
|
6278
|
-
c.keys(source,
|
|
6528
|
+
c.keys(source, AI_SOURCE_KEYS, 'source');
|
|
6279
6529
|
c.string(source, 'runId', 'source');
|
|
6280
6530
|
}
|
|
6281
6531
|
else {
|
|
@@ -6357,10 +6607,12 @@ function validateContentTypes(c, list) {
|
|
|
6357
6607
|
});
|
|
6358
6608
|
c.unique(names, j => `${path}.fields[${j}].name`, 'field');
|
|
6359
6609
|
const fieldSet = new Set(names.filter((n) => !!n));
|
|
6610
|
+
const preview = {};
|
|
6360
6611
|
for (const key of ['previewTextField', 'previewImageField']) {
|
|
6361
6612
|
const v = c.string(raw, key, path, false);
|
|
6362
6613
|
if (v && !fieldSet.has(v))
|
|
6363
6614
|
c.add(SitePlanIssueCode.UnknownField, `${path}.${key}`, `"${v}" is not a field of ${name}`);
|
|
6615
|
+
preview[key] = v && fieldSet.has(v) ? v : null;
|
|
6364
6616
|
}
|
|
6365
6617
|
const seed = raw['seed'];
|
|
6366
6618
|
if (seed !== undefined) {
|
|
@@ -6381,7 +6633,7 @@ function validateContentTypes(c, list) {
|
|
|
6381
6633
|
if (name && types.has(name))
|
|
6382
6634
|
c.add(SitePlanIssueCode.Duplicate, `${path}.name`, `content type "${name}" is used twice`);
|
|
6383
6635
|
else if (name)
|
|
6384
|
-
types.set(name, { fields: fieldSet });
|
|
6636
|
+
types.set(name, { fields: fieldSet, previewTextField: preview['previewTextField'] ?? undefined });
|
|
6385
6637
|
});
|
|
6386
6638
|
return types;
|
|
6387
6639
|
}
|
|
@@ -6392,7 +6644,7 @@ function validateDataSources(c, list, types) {
|
|
|
6392
6644
|
if (!isObject(raw))
|
|
6393
6645
|
return c.add(SitePlanIssueCode.Shape, path, 'must be an object');
|
|
6394
6646
|
const alias = c.string(raw, 'alias', path);
|
|
6395
|
-
if (alias &&
|
|
6647
|
+
if (alias && alias.startsWith(RESERVED_PREFIX)) {
|
|
6396
6648
|
c.add(SitePlanIssueCode.ReservedAlias, `${path}.alias`, `"${alias}" is reserved for the platform`);
|
|
6397
6649
|
}
|
|
6398
6650
|
if (raw['type'] === PlanDataSourceType.Cms) {
|
|
@@ -6557,6 +6809,15 @@ function validatePages(c, list, types, sources) {
|
|
|
6557
6809
|
if (scopes.has(BindScope.Next) && routeSourceOk && !nextSourceOf(sources, routeSourceAlias)) {
|
|
6558
6810
|
c.add(SitePlanIssueCode.MissingScope, `${sp}.pattern`, `"${patternId}" reads the next item — the plan needs a ${RelativeOp.SiblingsNext} source of ${routeSourceAlias}`);
|
|
6559
6811
|
}
|
|
6812
|
+
// A bound image's alt text is its item's previewTextField (W1.1): the item's type must name one.
|
|
6813
|
+
for (const scope of altSourceScopes(pattern)) {
|
|
6814
|
+
const alias = scope === BindScope.Item ? binding : routeSourceOk ? routeSourceAlias : null;
|
|
6815
|
+
const typeName = alias ? sources.get(alias)?.contentType : undefined;
|
|
6816
|
+
const type = typeName ? types.get(typeName) : undefined;
|
|
6817
|
+
if (type && !type.previewTextField) {
|
|
6818
|
+
c.add(SitePlanIssueCode.MissingPreviewField, `${sp}.pattern`, `"${patternId}" shows ${typeName} images — ${typeName} needs a previewTextField for their alt text`);
|
|
6819
|
+
}
|
|
6820
|
+
}
|
|
6560
6821
|
});
|
|
6561
6822
|
});
|
|
6562
6823
|
c.unique(list.map(p => (isObject(p) && isNonEmptyString(p['key']) ? p['key'] : null)), i => `pages[${i}].key`, 'page key');
|
|
@@ -6613,10 +6874,12 @@ function assertValidSitePlan(plan) {
|
|
|
6613
6874
|
/**
|
|
6614
6875
|
* THE composer — a Site Plan in, ordinary IOX data out (design-system plan §6b, W1). Deterministic and
|
|
6615
6876
|
* pure: the same plan composes to byte-identical output, with no clock, no randomness and no transport.
|
|
6616
|
-
* The wizard and the AI agent both end here; W2's site-build service writes what it returns.
|
|
6877
|
+
* The wizard and the AI agent both end here; W2's site-build service writes what it returns. The one
|
|
6878
|
+
* thing that changes with time — the copyright year — comes in through `ComposeContext`.
|
|
6617
6879
|
*
|
|
6618
6880
|
* Every node is created the way the builder creates one (`createSerializedNode`): the type's full
|
|
6619
|
-
* constructor defaults
|
|
6881
|
+
* constructor defaults — its empty-drop sizes released, so the pattern owns its box (`BASELINE_ROLES`) —
|
|
6882
|
+
* then ONE element style, then the pattern's layout. So a composed page is what
|
|
6620
6883
|
* an author would have built by hand from the palette — the builder reloads it and saves it unchanged,
|
|
6621
6884
|
* and the engine renders it with nothing to resolve. Never a colour, size or font a pattern did not
|
|
6622
6885
|
* name; never a fact the customer did not give (blank → a placeholder carrying the fact's label).
|
|
@@ -6635,9 +6898,18 @@ var ComposeIssueCode;
|
|
|
6635
6898
|
/** A slot the plan and the business left empty and the pattern gives no words for — the node keeps
|
|
6636
6899
|
* its type's default text. */
|
|
6637
6900
|
ComposeIssueCode["UnfilledSlot"] = "unfilled-slot";
|
|
6638
|
-
/** A
|
|
6639
|
-
ComposeIssueCode["
|
|
6901
|
+
/** A link to a page the plan leaves out now opens an email to the business (`mailto:`). */
|
|
6902
|
+
ComposeIssueCode["LinkToEmail"] = "link-to-email";
|
|
6903
|
+
/** A link or button to a page the plan leaves out, with no business email to send to: removed. */
|
|
6904
|
+
ComposeIssueCode["LinkRemoved"] = "link-removed";
|
|
6905
|
+
/** A linked box to a page the plan leaves out, with no business email: it keeps its contents as a
|
|
6906
|
+
* plain Container and loses only the link. */
|
|
6907
|
+
ComposeIssueCode["LinkUnlinked"] = "link-unlinked";
|
|
6640
6908
|
})(ComposeIssueCode || (ComposeIssueCode = {}));
|
|
6909
|
+
/** The issues that are a dangling link (a route the plan has no page for) and what was done about it. */
|
|
6910
|
+
const DANGLING_LINK_CODES = new Set([
|
|
6911
|
+
ComposeIssueCode.LinkToEmail, ComposeIssueCode.LinkRemoved, ComposeIssueCode.LinkUnlinked,
|
|
6912
|
+
]);
|
|
6641
6913
|
/** `{sampleImage:k}` — a bundled abstract sample image, never a stock photo (plan §6b). */
|
|
6642
6914
|
const SAMPLE_IMAGE_REF = /^\{sampleImage:(\d+)\}$/;
|
|
6643
6915
|
const SAMPLE_IMAGE_TEMPLATE = /\{sampleImage:n(?:\+(\d+))?\}/g;
|
|
@@ -6664,19 +6936,38 @@ const BIND_PROP = {
|
|
|
6664
6936
|
Link: 'label',
|
|
6665
6937
|
Image: 'src',
|
|
6666
6938
|
};
|
|
6667
|
-
|
|
6668
|
-
|
|
6939
|
+
const LINK_SLOTS = {
|
|
6940
|
+
Link: { url: 'url' },
|
|
6941
|
+
LinkedContainer: { url: 'url' },
|
|
6942
|
+
ButtonBlock: { url: 'href', fixed: { tag: 'a' } },
|
|
6943
|
+
};
|
|
6944
|
+
/** What a linked box becomes when its link has nowhere to go: the same box, unlinked. */
|
|
6945
|
+
const UNLINKED_TYPE = { LinkedContainer: 'Container' };
|
|
6669
6946
|
const BUTTON_BLOCK = 'ButtonBlock';
|
|
6670
6947
|
const FORM = 'Form';
|
|
6671
|
-
const
|
|
6672
|
-
const
|
|
6673
|
-
const
|
|
6948
|
+
const FIELD = 'Field';
|
|
6949
|
+
const IMAGE = 'Image';
|
|
6950
|
+
const SUBMIT_BUTTON = 'SubmitButton';
|
|
6951
|
+
const FORM_SUCCESS = 'FormSuccess';
|
|
6952
|
+
const FORM_ERROR = 'FormError';
|
|
6953
|
+
const FORM_STATE_BLOCKS = [FORM_SUCCESS, FORM_ERROR];
|
|
6954
|
+
/** The Field traits a preset field fills, 1:1 (`FormPresetField`). */
|
|
6955
|
+
const PRESET_FIELD_PROPS = ['label', 'control', 'required', 'help', 'placeholder', 'optionsText', 'rows'];
|
|
6674
6956
|
const LABEL_TAG = 'span';
|
|
6675
6957
|
const TEXT = 'Text';
|
|
6676
|
-
/** A button's label reads the button's type and ink (
|
|
6677
|
-
const
|
|
6958
|
+
/** A button's label reads the button's own type and ink (contract element style, W1.1 Q6). */
|
|
6959
|
+
const BUTTON_LABEL_STYLE = 'button-label';
|
|
6678
6960
|
const ITEM_VALUE_PATH = '$';
|
|
6679
6961
|
const STATUS_DRAFT = 'draft';
|
|
6962
|
+
const MAILTO = 'mailto:';
|
|
6963
|
+
/** A pattern's `alt: ''` — the image is decorative and its alt is written empty. */
|
|
6964
|
+
const DECORATIVE_ALT = '';
|
|
6965
|
+
/** What the composer writes for every node: the pattern owns the box (W1.1, Q1). */
|
|
6966
|
+
const COMPOSED = { releaseLayoutDefaults: true };
|
|
6967
|
+
/** The words a pattern may leave for compose time: the year (`ComposeContext`) and the business name. */
|
|
6968
|
+
const TEXT_VARIABLE = /\{(year|business\.name)\}/g;
|
|
6969
|
+
/** What `page.*` may read on a node stamped for a navigation page. */
|
|
6970
|
+
const STAMPED_PAGE_FIELDS = new Set(['name', 'route']);
|
|
6680
6971
|
const clone = (value) => JSON.parse(JSON.stringify(value));
|
|
6681
6972
|
/** 32-bit FNV-1a, base 36 — a short stable id fragment. */
|
|
6682
6973
|
function stableHash(text) {
|
|
@@ -6687,15 +6978,14 @@ function stableHash(text) {
|
|
|
6687
6978
|
}
|
|
6688
6979
|
return h.toString(36).padStart(7, '0').slice(-6);
|
|
6689
6980
|
}
|
|
6690
|
-
|
|
6691
|
-
* `/` → `./`), so a link stays inside the org's `<base href>`. */
|
|
6692
|
-
function baseRelativeHref(route) {
|
|
6693
|
-
const path = route.replace(/\/{2,}/g, '/').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
6694
|
-
return path || './';
|
|
6695
|
-
}
|
|
6981
|
+
const create = (id, type, props = {}, elementStyle) => createSerializedNode(id, type, props, elementStyle, COMPOSED);
|
|
6696
6982
|
function nonEmpty(v) {
|
|
6697
6983
|
return typeof v === 'string' && v.trim() !== '';
|
|
6698
6984
|
}
|
|
6985
|
+
/** A pattern's own words with the compose-time variables filled: `{year}`, `{business.name}`. */
|
|
6986
|
+
function fillVariables(text, ctx) {
|
|
6987
|
+
return text.replace(TEXT_VARIABLE, (_m, name) => (name === 'year' ? String(ctx.year) : ctx.plan.business.name));
|
|
6988
|
+
}
|
|
6699
6989
|
/** Where a node's words come from: the plan's slot → the business → the customer's fact → the pattern's
|
|
6700
6990
|
* own text. A missing fact shows its label and is a placeholder; nothing is ever made up. */
|
|
6701
6991
|
function chooseText(pn, ctx) {
|
|
@@ -6712,17 +7002,17 @@ function chooseText(pn, ctx) {
|
|
|
6712
7002
|
const given = ctx.plan.business.facts?.[fact];
|
|
6713
7003
|
return nonEmpty(given) ? { value: given } : { value: SITE_FACT_LABELS[fact], placeholder: { kind: PlaceholderKind.Fact, fact } };
|
|
6714
7004
|
}
|
|
7005
|
+
const own = pn.text === undefined ? undefined : fillVariables(pn.text, ctx);
|
|
6715
7006
|
if (pn.ph === PatternPlaceholder.Copy)
|
|
6716
|
-
return { value:
|
|
6717
|
-
return { value:
|
|
7007
|
+
return { value: own, placeholder: { kind: PlaceholderKind.Copy } };
|
|
7008
|
+
return { value: own };
|
|
6718
7009
|
}
|
|
6719
|
-
/** A `bind` / `param` / `src` path → the binding source + path, in this node's scope. */
|
|
7010
|
+
/** A `bind` / `alt` / `param` / `src` path → the binding source + path, in this node's scope. */
|
|
6720
7011
|
function resolveScoped(path, repeater, ctx, nodeId) {
|
|
6721
7012
|
const [scope, ...rest] = path.split('.');
|
|
6722
7013
|
const sub = rest.join('.');
|
|
6723
7014
|
switch (scope) {
|
|
6724
7015
|
case BindScope.Item:
|
|
6725
|
-
case BindScope.Page:
|
|
6726
7016
|
if (!repeater)
|
|
6727
7017
|
throw new ComposeError(`${nodeId}: "${path}" outside a Repeater`);
|
|
6728
7018
|
return { source: repeater.itemAlias, path: sub || ITEM_VALUE_PATH };
|
|
@@ -6738,6 +7028,25 @@ function resolveScoped(path, repeater, ctx, nodeId) {
|
|
|
6738
7028
|
throw new ComposeError(`${nodeId}: unknown binding scope "${scope}"`);
|
|
6739
7029
|
}
|
|
6740
7030
|
}
|
|
7031
|
+
/** `page.<field>` on a node stamped for a navigation page: that page's own words, written in. */
|
|
7032
|
+
function stampedPageValue(path, ctx, nodeId) {
|
|
7033
|
+
const field = path.split('.').slice(1).join('.');
|
|
7034
|
+
if (!ctx.navPage)
|
|
7035
|
+
throw new ComposeError(`${nodeId}: "${path}" on a node that is not stamped per page (each)`);
|
|
7036
|
+
if (!STAMPED_PAGE_FIELDS.has(field))
|
|
7037
|
+
throw new ComposeError(`${nodeId}: a page has no "${field}" (${[...STAMPED_PAGE_FIELDS].join(', ')})`);
|
|
7038
|
+
return ctx.navPage[field];
|
|
7039
|
+
}
|
|
7040
|
+
const isPageScope = (path) => !!path && path.split('.')[0] === BindScope.Page;
|
|
7041
|
+
/** The content type behind a data source alias (a relative source reads its `within` list's type). */
|
|
7042
|
+
function contentTypeOfSource(alias, plan) {
|
|
7043
|
+
const ds = plan.dataSources.find(d => d.alias === alias);
|
|
7044
|
+
if (!ds)
|
|
7045
|
+
return undefined;
|
|
7046
|
+
if (ds.type === PlanDataSourceType.Relative)
|
|
7047
|
+
return contentTypeOfSource(ds.within, plan);
|
|
7048
|
+
return plan.contentTypes.find(t => t.name === ds.contentType);
|
|
7049
|
+
}
|
|
6741
7050
|
function addBinding(node, binding) {
|
|
6742
7051
|
const declared = NODE_BASELINES[node.type]?.traits ?? [];
|
|
6743
7052
|
if (!declared.includes(binding.prop) && !binding.prop.startsWith('routeParam:')) {
|
|
@@ -6755,10 +7064,136 @@ function setProp(node, name, value) {
|
|
|
6755
7064
|
assertTraits(node.type, [name]);
|
|
6756
7065
|
node.props[name] = value;
|
|
6757
7066
|
}
|
|
6758
|
-
|
|
7067
|
+
function linkSlots(node) {
|
|
7068
|
+
const slots = LINK_SLOTS[node.type];
|
|
7069
|
+
if (!slots)
|
|
7070
|
+
throw new ComposeError(`${node.id}: ${node.type} cannot link`);
|
|
7071
|
+
return slots;
|
|
7072
|
+
}
|
|
7073
|
+
/** A link to one of the site's pages, by route — the builder shows the route, the engine resolves it. */
|
|
7074
|
+
function writePageLink(node, route) {
|
|
7075
|
+
const slots = linkSlots(node);
|
|
7076
|
+
for (const [k, v] of Object.entries(slots.fixed ?? {}))
|
|
7077
|
+
setProp(node, k, v);
|
|
7078
|
+
setProp(node, 'linkType', LinkType.Page);
|
|
7079
|
+
setProp(node, 'pageRoute', route);
|
|
7080
|
+
}
|
|
7081
|
+
function writeExternalLink(node, url) {
|
|
7082
|
+
const slots = linkSlots(node);
|
|
7083
|
+
for (const [k, v] of Object.entries(slots.fixed ?? {}))
|
|
7084
|
+
setProp(node, k, v);
|
|
7085
|
+
setProp(node, 'linkType', LinkType.External);
|
|
7086
|
+
setProp(node, slots.url, url);
|
|
7087
|
+
}
|
|
7088
|
+
/** What happens to a node linking to a route the plan has no page for (W1.1, Q11) — the same rule for
|
|
7089
|
+
* every pattern and every page: an email to the business when one is given; else a link or button is
|
|
7090
|
+
* removed and a linked box keeps its contents, unlinked. */
|
|
7091
|
+
var DanglingFate;
|
|
7092
|
+
(function (DanglingFate) {
|
|
7093
|
+
DanglingFate["Email"] = "email";
|
|
7094
|
+
DanglingFate["Remove"] = "remove";
|
|
7095
|
+
DanglingFate["Unlink"] = "unlink";
|
|
7096
|
+
})(DanglingFate || (DanglingFate = {}));
|
|
7097
|
+
function danglingFate(pn, ctx) {
|
|
7098
|
+
if (!pn.href || pn.href === PatternDirective.PageRoute || ctx.routes.has(pn.href))
|
|
7099
|
+
return null;
|
|
7100
|
+
if (ctx.email)
|
|
7101
|
+
return DanglingFate.Email;
|
|
7102
|
+
return UNLINKED_TYPE[pn.t] ? DanglingFate.Unlink : DanglingFate.Remove;
|
|
7103
|
+
}
|
|
7104
|
+
const DANGLING_ISSUE = {
|
|
7105
|
+
[DanglingFate.Email]: ComposeIssueCode.LinkToEmail,
|
|
7106
|
+
[DanglingFate.Remove]: ComposeIssueCode.LinkRemoved,
|
|
7107
|
+
[DanglingFate.Unlink]: ComposeIssueCode.LinkUnlinked,
|
|
7108
|
+
};
|
|
7109
|
+
function reportDangling(fate, pn, id, words, ctx) {
|
|
7110
|
+
const what = words ? `"${words}"` : `A ${pn.t === BUTTON_BLOCK ? 'button' : 'link'}`;
|
|
7111
|
+
const outcome = {
|
|
7112
|
+
[DanglingFate.Email]: `it now opens an email to ${ctx.email}`,
|
|
7113
|
+
[DanglingFate.Remove]: 'with no email address to send to instead, it was removed',
|
|
7114
|
+
[DanglingFate.Unlink]: 'with no email address to send to instead, the box keeps its contents without the link',
|
|
7115
|
+
}[fate];
|
|
7116
|
+
ctx.issues.push({
|
|
7117
|
+
code: DANGLING_ISSUE[fate], nodeId: id, owner: ctx.owner, route: pn.href,
|
|
7118
|
+
detail: `${what} linked to ${pn.href}, which this site has no page for — ${outcome}.`,
|
|
7119
|
+
});
|
|
7120
|
+
}
|
|
7121
|
+
/** The alt binding of a bound Image: the pattern's `alt`, else the bound item's preview text field. */
|
|
7122
|
+
function altBinding(pn, repeater, ctx, id) {
|
|
7123
|
+
if (pn.alt)
|
|
7124
|
+
return resolveScoped(pn.alt, repeater, ctx, id);
|
|
7125
|
+
const bound = resolveScoped(pn.bind, repeater, ctx, id);
|
|
7126
|
+
const field = contentTypeOfSource(bound.source, ctx.plan)?.previewTextField;
|
|
7127
|
+
if (!field)
|
|
7128
|
+
throw new ComposeError(`${id}: a bound image needs an alt — the pattern names none and "${bound.source}" has no previewTextField`);
|
|
7129
|
+
return { source: bound.source, path: field };
|
|
7130
|
+
}
|
|
7131
|
+
/** A preset Form's children — the forms-core preset's fields, submit and the WORDS of both state blocks. */
|
|
7132
|
+
function presetChildren(preset, id) {
|
|
7133
|
+
const fields = preset.fields.map((f, i) => {
|
|
7134
|
+
const props = Object.fromEntries(PRESET_FIELD_PROPS.filter(k => f[k] !== undefined).map(k => [k, f[k]]));
|
|
7135
|
+
assertTraits(FIELD, Object.keys(props));
|
|
7136
|
+
return create(`${id}-${i}`, FIELD, props);
|
|
7137
|
+
});
|
|
7138
|
+
const n = fields.length;
|
|
7139
|
+
const submit = create(`${id}-${n}`, SUBMIT_BUTTON, { label: preset.submitLabel });
|
|
7140
|
+
const blocks = [[FORM_SUCCESS, preset.successText], [FORM_ERROR, preset.errorText]].map(([type, words], i) => {
|
|
7141
|
+
const block = create(`${id}-${n + 1 + i}`, type);
|
|
7142
|
+
block.children = [create(`${id}-${n + 1 + i}-0`, TEXT, { content: words })];
|
|
7143
|
+
return block;
|
|
7144
|
+
});
|
|
7145
|
+
return [...fields, submit, ...blocks];
|
|
7146
|
+
}
|
|
7147
|
+
/** A Form: its stable id, its preset's name and children (or the pattern's own children), and both state
|
|
7148
|
+
* blocks — never optional (forms plan D7). */
|
|
7149
|
+
function composeForm(node, pn, id, ctx) {
|
|
7150
|
+
if (!pn.preset) {
|
|
7151
|
+
node.children = [...(node.children ?? []), ...FORM_STATE_BLOCKS.map((type, i) => create(`${id}-${(pn.c?.length ?? 0) + i}`, type))];
|
|
7152
|
+
return;
|
|
7153
|
+
}
|
|
7154
|
+
const preset = formPresetById(pn.preset);
|
|
7155
|
+
if (!preset)
|
|
7156
|
+
throw new ComposeError(`${id}: no form preset "${pn.preset}" in forms-core`);
|
|
7157
|
+
if (pn.c?.length)
|
|
7158
|
+
throw new ComposeError(`${id}: a preset Form takes its fields from the preset, not the pattern`);
|
|
7159
|
+
setProp(node, 'name', preset.formName);
|
|
7160
|
+
setProp(node, 'formId', `${preset.id}-${stableHash(`${ctx.owner}/${id}`)}`);
|
|
7161
|
+
node.children = presetChildren(preset, id);
|
|
7162
|
+
}
|
|
7163
|
+
/** A pattern node's children; an `each` child is stamped once per navigation page, its id carrying the
|
|
7164
|
+
* page key so it survives a page being left out or reordered. */
|
|
7165
|
+
function composeChildren(pn, id, repeater, ctx) {
|
|
7166
|
+
const out = [];
|
|
7167
|
+
(pn.c ?? []).forEach((child, i) => {
|
|
7168
|
+
const childId = `${id}-${i}`;
|
|
7169
|
+
if (!child.each) {
|
|
7170
|
+
const node = composeNode(child, childId, repeater, ctx);
|
|
7171
|
+
if (node)
|
|
7172
|
+
out.push(node);
|
|
7173
|
+
return;
|
|
7174
|
+
}
|
|
7175
|
+
if (child.each !== PatternDirective.NavPages)
|
|
7176
|
+
throw new ComposeError(`${childId}: "each" stamps "${PatternDirective.NavPages}" only`);
|
|
7177
|
+
for (const page of ctx.plan.pages.filter(p => p.inNav)) {
|
|
7178
|
+
const node = composeNode(child, `${childId}-${page.key}`, repeater, { ...ctx, navPage: page });
|
|
7179
|
+
if (node)
|
|
7180
|
+
out.push(node);
|
|
7181
|
+
}
|
|
7182
|
+
});
|
|
7183
|
+
return out;
|
|
7184
|
+
}
|
|
7185
|
+
/** One pattern node → one serialized node (and its subtree). `null` when the section leaves it out or a
|
|
7186
|
+
* dangling link is removed. */
|
|
6759
7187
|
function composeNode(pn, id, repeater, ctx) {
|
|
6760
7188
|
if (pn.optional && ctx.section?.without?.includes(pn.optional))
|
|
6761
7189
|
return null;
|
|
7190
|
+
const words = chooseText(pn, ctx);
|
|
7191
|
+
const fate = danglingFate(pn, ctx);
|
|
7192
|
+
if (fate === DanglingFate.Remove) {
|
|
7193
|
+
reportDangling(fate, pn, id, words.value, ctx);
|
|
7194
|
+
return null;
|
|
7195
|
+
}
|
|
7196
|
+
const type = fate === DanglingFate.Unlink ? UNLINKED_TYPE[pn.t] : pn.t;
|
|
6762
7197
|
const props = {};
|
|
6763
7198
|
if (pn.level)
|
|
6764
7199
|
props['level'] = pn.level;
|
|
@@ -6772,32 +7207,29 @@ function composeNode(pn, id, repeater, ctx) {
|
|
|
6772
7207
|
props['label'] = pn.label;
|
|
6773
7208
|
if (pn.required !== undefined)
|
|
6774
7209
|
props['required'] = pn.required;
|
|
6775
|
-
assertTraits(
|
|
7210
|
+
assertTraits(type, Object.keys(props));
|
|
6776
7211
|
// Props first: a Heading with no `es` wears its LEVEL's default style.
|
|
6777
|
-
const node =
|
|
7212
|
+
const node = create(id, type, props, pn.es ?? null);
|
|
6778
7213
|
// Words.
|
|
6779
|
-
const words = chooseText(pn, ctx);
|
|
6780
7214
|
if (words.placeholder) {
|
|
6781
|
-
const shown = words.value ?? String(NODE_BASELINES[
|
|
7215
|
+
const shown = words.value ?? String(NODE_BASELINES[type]?.props[TEXT_PROP[type]] ?? '');
|
|
6782
7216
|
ctx.placeholders.push({ nodeId: id, owner: ctx.owner, kind: words.placeholder.kind, ...(words.placeholder.fact ? { fact: words.placeholder.fact } : {}), ...(pn.slot ? { slot: pn.slot } : {}), text: shown });
|
|
6783
7217
|
}
|
|
6784
7218
|
else if (words.value === undefined && (pn.slot || pn.fromBusiness)) {
|
|
6785
|
-
ctx.issues.push({ code: ComposeIssueCode.UnfilledSlot, nodeId: id, owner: ctx.owner, detail: `no value for ${pn.slot ? `slot "${pn.slot}"` : `business "${pn.fromBusiness}"`} — keeps the ${
|
|
7219
|
+
ctx.issues.push({ code: ComposeIssueCode.UnfilledSlot, nodeId: id, owner: ctx.owner, detail: `no value for ${pn.slot ? `slot "${pn.slot}"` : `business "${pn.fromBusiness}"`} — keeps the ${type} default` });
|
|
6786
7220
|
}
|
|
6787
7221
|
if (words.value !== undefined) {
|
|
6788
|
-
if (
|
|
6789
|
-
|
|
6790
|
-
setStyleProps(label, BUTTON_LABEL_INHERITS);
|
|
6791
|
-
node.children = [label];
|
|
7222
|
+
if (type === BUTTON_BLOCK) {
|
|
7223
|
+
node.children = [create(`${id}-label`, TEXT, { content: words.value, tag: LABEL_TAG }, BUTTON_LABEL_STYLE)];
|
|
6792
7224
|
}
|
|
6793
7225
|
else {
|
|
6794
|
-
const prop = TEXT_PROP[
|
|
7226
|
+
const prop = TEXT_PROP[type];
|
|
6795
7227
|
if (!prop)
|
|
6796
|
-
throw new ComposeError(`${id}: ${
|
|
7228
|
+
throw new ComposeError(`${id}: ${type} cannot hold text`);
|
|
6797
7229
|
setProp(node, prop, words.value);
|
|
6798
7230
|
}
|
|
6799
7231
|
}
|
|
6800
|
-
// Images.
|
|
7232
|
+
// Images: an empty slot, or a bound one — both fall back to the DS3 empty image, never a stock URL.
|
|
6801
7233
|
if (pn.ph === PatternPlaceholder.Image) {
|
|
6802
7234
|
setProp(node, 'src', '');
|
|
6803
7235
|
ctx.placeholders.push({ nodeId: id, owner: ctx.owner, kind: PlaceholderKind.Image, text: '' });
|
|
@@ -6805,38 +7237,41 @@ function composeNode(pn, id, repeater, ctx) {
|
|
|
6805
7237
|
if (pn.aspect)
|
|
6806
7238
|
setStyleProps(node, { aspectRatio: pn.aspect });
|
|
6807
7239
|
// Links.
|
|
6808
|
-
if (pn.href) {
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
else {
|
|
6814
|
-
if (!ctx.routes.has(pn.href))
|
|
6815
|
-
ctx.issues.push({ code: ComposeIssueCode.DanglingLink, nodeId: id, owner: ctx.owner, detail: `links to ${pn.href}, which the plan has no page for` });
|
|
6816
|
-
if (PAGE_LINK_TYPES.has(pn.t)) {
|
|
6817
|
-
setProp(node, 'linkType', LINK_TYPE_PAGE);
|
|
6818
|
-
setProp(node, 'pageRoute', pn.href);
|
|
6819
|
-
}
|
|
6820
|
-
else if (pn.t === BUTTON_BLOCK) {
|
|
6821
|
-
setProp(node, 'tag', ANCHOR_TAG);
|
|
6822
|
-
setProp(node, 'href', baseRelativeHref(pn.href));
|
|
6823
|
-
}
|
|
6824
|
-
else {
|
|
6825
|
-
throw new ComposeError(`${id}: ${pn.t} cannot link`);
|
|
6826
|
-
}
|
|
6827
|
-
}
|
|
7240
|
+
if (pn.href === PatternDirective.PageRoute) {
|
|
7241
|
+
writePageLink(node, stampedPageValue(`${BindScope.Page}.route`, ctx, id));
|
|
7242
|
+
}
|
|
7243
|
+
else if (pn.href && !fate) {
|
|
7244
|
+
writePageLink(node, pn.href);
|
|
6828
7245
|
}
|
|
6829
|
-
if (
|
|
7246
|
+
else if (fate) {
|
|
7247
|
+
if (fate === DanglingFate.Email)
|
|
7248
|
+
writeExternalLink(node, `${MAILTO}${ctx.email}`);
|
|
7249
|
+
reportDangling(fate, pn, id, words.value, ctx);
|
|
7250
|
+
}
|
|
7251
|
+
if (pn.param && !fate) {
|
|
6830
7252
|
const names = pn.href ? routeParams(pn.href) : [];
|
|
6831
7253
|
if (names.length !== 1)
|
|
6832
7254
|
throw new ComposeError(`${id}: "param" needs an href with one :param`);
|
|
6833
7255
|
addBinding(node, { prop: `routeParam:${names[0]}`, ...resolveScoped(pn.param, repeater, ctx, id) });
|
|
6834
7256
|
}
|
|
6835
7257
|
if (pn.bind) {
|
|
6836
|
-
const prop = BIND_PROP[
|
|
7258
|
+
const prop = BIND_PROP[type];
|
|
6837
7259
|
if (!prop)
|
|
6838
|
-
throw new ComposeError(`${id}: ${
|
|
6839
|
-
|
|
7260
|
+
throw new ComposeError(`${id}: ${type} has no bindable value`);
|
|
7261
|
+
if (isPageScope(pn.bind))
|
|
7262
|
+
setProp(node, prop, stampedPageValue(pn.bind, ctx, id));
|
|
7263
|
+
else
|
|
7264
|
+
addBinding(node, { prop, ...resolveScoped(pn.bind, repeater, ctx, id) });
|
|
7265
|
+
}
|
|
7266
|
+
if (type === IMAGE && pn.bind && !isPageScope(pn.bind)) {
|
|
7267
|
+
setProp(node, 'src', '');
|
|
7268
|
+
if (pn.alt === DECORATIVE_ALT)
|
|
7269
|
+
setProp(node, 'alt', DECORATIVE_ALT);
|
|
7270
|
+
else
|
|
7271
|
+
addBinding(node, { prop: 'alt', ...altBinding(pn, repeater, ctx, id) });
|
|
7272
|
+
}
|
|
7273
|
+
else if (pn.alt !== undefined) {
|
|
7274
|
+
throw new ComposeError(`${id}: "alt" belongs on a bound Image`);
|
|
6840
7275
|
}
|
|
6841
7276
|
// Layout.
|
|
6842
7277
|
if (pn.lay)
|
|
@@ -6855,9 +7290,6 @@ function composeNode(pn, id, repeater, ctx) {
|
|
|
6855
7290
|
throw new ComposeError(`${id}: "$binding" with no section binding`);
|
|
6856
7291
|
binding = { prop: 'source', source: ctx.section.binding, path: '' };
|
|
6857
7292
|
}
|
|
6858
|
-
else if (pn.src === PatternDirective.NavPages) {
|
|
6859
|
-
binding = { prop: 'source', source: NAV_PAGES_ALIAS, path: '' };
|
|
6860
|
-
}
|
|
6861
7293
|
else {
|
|
6862
7294
|
const scoped = resolveScoped(pn.src, repeater, ctx, id);
|
|
6863
7295
|
binding = { prop: 'source', source: scoped.source, path: scoped.path === ITEM_VALUE_PATH ? '' : scoped.path };
|
|
@@ -6867,29 +7299,21 @@ function composeNode(pn, id, repeater, ctx) {
|
|
|
6867
7299
|
rowScope = { itemAlias: lastSegment || binding.source };
|
|
6868
7300
|
}
|
|
6869
7301
|
// Children.
|
|
6870
|
-
const children = (pn.c ?? [])
|
|
6871
|
-
.map((child, i) => composeNode(child, `${id}-${i}`, rowScope, ctx))
|
|
6872
|
-
.filter((c) => !!c);
|
|
6873
7302
|
if (pn.c?.length) {
|
|
6874
7303
|
if (!node.children)
|
|
6875
|
-
throw new ComposeError(`${id}: ${
|
|
6876
|
-
node.children = [...node.children, ...
|
|
6877
|
-
}
|
|
6878
|
-
// A Form: its stable id, and both state blocks — never optional (forms plan D7).
|
|
6879
|
-
if (pn.t === FORM) {
|
|
6880
|
-
if (pn.preset)
|
|
6881
|
-
setProp(node, 'formId', `${pn.preset}-${stableHash(`${ctx.owner}/${id}`)}`);
|
|
6882
|
-
node.children = [
|
|
6883
|
-
...(node.children ?? []),
|
|
6884
|
-
...FORM_STATE_BLOCKS.map((type, i) => createSerializedNode(`${id}-${(pn.c?.length ?? 0) + i}`, type)),
|
|
6885
|
-
];
|
|
7304
|
+
throw new ComposeError(`${id}: ${type} holds no children`);
|
|
7305
|
+
node.children = [...node.children, ...composeChildren(pn, id, rowScope, ctx)];
|
|
6886
7306
|
}
|
|
7307
|
+
if (type === FORM)
|
|
7308
|
+
composeForm(node, pn, id, ctx);
|
|
6887
7309
|
return node;
|
|
6888
7310
|
}
|
|
6889
7311
|
function composePattern(pattern, id, ctx) {
|
|
7312
|
+
if (pattern.node.each)
|
|
7313
|
+
throw new ComposeError(`${id}: a pattern's root cannot be stamped per page`);
|
|
6890
7314
|
const node = composeNode(pattern.node, id, null, ctx);
|
|
6891
7315
|
if (!node)
|
|
6892
|
-
throw new ComposeError(`${id}: a pattern's root cannot be optional`);
|
|
7316
|
+
throw new ComposeError(`${id}: a pattern's root cannot be optional or a link`);
|
|
6893
7317
|
node.label = pattern.label;
|
|
6894
7318
|
return node;
|
|
6895
7319
|
}
|
|
@@ -6909,12 +7333,6 @@ function composeDataSource(ds) {
|
|
|
6909
7333
|
request: { relative: { kind: 'siblings', from: ds.within, subjectFrom: ds.of, pick } },
|
|
6910
7334
|
};
|
|
6911
7335
|
}
|
|
6912
|
-
function navPagesSource(plan) {
|
|
6913
|
-
return {
|
|
6914
|
-
id: `ds-${NAV_PAGES_ALIAS}`, alias: NAV_PAGES_ALIAS, type: 'static', mode: PlanDataSourceMode.Query,
|
|
6915
|
-
request: { data: plan.pages.filter(p => p.inNav).map(p => ({ name: p.name, route: p.route })) },
|
|
6916
|
-
};
|
|
6917
|
-
}
|
|
6918
7336
|
function expandSeedValue(value, n) {
|
|
6919
7337
|
if (Array.isArray(value))
|
|
6920
7338
|
return value.map(v => expandSeedValue(v, n));
|
|
@@ -6938,25 +7356,20 @@ function composeItems(type) {
|
|
|
6938
7356
|
}
|
|
6939
7357
|
return items;
|
|
6940
7358
|
}
|
|
6941
|
-
/** True when a composed tree iterates the navigation's pages. */
|
|
6942
|
-
function bindsAlias(trees, alias) {
|
|
6943
|
-
let found = false;
|
|
6944
|
-
for (const tree of trees)
|
|
6945
|
-
walkSerialized(tree, node => { if (node.bindings.some(b => b.source === alias))
|
|
6946
|
-
found = true; });
|
|
6947
|
-
return found;
|
|
6948
|
-
}
|
|
6949
7359
|
/**
|
|
6950
7360
|
* Compose a Site Plan. Throws `SitePlanError` (every issue) when the plan does not validate — the
|
|
6951
|
-
* composer never repairs a plan.
|
|
7361
|
+
* composer never repairs a plan. `context` carries what the composer may not read for itself (the year).
|
|
6952
7362
|
*/
|
|
6953
|
-
function composeSite(plan) {
|
|
7363
|
+
function composeSite(plan, context) {
|
|
6954
7364
|
assertValidSitePlan(plan);
|
|
7365
|
+
if (!Number.isInteger(context?.year))
|
|
7366
|
+
throw new ComposeError('composeSite needs the year (ComposeContext.year) — the composer reads no clock');
|
|
6955
7367
|
const placeholders = [];
|
|
6956
7368
|
const issues = [];
|
|
6957
7369
|
const routes = new Set(plan.pages.map(p => p.route));
|
|
6958
7370
|
const relatives = new Map(plan.dataSources.map(d => [d.alias, d.type === PlanDataSourceType.Relative ? { type: d.type, of: d.of, op: d.op } : { type: d.type }]));
|
|
6959
|
-
const
|
|
7371
|
+
const email = nonEmpty(plan.business.facts?.email) ? plan.business.facts.email.trim() : null;
|
|
7372
|
+
const base = { plan, placeholders, issues, routes, year: context.year, email, navPage: null };
|
|
6960
7373
|
const pages = plan.pages.map(page => {
|
|
6961
7374
|
const nextAlias = page.routeSource ? nextSourceOf(relatives, page.routeSource) : null;
|
|
6962
7375
|
const layout = page.sections.map((section, i) => {
|
|
@@ -6980,16 +7393,13 @@ function composeSite(plan) {
|
|
|
6980
7393
|
node.globalPosition = pattern.global;
|
|
6981
7394
|
return node;
|
|
6982
7395
|
});
|
|
6983
|
-
const dataSources = plan.dataSources.map(composeDataSource);
|
|
6984
|
-
if (bindsAlias([...pages.map(p => p.layout), globals], NAV_PAGES_ALIAS))
|
|
6985
|
-
dataSources.push(navPagesSource(plan));
|
|
6986
7396
|
return {
|
|
6987
7397
|
source: clone(plan.source),
|
|
6988
7398
|
business: clone(plan.business),
|
|
6989
7399
|
design: resolveDesignChoices(clone(plan.design)),
|
|
6990
7400
|
schemas: clone(plan.contentTypes),
|
|
6991
7401
|
items: Object.fromEntries(plan.contentTypes.map(t => [t.name, composeItems(t)])),
|
|
6992
|
-
dataSources,
|
|
7402
|
+
dataSources: plan.dataSources.map(composeDataSource),
|
|
6993
7403
|
pages,
|
|
6994
7404
|
globals,
|
|
6995
7405
|
routes: plan.pages.map(p => ({ key: p.key, path: p.route, isHome: p.isHome === true })),
|
|
@@ -7014,5 +7424,5 @@ function composeSite(plan) {
|
|
|
7014
7424
|
* Generated bundle index. Do not edit.
|
|
7015
7425
|
*/
|
|
7016
7426
|
|
|
7017
|
-
export { BindScope, ComposeError, ComposeIssueCode, FACT_KEYS, GlobalSlot,
|
|
7427
|
+
export { BASELINE_ROLES, BaselineRole, BindScope, ComposeError, ComposeIssueCode, DANGLING_LINK_CODES, FACT_KEYS, GlobalSlot, NEUTRAL_STYLE_PROPS, NODE_BASELINES, PatternDirective, PatternGlobalPosition, PatternPlaceholder, PlaceholderKind, PlanDataSourceMode, PlanDataSourceType, RelativeOp, SAMPLE_IMAGE_REF, SITE_FACT_LABELS, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_FACTS, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_TEMPLATES, STUDIO_TEMPLATE, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, altSourceScopes, assertStyleSlots, assertValidSitePlan, composeSite, createSerializedNode, nextSourceOf, parseSampleImageRef, patternById, patternNeedsBinding, patternNodes, patternOptionalParts, patternScopes, patternSlots, planFromTemplate, releasedLayoutDefaults, requiredShapeKeys, routeParams, sampleImageRef, setStyleProps, shapeKeys, validateSitePlan, walkSerialized };
|
|
7018
7428
|
//# sourceMappingURL=vectoriox-iox-ui-generation-core.mjs.map
|