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.
@@ -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
+ ]}