mosaic-headless 1.29.1 → 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 +43 -4
- 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/data/component-wall-verification.csv +6 -0
- package/data/mobile-audit.csv +4 -3
- package/data/navigation-verification.csv +9 -9
- package/package.json +9 -2
- package/references/placement.md +31 -0
- package/references/responsive.md +1 -0
- package/scripts/check-release.mjs +7 -3
- 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_components.py +83 -0
- package/tools/verify_intro.py +4 -2
- package/tools/verify_rwd.py +4 -2
- package/sites/moksa.json +0 -62681
|
@@ -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
|
+
]}
|