mosaic-headless 1.29.2 → 1.30.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/README.md +17 -5
- package/SKILL.md +41 -2
- package/assets/templates/platforms/claude-ai.json +1 -1
- package/assets/templates/platforms/claude-code.json +1 -1
- package/assets/templates/platforms/codex-cli.json +1 -1
- package/assets/templates/platforms/copilot.json +1 -1
- package/assets/templates/platforms/gemini-cli.json +1 -1
- package/package.json +9 -2
- package/references/responsive.md +1 -0
- package/scripts/check-release.mjs +5 -1
- package/scripts/sync-version.mjs +3 -2
- package/sites/_components.py +560 -0
- package/sites/_content.py +341 -0
- package/sites/_conversion.py +225 -0
- package/sites/_dialogs.py +579 -0
- package/sites/_dynamic.py +311 -0
- package/sites/_forms.py +487 -0
- package/sites/_sliders.py +437 -0
- package/sites/_tokens.py +277 -0
- package/tools/build_page.py +4 -2
- package/tools/build_site.py +4 -2
- package/tools/copy_styles.py +4 -2
- package/tools/mosaic_config.py +71 -0
- package/tools/probe.py +4 -2
- package/tools/probe_accordion.py +4 -2
- package/tools/sweep_components.py +4 -2
- package/tools/sweep_interactions.py +4 -2
- package/tools/sweep_node_properties.py +4 -2
- package/tools/sweep_node_types.py +4 -2
- package/tools/sweep_properties.py +4 -2
- package/tools/sweep_style_properties.py +4 -2
- package/tools/sweep_style_states.py +4 -2
- package/tools/theme_zip.py +6 -3
- package/tools/verify_browser.py +4 -2
- package/tools/verify_intro.py +4 -2
- package/tools/verify_rwd.py +4 -2
- package/sites/moksa.json +0 -63681
package/sites/_forms.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
"""The form surface: every field type Mosaic ships, in one working form.
|
|
2
|
+
|
|
3
|
+
The component wall has a three-field enquiry form, which proves a form submits and
|
|
4
|
+
proves nothing about the surface. This is the rest of it - select, radio, checkbox,
|
|
5
|
+
file upload, fieldset and legend, a validation error slot - plus the one structure
|
|
6
|
+
that is genuinely hard to build by hand anywhere else: a MULTI-STEP form, where the
|
|
7
|
+
steps are a tab bar the visitor walks through with next and back.
|
|
8
|
+
|
|
9
|
+
Shapes, read out of the source rather than guessed:
|
|
10
|
+
|
|
11
|
+
choice-field nameAttribute, required, requiredMin
|
|
12
|
+
choice-legend the group's label
|
|
13
|
+
choice-content the rows
|
|
14
|
+
choice one option
|
|
15
|
+
choice-label its text
|
|
16
|
+
radio-input or checkbox-input, carrying `value`
|
|
17
|
+
|
|
18
|
+
select-input nameAttribute, required, multiple, size
|
|
19
|
+
select-option value + its text
|
|
20
|
+
|
|
21
|
+
form-multi-steps
|
|
22
|
+
form-multi-steps-steps > form-multi-steps-step the tab bar
|
|
23
|
+
multi-steps-forms > multi-steps-form-step the panels
|
|
24
|
+
multi-steps-next-button / multi-steps-back-button
|
|
25
|
+
|
|
26
|
+
`requiredMin` on a checkbox group is the one people miss: `required` alone means
|
|
27
|
+
"at least one", and a group that needs two answers says so here.
|
|
28
|
+
"""
|
|
29
|
+
import uuid
|
|
30
|
+
|
|
31
|
+
from _moksa import (CJK, DISPLAY, FOCUS_RING, MONO, RULE, T, bp, box, grid, ml,
|
|
32
|
+
mono, section, wrap)
|
|
33
|
+
|
|
34
|
+
U = lambda: str(uuid.uuid4())
|
|
35
|
+
|
|
36
|
+
FIELD_STYLE = {"&": {"_": {
|
|
37
|
+
"width": "100%", "backgroundColor": {"token": "--mk-paper"},
|
|
38
|
+
"fontFamily": CJK, "fontSize": "14px", "color": {"token": "--mk-ink"},
|
|
39
|
+
"paddingTop": "12px", "paddingBottom": "12px",
|
|
40
|
+
"paddingLeft": "14px", "paddingRight": "14px",
|
|
41
|
+
"customDeclarations": "border:1px solid rgba(22,24,28,.3);"}},
|
|
42
|
+
"focus-visible": {"_": dict(FOCUS_RING)}}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def strip(label, types):
|
|
46
|
+
return box("fm-strip-" + label.lower().replace(" ", "-").replace("/", ""), {
|
|
47
|
+
"display": "flex", "justifyContent": "space-between",
|
|
48
|
+
"alignItems": "baseline", "columnGap": "16px", "flexWrap": "wrap",
|
|
49
|
+
"rowGap": "6px", "paddingBottom": "14px", "marginBottom": "26px",
|
|
50
|
+
"customDeclarations": "border-bottom:1px solid rgb(22,24,28);"},
|
|
51
|
+
[mono(label, size="11px", color="--mk-ink", track="0.26em"),
|
|
52
|
+
mono(types, size="10px", color="--mk-faint", track="0.08em")])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _field(attr, children):
|
|
56
|
+
"""A `field` wrapper, which is not decoration.
|
|
57
|
+
|
|
58
|
+
`label`, `fileupload-input` and the rest declare `canBeNestedChildFor` and
|
|
59
|
+
walk the parent chain looking for a `field` (the new `nested_under` column in
|
|
60
|
+
`placement-rules.csv` names it). Commit them in a plain div and the server
|
|
61
|
+
accepts the write, drops every one of them, and returns a form that renders
|
|
62
|
+
its fieldsets and nothing inside them - no error, no warning. This page was
|
|
63
|
+
built that way first: two fieldsets, one stray label, zero controls.
|
|
64
|
+
"""
|
|
65
|
+
return {"type": "field", "data": {"attrID": attr + "-f"},
|
|
66
|
+
"style": {"&": {"_": {"display": "flex", "flexDirection": "column",
|
|
67
|
+
"rowGap": "7px"}}},
|
|
68
|
+
"children": children}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def labelled(attr, text, node_type, **data):
|
|
72
|
+
d = {"attrID": attr}
|
|
73
|
+
d.update(data)
|
|
74
|
+
return _field(attr, [
|
|
75
|
+
{"type": "label", "data": {"attrID": attr + "-lab", "forInput": attr},
|
|
76
|
+
"style": {"&": {"_": {"fontFamily": MONO, "fontSize": "10px",
|
|
77
|
+
"letterSpacing": "0.18em",
|
|
78
|
+
"color": {"token": "--mk-faint"}},
|
|
79
|
+
"_t": {"fontSize": "12px"}}},
|
|
80
|
+
"children": [{"type": "wysiwyg-text", "data": {"text": text}}]},
|
|
81
|
+
{"type": node_type, "data": d, "style": FIELD_STYLE},
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def select(attr, label, name, options, **extra):
|
|
86
|
+
d = {"attrID": attr, "nameAttribute": name}
|
|
87
|
+
d.update(extra)
|
|
88
|
+
return _field(attr, [
|
|
89
|
+
{"type": "label", "data": {"attrID": attr + "-lab", "forInput": attr},
|
|
90
|
+
"style": {"&": {"_": {"fontFamily": MONO, "fontSize": "10px",
|
|
91
|
+
"letterSpacing": "0.18em",
|
|
92
|
+
"color": {"token": "--mk-faint"}},
|
|
93
|
+
"_t": {"fontSize": "12px"}}},
|
|
94
|
+
"children": [{"type": "wysiwyg-text", "data": {"text": label}}]},
|
|
95
|
+
{"type": "select-input", "data": d, "style": FIELD_STYLE,
|
|
96
|
+
"children": [
|
|
97
|
+
# `select-option` is a LEAF. Its label is the `text` property, not
|
|
98
|
+
# a child - commit a wysiwyg-text under it and the option renders
|
|
99
|
+
# with an empty label and the dropdown looks blank. Its other
|
|
100
|
+
# properties are `value`, `selected` and `disabled`.
|
|
101
|
+
{"type": "select-option",
|
|
102
|
+
"data": {"attrID": "%s-o%d" % (attr, i), "value": v, "text": t}}
|
|
103
|
+
for i, (v, t) in enumerate(options)]},
|
|
104
|
+
])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def choice_group(attr, legend, name, options, kind="radio-input", **extra):
|
|
108
|
+
"""A radio or checkbox group.
|
|
109
|
+
|
|
110
|
+
`choice-field` owns the name and the required rule; the rows are
|
|
111
|
+
`choice-content` > `choice` > (`choice-label`, input). `requiredMin` is what
|
|
112
|
+
says "at least two of these", which `required` alone does not.
|
|
113
|
+
"""
|
|
114
|
+
d = {"attrID": attr, "nameAttribute": name}
|
|
115
|
+
d.update(extra)
|
|
116
|
+
return {"type": "choice-field", "data": d,
|
|
117
|
+
"style": {"&": {"_": {"display": "flex", "flexDirection": "column",
|
|
118
|
+
"rowGap": "10px"}}},
|
|
119
|
+
"children": [
|
|
120
|
+
{"type": "choice-legend", "data": {"attrID": attr + "-leg"},
|
|
121
|
+
"style": {"&": {"_": {"fontFamily": MONO, "fontSize": "10px",
|
|
122
|
+
"letterSpacing": "0.18em",
|
|
123
|
+
"color": {"token": "--mk-faint"}},
|
|
124
|
+
"_t": {"fontSize": "12px"}}},
|
|
125
|
+
"children": [{"type": "wysiwyg-text", "data": {"text": legend}}]},
|
|
126
|
+
{"type": "choice-content", "data": {"attrID": attr + "-rows"},
|
|
127
|
+
"style": bp({"display": "grid", "gridCols": "repeat(2, 1fr)",
|
|
128
|
+
"columnGap": "14px", "rowGap": "10px"},
|
|
129
|
+
None, {"gridCols": "repeat(1, 1fr)"}),
|
|
130
|
+
"children": [
|
|
131
|
+
{"type": "choice", "data": {"attrID": "%s-c%d" % (attr, i)},
|
|
132
|
+
"style": {"&": {"_": {
|
|
133
|
+
"display": "flex", "alignItems": "center",
|
|
134
|
+
"columnGap": "10px", "cursor": "pointer",
|
|
135
|
+
"paddingTop": "10px", "paddingBottom": "10px",
|
|
136
|
+
"paddingLeft": "12px", "paddingRight": "12px",
|
|
137
|
+
"customDeclarations": "border:1px solid " + RULE + ";"}},
|
|
138
|
+
"hover": {"_": {
|
|
139
|
+
"customDeclarations":
|
|
140
|
+
"border:1px solid rgb(22,24,28);"}}},
|
|
141
|
+
"children": [
|
|
142
|
+
{"type": kind,
|
|
143
|
+
"data": {"attrID": "%s-i%d" % (attr, i), "value": v},
|
|
144
|
+
"style": {"&": {"_": {"cursor": "pointer"}},
|
|
145
|
+
"focus-visible": {"_": dict(FOCUS_RING)}}},
|
|
146
|
+
{"type": "choice-label",
|
|
147
|
+
"data": {"attrID": "%s-l%d" % (attr, i)},
|
|
148
|
+
"style": {"&": {"_": {
|
|
149
|
+
"fontFamily": CJK, "fontSize": "14px",
|
|
150
|
+
"cursor": "pointer",
|
|
151
|
+
"color": {"token": "--mk-ink"}}}},
|
|
152
|
+
"children": [{"type": "wysiwyg-text",
|
|
153
|
+
"data": {"text": t}}]},
|
|
154
|
+
]}
|
|
155
|
+
for i, (v, t) in enumerate(options)]},
|
|
156
|
+
]}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def fieldset(attr, legend, children):
|
|
160
|
+
"""`fieldset` does not take fields directly.
|
|
161
|
+
|
|
162
|
+
It renders `fieldset-legend` + `fieldset-content`, and anything committed as a
|
|
163
|
+
direct child of the fieldset is dropped - silently, as always. Neither type is
|
|
164
|
+
in `node-types.csv`, because they are declared by an `ElementMResourceFactory`
|
|
165
|
+
and every extractor here walked `*ElementTypeFactory.php` only.
|
|
166
|
+
"""
|
|
167
|
+
return {"type": "fieldset", "data": {"attrID": attr},
|
|
168
|
+
"style": {"&": {"_": {
|
|
169
|
+
"paddingTop": "22px", "paddingBottom": "24px",
|
|
170
|
+
"paddingLeft": "22px", "paddingRight": "22px",
|
|
171
|
+
"customDeclarations": "border:1px solid " + RULE + ";"}}},
|
|
172
|
+
"children": [
|
|
173
|
+
{"type": "fieldset-legend", "data": {"attrID": attr + "-leg"},
|
|
174
|
+
"style": {"&": {"_": {
|
|
175
|
+
"fontFamily": MONO, "fontSize": "10px",
|
|
176
|
+
"letterSpacing": "0.22em",
|
|
177
|
+
"color": {"token": "--mk-accent-ink"}},
|
|
178
|
+
"_t": {"fontSize": "12px"}}},
|
|
179
|
+
"children": [{"type": "wysiwyg-text", "data": {"text": legend}}]},
|
|
180
|
+
{"type": "fieldset-content", "data": {"attrID": attr + "-body"},
|
|
181
|
+
"style": {"&": {"_": {
|
|
182
|
+
"display": "flex", "flexDirection": "column",
|
|
183
|
+
"rowGap": "20px", "marginTop": "16px"}}},
|
|
184
|
+
"children": children},
|
|
185
|
+
]}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def submit(attr, label):
|
|
189
|
+
return {"type": "submit-button", "data": {"attrID": attr},
|
|
190
|
+
"style": {"&": {"_": {
|
|
191
|
+
"backgroundColor": {"token": "--mk-accent"},
|
|
192
|
+
"color": {"token": "--mk-ink"}, "fontFamily": MONO,
|
|
193
|
+
"fontSize": "11px", "letterSpacing": "0.18em", "fontWeight": "500",
|
|
194
|
+
"cursor": "pointer", "width": "max-content",
|
|
195
|
+
"paddingTop": "14px", "paddingBottom": "14px",
|
|
196
|
+
"paddingLeft": "26px", "paddingRight": "26px",
|
|
197
|
+
"customDeclarations": "border:1px solid rgb(255,90,54);"}},
|
|
198
|
+
"hover": {"_": {"backgroundColor": {"token": "--mk-ink"},
|
|
199
|
+
"color": {"token": "--mk-paper"}}},
|
|
200
|
+
"focus-visible": {"_": dict(FOCUS_RING)}},
|
|
201
|
+
"children": [
|
|
202
|
+
{"type": "submit-label", "data": {"attrID": attr + "-l"},
|
|
203
|
+
"children": [{"type": "wysiwyg-text", "data": {"text": label}}]},
|
|
204
|
+
# shown while the request is in flight; without it a slow network
|
|
205
|
+
# looks like a button that did nothing
|
|
206
|
+
{"type": "submit-loading", "data": {"attrID": attr + "-wait"},
|
|
207
|
+
"children": [{"type": "wysiwyg-text",
|
|
208
|
+
"data": {"text": "送出中…"}}]},
|
|
209
|
+
]}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ---------------------------------------------------------------- the big form
|
|
213
|
+
|
|
214
|
+
BUDGETS = [("", "請選擇"), ("10", "10 萬以下"),
|
|
215
|
+
("30", "10–30 萬"), ("60", "30–60 萬"),
|
|
216
|
+
("60+", "60 萬以上")]
|
|
217
|
+
KINDS = [("web", "品牌官網"), ("shop", "電商"),
|
|
218
|
+
("erp", "ERP / 內部工具"), ("auto", "流程自動化")]
|
|
219
|
+
WANTS = [("seo", "SEO 優化"), ("maint", "上線後維運"),
|
|
220
|
+
("migrate", "舊站轉移"), ("ai", "AI 導入")]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def full_form():
|
|
224
|
+
return {"type": "form-wrapper", "data": {"attrID": "fm-w"},
|
|
225
|
+
"children": [
|
|
226
|
+
{"type": "form", "data": {"attrID": "fm-form"},
|
|
227
|
+
"style": {"&": {"_": {"display": "flex",
|
|
228
|
+
"flexDirection": "column",
|
|
229
|
+
"rowGap": "26px"}}},
|
|
230
|
+
"children": [
|
|
231
|
+
fieldset("fm-fs-who", "01 / 你是誰", [
|
|
232
|
+
grid("fm-who", 2, "16px", [
|
|
233
|
+
labelled("fm-name", "姓名", "text-input",
|
|
234
|
+
nameAttribute="name", required="1",
|
|
235
|
+
placeholderText="王小明"),
|
|
236
|
+
labelled("fm-mail", "電郵", "text-input",
|
|
237
|
+
nameAttribute="email", required="1",
|
|
238
|
+
type="email",
|
|
239
|
+
placeholderText="you@example.com"),
|
|
240
|
+
], tcols=2, mcols=1),
|
|
241
|
+
]),
|
|
242
|
+
fieldset("fm-fs-what", "02 / 要做什麼", [
|
|
243
|
+
choice_group("fm-kind",
|
|
244
|
+
"專案類型(單選)",
|
|
245
|
+
"kind", KINDS, kind="radio-input",
|
|
246
|
+
required="1"),
|
|
247
|
+
choice_group("fm-want",
|
|
248
|
+
"還需要(複選,至少一項)",
|
|
249
|
+
"wants", WANTS, kind="checkbox-input",
|
|
250
|
+
required="1", requiredMin="1"),
|
|
251
|
+
select("fm-budget", "預算區間", "budget",
|
|
252
|
+
BUDGETS, required="1"),
|
|
253
|
+
labelled("fm-brief", "簡述", "textarea-input",
|
|
254
|
+
nameAttribute="message", required="1",
|
|
255
|
+
placeholderText="寫三行就好"),
|
|
256
|
+
labelled("fm-file", "附件(選填)",
|
|
257
|
+
"fileupload-input", nameAttribute="brief",
|
|
258
|
+
accept=".pdf,.png,.jpg", multiple="1"),
|
|
259
|
+
]),
|
|
260
|
+
# where Mosaic puts a validation or server message
|
|
261
|
+
{"type": "form-error", "data": {"attrID": "fm-err"},
|
|
262
|
+
"style": {"&": {"_": {
|
|
263
|
+
"fontFamily": CJK, "fontSize": "13px",
|
|
264
|
+
"color": "rgb(191,68,40)"}}}},
|
|
265
|
+
submit("fm-send", "SEND"),
|
|
266
|
+
]},
|
|
267
|
+
{"type": "success-screen", "data": {"attrID": "fm-ok"},
|
|
268
|
+
"style": {"&": {"_": {
|
|
269
|
+
"paddingTop": "34px", "paddingBottom": "34px",
|
|
270
|
+
"paddingLeft": "28px", "paddingRight": "28px",
|
|
271
|
+
"backgroundColor": {"token": "--mk-ink"}}}},
|
|
272
|
+
"children": [
|
|
273
|
+
ml("p", "收到了。", fontFamily=DISPLAY,
|
|
274
|
+
fontWeight="600", fontSize="26px",
|
|
275
|
+
color="rgb(250,250,247)"),
|
|
276
|
+
T("p", "兩個工作天內回覆。",
|
|
277
|
+
fontFamily=CJK, fontSize="14px", lineHeight="1.85",
|
|
278
|
+
color="rgba(250,250,247,.78)", marginTop="10px"),
|
|
279
|
+
]},
|
|
280
|
+
]}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ---------------------------------------------------------------- multi-step
|
|
284
|
+
|
|
285
|
+
def step_tab(i, num, label):
|
|
286
|
+
"""One tab in the step bar.
|
|
287
|
+
|
|
288
|
+
The step the visitor is on and the ones already answered are separate states,
|
|
289
|
+
and - exactly like the tab component - the `___descendants` variant is the one
|
|
290
|
+
that reaches the labels inside, because its selector puts `&` in the
|
|
291
|
+
descendant position.
|
|
292
|
+
"""
|
|
293
|
+
return {"type": "form-multi-steps-step",
|
|
294
|
+
"data": {"attrID": "ms-tab-%d" % i},
|
|
295
|
+
"style": {"&": {"_": {
|
|
296
|
+
"display": "flex", "columnGap": "10px", "alignItems": "baseline",
|
|
297
|
+
"paddingTop": "12px", "paddingBottom": "12px",
|
|
298
|
+
"paddingLeft": "16px", "paddingRight": "16px",
|
|
299
|
+
"customDeclarations": "border:1px solid " + RULE + ";"}},
|
|
300
|
+
"___step--active": {"_": {
|
|
301
|
+
"backgroundColor": {"token": "--mk-ink"},
|
|
302
|
+
"customDeclarations": "border:1px solid rgb(22,24,28);"}},
|
|
303
|
+
"___step--active___descendants": {"_": {
|
|
304
|
+
"color": "rgb(250,250,247)"}},
|
|
305
|
+
"___step--completed___descendants": {"_": {
|
|
306
|
+
"color": {"token": "--mk-accent-ink"}}}},
|
|
307
|
+
"children": [
|
|
308
|
+
{"type": "text", "data": {"tagName": "p",
|
|
309
|
+
"attrID": "ms-tab-n-%d" % i},
|
|
310
|
+
"style": {"&": {"_": {"fontFamily": MONO, "fontSize": "10px",
|
|
311
|
+
"letterSpacing": "0.2em",
|
|
312
|
+
"color": {"token": "--mk-faint"}},
|
|
313
|
+
"_t": {"fontSize": "12px"}}},
|
|
314
|
+
"children": [{"type": "wysiwyg-text", "data": {"text": num}}]},
|
|
315
|
+
{"type": "text", "data": {"tagName": "p",
|
|
316
|
+
"attrID": "ms-tab-l-%d" % i},
|
|
317
|
+
"style": {"&": {"_": {"fontFamily": CJK, "fontSize": "13px",
|
|
318
|
+
"color": {"token": "--mk-ink"}}}},
|
|
319
|
+
"children": [{"type": "wysiwyg-text", "data": {"text": label}}]},
|
|
320
|
+
]}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def nav_button(kind, attr, label):
|
|
324
|
+
return {"type": kind, "data": {"attrID": attr},
|
|
325
|
+
"style": {"&": {"_": {
|
|
326
|
+
"fontFamily": MONO, "fontSize": "11px", "letterSpacing": "0.16em",
|
|
327
|
+
"cursor": "pointer", "width": "max-content",
|
|
328
|
+
"color": {"token": "--mk-ink"},
|
|
329
|
+
"paddingTop": "11px", "paddingBottom": "11px",
|
|
330
|
+
"paddingLeft": "18px", "paddingRight": "18px",
|
|
331
|
+
"customDeclarations": "border:1px solid rgba(22,24,28,.34);"}},
|
|
332
|
+
"hover": {"_": {"backgroundColor": {"token": "--mk-ink"},
|
|
333
|
+
"color": {"token": "--mk-paper"}}},
|
|
334
|
+
"focus-visible": {"_": dict(FOCUS_RING)}},
|
|
335
|
+
"children": [{"type": "wysiwyg-text", "data": {"text": label}}]}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def step_panel(i, children, first=False, last=False):
|
|
339
|
+
foot = []
|
|
340
|
+
if not first:
|
|
341
|
+
foot.append(nav_button("multi-steps-back-button", "ms-back-%d" % i,
|
|
342
|
+
"\u2190 BACK"))
|
|
343
|
+
if not last:
|
|
344
|
+
foot.append(nav_button("multi-steps-next-button", "ms-next-%d" % i,
|
|
345
|
+
"NEXT \u2192"))
|
|
346
|
+
# NO `display` on the step itself. Which step is shown is the plugin's
|
|
347
|
+
# business, and an author-level display on the element it toggles wins the
|
|
348
|
+
# cascade - the same mistake as putting display on a <dialog>. The layout
|
|
349
|
+
# goes on a plain child instead.
|
|
350
|
+
return {"type": "multi-steps-form-step",
|
|
351
|
+
"data": {"attrID": "ms-panel-%d" % i},
|
|
352
|
+
"children": [box("ms-in-%d" % i, {
|
|
353
|
+
"display": "flex", "flexDirection": "column", "rowGap": "18px"},
|
|
354
|
+
children + [
|
|
355
|
+
box("ms-foot-%d" % i, {
|
|
356
|
+
"display": "flex", "columnGap": "12px", "flexWrap": "wrap",
|
|
357
|
+
"rowGap": "10px", "marginTop": "6px"}, foot)])]}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
STEP_LABELS = [("01", "\u985e\u578b"), ("02", "\u9810\u7b97"),
|
|
361
|
+
("03", "\u806f\u7d61")]
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# Mosaic ships NO visibility CSS for a form step. The step navigation works - the
|
|
365
|
+
# plugin moves `m-form-step--active` and the step tabs change state - but every
|
|
366
|
+
# panel stays on screen, because hiding the inactive ones is the THEME's job: the
|
|
367
|
+
# variant catalog carries a `Form step` entry (`.m-form-step`) and that is where
|
|
368
|
+
# the rule is meant to live. Variants are theme-global and this install carries
|
|
369
|
+
# two brands, so the rule goes in a head `code` node instead, which is the same
|
|
370
|
+
# escape hatch the homepage uses for its keyframes.
|
|
371
|
+
STEP_CSS = {"type": "code",
|
|
372
|
+
"data": {"attrID": "ms-css", "insertLocation": "head",
|
|
373
|
+
"processShortcodes": "0",
|
|
374
|
+
# a head `code` node emits its content VERBATIM - it does
|
|
375
|
+
# not wrap anything - so the <style> tags are the author's
|
|
376
|
+
"content": "<style>"
|
|
377
|
+
".m-form-step:not(.m-form-step--active)"
|
|
378
|
+
"{display:none}"
|
|
379
|
+
"</style>"}}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def multi_step():
|
|
383
|
+
"""Three steps, a tab bar, next and back.
|
|
384
|
+
|
|
385
|
+
`form-multi-steps` heals in all four of its parts, so only the ones carrying
|
|
386
|
+
content need authoring. `multi-steps-forms` declares `canBeParentFor` ->
|
|
387
|
+
`form-multi-steps`, which is its own PARENT - the real child is
|
|
388
|
+
`multi-steps-form-step`, and the authority for that is the healed default
|
|
389
|
+
children, not canBeParentFor. Third container family with the same
|
|
390
|
+
disagreement.
|
|
391
|
+
"""
|
|
392
|
+
return {"type": "form-wrapper", "data": {"attrID": "ms-w"},
|
|
393
|
+
"children": [
|
|
394
|
+
{"type": "form", "data": {"attrID": "ms-form"},
|
|
395
|
+
"style": {"&": {"_": {"display": "flex",
|
|
396
|
+
"flexDirection": "column",
|
|
397
|
+
"rowGap": "22px"}}},
|
|
398
|
+
"children": [
|
|
399
|
+
{"type": "form-multi-steps", "data": {"attrID": "ms"},
|
|
400
|
+
"style": {"&": {"_": {"display": "flex",
|
|
401
|
+
"flexDirection": "column",
|
|
402
|
+
"rowGap": "22px"}}},
|
|
403
|
+
"children": [
|
|
404
|
+
{"type": "form-multi-steps-steps",
|
|
405
|
+
"data": {"attrID": "ms-tabs"},
|
|
406
|
+
"style": bp({"display": "flex", "columnGap": "10px",
|
|
407
|
+
"flexWrap": "wrap", "rowGap": "10px"},
|
|
408
|
+
None, {"flexDirection": "column"}),
|
|
409
|
+
"children": [step_tab(i, n, l)
|
|
410
|
+
for i, (n, l) in enumerate(STEP_LABELS)]},
|
|
411
|
+
{"type": "multi-steps-forms",
|
|
412
|
+
"data": {"attrID": "ms-panels"},
|
|
413
|
+
"children": [
|
|
414
|
+
step_panel(0, [
|
|
415
|
+
choice_group("ms-kind",
|
|
416
|
+
"\u5c08\u6848\u985e\u578b",
|
|
417
|
+
"kind2", KINDS,
|
|
418
|
+
kind="radio-input", required="1"),
|
|
419
|
+
], first=True),
|
|
420
|
+
step_panel(1, [
|
|
421
|
+
select("ms-budget", "\u9810\u7b97\u5340\u9593",
|
|
422
|
+
"budget2", BUDGETS, required="1"),
|
|
423
|
+
]),
|
|
424
|
+
step_panel(2, [
|
|
425
|
+
labelled("ms-mail", "\u96fb\u90f5", "text-input",
|
|
426
|
+
nameAttribute="email2", required="1",
|
|
427
|
+
type="email",
|
|
428
|
+
placeholderText="you@example.com"),
|
|
429
|
+
submit("ms-send", "SEND"),
|
|
430
|
+
], last=True),
|
|
431
|
+
]},
|
|
432
|
+
]},
|
|
433
|
+
]},
|
|
434
|
+
{"type": "success-screen", "data": {"attrID": "ms-ok"},
|
|
435
|
+
"style": {"&": {"_": {
|
|
436
|
+
"paddingTop": "28px", "paddingBottom": "28px",
|
|
437
|
+
"paddingLeft": "24px", "paddingRight": "24px",
|
|
438
|
+
"backgroundColor": {"token": "--mk-ink"}}}},
|
|
439
|
+
"children": [
|
|
440
|
+
ml("p", "\u6536\u5230\u4e86\u3002", fontFamily=DISPLAY,
|
|
441
|
+
fontWeight="600", fontSize="22px",
|
|
442
|
+
color="rgb(250,250,247)"),
|
|
443
|
+
]},
|
|
444
|
+
]}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# ---------------------------------------------------------------- the page
|
|
448
|
+
|
|
449
|
+
def band(attr, label, types, body, bg="--mk-paper"):
|
|
450
|
+
return section(attr, [wrap(attr + "-w", [
|
|
451
|
+
box(attr + "-pad", {"paddingTop": "70px"}, [], _m={"paddingTop": "44px"}),
|
|
452
|
+
strip(label, types),
|
|
453
|
+
body,
|
|
454
|
+
box(attr + "-foot", {"paddingTop": "70px"}, [], _m={"paddingTop": "44px"}),
|
|
455
|
+
])], bg=bg, pad_y="0px")
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
HEAD = section("fm-head", [wrap("fm-head-w", [
|
|
459
|
+
box("fm-head-pad", {"paddingTop": "116px"}, [], _m={"paddingTop": "26px"}),
|
|
460
|
+
mono("FORMS / 表單", size="11px", color="--mk-faint", track="0.26em"),
|
|
461
|
+
ml("h1", "欄位全部在這。",
|
|
462
|
+
fontFamily=DISPLAY, fontWeight="600", fontSize="58px", lineHeight="1.06",
|
|
463
|
+
letterSpacing="0.005em", color={"token": "--mk-ink"}, marginTop="18px",
|
|
464
|
+
_t={"fontSize": "46px"}, _m={"fontSize": "34px"}),
|
|
465
|
+
T("p",
|
|
466
|
+
"單行文字、多行文字、下拉、單選、複選、"
|
|
467
|
+
"檔案上傳、必填規則、錯誤訊息、送出中狀態、"
|
|
468
|
+
"成功畫面——全部是節點,沒有表單外掛。",
|
|
469
|
+
fontFamily=CJK, fontSize="16px", lineHeight="1.95",
|
|
470
|
+
color={"token": "--mk-ink"}, marginTop="20px", maxWidth="640px"),
|
|
471
|
+
box("fm-head-gap", {"paddingTop": "20px"}, []),
|
|
472
|
+
])], pad_y="0px")
|
|
473
|
+
|
|
474
|
+
TREE = {"type": "div", "data": {"attrID": "fm-page"},
|
|
475
|
+
"children": [
|
|
476
|
+
HEAD,
|
|
477
|
+
band("fm-main", "ENQUIRY / 專案洽詢",
|
|
478
|
+
"fieldset › label + text-input / select-input › select-option / "
|
|
479
|
+
"choice-field › choice › radio-input + checkbox-input / "
|
|
480
|
+
"fileupload-input / form-error / submit-loading",
|
|
481
|
+
full_form(), bg="--mk-panel"),
|
|
482
|
+
band("fm-ms", "MULTI-STEP / \u591a\u6b65\u9a5f",
|
|
483
|
+
"form-multi-steps \u203a form-multi-steps-steps \u203a step / "
|
|
484
|
+
"multi-steps-forms \u203a multi-steps-form-step \u203a next + back",
|
|
485
|
+
box("fm-ms-box", {}, [STEP_CSS, multi_step()]),
|
|
486
|
+
bg="--mk-paper"),
|
|
487
|
+
]}
|