mosaic-headless 1.2.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/LICENSE +21 -0
- package/README.ja.md +94 -0
- package/README.md +133 -0
- package/README.zh-CN.md +87 -0
- package/README.zh-TW.md +87 -0
- package/SKILL.md +353 -0
- package/assets/templates/platforms/claude-ai.json +34 -0
- package/assets/templates/platforms/claude-code.json +28 -0
- package/assets/templates/platforms/codex-cli.json +33 -0
- package/assets/templates/platforms/continue.json +31 -0
- package/assets/templates/platforms/copilot.json +46 -0
- package/assets/templates/platforms/cursor.json +36 -0
- package/assets/templates/platforms/gemini-cli.json +33 -0
- package/assets/templates/platforms/windsurf.json +34 -0
- package/bin/check-release.mjs +143 -0
- package/bin/install.mjs +452 -0
- package/bin/sync-version.mjs +63 -0
- package/data/animatable-properties.csv +23 -0
- package/data/condition-comparators.csv +13 -0
- package/data/condition-subjects.csv +60 -0
- package/data/db-columns.csv +207 -0
- package/data/default-children.csv +11 -0
- package/data/dynamic-variables.csv +75 -0
- package/data/element-classes.csv +152 -0
- package/data/evaluator-functions.csv +20 -0
- package/data/interaction-types.csv +13 -0
- package/data/node-properties.csv +182 -0
- package/data/node-property-verification.csv +182 -0
- package/data/node-types.csv +123 -0
- package/data/node-verification.csv +123 -0
- package/data/placement-rules.csv +123 -0
- package/data/pluggables.csv +208 -0
- package/data/property-verification.csv +171 -0
- package/data/rest-routes.csv +115 -0
- package/data/rwd-verification.csv +570 -0
- package/data/style-properties.csv +99 -0
- package/data/style-states.csv +54 -0
- package/data/style-value-shapes.csv +23 -0
- package/data/style-verification.csv +99 -0
- package/package.json +59 -0
- package/references/data-model.md +95 -0
- package/references/design-system.md +118 -0
- package/references/dynamic-content.md +113 -0
- package/references/failure-modes.md +182 -0
- package/references/interactions.md +126 -0
- package/references/placement.md +117 -0
- package/references/responsive.md +174 -0
- package/references/styling.md +172 -0
- package/references/templates-and-conditions.md +122 -0
- package/references/vs-elementor-gutenberg.md +73 -0
- package/references/write-protocol.md +79 -0
- package/sites/_moksa.py +1165 -0
- package/sites/moksa.json +8685 -0
- package/tools/bootstrap_probe_theme.php +68 -0
- package/tools/build_all.py +55 -0
- package/tools/build_page.py +352 -0
- package/tools/build_report.py +221 -0
- package/tools/build_site.py +174 -0
- package/tools/capture_live.py +130 -0
- package/tools/check_placement_predicts.py +72 -0
- package/tools/copy_styles.py +204 -0
- package/tools/extract_default_children.py +94 -0
- package/tools/extract_dynamic_variables.py +104 -0
- package/tools/extract_interactions.py +98 -0
- package/tools/extract_node_types.py +165 -0
- package/tools/extract_placement.py +135 -0
- package/tools/extract_pluggables.py +90 -0
- package/tools/extract_style_properties.py +163 -0
- package/tools/mint_session.php +52 -0
- package/tools/probe.py +144 -0
- package/tools/sweep_node_properties.py +271 -0
- package/tools/sweep_node_types.py +318 -0
- package/tools/sweep_properties.py +215 -0
- package/tools/sweep_style_properties.py +254 -0
- package/tools/theme_export.php +91 -0
- package/tools/theme_import.php +113 -0
- package/tools/verify_rwd.py +278 -0
package/sites/_moksa.py
ADDED
|
@@ -0,0 +1,1165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate the Moksa Web homepage spec consumed by tools/build_site.py.
|
|
3
|
+
|
|
4
|
+
The skill's worked example: a real studio homepage rebuilt through the Mosaic data
|
|
5
|
+
model alone - no visual editor, no post_content, every node written over REST.
|
|
6
|
+
|
|
7
|
+
Content and brand are Moksa Web's own, read off moksaweb.com rather than invented:
|
|
8
|
+
the accent is their #FF5A36, the display face is the Space Grotesk they already use,
|
|
9
|
+
and the figures, service lines, client list and testimonials are theirs.
|
|
10
|
+
|
|
11
|
+
Direction: a technical specification document.
|
|
12
|
+
|
|
13
|
+
A studio that builds integrations, ERP and automation lives in endpoints,
|
|
14
|
+
versions, schedules and tables. So the page is set as a spec sheet rather than
|
|
15
|
+
as a marketing site: monospace is the primary voice, not a decorative label
|
|
16
|
+
font; structure is carried entirely by hairlines, with no cards, no shadows and
|
|
17
|
+
no rounded corners anywhere; the work is a table with columns rather than a grid
|
|
18
|
+
of tiles; and every section is a numbered clause. Colour is almost absent - one
|
|
19
|
+
accent, used only where it means something.
|
|
20
|
+
|
|
21
|
+
Against sites/_zidanna.py - a Song serif, bronze, slow-luxury skincare site -
|
|
22
|
+
the pair is the argument: the same helpers and the same data model carry two
|
|
23
|
+
identities that share nothing.
|
|
24
|
+
|
|
25
|
+
Run from this directory: python _moksa.py
|
|
26
|
+
"""
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
|
|
30
|
+
# Monospace leads. The grotesque is the display face and appears at three sizes only;
|
|
31
|
+
# Noto Sans TC carries running Chinese text.
|
|
32
|
+
MONO = "'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace"
|
|
33
|
+
DISPLAY = "'Space Grotesk','Noto Sans TC','PingFang TC','Microsoft JhengHei',sans-serif"
|
|
34
|
+
CJK = "'Noto Sans TC','PingFang TC','Hiragino Sans TC','Microsoft JhengHei',sans-serif"
|
|
35
|
+
|
|
36
|
+
TOKENS = {
|
|
37
|
+
# Paper with a trace of warmth so it does not read as an unstyled white page, a
|
|
38
|
+
# near-black that is not pure black, one rule colour doing all the structural
|
|
39
|
+
# work, and the studio's own signal orange used sparingly.
|
|
40
|
+
"--mk-ink": {"type": "color", "value": "rgb(22,24,28)"},
|
|
41
|
+
"--mk-paper": {"type": "color", "value": "rgb(250,250,247)"},
|
|
42
|
+
"--mk-panel": {"type": "color", "value": "rgb(243,243,239)"},
|
|
43
|
+
"--mk-accent": {"type": "color", "value": "rgb(255,90,54)"},
|
|
44
|
+
"--mk-muted": {"type": "color", "value": "rgb(111,113,120)"},
|
|
45
|
+
"--mk-rule": {"type": "color", "value": "rgb(214,214,206)"},
|
|
46
|
+
"--mk-faint": {"type": "color", "value": "rgb(160,162,168)"},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
RULE = "rgba(22,24,28,.16)"
|
|
50
|
+
RULE_DARK = "rgba(250,250,247,.18)"
|
|
51
|
+
# the trim marks want to be read, not inferred, so they sit a step darker
|
|
52
|
+
RULE_INK = "rgba(22,24,28,.42)"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def bp(base, t=None, m=None):
|
|
56
|
+
"""Assemble a `&` state across the three breakpoints, dropping the empty ones.
|
|
57
|
+
|
|
58
|
+
`_t` is <=1079px and `_m` is <=767px, both `direction:down`, and `_m` is ordered
|
|
59
|
+
after `_t` - so a mobile value overrides a tablet one rather than fighting it.
|
|
60
|
+
"""
|
|
61
|
+
out = {"_": base}
|
|
62
|
+
if t:
|
|
63
|
+
out["_t"] = t
|
|
64
|
+
if m:
|
|
65
|
+
out["_m"] = m
|
|
66
|
+
return {"&": out} if (base or t or m) else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def T(tag, text, _t=None, _m=None, **st):
|
|
70
|
+
return {"type": "text", "data": {"tagName": tag}, "text": text,
|
|
71
|
+
"style": bp(st, _t, _m) if (st or _t or _m) else None}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def box(attr, style, children, hover=None, _t=None, _m=None):
|
|
75
|
+
s = bp(style, _t, _m) or {"&": {"_": {}}}
|
|
76
|
+
if hover:
|
|
77
|
+
s["hover"] = {"_": hover}
|
|
78
|
+
return {"type": "div", "data": {"attrID": attr}, "style": s, "children": children}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def grid(attr, cols, gap, children, tcols=None, mcols=1, **extra):
|
|
82
|
+
"""Three column counts, not two - the 768-1079px band is where tablets and
|
|
83
|
+
half-width desktop windows actually sit."""
|
|
84
|
+
st = {"display": "grid", "gridCols": "repeat(%d, 1fr)" % cols,
|
|
85
|
+
"columnGap": gap, "rowGap": gap}
|
|
86
|
+
st.update(extra)
|
|
87
|
+
if tcols is None:
|
|
88
|
+
tcols = 2 if cols >= 3 else cols
|
|
89
|
+
t = {"gridCols": "repeat(%d, 1fr)" % tcols} if tcols != cols else None
|
|
90
|
+
return {"type": "div", "data": {"attrID": attr},
|
|
91
|
+
"style": bp(st, t, {"gridCols": "repeat(%d, 1fr)" % mcols}),
|
|
92
|
+
"children": children}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def section(attr, children, bg="--mk-paper"):
|
|
96
|
+
"""Sections carry no vertical padding here - the document's rhythm is set by the
|
|
97
|
+
rules between clauses, not by a stack of padded bands."""
|
|
98
|
+
return {"type": "section", "data": {"attrID": attr},
|
|
99
|
+
"style": bp({"backgroundColor": {"token": bg},
|
|
100
|
+
"paddingLeft": "40px", "paddingRight": "40px",
|
|
101
|
+
"fontFamily": CJK},
|
|
102
|
+
{"paddingLeft": "28px", "paddingRight": "28px"},
|
|
103
|
+
{"paddingLeft": "18px", "paddingRight": "18px"}),
|
|
104
|
+
"children": children}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def wrap(attr, children, maxw="1240px", **extra):
|
|
108
|
+
st = {"width": "100%", "maxWidth": maxw, "marginLeft": "auto", "marginRight": "auto"}
|
|
109
|
+
st.update(extra)
|
|
110
|
+
return {"type": "div", "data": {"attrID": attr}, "style": {"&": {"_": st}},
|
|
111
|
+
"children": children}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def ml(tag, text, **st):
|
|
115
|
+
"""A heading whose own line breaks are honoured.
|
|
116
|
+
|
|
117
|
+
Without this a "\\n" collapses to a space and the browser breaks wherever it
|
|
118
|
+
likes, which in Chinese means splitting a compound mid-word.
|
|
119
|
+
"""
|
|
120
|
+
st.setdefault("whiteSpace", "pre-wrap")
|
|
121
|
+
return T(tag, text, **st)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def mono(text, size="11px", color="--mk-muted", track="0.14em", **st):
|
|
125
|
+
"""The page's primary voice. Everything that is a label, a key, a figure or an
|
|
126
|
+
address is set in it."""
|
|
127
|
+
st.setdefault("fontFamily", MONO)
|
|
128
|
+
st.setdefault("fontWeight", "400")
|
|
129
|
+
return T("p", text,
|
|
130
|
+
color={"token": color} if color.startswith("--") else color,
|
|
131
|
+
fontSize=size, letterSpacing=track, **st)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def figure(attr, num, size="40px", t="34px", m="27px"):
|
|
135
|
+
"""A number set as one masked box per digit.
|
|
136
|
+
|
|
137
|
+
A count-up needs JavaScript; a digit rolling out from behind its own edge does
|
|
138
|
+
not, and it reads as deliberate rather than as a gimmick. Each digit carries its
|
|
139
|
+
own delay through an inline custom property, so one keyframe serves all of them.
|
|
140
|
+
"""
|
|
141
|
+
return {"type": "div", "data": {"attrID": attr},
|
|
142
|
+
"style": {"&": {"_": {"display": "flex", "alignItems": "baseline",
|
|
143
|
+
"columnGap": "0px",
|
|
144
|
+
"customStyles":
|
|
145
|
+
"font-variant-numeric:tabular-nums;"}}},
|
|
146
|
+
"children": [
|
|
147
|
+
{"type": "div", "data": {"attrID": attr + "-d%d" % j},
|
|
148
|
+
"style": {"&": {"_": {"customStyles":
|
|
149
|
+
"overflow:hidden;display:inline-block;"
|
|
150
|
+
"--d:%dms;" % (j * 70)}}},
|
|
151
|
+
"children": [T("h3", ch, color={"token": "--mk-ink"}, fontSize=size,
|
|
152
|
+
fontWeight="500", letterSpacing="-0.04em",
|
|
153
|
+
lineHeight="1.05", fontFamily=MONO,
|
|
154
|
+
_t={"fontSize": t}, _m={"fontSize": m})]}
|
|
155
|
+
for j, ch in enumerate(num)
|
|
156
|
+
]}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def clause(num, en, zh, attr):
|
|
160
|
+
"""A numbered section head.
|
|
161
|
+
|
|
162
|
+
The numbers are not ornament: the page is written as a document, and they are how
|
|
163
|
+
you would refer to a part of it out loud.
|
|
164
|
+
"""
|
|
165
|
+
return box(attr, {"display": "grid", "gridCols": "84px 1fr",
|
|
166
|
+
"columnGap": "0px", "alignItems": "start",
|
|
167
|
+
"paddingTop": "22px",
|
|
168
|
+
"customStyles": "border-top:1px solid " + RULE + ";"},
|
|
169
|
+
_m={"gridCols": "48px 1fr"},
|
|
170
|
+
children=[
|
|
171
|
+
mono("§" + num, size="12px", color="--mk-accent", track="0.06em"),
|
|
172
|
+
box(attr + "-t", {}, [
|
|
173
|
+
mono(en, size="11px", color="--mk-muted"),
|
|
174
|
+
T("h2", zh, color={"token": "--mk-ink"}, fontSize="34px",
|
|
175
|
+
fontWeight="600", letterSpacing="-0.025em", lineHeight="1.3",
|
|
176
|
+
fontFamily=DISPLAY, marginTop="10px",
|
|
177
|
+
_t={"fontSize": "29px"}, _m={"fontSize": "23px"}),
|
|
178
|
+
]),
|
|
179
|
+
])
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def apply_type(node, display, body, weight="600"):
|
|
183
|
+
"""Bake the type system into the nodes.
|
|
184
|
+
|
|
185
|
+
Element classes are theme-global - `Heading 1` styles every h1 in the install, so
|
|
186
|
+
two brands in one theme cannot each have their own. Setting the family on the
|
|
187
|
+
nodes at generation time costs nothing and removes the collision entirely.
|
|
188
|
+
`setdefault` means anything that already asked for a face (a monospace label, a
|
|
189
|
+
figure set in another family) keeps it.
|
|
190
|
+
"""
|
|
191
|
+
if isinstance(node, dict):
|
|
192
|
+
if node.get("type") == "text":
|
|
193
|
+
tag = (node.get("data") or {}).get("tagName")
|
|
194
|
+
if tag in ("h1", "h2", "h3", "h4", "p", "span", "div"):
|
|
195
|
+
style = node.get("style")
|
|
196
|
+
if style is None:
|
|
197
|
+
style = {"&": {"_": {}}}
|
|
198
|
+
node["style"] = style
|
|
199
|
+
base = style.setdefault("&", {}).setdefault("_", {})
|
|
200
|
+
if tag in ("h1", "h2", "h3", "h4"):
|
|
201
|
+
base.setdefault("fontFamily", display)
|
|
202
|
+
base.setdefault("fontWeight", weight)
|
|
203
|
+
else:
|
|
204
|
+
base.setdefault("fontFamily", body)
|
|
205
|
+
for v in node.values():
|
|
206
|
+
apply_type(v, display, body, weight)
|
|
207
|
+
elif isinstance(node, list):
|
|
208
|
+
for v in node:
|
|
209
|
+
apply_type(v, display, body, weight)
|
|
210
|
+
return node
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def mask_line(attr, text, tag="h1", **st):
|
|
214
|
+
"""One display line in an overflow mask, so it can rise from behind its own edge.
|
|
215
|
+
|
|
216
|
+
The reveal only works if each line is its own element - a single text node with a
|
|
217
|
+
newline gives the browser one box and nothing to slide behind.
|
|
218
|
+
"""
|
|
219
|
+
return {"type": "div", "data": {"attrID": attr + "-mask"},
|
|
220
|
+
"style": {"&": {"_": {"customStyles":
|
|
221
|
+
"overflow:hidden;padding-bottom:.08em;"}}},
|
|
222
|
+
"children": [T(tag, text, **st)]}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ── content, all of it Moksa Web's own ───────────────────────────────────────
|
|
226
|
+
NAV = [("服務", "#services"), ("作品", "#works"),
|
|
227
|
+
("產品", "#products"), ("聯絡", "#contact")]
|
|
228
|
+
|
|
229
|
+
# key / figure / caption, read as a datasheet rather than as four big numbers
|
|
230
|
+
SPEC = [
|
|
231
|
+
("EXPERIENCE", "9", "年技術開發經驗"),
|
|
232
|
+
("DELIVERED", "252", "完成客製化專案"),
|
|
233
|
+
("SELECTED", "17", "精選上線作品"),
|
|
234
|
+
("SUPPORT", "876", "技術支援與服務"),
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
SERVICES = [
|
|
238
|
+
("01", "WEB & E-COMMERCE", "網站與電商",
|
|
239
|
+
"品牌官網、電商平台與 WordPress 開發,乾淨的結構與好管理的後台。"),
|
|
240
|
+
("02", "AI & AUTOMATION", "AI・自動化・軟體",
|
|
241
|
+
"AI 客服與自動化流程導入、客製軟體與 ERP 開發,把重複工作交給機器。"),
|
|
242
|
+
("03", "INTEGRATION & MIGRATION", "串接與轉移",
|
|
243
|
+
"API、金物流串接與平台無痛轉移,讓資料在系統之間自動流通。"),
|
|
244
|
+
("04", "OPTIMIZE & MAINTAIN", "優化與維運",
|
|
245
|
+
"SEO 優化、主機代管與資安維護,上線後的長期穩定照顧。"),
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
WORKS = [
|
|
249
|
+
("覽陽洋露營區", "WEBSITE + BOOKING", "lyycamping.com"),
|
|
250
|
+
("EM925 純銀飾品", "WOOCOMMERCE", "em925.com"),
|
|
251
|
+
("Dr.pen 台灣總代理", "BRAND E-COMMERCE", "drpen.tw"),
|
|
252
|
+
("Sweetluck 甜點禮盒", "WOOCOMMERCE", "sweetlucktw.com"),
|
|
253
|
+
("安民家庭醫學科診所", "MEDICAL WEBSITE", "am-clinic.com"),
|
|
254
|
+
("iGoods 愛物資", "PLATFORM", "igoods.com.tw"),
|
|
255
|
+
("大勢邸 POWERHOUSE", "CONTENT SITE", "myhouse168.com.tw"),
|
|
256
|
+
("Pro95 Eyewear", "BRAND WEBSITE", "pro95eyewear.com"),
|
|
257
|
+
("酸奶多 YogurtDuo", "BRAND WEBSITE", "yogurtduo.com"),
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
PRODUCTS = [
|
|
261
|
+
("WOOCOMMERCE MANAGEMENT", "StoreDash",
|
|
262
|
+
"WooCommerce 雲端管理後台,專為台灣電商設計。訂單、商品、庫存、LINE 通知,"
|
|
263
|
+
"一支手機就能管。", "FREE TIER"),
|
|
264
|
+
("BUSINESS MANAGEMENT", "Freelancer CRM",
|
|
265
|
+
"專為自由工作者打造的業務管理系統。客戶管理、報價追蹤、財務報表與案件進度,"
|
|
266
|
+
"接案人生一站搞定。", "IN BETA"),
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
VOICES = [
|
|
270
|
+
("從品牌建立到官網架設,Moksa 完整協助我們打造出專屬的視覺與風格,"
|
|
271
|
+
"讓品牌從無到有,在網路上正式亮相。", "Jason 詹", "寵物食品創辦人"),
|
|
272
|
+
("原本的網站老舊又不穩定,Moksa 重新設計頁面、轉移網站並提供主機代管。"
|
|
273
|
+
"現在的網站不僅美觀、速度快,客戶回饋也變多了。", "Elan 李", "Beauty 行銷經理"),
|
|
274
|
+
("不但設計出極符合品牌調性的版型,還處理好行動版優化與金流串接,"
|
|
275
|
+
"從前端到後台都非常專業。", "Emily 黃", "睡衣品牌創意總監"),
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
# how an engagement actually runs, as a sequence
|
|
279
|
+
PROCESS = [
|
|
280
|
+
("需求訪談", "DISCOVERY", "先弄清楚要解決什麼問題、誰在用、成功長什麼樣子。"),
|
|
281
|
+
("規劃報價", "SCOPE", "把需求拆成可估的項目,報價與時程一次講清楚。"),
|
|
282
|
+
("設計開發", "BUILD", "設計、開發、串接與測試,過程中持續給你看得到的進度。"),
|
|
283
|
+
("上線維運", "OPERATE", "上線只是開始。主機、備份、更新與後續調整持續照顧。"),
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
# the tools actually used, grouped the way a spec sheet groups them
|
|
287
|
+
STACK = [
|
|
288
|
+
("FRONT", ["WordPress", "Gutenberg", "Tailwind", "Alpine.js", "Vite"]),
|
|
289
|
+
("COMMERCE", ["WooCommerce", "綠界 ECPay", "藍新 NewebPay", "新竹貨運", "7-11 C2C"]),
|
|
290
|
+
("AUTOMATION", ["n8n", "Claude API", "OpenAI", "LINE Messaging", "Webhooks"]),
|
|
291
|
+
("BACKEND", ["PHP 8", "Laravel", "MySQL", "Redis", "REST / GraphQL"]),
|
|
292
|
+
("INFRA", ["Cloudways", "Cloudflare", "Docker", "GitHub Actions", "Sentry"]),
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
# the clause index, fixed to the left margin. One entry per section, and the entry
|
|
296
|
+
# lights up while its section is on screen - see the named view timelines below.
|
|
297
|
+
CLAUSES = [
|
|
298
|
+
("01", "SERVICES", "#services", "--mk-s1"),
|
|
299
|
+
("02", "PROCESS", "#process", "--mk-s2"),
|
|
300
|
+
("03", "WORKS", "#works", "--mk-s3"),
|
|
301
|
+
("04", "STACK", "#stack", "--mk-s4"),
|
|
302
|
+
("05", "PRODUCTS", "#products", "--mk-s5"),
|
|
303
|
+
("06", "VOICES", "#mk-voices", "--mk-s6"),
|
|
304
|
+
("07", "CONTACT", "#contact", "--mk-s7"),
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
TICKER = ["WEB", "AI", "AUTOMATION", "SOFTWARE", "ERP", "SEO",
|
|
308
|
+
"HOSTING", "WORDPRESS", "WOOCOMMERCE", "N8N"]
|
|
309
|
+
|
|
310
|
+
REVEALS = [
|
|
311
|
+
("mk-spec", 0),
|
|
312
|
+
("mk-svc-head", 0), ("mk-svc-0", 1), ("mk-svc-1", 2), ("mk-svc-2", 3),
|
|
313
|
+
("mk-svc-3", 4),
|
|
314
|
+
("mk-proc-head", 0), ("mk-proc-0", 1), ("mk-proc-1", 2), ("mk-proc-2", 3),
|
|
315
|
+
("mk-proc-3", 4),
|
|
316
|
+
("mk-works-head", 0),
|
|
317
|
+
("mk-work-0", 1), ("mk-work-1", 1), ("mk-work-2", 2), ("mk-work-3", 2),
|
|
318
|
+
("mk-work-4", 3), ("mk-work-5", 3), ("mk-work-6", 4), ("mk-work-7", 4),
|
|
319
|
+
("mk-work-8", 5),
|
|
320
|
+
("mk-stack-head", 0), ("mk-stack-0", 1), ("mk-stack-1", 2), ("mk-stack-2", 3),
|
|
321
|
+
("mk-stack-3", 4), ("mk-stack-4", 5),
|
|
322
|
+
("mk-prod-head", 0), ("mk-prod-0", 1), ("mk-prod-1", 2),
|
|
323
|
+
("mk-voice-head", 0), ("mk-voice-0", 1), ("mk-voice-1", 2), ("mk-voice-2", 3),
|
|
324
|
+
("mk-contact-head", 0),
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def motion_css():
|
|
329
|
+
"""The head stylesheet: layout the style compiler cannot express, plus all motion.
|
|
330
|
+
|
|
331
|
+
Built by concatenation rather than %-formatting - this block is full of literal
|
|
332
|
+
percent signs and a format string would need every one of them doubled.
|
|
333
|
+
"""
|
|
334
|
+
reveal_targets = ",".join("#" + a for a, _ in REVEALS)
|
|
335
|
+
reveal_stagger = "\n".join(
|
|
336
|
+
" #" + a + "{animation-range:entry " + str(2 + i * 4) + "% cover "
|
|
337
|
+
+ str(36 + i * 4) + "%}"
|
|
338
|
+
for a, i in REVEALS if i)
|
|
339
|
+
work_rows = ",".join("#mk-work-%d" % i for i in range(len(WORKS)))
|
|
340
|
+
digits = ",".join("#mk-fig-%d>div>*" % i for i in range(len(SPEC)))
|
|
341
|
+
digit_boxes = ",".join("#mk-fig-%d>div" % i for i in range(len(SPEC)))
|
|
342
|
+
proc_dots = ",".join("#mk-proc-dot-%d" % i for i in range(len(PROCESS)))
|
|
343
|
+
tags = ",".join("#mk-tag-%d-%d" % (i, j)
|
|
344
|
+
for i, (_g, items) in enumerate(STACK)
|
|
345
|
+
for j in range(len(items)))
|
|
346
|
+
tag_hovers = ",".join("#mk-tag-%d-%d:hover" % (i, j)
|
|
347
|
+
for i, (_g, items) in enumerate(STACK)
|
|
348
|
+
for j in range(len(items)))
|
|
349
|
+
marks = ",".join("#mk-mark-%d" % i for i in range(4))
|
|
350
|
+
# a named view timeline per section, declared on the section and consumed by its
|
|
351
|
+
# index entry - `timeline-scope` on #mk-doc is what lets the name cross between
|
|
352
|
+
# two elements that are not ancestor and descendant
|
|
353
|
+
section_timelines = "\n".join(
|
|
354
|
+
"%s{view-timeline-name:%s;view-timeline-inset:45%% 45%%}"
|
|
355
|
+
% (href, tl) for _n, _l, href, tl in CLAUSES)
|
|
356
|
+
index_actives = "\n".join(
|
|
357
|
+
" #mk-idx-%d{animation:mk-idxon linear;animation-timeline:%s;"
|
|
358
|
+
"animation-range:cover 0%% cover 100%%}" % (i, tl)
|
|
359
|
+
for i, (_n, _l, _h, tl) in enumerate(CLAUSES))
|
|
360
|
+
work_hovers = ",".join("#mk-work-%d:hover" % i for i in range(len(WORKS)))
|
|
361
|
+
idx_hovers = ",".join("#mk-work-%d:hover #mk-work-idx-%d" % (i, i)
|
|
362
|
+
for i in range(len(WORKS)))
|
|
363
|
+
|
|
364
|
+
return "\n".join([
|
|
365
|
+
'<link rel="preconnect" href="https://fonts.googleapis.com">',
|
|
366
|
+
'<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>',
|
|
367
|
+
'<link rel="stylesheet" href="https://fonts.googleapis.com/css2?'
|
|
368
|
+
'family=IBM+Plex+Mono:wght@400;500&'
|
|
369
|
+
'family=Space+Grotesk:wght@500;600;700&'
|
|
370
|
+
'family=Noto+Sans+TC:wght@300;400;500&display=swap">',
|
|
371
|
+
'<style id="mk-motion">',
|
|
372
|
+
|
|
373
|
+
# ── keyframes ────────────────────────────────────────────────────────
|
|
374
|
+
"@keyframes mk-linein{from{transform:translateY(110%)}to{transform:translateY(0)}}",
|
|
375
|
+
"@keyframes mk-softin{from{opacity:0;transform:translateY(10px)}"
|
|
376
|
+
"to{opacity:1;transform:none}}",
|
|
377
|
+
"@keyframes mk-rise{from{opacity:0;transform:translateY(18px)}"
|
|
378
|
+
"to{opacity:1;transform:none}}",
|
|
379
|
+
"@keyframes mk-drawx{from{transform:scaleX(0)}to{transform:scaleX(1)}}",
|
|
380
|
+
"@keyframes mk-ticker{from{transform:translateX(0)}"
|
|
381
|
+
"to{transform:translateX(-50%)}}",
|
|
382
|
+
"@keyframes mk-progress{from{width:0%}to{width:100%}}",
|
|
383
|
+
"@keyframes mk-headrule{from{opacity:0}to{opacity:1}}",
|
|
384
|
+
"@keyframes mk-headbg{from{background:rgba(250,250,247,0)}"
|
|
385
|
+
"to{background:rgba(250,250,247,.9)}}",
|
|
386
|
+
"@keyframes mk-caret{0%,45%{opacity:1}50%,95%{opacity:0}100%{opacity:1}}",
|
|
387
|
+
"@keyframes mk-cuearrow{"
|
|
388
|
+
"0%{transform:translateY(-3px) rotate(45deg);opacity:0}"
|
|
389
|
+
"25%{opacity:1}70%{opacity:1}"
|
|
390
|
+
"100%{transform:translateY(20px) rotate(45deg);opacity:0}}",
|
|
391
|
+
"@keyframes mk-cuefade{0%,25%{opacity:1}100%{opacity:0}}",
|
|
392
|
+
# a digit rolling out from behind its own edge
|
|
393
|
+
"@keyframes mk-digit{from{transform:translateY(105%)}"
|
|
394
|
+
"to{transform:translateY(0)}}",
|
|
395
|
+
# the accent line that sweeps a table row as it arrives
|
|
396
|
+
"@keyframes mk-sweep{from{transform:scaleX(0)}to{transform:scaleX(1)}}",
|
|
397
|
+
# registration marks, drawn corner by corner
|
|
398
|
+
"@keyframes mk-markin{from{opacity:0;transform:scale(.4)}"
|
|
399
|
+
"to{opacity:1;transform:scale(1)}}",
|
|
400
|
+
# the index entry for whichever section is on screen
|
|
401
|
+
"@keyframes mk-idxon{from,to{color:rgb(255,90,54);"
|
|
402
|
+
"letter-spacing:.2em}}",
|
|
403
|
+
"@keyframes mk-dotin{from{transform:scale(0)}to{transform:scale(1)}}",
|
|
404
|
+
|
|
405
|
+
# ── the document frame ───────────────────────────────────────────────
|
|
406
|
+
# A spec sheet has margins. These two fixed rules are what say the content
|
|
407
|
+
# sits inside a measured field rather than floating on a page.
|
|
408
|
+
"#mk-doc{position:relative}",
|
|
409
|
+
'#mk-doc::before,#mk-doc::after{content:"";position:fixed;top:0;bottom:0;'
|
|
410
|
+
"width:1px;background:" + RULE + ";z-index:1;pointer-events:none}",
|
|
411
|
+
"#mk-doc::before{left:40px}",
|
|
412
|
+
"#mk-doc::after{right:40px}",
|
|
413
|
+
"@media (max-width:1079px){#mk-doc::before{left:28px}"
|
|
414
|
+
"#mk-doc::after{right:28px}}",
|
|
415
|
+
"@media (max-width:767px){#mk-doc::before,#mk-doc::after{display:none}}",
|
|
416
|
+
|
|
417
|
+
# ── header: a flush document bar, not a floating panel ───────────────
|
|
418
|
+
"#mk-header{position:fixed;top:0;left:0;right:0;z-index:100;"
|
|
419
|
+
"background:rgba(250,250,247,0);"
|
|
420
|
+
"-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}",
|
|
421
|
+
'#mk-header::after{content:"";position:absolute;left:0;right:0;bottom:0;'
|
|
422
|
+
"height:1px;background:" + RULE + ";opacity:0}",
|
|
423
|
+
"#mk-nav>*{position:relative}",
|
|
424
|
+
'#mk-nav>*::after{content:"";position:absolute;left:0;right:100%;bottom:-5px;'
|
|
425
|
+
"height:1px;background:rgb(255,90,54);"
|
|
426
|
+
"transition:right .28s cubic-bezier(.2,.7,.3,1)}",
|
|
427
|
+
"#mk-nav>*:hover::after{right:0}",
|
|
428
|
+
"@media (hover:none){#mk-nav>*::after{right:0;opacity:.45}}",
|
|
429
|
+
|
|
430
|
+
"#mk-progress-track{position:fixed;top:0;left:0;right:0;height:2px;"
|
|
431
|
+
"z-index:101;pointer-events:none;background:rgba(255,90,54,.16)}",
|
|
432
|
+
"#mk-progress{position:absolute;left:0;top:0;width:0;height:100%;"
|
|
433
|
+
"background:rgb(255,90,54)}",
|
|
434
|
+
|
|
435
|
+
# ── masthead ─────────────────────────────────────────────────────────
|
|
436
|
+
"#mk-mast-in{padding-top:128px}",
|
|
437
|
+
"@media (max-width:1079px){#mk-mast-in{padding-top:104px}}",
|
|
438
|
+
"@media (max-width:767px){#mk-mast-in{padding-top:88px}}",
|
|
439
|
+
# the blinking caret under the headline: the one piece of ornament, and it
|
|
440
|
+
# belongs to a page set in a terminal face
|
|
441
|
+
"#mk-caret{width:13px;height:19px;background:rgb(255,90,54)}",
|
|
442
|
+
"#mk-mast-rule,#mk-spec-rule{height:1px;background:" + RULE + ";"
|
|
443
|
+
"transform:scaleX(0);transform-origin:0 50%}",
|
|
444
|
+
digits + "{display:block;transform:translateY(105%)}",
|
|
445
|
+
|
|
446
|
+
# ── the work table ───────────────────────────────────────────────────
|
|
447
|
+
work_rows + "{position:relative;transition:background-color .2s ease,"
|
|
448
|
+
"padding-left .28s cubic-bezier(.2,.7,.3,1)}",
|
|
449
|
+
",".join("#mk-work-%d::after" % i for i in range(len(WORKS)))
|
|
450
|
+
+ '{content:"";position:absolute;left:0;right:0;bottom:-1px;height:1px;'
|
|
451
|
+
"background:rgb(255,90,54);transform:scaleX(0);transform-origin:0 50%}",
|
|
452
|
+
work_hovers + "{background-color:rgba(255,90,54,.06);padding-left:14px}",
|
|
453
|
+
idx_hovers + "{color:rgb(255,90,54)}",
|
|
454
|
+
|
|
455
|
+
# registration marks at the trim, one per corner
|
|
456
|
+
marks + "{position:fixed;width:13px;height:13px;z-index:2;"
|
|
457
|
+
"pointer-events:none;opacity:0}",
|
|
458
|
+
"#mk-mark-0{top:66px;left:40px;border-left:1px solid " + RULE_INK
|
|
459
|
+
+ ";border-top:1px solid " + RULE_INK + "}",
|
|
460
|
+
"#mk-mark-1{top:66px;right:40px;border-right:1px solid " + RULE_INK
|
|
461
|
+
+ ";border-top:1px solid " + RULE_INK + "}",
|
|
462
|
+
"#mk-mark-2{bottom:20px;left:40px;border-left:1px solid " + RULE_INK
|
|
463
|
+
+ ";border-bottom:1px solid " + RULE_INK + "}",
|
|
464
|
+
"#mk-mark-3{bottom:20px;right:40px;border-right:1px solid " + RULE_INK
|
|
465
|
+
+ ";border-bottom:1px solid " + RULE_INK + "}",
|
|
466
|
+
"@media (max-width:1279px){" + marks + "{display:none}}",
|
|
467
|
+
|
|
468
|
+
# the clause index. It needs room outside the document margin, so it only
|
|
469
|
+
# appears once the viewport is wide enough to have that room.
|
|
470
|
+
"#mk-index{position:fixed;left:14px;top:50%;transform:translateY(-50%);"
|
|
471
|
+
"z-index:3;display:grid;row-gap:11px;pointer-events:auto}",
|
|
472
|
+
"@media (max-width:1439px){#mk-index{display:none}}",
|
|
473
|
+
|
|
474
|
+
# process: the sequence marker on each step, and the line it sits on
|
|
475
|
+
proc_dots + "{width:7px;height:7px;background:rgb(255,90,54);"
|
|
476
|
+
"border-radius:50%;transform:scale(0);margin-top:-4px}",
|
|
477
|
+
|
|
478
|
+
# stack tags
|
|
479
|
+
tag_hovers + "{border-color:rgb(255,90,54)}",
|
|
480
|
+
|
|
481
|
+
"#mk-ticker-track{display:flex;width:max-content;will-change:transform}",
|
|
482
|
+
# a ticker you cannot read is decoration; stopping it on hover makes it
|
|
483
|
+
# content again
|
|
484
|
+
"#mk-ticker:hover #mk-ticker-track{animation-play-state:paused}",
|
|
485
|
+
"html{scroll-behavior:smooth}",
|
|
486
|
+
"#services,#works,#products,#contact{scroll-margin-top:82px}",
|
|
487
|
+
"#mk-home{scroll-margin-top:0}",
|
|
488
|
+
"@media (max-width:767px){#services,#works,#products,#contact"
|
|
489
|
+
"{scroll-margin-top:68px}}",
|
|
490
|
+
|
|
491
|
+
# ── the scroll cue, set as a field marker rather than a badge ────────
|
|
492
|
+
"#mk-cue{position:fixed;right:40px;bottom:24px;z-index:90;"
|
|
493
|
+
"display:flex;align-items:center;column-gap:10px;pointer-events:none}",
|
|
494
|
+
"#mk-cue-rail{position:relative;width:9px;height:24px}",
|
|
495
|
+
'#mk-cue-rail::after{content:"";position:absolute;left:50%;top:0;'
|
|
496
|
+
"width:7px;height:7px;margin-left:-4px;opacity:0;"
|
|
497
|
+
"border-right:1px solid rgb(255,90,54);"
|
|
498
|
+
"border-bottom:1px solid rgb(255,90,54)}",
|
|
499
|
+
"@media (max-width:1079px){#mk-cue{right:28px}}",
|
|
500
|
+
"@media (max-width:767px){#mk-cue{right:18px;bottom:16px}}",
|
|
501
|
+
|
|
502
|
+
# ── motion, all of it opt-out-able ───────────────────────────────────
|
|
503
|
+
"@media (prefers-reduced-motion:no-preference){",
|
|
504
|
+
" #mk-hl1-mask>*,#mk-hl2-mask>*{transform:translateY(112%);"
|
|
505
|
+
"animation:mk-linein .95s cubic-bezier(.16,1,.3,1) forwards}",
|
|
506
|
+
" #mk-hl1-mask>*{animation-delay:.2s}",
|
|
507
|
+
" #mk-hl2-mask>*{animation-delay:.32s}",
|
|
508
|
+
" #mk-mast-meta,#mk-mast-lede,#mk-mast-cta{opacity:0;"
|
|
509
|
+
"animation:mk-softin .8s cubic-bezier(.16,1,.3,1) forwards}",
|
|
510
|
+
" #mk-mast-meta{animation-delay:.06s}",
|
|
511
|
+
" #mk-mast-lede{animation-delay:.66s}",
|
|
512
|
+
" #mk-mast-cta{animation-delay:.78s}",
|
|
513
|
+
" #mk-caret{animation:mk-caret 1.15s steps(1,end) infinite;"
|
|
514
|
+
"animation-delay:.95s}",
|
|
515
|
+
" #mk-mast-rule{animation:mk-drawx .9s cubic-bezier(.2,.7,.3,1) forwards;"
|
|
516
|
+
"animation-delay:.44s}",
|
|
517
|
+
" #mk-spec-rule{animation:mk-drawx .9s cubic-bezier(.2,.7,.3,1) forwards;"
|
|
518
|
+
"animation-delay:.9s}",
|
|
519
|
+
" #mk-cue{opacity:0;animation:mk-softin .8s ease forwards;"
|
|
520
|
+
"animation-delay:1.1s}",
|
|
521
|
+
" #mk-cue-rail::after{animation:mk-cuearrow 1.9s cubic-bezier(.4,0,.5,1) "
|
|
522
|
+
"infinite}",
|
|
523
|
+
" #mk-ticker-track{animation:mk-ticker 40s linear infinite}",
|
|
524
|
+
" " + marks + "{animation:mk-markin .5s cubic-bezier(.16,1,.3,1) forwards}",
|
|
525
|
+
" #mk-mark-0{animation-delay:1.15s}",
|
|
526
|
+
" #mk-mark-1{animation-delay:1.24s}",
|
|
527
|
+
" #mk-mark-2{animation-delay:1.33s}",
|
|
528
|
+
" #mk-mark-3{animation-delay:1.42s}",
|
|
529
|
+
" @supports (animation-timeline:view()){",
|
|
530
|
+
" " + reveal_targets + "{animation:mk-rise .01s linear both;"
|
|
531
|
+
"animation-timeline:view();animation-range:entry 2% cover 36%}",
|
|
532
|
+
reveal_stagger,
|
|
533
|
+
# the figures roll in a digit at a time, each digit carrying its own delay
|
|
534
|
+
# through the --d custom property set on its mask
|
|
535
|
+
" " + digits + "{animation:mk-digit .7s cubic-bezier(.16,1,.3,1) both;"
|
|
536
|
+
"animation-delay:var(--d,0ms)}",
|
|
537
|
+
" " + digit_boxes + "{animation:mk-rise .01s linear both;"
|
|
538
|
+
"animation-timeline:view();animation-range:entry 0% entry 1%}",
|
|
539
|
+
# each row's rule draws itself as the row arrives, left to right
|
|
540
|
+
" " + ",".join("#mk-work-%d::after" % i for i in range(len(WORKS)))
|
|
541
|
+
+ "{animation:mk-sweep .01s linear both;animation-timeline:view();"
|
|
542
|
+
"animation-range:entry 12% cover 24%}",
|
|
543
|
+
# the sequence markers pop as each step arrives
|
|
544
|
+
" " + proc_dots + "{animation:mk-dotin .01s linear both;"
|
|
545
|
+
"animation-timeline:view();animation-range:entry 14% cover 26%}",
|
|
546
|
+
" }",
|
|
547
|
+
" @supports (timeline-scope:--x){",
|
|
548
|
+
# the index entry lights while its own section is on screen. No fill mode, so
|
|
549
|
+
# outside the range each entry simply falls back to its resting colour.
|
|
550
|
+
" #mk-doc{timeline-scope:" + ",".join(tl for _n, _l, _h, tl in CLAUSES)
|
|
551
|
+
+ "}",
|
|
552
|
+
section_timelines,
|
|
553
|
+
index_actives,
|
|
554
|
+
" }",
|
|
555
|
+
" @supports (animation-timeline:scroll()){",
|
|
556
|
+
" #mk-progress{animation:mk-progress linear both;"
|
|
557
|
+
"animation-timeline:scroll(root)}",
|
|
558
|
+
" #mk-cue{animation:mk-cuefade linear both;"
|
|
559
|
+
"animation-timeline:scroll(root);animation-range:0px 260px}",
|
|
560
|
+
" #mk-header{animation:mk-headbg linear both;"
|
|
561
|
+
"animation-timeline:scroll(root);animation-range:20px 120px}",
|
|
562
|
+
" #mk-header::after{animation:mk-headrule linear both;"
|
|
563
|
+
"animation-timeline:scroll(root);animation-range:20px 120px}",
|
|
564
|
+
" }",
|
|
565
|
+
"}",
|
|
566
|
+
"</style>",
|
|
567
|
+
])
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
# ── the shared shell ──────────────────────────────────────────────────────────
|
|
571
|
+
HEADER = box("mk-shell-top", {}, [
|
|
572
|
+
# a `code` node with insertLocation "head" is the only way to get @keyframes into
|
|
573
|
+
# the document - customStyles is emitted inside a rule and cannot hold one
|
|
574
|
+
{"type": "code", "data": {"attrID": "mk-motion-css", "insertLocation": "head",
|
|
575
|
+
"content": motion_css(), "processShortcodes": "0"}},
|
|
576
|
+
# the fill and the track are separate elements: nested, the fill's width is a
|
|
577
|
+
# percentage of the track and the two cannot drift apart
|
|
578
|
+
box("mk-progress-track", {}, [box("mk-progress", {}, [])]),
|
|
579
|
+
box("mk-header",
|
|
580
|
+
{"paddingTop": "17px", "paddingBottom": "17px",
|
|
581
|
+
"paddingLeft": "40px", "paddingRight": "40px", "fontFamily": MONO},
|
|
582
|
+
_t={"paddingLeft": "28px", "paddingRight": "28px"},
|
|
583
|
+
_m={"paddingLeft": "18px", "paddingRight": "18px",
|
|
584
|
+
"paddingTop": "13px", "paddingBottom": "13px"},
|
|
585
|
+
children=
|
|
586
|
+
[wrap("mk-header-in", [
|
|
587
|
+
{"type": "div", "data": {"attrID": "mk-header-row"},
|
|
588
|
+
"style": bp({"display": "flex", "alignItems": "center",
|
|
589
|
+
"justifyContent": "space-between", "columnGap": "40px"},
|
|
590
|
+
{"columnGap": "24px"}, {"columnGap": "12px"}),
|
|
591
|
+
"children": [
|
|
592
|
+
# the wordmark is the only route back to the top on a one-pager, so
|
|
593
|
+
# it has to be a link. `menu-link` with a url renders a real <a>.
|
|
594
|
+
{"type": "menu-link", "data": {"attrID": "mk-logo", "url": "#mk-home"},
|
|
595
|
+
"style": {"&": {"_": {"display": "flex", "alignItems": "baseline",
|
|
596
|
+
"columnGap": "9px", "cursor": "pointer"}}},
|
|
597
|
+
"children":
|
|
598
|
+
[T("h3", "MOKSA WEB", color={"token": "--mk-ink"},
|
|
599
|
+
fontSize="14px", fontWeight="500", letterSpacing="0.05em",
|
|
600
|
+
fontFamily=MONO, _m={"fontSize": "13px"}),
|
|
601
|
+
T("p", "/ STUDIO", color={"token": "--mk-faint"},
|
|
602
|
+
fontSize="11px", letterSpacing="0.06em", fontFamily=MONO,
|
|
603
|
+
_m={"display": "none"})]},
|
|
604
|
+
{"type": "menu", "data": {"attrID": "mk-nav"},
|
|
605
|
+
"style": bp({"display": "flex", "columnGap": "30px",
|
|
606
|
+
"alignItems": "center"},
|
|
607
|
+
{"columnGap": "20px"}, {"columnGap": "15px"}),
|
|
608
|
+
"children": [
|
|
609
|
+
{"type": "menu-link",
|
|
610
|
+
"data": {"attrID": "mk-nav-%d" % i, "url": href},
|
|
611
|
+
"style": {"&": {"_": {"color": {"token": "--mk-ink"},
|
|
612
|
+
"fontSize": "13px", "fontWeight": "400",
|
|
613
|
+
"letterSpacing": "0.04em",
|
|
614
|
+
"fontFamily": MONO,
|
|
615
|
+
"transitionAll": "160ms ease",
|
|
616
|
+
"cursor": "pointer"},
|
|
617
|
+
"_m": {"fontSize": "12px"}},
|
|
618
|
+
"hover": {"_": {"color": {"token": "--mk-accent"}}}},
|
|
619
|
+
"text": text}
|
|
620
|
+
for i, (text, href) in enumerate(NAV)
|
|
621
|
+
]},
|
|
622
|
+
# not a pill: a bracketed link, the way a document cross-references
|
|
623
|
+
{"type": "button", "data": {"attrID": "mk-header-cta",
|
|
624
|
+
"url": "#contact"},
|
|
625
|
+
"style": {"&": {"_m": {"display": "none"},
|
|
626
|
+
"_t": {"fontSize": "12px"},
|
|
627
|
+
"_": {"backgroundColor": "rgba(0,0,0,0)",
|
|
628
|
+
"color": {"token": "--mk-accent"},
|
|
629
|
+
"fontSize": "13px", "fontFamily": MONO,
|
|
630
|
+
"letterSpacing": "0.06em",
|
|
631
|
+
"paddingTop": "6px", "paddingBottom": "6px",
|
|
632
|
+
"paddingLeft": "12px", "paddingRight": "12px",
|
|
633
|
+
"radius": "0px", "cursor": "pointer",
|
|
634
|
+
"border": {"width": "1px", "style": "solid",
|
|
635
|
+
"color": "rgb(255,90,54)"},
|
|
636
|
+
"transitionAll": "180ms ease"}},
|
|
637
|
+
"hover": {"_": {"backgroundColor": {"token": "--mk-accent"},
|
|
638
|
+
"color": {"token": "--mk-paper"}}}},
|
|
639
|
+
"text": "START A PROJECT"},
|
|
640
|
+
]}
|
|
641
|
+
])]),
|
|
642
|
+
])
|
|
643
|
+
|
|
644
|
+
FOOTER = box("mk-footer",
|
|
645
|
+
{"backgroundColor": {"token": "--mk-ink"}, "paddingBottom": "26px",
|
|
646
|
+
"paddingLeft": "40px", "paddingRight": "40px", "fontFamily": MONO},
|
|
647
|
+
_t={"paddingLeft": "28px", "paddingRight": "28px"},
|
|
648
|
+
_m={"paddingLeft": "18px", "paddingRight": "18px"},
|
|
649
|
+
children=
|
|
650
|
+
[wrap("mk-footer-in", [
|
|
651
|
+
box("mk-f-top",
|
|
652
|
+
{"display": "grid", "gridCols": "1.1fr .9fr",
|
|
653
|
+
"columnGap": "56px", "rowGap": "36px", "alignItems": "start",
|
|
654
|
+
"paddingTop": "58px", "paddingBottom": "48px"},
|
|
655
|
+
_m={"gridCols": "repeat(1, 1fr)", "paddingTop": "42px",
|
|
656
|
+
"paddingBottom": "34px"},
|
|
657
|
+
children=[
|
|
658
|
+
box("mk-f-brand", {}, [
|
|
659
|
+
mono("MAKE WEB MEANINGFUL", size="11px", color="--mk-accent",
|
|
660
|
+
track="0.2em"),
|
|
661
|
+
T("h2", "打造有價值的網站體驗。", color={"token": "--mk-paper"},
|
|
662
|
+
fontSize="30px", fontWeight="500", letterSpacing="-0.01em",
|
|
663
|
+
lineHeight="1.5", fontFamily=CJK, marginTop="16px",
|
|
664
|
+
_m={"fontSize": "22px"}),
|
|
665
|
+
box("mk-f-contact",
|
|
666
|
+
{"marginTop": "30px", "display": "grid", "rowGap": "6px"}, [
|
|
667
|
+
mono("+886-958-839-939", size="14px",
|
|
668
|
+
color="rgb(250,250,247)", track="0.02em"),
|
|
669
|
+
mono("services@moksaweb.com", size="14px",
|
|
670
|
+
color="rgb(255,90,54)", track="0.02em"),
|
|
671
|
+
mono("TAICHUNG, TAIWAN", size="11px",
|
|
672
|
+
color="rgb(140,142,150)", track="0.16em"),
|
|
673
|
+
]),
|
|
674
|
+
]),
|
|
675
|
+
grid("mk-f-links", 2, "28px", tcols=2, mcols=2, children=[
|
|
676
|
+
box("mk-f-col-0", {}, [
|
|
677
|
+
mono("PAGES", size="10px", color="rgb(122,124,132)",
|
|
678
|
+
track="0.2em"),
|
|
679
|
+
box("mk-f-col-0-list",
|
|
680
|
+
{"marginTop": "16px", "display": "grid", "rowGap": "9px"},
|
|
681
|
+
[mono(t, size="13px", color="rgb(178,180,186)",
|
|
682
|
+
track="0.02em")
|
|
683
|
+
for t in ["作品集", "團隊成員", "關於我們", "服務報價"]]),
|
|
684
|
+
]),
|
|
685
|
+
box("mk-f-col-1", {}, [
|
|
686
|
+
mono("LEARN", size="10px", color="rgb(122,124,132)",
|
|
687
|
+
track="0.2em"),
|
|
688
|
+
box("mk-f-col-1-list",
|
|
689
|
+
{"marginTop": "16px", "display": "grid", "rowGap": "9px"},
|
|
690
|
+
[mono(t, size="13px", color="rgb(178,180,186)",
|
|
691
|
+
track="0.02em")
|
|
692
|
+
for t in ["Claude Code 教學", "n8n 教學", "全部文章",
|
|
693
|
+
"開源計畫"]]),
|
|
694
|
+
]),
|
|
695
|
+
]),
|
|
696
|
+
]),
|
|
697
|
+
box("mk-f-legal",
|
|
698
|
+
{"paddingTop": "18px",
|
|
699
|
+
"customStyles": "border-top:1px solid " + RULE_DARK + ";"},
|
|
700
|
+
[{"type": "div", "data": {"attrID": "mk-f-legal-row"},
|
|
701
|
+
"style": bp({"display": "flex", "justifyContent": "space-between",
|
|
702
|
+
"columnGap": "20px", "rowGap": "6px",
|
|
703
|
+
"customStyles": "flex-wrap:wrap;"}, None, None),
|
|
704
|
+
"children": [
|
|
705
|
+
mono("© 2026 MOKSA WEB — ALL RIGHTS RESERVED", size="10px",
|
|
706
|
+
color="rgb(112,114,122)", track="0.1em"),
|
|
707
|
+
mono("BUILT HEADLESS ON MOSAIC", size="10px",
|
|
708
|
+
color="rgb(112,114,122)", track="0.1em"),
|
|
709
|
+
]}]),
|
|
710
|
+
])])
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
# ── the page ──────────────────────────────────────────────────────────────────
|
|
714
|
+
HERO_LINE = dict(color={"token": "--mk-ink"}, fontSize="62px", fontWeight="600",
|
|
715
|
+
lineHeight="1.12", letterSpacing="-0.035em", fontFamily=DISPLAY,
|
|
716
|
+
_t={"fontSize": "46px"}, _m={"fontSize": "31px"})
|
|
717
|
+
|
|
718
|
+
MASTHEAD = section("mk-mast", [wrap("mk-mast-in", [
|
|
719
|
+
# the document's own header block: what this is, where it is from, which revision
|
|
720
|
+
box("mk-mast-meta",
|
|
721
|
+
{"display": "flex", "justifyContent": "space-between", "columnGap": "20px",
|
|
722
|
+
"rowGap": "6px", "customStyles": "flex-wrap:wrap;"},
|
|
723
|
+
[mono("MOKSA WEB — STUDIO PROFILE", color="--mk-ink", track="0.16em"),
|
|
724
|
+
mono("TAICHUNG, TW", track="0.16em"),
|
|
725
|
+
mono("REV. 2026.09", track="0.16em")]),
|
|
726
|
+
box("mk-mast-rule", {"marginTop": "16px"}, []),
|
|
727
|
+
box("mk-mast-lines", {"marginTop": "44px"},
|
|
728
|
+
_m={"marginTop": "30px"},
|
|
729
|
+
children=[
|
|
730
|
+
mask_line("mk-hl1", "網站開發 × AI 導入", **HERO_LINE),
|
|
731
|
+
mask_line("mk-hl2", "流程自動化", tag="h2", **HERO_LINE),
|
|
732
|
+
box("mk-caret-row",
|
|
733
|
+
{"display": "flex", "alignItems": "center", "marginTop": "10px"},
|
|
734
|
+
[box("mk-caret", {}, [])]),
|
|
735
|
+
]),
|
|
736
|
+
{"type": "text", "data": {"tagName": "p", "attrID": "mk-mast-lede"},
|
|
737
|
+
"style": bp({"color": {"token": "--mk-muted"}, "fontSize": "16px",
|
|
738
|
+
"lineHeight": "2", "marginTop": "24px", "maxWidth": "34em"},
|
|
739
|
+
None,
|
|
740
|
+
{"fontSize": "14px", "lineHeight": "1.95", "marginTop": "20px"}),
|
|
741
|
+
"text": "我打造網站、開發軟體、導入 AI、串起自動化流程,"
|
|
742
|
+
"讓技術不只是工具,而是幫你省下時間、長出業績的數位夥伴。"},
|
|
743
|
+
{"type": "div", "data": {"attrID": "mk-mast-cta"},
|
|
744
|
+
"style": bp({"display": "flex", "columnGap": "26px", "rowGap": "10px",
|
|
745
|
+
"marginTop": "32px", "flexWrap": "wrap"},
|
|
746
|
+
None, {"marginTop": "26px", "columnGap": "18px"}),
|
|
747
|
+
"children": [
|
|
748
|
+
# underlined links, not buttons: a document points, it does not sell
|
|
749
|
+
{"type": "button", "data": {"attrID": "mk-cta-%d" % n, "url": href},
|
|
750
|
+
"style": {"&": {"_": {"backgroundColor": "rgba(0,0,0,0)",
|
|
751
|
+
"color": {"token": "--mk-ink"}, "fontSize": "14px",
|
|
752
|
+
"fontFamily": MONO, "letterSpacing": "0.06em",
|
|
753
|
+
"paddingTop": "8px", "paddingBottom": "8px",
|
|
754
|
+
"radius": "0px", "cursor": "pointer",
|
|
755
|
+
"customStyles":
|
|
756
|
+
"border-bottom:1px solid rgb(22,24,28);",
|
|
757
|
+
"transitionAll": "180ms ease"}},
|
|
758
|
+
"hover": {"_": {"color": {"token": "--mk-accent"}}}},
|
|
759
|
+
"text": text}
|
|
760
|
+
for n, (text, href) in enumerate([("→ 服務項目", "#services"),
|
|
761
|
+
("→ 精選作品", "#works")], start=1)
|
|
762
|
+
]},
|
|
763
|
+
box("mk-spec-rule", {"marginTop": "56px"}, [], _m={"marginTop": "40px"}),
|
|
764
|
+
# the figures as a datasheet, not as four big numbers looking for attention
|
|
765
|
+
grid("mk-spec", 4, "0px", tcols=2, mcols=2,
|
|
766
|
+
paddingTop="22px", paddingBottom="58px", children=[
|
|
767
|
+
box("mk-spec-%d" % i,
|
|
768
|
+
{"paddingRight": "22px",
|
|
769
|
+
"paddingLeft": "0px" if i == 0 else "22px",
|
|
770
|
+
"customStyles": ("" if i == 0
|
|
771
|
+
else "border-left:1px solid " + RULE + ";")},
|
|
772
|
+
# at two-up the rule falls on the odd cells instead, and
|
|
773
|
+
# `border-left:0` has to be set explicitly - omitting it leaves the
|
|
774
|
+
# four-up rule standing
|
|
775
|
+
_t={"paddingTop": "18px", "paddingBottom": "18px",
|
|
776
|
+
"paddingLeft": "0px" if i % 2 == 0 else "18px",
|
|
777
|
+
"customStyles": ("border-left:0;" if i % 2 == 0
|
|
778
|
+
else "border-left:1px solid " + RULE + ";")
|
|
779
|
+
+ ("border-top:1px solid " + RULE + ";" if i > 1
|
|
780
|
+
else "border-top:0;")},
|
|
781
|
+
_m={"paddingRight": "12px",
|
|
782
|
+
"paddingLeft": "0px" if i % 2 == 0 else "14px"},
|
|
783
|
+
children=[
|
|
784
|
+
mono(key, size="10px", color="--mk-faint", track="0.18em"),
|
|
785
|
+
box("mk-spec-n-%d" % i,
|
|
786
|
+
{"display": "flex", "alignItems": "baseline",
|
|
787
|
+
"columnGap": "3px", "marginTop": "12px"},
|
|
788
|
+
[figure("mk-fig-%d" % i, num),
|
|
789
|
+
T("p", "+", color={"token": "--mk-accent"},
|
|
790
|
+
fontSize="17px", fontFamily=MONO, fontWeight="500")]),
|
|
791
|
+
T("p", zh, color={"token": "--mk-muted"}, fontSize="12px",
|
|
792
|
+
marginTop="10px", lineHeight="1.7", _m={"fontSize": "11px"}),
|
|
793
|
+
])
|
|
794
|
+
for i, (key, num, zh) in enumerate(SPEC)
|
|
795
|
+
]),
|
|
796
|
+
])])
|
|
797
|
+
|
|
798
|
+
TICKER_BAND = box("mk-ticker",
|
|
799
|
+
{"backgroundColor": {"token": "--mk-ink"}, "paddingTop": "11px",
|
|
800
|
+
"paddingBottom": "11px", "customStyles": "overflow:hidden;"},
|
|
801
|
+
[box("mk-ticker-track", {"display": "flex", "columnGap": "0px"},
|
|
802
|
+
# duplicated so the loop can translate exactly -50% and never show a seam
|
|
803
|
+
[T("p", w, color="rgb(250,250,247)", fontSize="11px", fontWeight="400",
|
|
804
|
+
letterSpacing="0.2em", fontFamily=MONO,
|
|
805
|
+
customStyles=("padding:0 22px;white-space:nowrap;"
|
|
806
|
+
"border-right:1px solid rgba(250,250,247,.22);"))
|
|
807
|
+
for w in TICKER * 2])])
|
|
808
|
+
|
|
809
|
+
SERVICE_SEC = section("services", [wrap("mk-svc-in", [
|
|
810
|
+
box("mk-svc-pad", {"paddingTop": "84px"}, _m={"paddingTop": "56px"}, children=[
|
|
811
|
+
clause("01", "SERVICES — 12 ITEMS, ONE CONTACT", "從一個窗口把技術整合完",
|
|
812
|
+
"mk-svc-head"),
|
|
813
|
+
box("mk-svc-list", {"marginTop": "44px"}, _m={"marginTop": "32px"}, children=[
|
|
814
|
+
box("mk-svc-%d" % i,
|
|
815
|
+
{"display": "grid", "gridCols": "84px 1fr 1.35fr",
|
|
816
|
+
"columnGap": "24px", "alignItems": "start",
|
|
817
|
+
"paddingTop": "26px", "paddingBottom": "26px",
|
|
818
|
+
"customStyles": "border-top:1px solid " + RULE + ";"},
|
|
819
|
+
_t={"gridCols": "72px 1fr 1.2fr", "columnGap": "18px"},
|
|
820
|
+
_m={"gridCols": "repeat(1, 1fr)", "rowGap": "10px",
|
|
821
|
+
"paddingTop": "20px", "paddingBottom": "20px"},
|
|
822
|
+
children=[
|
|
823
|
+
mono(num, size="12px", color="--mk-faint", track="0.06em"),
|
|
824
|
+
box("mk-svc-t-%d" % i, {}, [
|
|
825
|
+
mono(en, size="10px", color="--mk-accent", track="0.16em"),
|
|
826
|
+
T("h3", zh, color={"token": "--mk-ink"}, fontSize="19px",
|
|
827
|
+
fontWeight="500", marginTop="9px", fontFamily=CJK,
|
|
828
|
+
letterSpacing="0.01em"),
|
|
829
|
+
]),
|
|
830
|
+
T("p", body, color={"token": "--mk-muted"}, fontSize="13px",
|
|
831
|
+
lineHeight="2",
|
|
832
|
+
),
|
|
833
|
+
])
|
|
834
|
+
for i, (num, en, zh, body) in enumerate(SERVICES)
|
|
835
|
+
]),
|
|
836
|
+
]),
|
|
837
|
+
])])
|
|
838
|
+
|
|
839
|
+
PROCESS_SEC = section("process", [wrap("mk-proc-in", [
|
|
840
|
+
box("mk-proc-pad", {"paddingTop": "84px"}, _m={"paddingTop": "56px"}, children=[
|
|
841
|
+
clause("02", "HOW AN ENGAGEMENT RUNS", "從第一次談,到長期照顧",
|
|
842
|
+
"mk-proc-head"),
|
|
843
|
+
# A sequence, so it is drawn as one: four stops on a single line, with the
|
|
844
|
+
# line drawing itself between them as you arrive.
|
|
845
|
+
box("mk-proc-track", {"marginTop": "52px"}, _m={"marginTop": "34px"},
|
|
846
|
+
children=[
|
|
847
|
+
grid("mk-proc-grid", 4, "24px", tcols=2, mcols=1, children=[
|
|
848
|
+
box("mk-proc-%d" % i,
|
|
849
|
+
{"paddingTop": "26px", "paddingRight": "18px",
|
|
850
|
+
"customStyles": "border-top:1px solid " + RULE + ";"},
|
|
851
|
+
_m={"paddingTop": "20px", "paddingRight": "0px"},
|
|
852
|
+
children=[
|
|
853
|
+
box("mk-proc-dot-%d" % i, {}, []),
|
|
854
|
+
mono("STEP %02d" % (i + 1), size="10px",
|
|
855
|
+
color="--mk-faint", track="0.18em",
|
|
856
|
+
marginTop="16px"),
|
|
857
|
+
T("h3", zh, color={"token": "--mk-ink"}, fontSize="19px",
|
|
858
|
+
fontWeight="500", fontFamily=CJK, marginTop="10px",
|
|
859
|
+
letterSpacing="0.01em"),
|
|
860
|
+
mono(en, size="10px", color="--mk-accent", track="0.16em",
|
|
861
|
+
marginTop="7px"),
|
|
862
|
+
T("p", body, color={"token": "--mk-muted"},
|
|
863
|
+
fontSize="13px", lineHeight="2", marginTop="12px"),
|
|
864
|
+
])
|
|
865
|
+
for i, (zh, en, body) in enumerate(PROCESS)
|
|
866
|
+
]),
|
|
867
|
+
]),
|
|
868
|
+
]),
|
|
869
|
+
])])
|
|
870
|
+
|
|
871
|
+
STACK_SEC = section("stack", [wrap("mk-stack-in", [
|
|
872
|
+
box("mk-stack-pad", {"paddingTop": "84px"}, _m={"paddingTop": "56px"}, children=[
|
|
873
|
+
clause("04", "STACK — WHAT WE BUILD WITH", "工具是選的,不是信仰",
|
|
874
|
+
"mk-stack-head"),
|
|
875
|
+
box("mk-stack-list", {"marginTop": "44px"}, _m={"marginTop": "32px"},
|
|
876
|
+
children=[
|
|
877
|
+
box("mk-stack-%d" % i,
|
|
878
|
+
{"display": "grid", "gridCols": "160px 1fr", "columnGap": "24px",
|
|
879
|
+
"alignItems": "baseline",
|
|
880
|
+
"paddingTop": "18px", "paddingBottom": "18px",
|
|
881
|
+
"customStyles": "border-top:1px solid " + RULE + ";"},
|
|
882
|
+
_m={"gridCols": "1fr", "rowGap": "10px",
|
|
883
|
+
"paddingTop": "16px", "paddingBottom": "16px"},
|
|
884
|
+
children=[
|
|
885
|
+
mono(group, size="10px", color="--mk-faint", track="0.18em"),
|
|
886
|
+
box("mk-stack-tags-%d" % i,
|
|
887
|
+
{"display": "flex", "columnGap": "8px", "rowGap": "8px",
|
|
888
|
+
"customStyles": "flex-wrap:wrap;"},
|
|
889
|
+
[box("mk-tag-%d-%d" % (i, j),
|
|
890
|
+
{"customStyles":
|
|
891
|
+
"border:1px solid " + RULE + ";"
|
|
892
|
+
"padding:5px 10px;"
|
|
893
|
+
"transition:border-color .2s ease,"
|
|
894
|
+
"color .2s ease;"},
|
|
895
|
+
[mono(name, size="11px", color="--mk-ink",
|
|
896
|
+
track="0.04em")])
|
|
897
|
+
for j, name in enumerate(items)]),
|
|
898
|
+
])
|
|
899
|
+
for i, (group, items) in enumerate(STACK)
|
|
900
|
+
]),
|
|
901
|
+
]),
|
|
902
|
+
])])
|
|
903
|
+
|
|
904
|
+
WORK_SEC = section("works", [wrap("mk-works-in", [
|
|
905
|
+
box("mk-works-pad", {"paddingTop": "84px"}, _m={"paddingTop": "56px"}, children=[
|
|
906
|
+
clause("03", "SELECTED WORKS — ALL LIVE", "全部正式上線,真實運轉中",
|
|
907
|
+
"mk-works-head"),
|
|
908
|
+
# A table, not a grid of tiles: nine clients each with a discipline and a
|
|
909
|
+
# domain is tabular data, and a table is how tabular data is read.
|
|
910
|
+
box("mk-works-table", {"marginTop": "44px"}, _m={"marginTop": "32px"},
|
|
911
|
+
children=[
|
|
912
|
+
box("mk-works-thead",
|
|
913
|
+
{"display": "grid", "gridCols": "60px 1.4fr 1fr 1fr",
|
|
914
|
+
"columnGap": "20px", "paddingBottom": "12px",
|
|
915
|
+
"customStyles":
|
|
916
|
+
"border-bottom:1px solid rgba(22,24,28,.34);"},
|
|
917
|
+
_m={"display": "none"},
|
|
918
|
+
children=[
|
|
919
|
+
mono("IDX", size="10px", color="--mk-faint", track="0.18em"),
|
|
920
|
+
mono("CLIENT", size="10px", color="--mk-faint",
|
|
921
|
+
track="0.18em"),
|
|
922
|
+
mono("DISCIPLINE", size="10px", color="--mk-faint",
|
|
923
|
+
track="0.18em"),
|
|
924
|
+
mono("DOMAIN", size="10px", color="--mk-faint",
|
|
925
|
+
track="0.18em"),
|
|
926
|
+
]),
|
|
927
|
+
] + [
|
|
928
|
+
box("mk-work-%d" % i,
|
|
929
|
+
{"display": "grid", "gridCols": "60px 1.4fr 1fr 1fr",
|
|
930
|
+
"columnGap": "20px", "alignItems": "baseline",
|
|
931
|
+
"paddingTop": "17px", "paddingBottom": "17px",
|
|
932
|
+
"cursor": "pointer",
|
|
933
|
+
"customStyles": "border-bottom:1px solid " + RULE + ";"},
|
|
934
|
+
_m={"gridCols": "1fr", "rowGap": "5px",
|
|
935
|
+
"paddingTop": "15px", "paddingBottom": "15px"},
|
|
936
|
+
children=[
|
|
937
|
+
{"type": "text",
|
|
938
|
+
"data": {"tagName": "p", "attrID": "mk-work-idx-%d" % i},
|
|
939
|
+
"style": {"&": {"_": {"color": {"token": "--mk-faint"},
|
|
940
|
+
"fontSize": "12px",
|
|
941
|
+
"fontFamily": MONO,
|
|
942
|
+
"letterSpacing": "0.04em",
|
|
943
|
+
"transitionAll": "180ms ease"}}},
|
|
944
|
+
"text": "%02d" % (i + 1)},
|
|
945
|
+
T("h3", name, color={"token": "--mk-ink"}, fontSize="17px",
|
|
946
|
+
fontWeight="500", fontFamily=CJK, letterSpacing="0.01em",
|
|
947
|
+
_m={"fontSize": "16px"}),
|
|
948
|
+
mono(cat, size="11px", color="--mk-muted", track="0.1em"),
|
|
949
|
+
mono(domain, size="12px", color="--mk-accent", track="0.02em"),
|
|
950
|
+
])
|
|
951
|
+
for i, (name, cat, domain) in enumerate(WORKS)
|
|
952
|
+
]),
|
|
953
|
+
box("mk-works-foot", {"paddingTop": "16px"},
|
|
954
|
+
[mono("17 SELECTED — 9 LISTED", size="11px", color="--mk-faint",
|
|
955
|
+
track="0.16em")]),
|
|
956
|
+
]),
|
|
957
|
+
])])
|
|
958
|
+
|
|
959
|
+
PRODUCT_SEC = section("products", [wrap("mk-prod-in", [
|
|
960
|
+
box("mk-prod-pad", {"paddingTop": "80px", "paddingBottom": "80px"},
|
|
961
|
+
_m={"paddingTop": "54px", "paddingBottom": "54px"}, children=[
|
|
962
|
+
box("mk-prod-head",
|
|
963
|
+
{"display": "grid", "gridCols": "84px 1fr", "columnGap": "0px",
|
|
964
|
+
"alignItems": "start", "paddingTop": "22px",
|
|
965
|
+
"customStyles": "border-top:1px solid " + RULE_DARK + ";"},
|
|
966
|
+
_m={"gridCols": "48px 1fr"},
|
|
967
|
+
children=[
|
|
968
|
+
mono("§05", size="12px", color="--mk-accent", track="0.06em"),
|
|
969
|
+
box("mk-prod-head-t", {}, [
|
|
970
|
+
mono("OUR PRODUCTS", size="11px", color="rgb(140,142,150)"),
|
|
971
|
+
T("h2", "不只接案,也開發自己的產品",
|
|
972
|
+
color={"token": "--mk-paper"}, fontSize="34px",
|
|
973
|
+
fontWeight="600", letterSpacing="-0.025em",
|
|
974
|
+
lineHeight="1.3", fontFamily=DISPLAY, marginTop="10px",
|
|
975
|
+
_t={"fontSize": "29px"}, _m={"fontSize": "23px"}),
|
|
976
|
+
]),
|
|
977
|
+
]),
|
|
978
|
+
box("mk-prod-list", {"marginTop": "40px"}, _m={"marginTop": "28px"},
|
|
979
|
+
children=[
|
|
980
|
+
box("mk-prod-%d" % i,
|
|
981
|
+
{"display": "grid", "gridCols": "84px 1fr 1.3fr",
|
|
982
|
+
"columnGap": "24px", "alignItems": "start",
|
|
983
|
+
"paddingTop": "26px", "paddingBottom": "26px",
|
|
984
|
+
"customStyles": "border-top:1px solid " + RULE_DARK + ";"},
|
|
985
|
+
_t={"gridCols": "72px 1fr 1.2fr", "columnGap": "18px",
|
|
986
|
+
"rowGap": "14px"},
|
|
987
|
+
_m={"gridCols": "repeat(1, 1fr)", "rowGap": "12px",
|
|
988
|
+
"paddingTop": "20px", "paddingBottom": "20px"},
|
|
989
|
+
children=[
|
|
990
|
+
mono("%02d" % (i + 1), size="12px",
|
|
991
|
+
color="rgb(122,124,132)", track="0.06em"),
|
|
992
|
+
box("mk-prod-t-%d" % i, {}, [
|
|
993
|
+
mono(en, size="10px", color="--mk-accent",
|
|
994
|
+
track="0.16em"),
|
|
995
|
+
T("h3", name, color={"token": "--mk-paper"},
|
|
996
|
+
fontSize="24px", fontWeight="600",
|
|
997
|
+
fontFamily=DISPLAY, marginTop="9px",
|
|
998
|
+
letterSpacing="-0.02em", _m={"fontSize": "21px"}),
|
|
999
|
+
# a property of the product, not a column of the
|
|
1000
|
+
# table: as a fourth grid child it landed alone in
|
|
1001
|
+
# the 72px number gutter on row two at tablet
|
|
1002
|
+
box("mk-prod-s-%d" % i,
|
|
1003
|
+
{"marginTop": "14px",
|
|
1004
|
+
"customStyles":
|
|
1005
|
+
"border:1px solid rgba(255,90,54,.6);"
|
|
1006
|
+
"padding:5px 10px;display:inline-block;"
|
|
1007
|
+
"width:max-content;"},
|
|
1008
|
+
[mono(status, size="10px", color="--mk-accent",
|
|
1009
|
+
track="0.12em")]),
|
|
1010
|
+
]),
|
|
1011
|
+
T("p", body, color="rgb(158,160,168)", fontSize="13px",
|
|
1012
|
+
lineHeight="2"),
|
|
1013
|
+
])
|
|
1014
|
+
for i, (en, name, body, status) in enumerate(PRODUCTS)
|
|
1015
|
+
]),
|
|
1016
|
+
]),
|
|
1017
|
+
])], bg="--mk-ink")
|
|
1018
|
+
|
|
1019
|
+
VOICE_SEC = section("mk-voices", [wrap("mk-voice-in", [
|
|
1020
|
+
box("mk-voice-pad", {"paddingTop": "84px"}, _m={"paddingTop": "56px"}, children=[
|
|
1021
|
+
clause("06", "TESTIMONIALS", "客戶怎麼說", "mk-voice-head"),
|
|
1022
|
+
grid("mk-voice-grid", 3, "0px", tcols=1, mcols=1, marginTop="42px", children=[
|
|
1023
|
+
box("mk-voice-%d" % i,
|
|
1024
|
+
{"paddingTop": "26px", "paddingRight": "28px", "paddingBottom": "26px",
|
|
1025
|
+
"paddingLeft": "0px" if i == 0 else "28px",
|
|
1026
|
+
"customStyles": "border-top:1px solid " + RULE + ";"
|
|
1027
|
+
+ ("" if i == 0
|
|
1028
|
+
else "border-left:1px solid " + RULE + ";")},
|
|
1029
|
+
# one column: the column rule and the gutter both have to be switched
|
|
1030
|
+
# off explicitly - omitting them leaves the three-up rule standing
|
|
1031
|
+
_t={"paddingLeft": "0px", "paddingRight": "0px",
|
|
1032
|
+
"customStyles": "border-top:1px solid " + RULE + ";"
|
|
1033
|
+
"border-left:0;"},
|
|
1034
|
+
_m={"paddingLeft": "0px", "paddingRight": "0px",
|
|
1035
|
+
"customStyles": "border-top:1px solid " + RULE + ";"
|
|
1036
|
+
"border-left:0;"},
|
|
1037
|
+
children=[
|
|
1038
|
+
mono("“", size="24px", color="--mk-accent", track="0"),
|
|
1039
|
+
T("p", quote, color={"token": "--mk-ink"}, fontSize="14px",
|
|
1040
|
+
lineHeight="2.05", marginTop="4px"),
|
|
1041
|
+
box("mk-voice-a-%d" % i, {"marginTop": "22px"}, [
|
|
1042
|
+
mono(who, size="12px", color="--mk-ink", track="0.04em"),
|
|
1043
|
+
mono(role, size="10px", color="--mk-faint", track="0.14em",
|
|
1044
|
+
marginTop="5px"),
|
|
1045
|
+
]),
|
|
1046
|
+
])
|
|
1047
|
+
for i, (quote, who, role) in enumerate(VOICES)
|
|
1048
|
+
]),
|
|
1049
|
+
]),
|
|
1050
|
+
])])
|
|
1051
|
+
|
|
1052
|
+
CONTACT = section("contact", [wrap("mk-contact-in", [
|
|
1053
|
+
box("mk-contact-pad", {"paddingTop": "84px", "paddingBottom": "96px"},
|
|
1054
|
+
_m={"paddingTop": "56px", "paddingBottom": "64px"}, children=[
|
|
1055
|
+
box("mk-contact-head",
|
|
1056
|
+
{"display": "grid", "gridCols": "84px 1fr", "columnGap": "0px",
|
|
1057
|
+
"alignItems": "start", "paddingTop": "22px",
|
|
1058
|
+
"customStyles": "border-top:1px solid " + RULE + ";"},
|
|
1059
|
+
_m={"gridCols": "48px 1fr"},
|
|
1060
|
+
children=[
|
|
1061
|
+
mono("§07", size="12px", color="--mk-accent", track="0.06em"),
|
|
1062
|
+
box("mk-contact-head-t", {}, [
|
|
1063
|
+
mono("START A PROJECT", size="11px", color="--mk-muted"),
|
|
1064
|
+
ml("h2", "準備好升級\n你的數位競爭力了嗎?",
|
|
1065
|
+
color={"token": "--mk-ink"}, fontSize="44px",
|
|
1066
|
+
fontWeight="600", letterSpacing="-0.03em",
|
|
1067
|
+
lineHeight="1.24", fontFamily=DISPLAY, marginTop="12px",
|
|
1068
|
+
_t={"fontSize": "36px"}, _m={"fontSize": "26px"}),
|
|
1069
|
+
T("p", "先看方案抓預算,或直接告訴我們你想解決的問題,"
|
|
1070
|
+
"一個工作天內回覆。",
|
|
1071
|
+
color={"token": "--mk-muted"}, fontSize="15px",
|
|
1072
|
+
lineHeight="2", marginTop="18px", maxWidth="30em",
|
|
1073
|
+
_m={"fontSize": "13.5px"}),
|
|
1074
|
+
# the contact routes as a key/value block, like the rest of
|
|
1075
|
+
# the document
|
|
1076
|
+
box("mk-contact-rows",
|
|
1077
|
+
{"marginTop": "34px", "display": "grid", "rowGap": "0px"},
|
|
1078
|
+
[box("mk-contact-r-%d" % i,
|
|
1079
|
+
{"display": "grid", "gridCols": "150px 1fr",
|
|
1080
|
+
"columnGap": "20px", "alignItems": "baseline",
|
|
1081
|
+
"paddingTop": "15px", "paddingBottom": "15px",
|
|
1082
|
+
"customStyles":
|
|
1083
|
+
"border-top:1px solid " + RULE + ";"},
|
|
1084
|
+
_m={"gridCols": "84px 1fr", "columnGap": "12px"},
|
|
1085
|
+
children=[
|
|
1086
|
+
mono(k, size="10px", color="--mk-faint",
|
|
1087
|
+
track="0.18em"),
|
|
1088
|
+
mono(v, size="15px", color=vc, track="0.02em",
|
|
1089
|
+
_m={"fontSize": "13px"}),
|
|
1090
|
+
])
|
|
1091
|
+
for i, (k, v, vc) in enumerate([
|
|
1092
|
+
("EMAIL", "services@moksaweb.com", "--mk-accent"),
|
|
1093
|
+
("PHONE", "+886-958-839-939", "--mk-ink"),
|
|
1094
|
+
("LOCATION", "TAICHUNG, TAIWAN", "--mk-ink"),
|
|
1095
|
+
("RESPONSE", "WITHIN 1 BUSINESS DAY", "--mk-ink"),
|
|
1096
|
+
])]),
|
|
1097
|
+
]),
|
|
1098
|
+
]),
|
|
1099
|
+
]),
|
|
1100
|
+
])])
|
|
1101
|
+
|
|
1102
|
+
# Registration marks. A spec sheet is a printed object and these are how one is
|
|
1103
|
+
# trimmed; here they simply say the page has edges that were decided.
|
|
1104
|
+
MARKS = box("mk-marks", {}, [box("mk-mark-%d" % i, {}, []) for i in range(4)])
|
|
1105
|
+
|
|
1106
|
+
# The clause index, fixed to the left margin on wide screens. Each entry lights up
|
|
1107
|
+
# while its own section is on screen, through a named view timeline declared on the
|
|
1108
|
+
# section and consumed here - no JavaScript, no scroll listener.
|
|
1109
|
+
INDEX = box("mk-index", {}, [
|
|
1110
|
+
{"type": "menu-link", "data": {"attrID": "mk-idx-%d" % i, "url": href},
|
|
1111
|
+
"style": {"&": {"_": {"display": "flex", "columnGap": "8px",
|
|
1112
|
+
"alignItems": "baseline", "cursor": "pointer",
|
|
1113
|
+
"color": {"token": "--mk-faint"},
|
|
1114
|
+
"fontFamily": MONO, "fontSize": "10px",
|
|
1115
|
+
"letterSpacing": "0.14em",
|
|
1116
|
+
"transitionAll": "220ms ease"}},
|
|
1117
|
+
"hover": {"_": {"color": {"token": "--mk-ink"}}}},
|
|
1118
|
+
"text": "§" + num + " " + name}
|
|
1119
|
+
for i, (num, name, href, _tl) in enumerate(CLAUSES)
|
|
1120
|
+
])
|
|
1121
|
+
|
|
1122
|
+
CUE = box("mk-cue", {}, [
|
|
1123
|
+
mono("SCROLL", size="10px", color="--mk-faint", track="0.22em"),
|
|
1124
|
+
box("mk-cue-rail", {}, []),
|
|
1125
|
+
])
|
|
1126
|
+
|
|
1127
|
+
HOME_TREE = {"type": "div", "data": {"attrID": "mk-home"},
|
|
1128
|
+
"children": [box("mk-doc", {}, [
|
|
1129
|
+
MARKS, INDEX, CUE,
|
|
1130
|
+
MASTHEAD, TICKER_BAND, SERVICE_SEC, PROCESS_SEC, WORK_SEC,
|
|
1131
|
+
STACK_SEC, PRODUCT_SEC, VOICE_SEC, CONTACT,
|
|
1132
|
+
])]}
|
|
1133
|
+
|
|
1134
|
+
SITE = {
|
|
1135
|
+
"master": "Moksa Web shell",
|
|
1136
|
+
"theme": {
|
|
1137
|
+
"variables": TOKENS,
|
|
1138
|
+
# Element classes are theme-global, and this install carries two brands,
|
|
1139
|
+
# so the type system is baked into the nodes by apply_type() instead.
|
|
1140
|
+
"elementClasses": {},
|
|
1141
|
+
},
|
|
1142
|
+
"shell": {"header": apply_type(HEADER, DISPLAY, CJK, "600"),
|
|
1143
|
+
"footer": apply_type(FOOTER, DISPLAY, CJK, "600")},
|
|
1144
|
+
"pages": [
|
|
1145
|
+
{"slug": "moksa", "post_id": 26, "title": "Moksa Web",
|
|
1146
|
+
"tree": apply_type(HOME_TREE, DISPLAY, CJK, "600")},
|
|
1147
|
+
],
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def clean(node):
|
|
1152
|
+
"""Drop the None styles flatten() would otherwise have to special-case."""
|
|
1153
|
+
if isinstance(node, dict):
|
|
1154
|
+
return {k: clean(v) for k, v in node.items() if v is not None}
|
|
1155
|
+
if isinstance(node, list):
|
|
1156
|
+
return [clean(v) for v in node]
|
|
1157
|
+
return node
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
if __name__ == "__main__":
|
|
1161
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
1162
|
+
path = os.path.join(here, "moksa.json")
|
|
1163
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
1164
|
+
json.dump(clean(SITE), fh, indent=1, ensure_ascii=False)
|
|
1165
|
+
print("wrote", path)
|