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,560 @@
1
+ """The component wall: things that work, with the explanation cut to a label.
2
+
3
+ The other showcase pages explain mechanisms. This one does not explain anything -
4
+ every block is a working component carrying Moksa Web's own copy, and the only
5
+ text that is not content is a one-line strip naming the node types that built it.
6
+ If you want to know how a thing works, open it in the editor; this page is for
7
+ seeing that it works at all.
8
+
9
+ Everything here is interactive and none of it is a screenshot:
10
+
11
+ tabs the studio's four service lines, switched
12
+ accordion the questions clients actually ask, opened
13
+ form a real enquiry form with its own success screen
14
+ openstreetmap the studio's location, dragged and zoomed
15
+
16
+ All four are node types, not plugins and not embeds, and the whole page is one
17
+ Mosaic document.
18
+ """
19
+ import uuid
20
+
21
+ from _moksa import (CJK, DISPLAY, FOCUS_RING, MONO, RULE, SERVICES, T, bp, box,
22
+ grid, ml, mono, section, slot, wrap)
23
+
24
+ U = lambda: str(uuid.uuid4())
25
+
26
+
27
+ def strip(label, types):
28
+ """The only non-content text on the page: what it is, and what built it."""
29
+ return box("cp-strip-" + label.lower().replace(" ", "-"), {
30
+ "display": "flex", "justifyContent": "space-between",
31
+ "alignItems": "baseline", "columnGap": "16px", "flexWrap": "wrap",
32
+ "rowGap": "6px", "paddingBottom": "14px", "marginBottom": "26px",
33
+ "customDeclarations": "border-bottom:1px solid rgb(22,24,28);"},
34
+ [mono(label, size="11px", color="--mk-ink", track="0.26em"),
35
+ mono(types, size="10px", color="--mk-faint", track="0.08em")])
36
+
37
+
38
+ # ---------------------------------------------------------------- tabs
39
+
40
+ TAB_BODY = {
41
+ "01": ("品牌官網、電商平台與 WordPress 開發。",
42
+ ["定制主題與區塊開發",
43
+ "WooCommerce 並接金物流與發票",
44
+ "後台交給後可自己維護"]),
45
+ "02": ("AI 客服與自動化流程導入、客製軟體與 ERP。",
46
+ ["n8n / 自建流程串接內部系統",
47
+ "LINE、訂單與庫存通知自動化",
48
+ "進銷存、訂位、排程等內部工具"]),
49
+ "03": ("API、金物流串接與平台無痛轉移。",
50
+ ["Weebly / Wix / Shopify 搬到 WordPress",
51
+ "不斷線切換,網址與 SEO 保留",
52
+ "資料在系統之間自動流通"]),
53
+ "04": ("SEO 優化、主機代管與資安維護。",
54
+ ["技術 SEO 與內容結構整理",
55
+ "備份、監控與漏洞修補",
56
+ "上線後的長期穩定照顧"]),
57
+ }
58
+
59
+
60
+ def tabs_block():
61
+ tabs, panes = [], []
62
+ for i, (num, en, zh, _lead) in enumerate(SERVICES):
63
+ lead, bullets = TAB_BODY[num]
64
+ tabs.append({
65
+ "type": "tabs-tab", "data": {"attrID": "cp-tab-%d" % i},
66
+ "style": {"&": {"_": {
67
+ "cursor": "pointer", "display": "flex", "flexDirection": "column",
68
+ "rowGap": "4px", "paddingTop": "14px", "paddingBottom": "14px",
69
+ "paddingLeft": "18px", "paddingRight": "18px",
70
+ "customDeclarations": "border:1px solid " + RULE + ";"
71
+ "border-bottom:0;"}},
72
+ "hover": {"_": {"backgroundColor": {"token": "--mk-panel"}}},
73
+ # The plugin darkens the active tab; without saying so the LABEL
74
+ # stays ink and the current tab is dark text on a dark ground.
75
+ # `___tab--active` styles the tab, `___tab--active___descendants`
76
+ # reaches the type inside it - two states, because one of them
77
+ # cannot see the children.
78
+ "___tab--active": {"_": {
79
+ "backgroundColor": {"token": "--mk-ink"},
80
+ "customDeclarations": "border:1px solid rgb(22,24,28);"
81
+ "border-bottom:0;"}},
82
+ "focus-visible": {"_": dict(FOCUS_RING)}},
83
+ "children": [
84
+ # `___tab--active___descendants` compiles to `.m-tab--active &`,
85
+ # so it has to sit on the CHILD. On the tab itself it would read
86
+ # "a descendant of an active tab that is also that tab" and match
87
+ # nothing - which is why the label stayed ink on an ink ground.
88
+ {"type": "text", "data": {"tagName": "p",
89
+ "attrID": "cp-tab-en-%d" % i},
90
+ "style": {"&": {"_": {"fontFamily": MONO, "fontSize": "10px",
91
+ "letterSpacing": "0.18em",
92
+ "fontWeight": "400",
93
+ "color": {"token": "--mk-faint"}},
94
+ "_t": {"fontSize": "12px",
95
+ "letterSpacing": "0.1em"}},
96
+ "___tab--active___descendants": {
97
+ "_": {"color": "rgba(250,250,247,.6)"}}},
98
+ "children": [{"type": "wysiwyg-text", "data": {"text": en}}]},
99
+ {"type": "text", "data": {"tagName": "p",
100
+ "attrID": "cp-tab-zh-%d" % i},
101
+ "style": {"&": {"_": {"fontFamily": CJK, "fontSize": "14px",
102
+ "fontWeight": "500",
103
+ "color": {"token": "--mk-ink"}}},
104
+ "___tab--active___descendants": {
105
+ "_": {"color": "rgb(250,250,247)"}}},
106
+ "children": [{"type": "wysiwyg-text", "data": {"text": zh}}]},
107
+ ]})
108
+ panes.append({
109
+ "type": "tabs-tab-pane", "data": {"attrID": "cp-pane-%d" % i},
110
+ "style": bp({"paddingTop": "28px", "paddingBottom": "30px",
111
+ "paddingLeft": "28px", "paddingRight": "28px",
112
+ "backgroundColor": {"token": "--mk-panel"}},
113
+ None, {"paddingLeft": "20px", "paddingRight": "20px"}),
114
+ "children": [
115
+ ml("p", lead, fontFamily=DISPLAY, fontWeight="600",
116
+ fontSize="22px", lineHeight="1.4", color={"token": "--mk-ink"},
117
+ _m={"fontSize": "18px"}),
118
+ box("cp-pane-%d-list" % i, {"marginTop": "18px"},
119
+ [box("cp-pane-%d-li-%d" % (i, j), {
120
+ "display": "flex", "columnGap": "12px",
121
+ "alignItems": "baseline", "paddingTop": "7px",
122
+ "paddingBottom": "7px",
123
+ "customDeclarations": "border-top:1px solid " + RULE + ";"},
124
+ [mono("%02d" % (j + 1), size="10px", color="--mk-accent-ink",
125
+ track="0.16em"),
126
+ T("p", b, fontFamily=CJK, fontSize="14px",
127
+ lineHeight="1.7", color={"token": "--mk-muted"})])
128
+ for j, b in enumerate(bullets)]),
129
+ ]})
130
+ return {"type": "tabs", "data": {"attrID": "cp-tabs"},
131
+ "children": [
132
+ {"type": "tabs-menu", "data": {"attrID": "cp-tabs-menu"},
133
+ "style": bp({"display": "grid",
134
+ "gridCols": "repeat(4, 1fr)", "columnGap": "0px"},
135
+ {"gridCols": "repeat(2, 1fr)"},
136
+ {"gridCols": "repeat(2, 1fr)"}),
137
+ "children": tabs},
138
+ {"type": "tabs-content", "data": {"attrID": "cp-tabs-content"},
139
+ "style": {"&": {"_": {
140
+ "customDeclarations": "border:1px solid " + RULE + ";"}}},
141
+ "children": panes},
142
+ ]}
143
+
144
+
145
+ # ---------------------------------------------------------------- accordion
146
+
147
+ FAQ = [
148
+ ("做一個站要多久?",
149
+ "形象官網大約 3–6 週,電商與串接案看範圍。"
150
+ "第一週先把範圍、結構與里程碑寫成一份文件,"
151
+ "後面每一步都對著那份文件走。"),
152
+ ("上線之後我自己改得動嗎?",
153
+ "改得動。交給的時候會附一份後台操作說明,"
154
+ "文字、圖片、文章、商品都是你自己管。"
155
+ "結構性的調整再找我就好。"),
156
+ ("舊站的資料與排名會不會不見?",
157
+ "不會。搬站會先把所有網址、文字、圖片與連結"
158
+ "列成清單,轉完再逐項核對;"
159
+ "網址有變動就做 301,不讓排名斷掉。"),
160
+ ("可以只做自動化、不做網站嗎?",
161
+ "可以。訂單通知、庫存同步、客服機器人、"
162
+ "內部報表這些都能單獨做,"
163
+ "接在你現有的系統上。"),
164
+ ]
165
+
166
+
167
+ def accordion_block():
168
+ items = []
169
+ for i, (q, a) in enumerate(FAQ):
170
+ items.append({
171
+ "type": "accordion-item", "data": {"attrID": "cp-faq-%d" % i},
172
+ "style": {"&": {"_": {
173
+ "customDeclarations": "border-bottom:1px solid " + RULE + ";"}}},
174
+ "children": [
175
+ {"type": "accordion-title", "data": {"attrID": "cp-faq-q-%d" % i},
176
+ "style": {"&": {"_": {
177
+ "cursor": "pointer", "display": "flex",
178
+ "justifyContent": "space-between", "alignItems": "baseline",
179
+ "columnGap": "18px",
180
+ "paddingTop": "20px", "paddingBottom": "20px"}},
181
+ "hover": {"_": {"color": {"token": "--mk-accent-ink"}}},
182
+ "focus-visible": {"_": dict(FOCUS_RING)}},
183
+ "children": [
184
+ T("p", q, fontFamily=CJK, fontSize="16px", fontWeight="500",
185
+ lineHeight="1.5", color={"token": "--mk-ink"},
186
+ _m={"fontSize": "15px"}),
187
+ mono("+", size="14px", color="--mk-faint", track="0"),
188
+ ]},
189
+ {"type": "accordion-content", "data": {"attrID": "cp-faq-a-%d" % i},
190
+ "children": [
191
+ T("p", a, fontFamily=CJK, fontSize="14px", lineHeight="1.9",
192
+ color={"token": "--mk-muted"}, paddingBottom="22px",
193
+ maxWidth="640px"),
194
+ ]},
195
+ ]})
196
+ return {"type": "accordion", "data": {"attrID": "cp-faq"},
197
+ "style": {"&": {"_": {
198
+ "customDeclarations": "border-top:1px solid rgb(22,24,28);"}}},
199
+ "children": items}
200
+
201
+
202
+ # ---------------------------------------------------------------- form
203
+
204
+ def field(attr, label, node, **data):
205
+ base = {"attrID": attr}
206
+ base.update(data)
207
+ style = {"&": {"_": {
208
+ "width": "100%", "backgroundColor": {"token": "--mk-paper"},
209
+ "fontFamily": CJK, "fontSize": "14px", "color": {"token": "--mk-ink"},
210
+ "paddingTop": "12px", "paddingBottom": "12px",
211
+ "paddingLeft": "14px", "paddingRight": "14px",
212
+ "customDeclarations": "border:1px solid rgba(22,24,28,.3);"}},
213
+ "focus-visible": {"_": dict(FOCUS_RING)}}
214
+ return box(attr + "-row", {
215
+ "display": "flex", "flexDirection": "column", "rowGap": "7px"}, [
216
+ mono(label, size="10px", color="--mk-faint", track="0.18em"),
217
+ {"type": node, "data": base, "style": style},
218
+ ])
219
+
220
+
221
+ def form_block():
222
+ return {"type": "form-wrapper", "data": {"attrID": "cp-formw"},
223
+ "children": [
224
+ {"type": "form", "data": {"attrID": "cp-form"},
225
+ "style": {"&": {"_": {"display": "flex",
226
+ "flexDirection": "column",
227
+ "rowGap": "16px"}}},
228
+ "children": [
229
+ grid("cp-form-top", 2, "16px", [
230
+ field("cp-f-name", "姓名 / NAME", "text-input",
231
+ nameAttribute="name", required="1",
232
+ placeholderText="王小明"),
233
+ field("cp-f-mail", "電郵 / EMAIL", "text-input",
234
+ nameAttribute="email", required="1", type="email",
235
+ placeholderText="you@example.com"),
236
+ ], tcols=2, mcols=1),
237
+ field("cp-f-msg", "想做的事 / BRIEF",
238
+ "textarea-input", nameAttribute="message",
239
+ required="1",
240
+ placeholderText="寫三行就好:"
241
+ "想做什麼、"
242
+ "現在卡在哪、"
243
+ "希望什麼時候上線"),
244
+ {"type": "submit-button", "data": {"attrID": "cp-f-send"},
245
+ "style": {"&": {"_": {
246
+ "backgroundColor": {"token": "--mk-accent"},
247
+ "color": {"token": "--mk-ink"}, "fontFamily": MONO,
248
+ "fontSize": "11px", "letterSpacing": "0.18em",
249
+ "fontWeight": "500", "cursor": "pointer",
250
+ "width": "max-content",
251
+ "paddingTop": "14px", "paddingBottom": "14px",
252
+ "paddingLeft": "26px", "paddingRight": "26px",
253
+ "customDeclarations": "border:1px solid rgb(255,90,54);"}},
254
+ "hover": {"_": {"backgroundColor": {"token": "--mk-ink"},
255
+ "color": {"token": "--mk-paper"}}},
256
+ "focus-visible": {"_": dict(FOCUS_RING)}},
257
+ "children": [
258
+ {"type": "submit-label", "data": {"attrID": "cp-f-send-l"},
259
+ "children": [{"type": "wysiwyg-text",
260
+ "data": {"text": "SEND"}}]},
261
+ ]},
262
+ ]},
263
+ {"type": "success-screen", "data": {"attrID": "cp-form-ok"},
264
+ "style": {"&": {"_": {
265
+ "paddingTop": "34px", "paddingBottom": "34px",
266
+ "paddingLeft": "28px", "paddingRight": "28px",
267
+ "backgroundColor": {"token": "--mk-ink"}}},
268
+ },
269
+ "children": [
270
+ ml("p", "收到了。",
271
+ fontFamily=DISPLAY, fontWeight="600", fontSize="26px",
272
+ color="rgb(250,250,247)"),
273
+ T("p", "兩個工作天內會收到一份"
274
+ "能直接討論的提案:"
275
+ "範圍、做法、時間。",
276
+ fontFamily=CJK, fontSize="14px", lineHeight="1.85",
277
+ color="rgba(250,250,247,.78)", marginTop="10px"),
278
+ ]},
279
+ ]}
280
+
281
+
282
+ # ---------------------------------------------------------------- map
283
+
284
+ def map_block():
285
+ return {"type": "openstreetmap",
286
+ "data": {"attrID": "cp-map",
287
+ # `latitude` / `longitude`, NOT lat / lon - an unknown
288
+ # property name is dropped without a word and the map comes
289
+ # back centred on its default, which is Times Square. These
290
+ # take ValidatorDynamicCodeObject, so the object form.
291
+ "latitude": {"v": "24.1477"},
292
+ "longitude": {"v": "120.6736"},
293
+ "zoom": {"v": "15"}, "layer": "mapnik"},
294
+ "style": bp({"height": "360px", "width": "100%",
295
+ "customDeclarations": "border:1px solid rgb(22,24,28);"},
296
+ None, {"height": "280px"})}
297
+
298
+
299
+ # ---------------------------------------------------------------- component
300
+
301
+ # A component is ONE definition the whole site shares. An instance carries only
302
+ # what differs - the overrides - so the three cards below are the same seven nodes
303
+ # three times over, not twenty-one nodes that happen to look alike. Change the
304
+ # definition and all three change; that is the part a page builder's "copy this
305
+ # section" cannot do.
306
+ CASE_CARD = box("cs-card", {
307
+ "display": "flex", "flexDirection": "column", "rowGap": "0px",
308
+ "paddingTop": "24px", "paddingBottom": "22px",
309
+ "paddingLeft": "22px", "paddingRight": "22px",
310
+ "backgroundColor": {"token": "--mk-paper"},
311
+ "customDeclarations": "border:1px solid " + RULE + ";"},
312
+ children=[
313
+ box("cs-head", {
314
+ "display": "flex", "justifyContent": "space-between",
315
+ "alignItems": "baseline", "columnGap": "12px",
316
+ "paddingBottom": "12px",
317
+ "customDeclarations": "border-bottom:1px solid " + RULE + ";"},
318
+ # slot() goes through T(), not mono(), so the mobile type floor
319
+ # never sees it - the breakpoint has to be written here
320
+ [slot("p", "cs-kind", "KIND", color={"token": "--mk-accent-ink"},
321
+ fontSize="10px", fontFamily=MONO, letterSpacing="0.2em",
322
+ _t={"fontSize": "12px", "letterSpacing": "0.14em"}),
323
+ slot("p", "cs-year", "2026", color={"token": "--mk-faint"},
324
+ fontSize="10px", fontFamily=MONO, letterSpacing="0.14em",
325
+ _t={"fontSize": "12px"})]),
326
+ slot("h3", "cs-name", "專案", color={"token": "--mk-ink"},
327
+ fontSize="20px", fontWeight="600", fontFamily=DISPLAY,
328
+ marginTop="16px", lineHeight="1.35"),
329
+ slot("p", "cs-body", "說明", color={"token": "--mk-muted"},
330
+ fontSize="13px", lineHeight="1.9", marginTop="10px"),
331
+ slot("p", "cs-metric", "成果", color={"token": "--mk-ink"},
332
+ fontSize="11px", fontFamily=MONO, letterSpacing="0.12em",
333
+ marginTop="16px", _t={"fontSize": "12px"}),
334
+ ])
335
+
336
+ COMPONENTS = {"case-card": CASE_CARD}
337
+
338
+ CASES = [
339
+ ("E-COMMERCE", "2025", "Dr.pen 電商自動化",
340
+ "訂單、庫存與出貨通知串成一條流程,"
341
+ "客服不再手抄訂單。", "→ 人工核對時間大幅下降"),
342
+ ("BOOKING", "2025", "覽陽洋露營區訂位",
343
+ "營位、期間與價格規則自己算,"
344
+ "假日不用再回訊息。", "→ 線上完成訂位"),
345
+ ("ERP", "2026", "馬術用品進銷存",
346
+ "進貨、店面與線上庫存同一本帳,"
347
+ "盤點從一天變一小時。", "→ 單一帳本"),
348
+ ]
349
+
350
+
351
+ def case_instance(i, kind, year, name, body, metric):
352
+ """An instance carries only what differs from the definition."""
353
+ return {"type": "div", "component": "case-card",
354
+ "data": {"attrID": "cs-%d" % i},
355
+ "overrides": {"cs-kind": {"text": kind},
356
+ "cs-year": {"text": year},
357
+ "cs-name": {"text": name},
358
+ "cs-body": {"text": body},
359
+ "cs-metric": {"text": metric}}}
360
+
361
+
362
+ def component_block():
363
+ return grid("cs-grid", 3, "18px",
364
+ [case_instance(i, *c) for i, c in enumerate(CASES)],
365
+ tcols=2, mcols=1)
366
+
367
+
368
+ # ---------------------------------------------------------------- navbar
369
+
370
+ NAV_ITEMS = [("\u5143\u4ef6\u7246", "/components/"), ("\u8868\u55ae", "/forms/"),
371
+ ("\u5167\u5bb9\u9a45\u52d5", "/content/"), ("\u8f2a\u64ad", "/sliders/")]
372
+
373
+
374
+ def navbar_block():
375
+ """A self-contained responsive menu.
376
+
377
+ `navbar` holds a `navbar-toggle` and a `navigation`; below `buttonBreakpoint`
378
+ the toggle appears and the navigation collapses behind it, and `animation`
379
+ picks how it arrives. The site header does not use this - it wraps instead -
380
+ so this is the component on its own, where it can be seen at any width.
381
+ """
382
+ return {"type": "navbar",
383
+ "data": {"attrID": "nb", "animation": "fadeDown",
384
+ "buttonBreakpoint": "_t", "disableScroll": "1"},
385
+ "style": {"&": {"_": {
386
+ "display": "flex", "alignItems": "center",
387
+ "justifyContent": "space-between", "columnGap": "20px",
388
+ "paddingTop": "16px", "paddingBottom": "16px",
389
+ "paddingLeft": "20px", "paddingRight": "20px",
390
+ "customDeclarations": "border:1px solid " + RULE + ";"}}},
391
+ "children": [
392
+ {"type": "navbar-toggle", "data": {"attrID": "nb-toggle"},
393
+ "style": {"&": {"_": {
394
+ "cursor": "pointer", "fontFamily": MONO, "fontSize": "11px",
395
+ "letterSpacing": "0.18em", "color": {"token": "--mk-ink"},
396
+ "paddingTop": "10px", "paddingBottom": "10px",
397
+ "paddingLeft": "14px", "paddingRight": "14px",
398
+ "customDeclarations": "border:1px solid rgba(22,24,28,.34);"}},
399
+ "hover": {"_": {"backgroundColor": {"token": "--mk-ink"},
400
+ "color": {"token": "--mk-paper"}}},
401
+ "focus-visible": {"_": dict(FOCUS_RING)}},
402
+ "children": [{"type": "wysiwyg-text", "data": {"text": "MENU"}}]},
403
+ {"type": "navigation", "data": {"attrID": "nb-nav"},
404
+ "style": {"&": {"_": {
405
+ "display": "flex", "columnGap": "22px", "rowGap": "10px",
406
+ "alignItems": "center", "flexWrap": "wrap"}}},
407
+ "children": [
408
+ {"type": "menu-link",
409
+ "data": {"attrID": "nb-l%d" % i, "url": href},
410
+ "style": {"&": {"_": {
411
+ "fontFamily": CJK, "fontSize": "14px",
412
+ "cursor": "pointer", "color": {"token": "--mk-ink"},
413
+ "paddingTop": "8px", "paddingBottom": "8px"}},
414
+ "hover": {"_": {"color": {"token": "--mk-accent-ink"}}},
415
+ "focus-visible": {"_": dict(FOCUS_RING)}},
416
+ "children": [{"type": "wysiwyg-text",
417
+ "data": {"text": label}}]}
418
+ for i, (label, href) in enumerate(NAV_ITEMS)]},
419
+ ]}
420
+
421
+
422
+ # ---------------------------------------------------------------- media
423
+
424
+ # `icon` takes RAW SVG in its `svg` property - not a URL, not a library name.
425
+ ICONS = [
426
+ ("\u901f\u5ea6", "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' "
427
+ "fill='none' stroke='currentColor' stroke-width='1.6' stroke-linecap='round' "
428
+ "stroke-linejoin='round'><path d='M13 2L4.09 12.97h6.18L11 22l8.91-10.97h-6.18z'/></svg>"),
429
+ ("\u7d50\u69cb", "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' "
430
+ "fill='none' stroke='currentColor' stroke-width='1.6' stroke-linecap='round' "
431
+ "stroke-linejoin='round'><rect x='3' y='3' width='7' height='7'/>"
432
+ "<rect x='14' y='3' width='7' height='7'/><rect x='3' y='14' width='7' height='7'/>"
433
+ "<rect x='14' y='14' width='7' height='7'/></svg>"),
434
+ ("\u6e2c\u91cf", "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' "
435
+ "fill='none' stroke='currentColor' stroke-width='1.6' stroke-linecap='round' "
436
+ "stroke-linejoin='round'><path d='M3 3v18h18'/><path d='M7 15l4-5 3 3 5-7'/></svg>"),
437
+ ]
438
+
439
+
440
+ def media_block():
441
+ return box("md-wrap", {"display": "flex", "flexDirection": "column",
442
+ "rowGap": "26px"}, [
443
+ {"type": "image",
444
+ "data": {"attrID": "md-img",
445
+ # the attachment protocol resolves width and height; the last
446
+ # segment is UPLOADS-relative, and a full path is silently doubled
447
+ "image": "wp-attachment://image/147/full/"
448
+ "2026/09/moksa-richmenu-test.png",
449
+ "alt": "Moksa Web rich menu",
450
+ "loading": "lazy", "fetchPriority": "low"},
451
+ "style": {"&": {"_": {
452
+ "width": "100%", "height": "auto",
453
+ "customDeclarations": "border:1px solid " + RULE + ";"}}}},
454
+ box("md-icons", {"display": "flex", "columnGap": "28px", "rowGap": "18px",
455
+ "flexWrap": "wrap", "alignItems": "center"},
456
+ [box("md-icon-%d" % i, {
457
+ "display": "flex", "columnGap": "10px", "alignItems": "center"},
458
+ [{"type": "icon",
459
+ "data": {"attrID": "md-i%d" % i, "svg": svg, "autoScale": "1"},
460
+ "style": {"&": {"_": {"width": "26px", "height": "26px",
461
+ "color": {"token": "--mk-accent-ink"}}}}},
462
+ mono(label, size="11px", color="--mk-ink", track="0.1em")])
463
+ for i, (label, svg) in enumerate(ICONS)]),
464
+ ])
465
+
466
+
467
+ # ---------------------------------------------------------------- wordpress
468
+
469
+ def wp_block():
470
+ """Two nodes that hand the page back to WordPress.
471
+
472
+ `wpShortcode` runs a shortcode - the products below are WooCommerce's own
473
+ output, not a reimplementation - and `wpContent` drops a block of WordPress
474
+ content in. Between them a Mosaic page can carry anything the rest of the
475
+ install already produces.
476
+ """
477
+ return box("wp-wrap", {"display": "flex", "flexDirection": "column",
478
+ "rowGap": "24px"}, [
479
+ {"type": "wpShortcode",
480
+ "data": {"attrID": "wp-sc", "shortcode": '[products limit="3" columns="3"]'},
481
+ "style": {"&": {"_": {"width": "100%"}}}},
482
+ {"type": "wpContent",
483
+ "data": {"attrID": "wp-ct",
484
+ # ValidatorDynamicCodeObject, so the {"v": \u2026} form - a bare
485
+ # string is dropped and the block renders empty. Its DEFAULT is
486
+ # @VAR('post/content'), which is why this node usually sits
487
+ # inside a loop and needs nothing written at all. The property
488
+ # is also gated on the unfiltered_html capability.
489
+ "content": {"v": "<p>\u9019\u6bb5\u662f wpContent\uff1a"
490
+ "WordPress \u81ea\u5df1\u7684\u5167\u5bb9\u5340\u584a\uff0c"
491
+ "\u7d93\u904e\u5b83\u81ea\u5df1\u7684"
492
+ "\u904e\u6ffe\u5668\u6e32\u67d3\u3002</p>"}},
493
+ "style": {"&": {"_": {
494
+ "fontFamily": CJK, "fontSize": "14px", "lineHeight": "1.9",
495
+ "color": {"token": "--mk-muted"},
496
+ "paddingTop": "18px", "paddingBottom": "18px",
497
+ "paddingLeft": "20px", "paddingRight": "20px",
498
+ "backgroundColor": {"token": "--mk-panel"}}}}},
499
+ ])
500
+
501
+
502
+ # ---------------------------------------------------------------- the page
503
+
504
+ def band(attr, label, types, body, bg="--mk-paper"):
505
+ return section(attr, [wrap(attr + "-w", [
506
+ box(attr + "-pad", {"paddingTop": "70px"}, [], _m={"paddingTop": "44px"}),
507
+ strip(label, types),
508
+ body,
509
+ box(attr + "-foot", {"paddingTop": "70px"}, [], _m={"paddingTop": "44px"}),
510
+ ])], bg=bg, pad_y="0px")
511
+
512
+
513
+ HEAD = section("cp-head", [wrap("cp-head-w", [
514
+ box("cp-head-pad", {"paddingTop": "116px"}, [], _m={"paddingTop": "26px"}),
515
+ mono("COMPONENTS / 元件", size="11px", color="--mk-faint", track="0.26em"),
516
+ ml("h1", "按看看。",
517
+ fontFamily=DISPLAY, fontWeight="600", fontSize="62px", lineHeight="1.06",
518
+ letterSpacing="0.005em", color={"token": "--mk-ink"}, marginTop="18px",
519
+ _t={"fontSize": "48px"}, _m={"fontSize": "36px"}),
520
+ T("p",
521
+ "這一頁不解釋任何東西。"
522
+ "下面每一塊都是可以直接操作的元件,"
523
+ "內容是 Moksa Web 自己的——不是截圖,"
524
+ "也沒有外掛,整頁就是一份 Mosaic 文件。",
525
+ fontFamily=CJK, fontSize="16px", lineHeight="1.95",
526
+ color={"token": "--mk-ink"}, marginTop="20px", maxWidth="620px"),
527
+ box("cp-head-gap", {"paddingTop": "20px"}, []),
528
+ ])], pad_y="0px")
529
+
530
+ TREE = {"type": "div", "data": {"attrID": "cp-page"},
531
+ "children": [
532
+ HEAD,
533
+ band("cp-tabs-s", "TABS / 分頁",
534
+ "tabs › tabs-menu › tabs-tab + tabs-content › tabs-tab-pane",
535
+ tabs_block(), bg="--mk-paper"),
536
+ band("cp-faq-s", "ACCORDION / 手風琴",
537
+ "accordion › accordion-item › accordion-title + accordion-content",
538
+ accordion_block(), bg="--mk-panel"),
539
+ band("cp-form-s", "FORM / 表單",
540
+ "form-wrapper › form › text-input + textarea-input + "
541
+ "submit-button / success-screen",
542
+ form_block(), bg="--mk-paper"),
543
+ band("cp-comp-s", "COMPONENT / 元件實例",
544
+ "一個定義 × 3 個實例,"
545
+ "實例只帶不一樣的那五個欄位",
546
+ component_block(), bg="--mk-paper"),
547
+ band("cp-nav-s", "NAVBAR / \u884c\u52d5\u9078\u55ae",
548
+ "navbar \u203a navbar-toggle + navigation \u2014 "
549
+ "animation / buttonBreakpoint / disableScroll",
550
+ navbar_block(), bg="--mk-paper"),
551
+ band("cp-md-s", "MEDIA / \u5a92\u9ad4",
552
+ "image \u2014 wp-attachment:// / icon \u2014 \u539f\u59cb SVG",
553
+ media_block(), bg="--mk-panel"),
554
+ band("cp-wp-s", "WORDPRESS / \u63a5\u56de\u539f\u751f\u5167\u5bb9",
555
+ "wpShortcode \u2014 [products] / wpContent",
556
+ wp_block(), bg="--mk-paper"),
557
+ band("cp-map-s", "MAP / \u5730\u5716",
558
+ "openstreetmap — latitude / longitude / zoom",
559
+ map_block(), bg="--mk-panel"),
560
+ ]}