mosaic-headless 1.24.0 → 1.25.1
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/SKILL.md +7 -3
- 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/default-children.csv +26 -11
- package/data/dialog-form-verification.csv +2 -2
- package/data/mobile-audit.csv +55 -0
- package/data/navigation-verification.csv +22 -46
- package/data/placement-rules.csv +127 -127
- package/data/slider-verification.csv +15 -15
- package/data/token-verification.csv +16 -0
- package/package.json +1 -1
- package/references/design-system.md +32 -0
- package/references/responsive.md +53 -0
- package/references/styling.md +17 -3
- package/scripts/check-release.mjs +3 -1
- package/sites/_moksa.py +185 -14
- package/sites/moksa.json +14264 -1289
- package/tools/audit_mobile.py +147 -0
- package/tools/build_page.py +8 -1
- package/tools/extract_default_children.py +66 -4
- package/tools/extract_placement.py +12 -1
- package/tools/verify_tokens.py +170 -0
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
slider,check,result,detail
|
|
2
|
-
"hero, 4 slides,
|
|
3
|
-
"hero, 4 slides,
|
|
4
|
-
"hero, 4 slides,
|
|
5
|
-
"hero, 4 slides,
|
|
6
|
-
"hero, 4 slides,
|
|
7
|
-
"hero, 4 slides,
|
|
8
|
-
"hero, 4 slides,
|
|
9
|
-
"hero, 4 slides,
|
|
10
|
-
"hero, 4 slides,
|
|
11
|
-
"hero, 4 slides,
|
|
12
|
-
"hero, 4 slides,
|
|
13
|
-
"hero, 4 slides,
|
|
14
|
-
"hero, 4 slides,
|
|
15
|
-
"hero, 4 slides,
|
|
16
|
-
"hero, 4 slides,
|
|
2
|
+
"hero, 4 slides, carousel",HOST,PASS,"<mosaic-slider> animation=slide carousel=true, 4 slides"
|
|
3
|
+
"hero, 4 slides, carousel",TRACK,PASS,<mosaic-slider-slides> aria-live=off
|
|
4
|
+
"hero, 4 slides, carousel",HAS_CURRENT,PASS,slide 0 is current at rest
|
|
5
|
+
"hero, 4 slides, carousel",BULLET_TEMPLATE,PASS,"4 bullets for 4 slides, all with id='sl-main-nav-dot' - one authored bullet is repeated per slide, duplicate ids included"
|
|
6
|
+
"hero, 4 slides, carousel",BULLET_ARIA,PASS,"role=button tabindex=0 labels=一、輪播不是相簿, 二、四種動畫, 三、箭頭會自己消失, 四、不想動就不動 - taken from each slide's own title"
|
|
7
|
+
"hero, 4 slides, carousel",ADVANCES,PASS,reached 4 of 4 slides unaided
|
|
8
|
+
"hero, 4 slides, carousel",AUTOPLAY_DECLARED,PASS,data-autoplay=true delay=3600
|
|
9
|
+
"hero, 4 slides, carousel",BULLET_GOES,PASS,bullet 0 -> slide 0
|
|
10
|
+
"hero, 4 slides, carousel",ARROW_ENDS,PASS,"carousel: it wraps, so neither arrow hides (prev hidden=False, next hidden=False)"
|
|
11
|
+
"hero, 4 slides, carousel",CAROUSEL_LAYOUT,PASS,"grid ""slide"", every slide still one track wide ([1200] vs 1200) - isCarousel wraps the ends, it does not show more than one"
|
|
12
|
+
"hero, 4 slides, carousel",ARROW_ARIA,PASS,role=button tabindex=0 label='下一張投影片'
|
|
13
|
+
"hero, 4 slides, carousel",NEXT,PASS,slide 0 -> 1
|
|
14
|
+
"hero, 4 slides, carousel",PREV,PASS,slide 1 -> 0
|
|
15
|
+
"hero, 4 slides, carousel",KEYBOARD,PASS,"focus bullet 3 taken, Enter -> slide 3"
|
|
16
|
+
"hero, 4 slides, carousel",REDUCED,PASS,under prefers-reduced-motion it holds still (slide 0 -> 0)
|
|
17
17
|
"carousel, 6 slides, wraps",HOST,PASS,"<mosaic-slider> animation=slide carousel=true, 6 slides"
|
|
18
18
|
"carousel, 6 slides, wraps",TRACK,PASS,<mosaic-slider-slides> aria-live=off
|
|
19
19
|
"carousel, 6 slides, wraps",HAS_CURRENT,PASS,slide 0 is current at rest
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
check,result,detail
|
|
2
|
+
ROOT_PRESENT,PASS,9 custom properties on :root matching '--mk-'
|
|
3
|
+
NO_DUPLICATES,PASS,every property declared once
|
|
4
|
+
NO_EMPTY,PASS,every token has a value
|
|
5
|
+
NOT_DEFAULT_WHITE,PASS,none
|
|
6
|
+
ALL_COLOUR_TOKENS_USED,PASS,all 9 colour tokens are resolved by at least one element
|
|
7
|
+
NON_COLOUR_TOKENS,PASS,"3 not judged by colour (they are numbers or lengths): --mk-m=0, --mk-n=0, --mk-s=0"
|
|
8
|
+
USED/--mk-accent,PASS,"5 element(s), e.g. mk-progress-track, mk-progress, mk-clock-dot"
|
|
9
|
+
USED/--mk-accent-ink,PASS,"2 element(s), e.g. mk-header-cta, tk-chip-mk-accent-ink"
|
|
10
|
+
USED/--mk-ai,PASS,"1 element(s), e.g. tk-chip-mk-ai"
|
|
11
|
+
USED/--mk-faint,PASS,"1 element(s), e.g. tk-chip-mk-faint"
|
|
12
|
+
USED/--mk-ink,PASS,"76 element(s), e.g. mk-clock-m, mk-clock-m, mk-clock-s"
|
|
13
|
+
USED/--mk-muted,PASS,"1 element(s), e.g. tk-chip-mk-muted"
|
|
14
|
+
USED/--mk-panel,PASS,"3 element(s), e.g. tk-sw, tk-chip-mk-panel, tk-props"
|
|
15
|
+
USED/--mk-paper,PASS,"7 element(s), e.g. tk-head, tk-chip-mk-paper, tk-pipe"
|
|
16
|
+
USED/--mk-rule,PASS,"1 element(s), e.g. tk-chip-mk-rule"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mosaic-headless",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.1",
|
|
4
4
|
"description": "AI-agent skill: build Mosaic Pro (Nextend) WordPress sites by writing the underlying data model directly - 23 custom tables, no visual editor, no DOM. Every node type, style property and node property swept against a live install and asserted on the delivered HTML and compiled CSS. Installs into Claude Code, Cursor, Codex CLI, Gemini CLI, Copilot, Continue, Windsurf and Claude.ai.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wordpress",
|
|
@@ -128,3 +128,35 @@ this (they are gitignored - they were scaffolding, not a deliverable) — they s
|
|
|
128
128
|
everything
|
|
129
129
|
per node, because their job was to exercise the style compiler across eight visual
|
|
130
130
|
languages, not to model a maintainable theme. A real site inverts that ratio.
|
|
131
|
+
|
|
132
|
+
## A grouped menu, and two more places the placement table lied
|
|
133
|
+
|
|
134
|
+
Nine flat links in a header is what happens when a site grows and nobody re-reads
|
|
135
|
+
the nav. Mosaic's `dropdown` family is the fix and is worth knowing for its own
|
|
136
|
+
sake:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
dropdown horizontalAlign: left | center | right
|
|
140
|
+
dropdown-toggle url, target, rel; takes the label as children
|
|
141
|
+
dropdown-wrapper takes the links as children
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Both children are inserted by `onHeal()` if you leave them out, so only the two
|
|
145
|
+
need authoring. **It opens on CLICK, not hover** — the host gains
|
|
146
|
+
`dropdown--opening dropdown--opened` and the wrapper goes from `display:none` to
|
|
147
|
+
`display:flex`. A hover test reports a dropdown that does not work.
|
|
148
|
+
|
|
149
|
+
`dropdown-wrapper` reads as a leaf in `placement-rules.csv`, because `rule` is the
|
|
150
|
+
`canBeParentFor()` answer and that method is never overridden on it. But its
|
|
151
|
+
factory is `getDropdownWrapperElementDefaultData($data, $children)` and the editor
|
|
152
|
+
fills it. "Will the editor let me drop this here" is not the same question as "can
|
|
153
|
+
this hold children", and a build guard that reads only the first refuses correct
|
|
154
|
+
structures — it refused this menu. `extract_placement.py` now also records
|
|
155
|
+
`takes_children` from the factory signature, and `build_page.py` reads it.
|
|
156
|
+
|
|
157
|
+
That is the second table to be caught short the same way this week. The first was
|
|
158
|
+
`default-children.csv`, which held only the declared `getDefaultData()` statics and
|
|
159
|
+
so knew nothing about children a type HEALS into itself — modal's overlay, loop's
|
|
160
|
+
`loop-items`, slider's `slider-slides`, dropdown's own two. Both fixes are the same
|
|
161
|
+
move: the question the table answers was narrower than the question the table's
|
|
162
|
+
name suggests, so read the other source as well and say which is which.
|
package/references/responsive.md
CHANGED
|
@@ -172,3 +172,56 @@ scores its own blind spots as successes is worse than no tool.
|
|
|
172
172
|
responsive behaviour you want checked.
|
|
173
173
|
|
|
174
174
|
The tool tells you the values arrived. Only the browser tells you they add up.
|
|
175
|
+
|
|
176
|
+
## Five ways a page passes the width check and is still broken on a phone
|
|
177
|
+
|
|
178
|
+
`verify_navigation.py --viewports` asks one question — does the document scroll
|
|
179
|
+
sideways — and a page can pass it while being unusable. `tools/audit_mobile.py`
|
|
180
|
+
asks the rest, re-opening each page at every width and reporting WIDE, OUTSIDE,
|
|
181
|
+
CLIPPED, TINY_TYPE, SMALL_TARGET, GRID_NOT_COLLAPSED and LONG_MEASURE. Run on the
|
|
182
|
+
five showcase pages it found all five of the following, none of which the width
|
|
183
|
+
check could see.
|
|
184
|
+
|
|
185
|
+
**`flexSizing: {"type": "grow"}` is `flex-grow:1; flex-shrink:0`.** It is not
|
|
186
|
+
`flex: 1`. A value column set with it can grow but can never give ground, so one
|
|
187
|
+
long mono string in a 310px card pushed the whole document out to 440px — and the
|
|
188
|
+
page still "did not scroll sideways", because the document itself had simply become
|
|
189
|
+
wider. The four types compile as:
|
|
190
|
+
|
|
191
|
+
| type | emitted |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `none` | `flex-grow:0; flex-shrink:0` |
|
|
194
|
+
| `shrink` | `flex-grow:0; flex-shrink:1` |
|
|
195
|
+
| `grow` | `flex-grow:1; flex-shrink:0` |
|
|
196
|
+
| `custom` | whichever of `flexGrow` / `flexShrink` you set |
|
|
197
|
+
|
|
198
|
+
So an item that must both fill and yield needs `custom` with both set to `1`.
|
|
199
|
+
|
|
200
|
+
**A grid or flex item defaults to `min-width: auto`, which means min-content.** A
|
|
201
|
+
code block in `white-space: pre` has a min-content width of its longest LINE, so
|
|
202
|
+
the track refuses to shrink and the grid overflows its container. `overflow: auto`
|
|
203
|
+
on the block does not help — the item ABOVE it is what is too wide. `minWidth: 0`
|
|
204
|
+
on the item is the fix.
|
|
205
|
+
|
|
206
|
+
**`section` arrives with 80px of vertical padding.** It is invisible until a page
|
|
207
|
+
adds its own spacing, and then every band carries 80 + 80 + yours; the showcase
|
|
208
|
+
pages opened with about 200px of nothing. `section()` in the demo now takes a
|
|
209
|
+
`pad_y` so a page can take that rhythm into its own hands.
|
|
210
|
+
|
|
211
|
+
**A fixed header's height is not a constant.** The demo's nav grew from four items
|
|
212
|
+
to nine as pages were added; on a phone it wrapped to two rows and took the fixed
|
|
213
|
+
header to 177px, with the first 97px of every page hidden underneath it, because
|
|
214
|
+
the pages cleared a height decided when it was one line. Below 768px the header
|
|
215
|
+
simply joins the flow now, which removes the whole class of bug.
|
|
216
|
+
|
|
217
|
+
**A type scale needs a floor, not just a ramp.** 10px mono labels are part of the
|
|
218
|
+
hierarchy on a large screen and illegible on a phone; the audit found 80 runs of
|
|
219
|
+
them on one page. The demo's `mono()` raises anything under 12px at `_t` — which
|
|
220
|
+
is `<=1079px`, so it covers the tablet too, where a 768px touch device was still
|
|
221
|
+
being served desktop values.
|
|
222
|
+
|
|
223
|
+
`data/mobile-audit.csv` is the standing result. Three classes in it are accepted
|
|
224
|
+
rather than fixed, and are worth stating so they are not re-opened: the footer link
|
|
225
|
+
list is deliberately two columns, the slider bullets are 26x28 (a secondary control
|
|
226
|
+
below the 30px the tool asks for), and LONG_MEASURE flags CJK lines over 42
|
|
227
|
+
characters, which at 768px is a preference rather than a defect.
|
package/references/styling.md
CHANGED
|
@@ -201,9 +201,23 @@ type-switched group, `flexSizing`:
|
|
|
201
201
|
"flexSizing": {"type": "custom", "customOptions": {…}}
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
-
`
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
**`grow` carries `flex-shrink:0`, so it is not `flex: 1`.** An item set with it
|
|
205
|
+
grows but never yields, and one long string then pushes its whole container wider
|
|
206
|
+
than the viewport — which is a bug a horizontal-overflow check does not catch,
|
|
207
|
+
because the document simply becomes wider rather than scrolling. The emitted CSS:
|
|
208
|
+
|
|
209
|
+
| type | emitted |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `none` | `flex-grow:0; flex-shrink:0` |
|
|
212
|
+
| `shrink` | `flex-grow:0; flex-shrink:1` |
|
|
213
|
+
| `grow` | `flex-grow:1; flex-shrink:0` |
|
|
214
|
+
| `custom` | whichever of `flexGrow` / `flexShrink` you set |
|
|
215
|
+
|
|
216
|
+
`none` is right for a label column that must not be squeezed. For the cell that
|
|
217
|
+
takes the slack AND has to give ground on a phone, `custom` with both set to `1`
|
|
218
|
+
is the only type that says so. Pair it with `minWidth: 0`, because a flex or grid
|
|
219
|
+
item defaults to `min-width: auto` — min-content — and a `white-space: pre` block
|
|
220
|
+
inside one will hold the track open at its longest line.
|
|
207
221
|
|
|
208
222
|
**There is no `overflowX` or `overflowY`** — only the shorthand `overflow`, with
|
|
209
223
|
`visible | hidden | clip | scroll | auto`. One axis at a time is not expressible
|
|
@@ -100,7 +100,9 @@ const counts = {
|
|
|
100
100
|
"data/custom-fields-verification.csv": 62,
|
|
101
101
|
"data/dialog-verification.csv": 19,
|
|
102
102
|
"data/slider-verification.csv": 30,
|
|
103
|
-
"data/
|
|
103
|
+
"data/token-verification.csv": 15,
|
|
104
|
+
"data/mobile-audit.csv": 54,
|
|
105
|
+
"data/navigation-verification.csv": 22,
|
|
104
106
|
"data/page-modal-verification.csv": 19,
|
|
105
107
|
"data/interaction-rules-verification.csv": 17,
|
|
106
108
|
"data/dialog-form-verification.csv": 49,
|
package/sites/_moksa.py
CHANGED
|
@@ -110,13 +110,24 @@ def grid(attr, cols, gap, children, tcols=None, mcols=1, **extra):
|
|
|
110
110
|
"children": children}
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
def section(attr, children, bg="--mk-paper"):
|
|
114
|
-
"""
|
|
115
|
-
|
|
113
|
+
def section(attr, children, bg="--mk-paper", pad_y=None):
|
|
114
|
+
"""A band of the document.
|
|
115
|
+
|
|
116
|
+
The comment here used to say sections carry no vertical padding. They do:
|
|
117
|
+
`section` arrives with 80px top and bottom of its own, which is Mosaic's
|
|
118
|
+
default and is what sets this page's rhythm. It is invisible until a page adds
|
|
119
|
+
its own spacing on top, and then every band on a phone carries 80 + 80 + your
|
|
120
|
+
own - the showcase pages opened with about 200px of nothing above the first
|
|
121
|
+
line. Pass `pad_y` to take the vertical rhythm into your own hands.
|
|
122
|
+
"""
|
|
123
|
+
base = {"backgroundColor": {"token": bg},
|
|
124
|
+
"paddingLeft": "40px", "paddingRight": "40px",
|
|
125
|
+
"fontFamily": CJK}
|
|
126
|
+
if pad_y is not None:
|
|
127
|
+
base["paddingTop"] = pad_y
|
|
128
|
+
base["paddingBottom"] = pad_y
|
|
116
129
|
return {"type": "section", "data": {"attrID": attr},
|
|
117
|
-
"style": bp(
|
|
118
|
-
"paddingLeft": "40px", "paddingRight": "40px",
|
|
119
|
-
"fontFamily": CJK},
|
|
130
|
+
"style": bp(base,
|
|
120
131
|
{"paddingLeft": "28px", "paddingRight": "28px"},
|
|
121
132
|
{"paddingLeft": "18px", "paddingRight": "18px"}),
|
|
122
133
|
"children": children}
|
|
@@ -139,11 +150,30 @@ def ml(tag, text, **st):
|
|
|
139
150
|
return T(tag, text, **st)
|
|
140
151
|
|
|
141
152
|
|
|
153
|
+
# Below this, mono text stops being quiet and starts being unreadable - on a phone
|
|
154
|
+
# in particular, where there is no leaning in. The audit at 390px found 80 runs at
|
|
155
|
+
# 10px on one page alone, which is a decision taken 80 times by accident.
|
|
156
|
+
MONO_FLOOR_M = 12
|
|
157
|
+
|
|
158
|
+
|
|
142
159
|
def mono(text, size="11px", color="--mk-muted", track="0.14em", **st):
|
|
143
160
|
"""The page's primary voice. Everything that is a label, a key, a figure or an
|
|
144
|
-
address is set in it.
|
|
161
|
+
address is set in it.
|
|
162
|
+
|
|
163
|
+
A size authored below `MONO_FLOOR_M` is raised to it on phones. The desktop
|
|
164
|
+
value is left alone: 10px works at arm's length on a large screen and is part
|
|
165
|
+
of the typographic hierarchy there, so this is a floor rather than a rewrite.
|
|
166
|
+
"""
|
|
145
167
|
st.setdefault("fontFamily", MONO)
|
|
146
168
|
st.setdefault("fontWeight", "400")
|
|
169
|
+
try:
|
|
170
|
+
authored = float(str(size).replace("px", ""))
|
|
171
|
+
except ValueError:
|
|
172
|
+
authored = None
|
|
173
|
+
if authored is not None and authored < MONO_FLOOR_M:
|
|
174
|
+
t = dict(st.get("_t") or {})
|
|
175
|
+
t.setdefault("fontSize", "%dpx" % MONO_FLOOR_M)
|
|
176
|
+
st["_t"] = t
|
|
147
177
|
return T("p", text,
|
|
148
178
|
color={"token": color} if color.startswith("--") else color,
|
|
149
179
|
fontSize=size, letterSpacing=track, **st)
|
|
@@ -247,9 +277,40 @@ def mask_line(attr, text, tag="h1", **st):
|
|
|
247
277
|
|
|
248
278
|
|
|
249
279
|
# ── content, all of it Moksa Web's own ───────────────────────────────────────
|
|
280
|
+
# Nine flat links was the wrong answer to "the site grew". The top level is the
|
|
281
|
+
# studio's own site; everything that demonstrates what Mosaic can DO is one
|
|
282
|
+
# labelled group, which is also the only honest shape - those five pages are a
|
|
283
|
+
# set, not five more sections of the homepage.
|
|
250
284
|
NAV = [("服務", "#services"), ("作品", "#works"),
|
|
251
|
-
("產品", "#products"), ("
|
|
252
|
-
|
|
285
|
+
("產品", "#products"), ("聯絡", "#contact")]
|
|
286
|
+
|
|
287
|
+
# Read from the shipped tables rather than typed, so the menu cannot claim a
|
|
288
|
+
# number the evidence no longer supports.
|
|
289
|
+
def _count(table, fn):
|
|
290
|
+
import csv as _c, io as _i, os as _o
|
|
291
|
+
path = _o.path.join(_o.path.dirname(_o.path.abspath(__file__)), "..", "data", table)
|
|
292
|
+
with _i.open(path, encoding="utf-8") as fh:
|
|
293
|
+
return fn(list(_c.DictReader(fh)))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
TOKENABLE_COUNT = _count("style-properties.csv",
|
|
297
|
+
lambda rs: sum(1 for r in rs if r["tokenable"]))
|
|
298
|
+
CONVERTED_COUNT = _count("conversion-batch.csv",
|
|
299
|
+
lambda rs: sum(int(r["converted"]) for r in rs))
|
|
300
|
+
|
|
301
|
+
# label, href, one line of what it demonstrates
|
|
302
|
+
CAPABILITIES = [
|
|
303
|
+
("彈窗與觸發", "/dialogs/",
|
|
304
|
+
"九種形態、四種觸發、四種執行規則"),
|
|
305
|
+
("輪播", "/sliders/",
|
|
306
|
+
"四種動畫並排、輪轉模式、鍵盤與減少動態"),
|
|
307
|
+
("設計系統", "/tokens/",
|
|
308
|
+
"設計代幣、variant 層、%d 個接得住代幣的屬性" % TOKENABLE_COUNT),
|
|
309
|
+
("動態內容", "/dynamic/",
|
|
310
|
+
"迴圈、@VAR 表達式、自訂欄位"),
|
|
311
|
+
("Elementor 轉換", "/conversion/",
|
|
312
|
+
"19 頁、%s 個元素、每一段字都核過" % "{:,}".format(CONVERTED_COUNT)),
|
|
313
|
+
]
|
|
253
314
|
|
|
254
315
|
# key / figure / caption, read as a datasheet rather than as four big numbers
|
|
255
316
|
SPEC = [
|
|
@@ -611,6 +672,17 @@ def motion_css():
|
|
|
611
672
|
"-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}",
|
|
612
673
|
'#mk-header::after{content:"";position:absolute;left:0;right:0;bottom:0;'
|
|
613
674
|
"height:1px;background:" + RULE + ";opacity:0}",
|
|
675
|
+
# On a phone the bar is no longer one line. The nav grew to nine items
|
|
676
|
+
# and wraps to two rows, which took the FIXED header to 177px - a quarter
|
|
677
|
+
# of the viewport permanently, with the first 97px of every page hidden
|
|
678
|
+
# underneath it, because the pages clear a header height that was decided
|
|
679
|
+
# when it was one line. A fixed element whose height depends on its
|
|
680
|
+
# content cannot be cleared by a constant, so below 768px it simply joins
|
|
681
|
+
# the flow and the problem cannot come back.
|
|
682
|
+
"@media (max-width:767px){#mk-header{position:static;"
|
|
683
|
+
"background:rgb(250,250,247);"
|
|
684
|
+
"-webkit-backdrop-filter:none;backdrop-filter:none;"
|
|
685
|
+
"border-bottom:1px solid " + RULE + "}}",
|
|
614
686
|
"#mk-nav>*{position:relative}",
|
|
615
687
|
'#mk-nav>*::after{content:"";position:absolute;left:0;right:100%;bottom:-5px;'
|
|
616
688
|
"height:1px;background:rgb(255,90,54);"
|
|
@@ -1281,6 +1353,76 @@ def motion_css():
|
|
|
1281
1353
|
|
|
1282
1354
|
|
|
1283
1355
|
# ── the shared shell ──────────────────────────────────────────────────────────
|
|
1356
|
+
NAV_LINK = {"color": {"token": "--mk-ink"}, "fontSize": "13px",
|
|
1357
|
+
"fontWeight": "400", "letterSpacing": "0.04em", "fontFamily": MONO,
|
|
1358
|
+
"cursor": "pointer"}
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
def capability_dropdown():
|
|
1362
|
+
"""The capability pages as one labelled group.
|
|
1363
|
+
|
|
1364
|
+
`dropdown` heals in its own `dropdown-toggle` and `dropdown-wrapper`, so only
|
|
1365
|
+
the two are authored here and the wrapper's children are the links. The
|
|
1366
|
+
wrapper reads as a leaf in `placement-rules.csv` because `canBeParentFor()`
|
|
1367
|
+
is never overridden on it - but its factory is
|
|
1368
|
+
`getDropdownWrapperElementDefaultData($data, $children)`, which is why that
|
|
1369
|
+
table now carries a `takes_children` column and the build guard reads it.
|
|
1370
|
+
"""
|
|
1371
|
+
return {"type": "dropdown",
|
|
1372
|
+
"data": {"attrID": "mk-cap", "horizontalAlign": "left"},
|
|
1373
|
+
"style": {"&": {"_": {"position": "relative"}}},
|
|
1374
|
+
"children": [
|
|
1375
|
+
{"type": "dropdown-toggle",
|
|
1376
|
+
"data": {"attrID": "mk-cap-toggle"},
|
|
1377
|
+
"style": {"&": {"_": dict(NAV_LINK, **{
|
|
1378
|
+
"display": "flex", "alignItems": "center", "columnGap": "6px",
|
|
1379
|
+
"customDeclarations":
|
|
1380
|
+
"white-space:nowrap;"}),
|
|
1381
|
+
"_t": {"fontSize": "12px", "paddingTop": "8px",
|
|
1382
|
+
"paddingBottom": "8px"}},
|
|
1383
|
+
"hover": {"_": {"color": {"token": "--mk-accent"}}},
|
|
1384
|
+
"focus-visible": {"_": dict(FOCUS_RING)}},
|
|
1385
|
+
"children": [
|
|
1386
|
+
T("p", "能力展示", fontFamily=MONO,
|
|
1387
|
+
fontSize="13px", letterSpacing="0.04em",
|
|
1388
|
+
color={"token": "--mk-ink"}, _t={"fontSize": "12px"}),
|
|
1389
|
+
T("p", "+", fontFamily=MONO, fontSize="11px",
|
|
1390
|
+
color={"token": "--mk-faint"}),
|
|
1391
|
+
]},
|
|
1392
|
+
{"type": "dropdown-wrapper",
|
|
1393
|
+
"data": {"attrID": "mk-cap-menu"},
|
|
1394
|
+
"style": bp({"backgroundColor": {"token": "--mk-paper"},
|
|
1395
|
+
"paddingTop": "10px", "paddingBottom": "10px",
|
|
1396
|
+
"minWidth": "272px",
|
|
1397
|
+
"customDeclarations":
|
|
1398
|
+
"border:1px solid rgb(22,24,28);"
|
|
1399
|
+
"box-shadow:0 20px 48px rgba(22,24,28,.14);"},
|
|
1400
|
+
None,
|
|
1401
|
+
{"minWidth": "0px", "width": "100%",
|
|
1402
|
+
"customDeclarations":
|
|
1403
|
+
"border:1px solid " + RULE + ";box-shadow:none;"}),
|
|
1404
|
+
"children": [
|
|
1405
|
+
{"type": "menu-link",
|
|
1406
|
+
"data": {"attrID": "mk-cap-%d" % i, "url": href},
|
|
1407
|
+
"style": {"&": {"_": {
|
|
1408
|
+
"display": "block", "cursor": "pointer",
|
|
1409
|
+
"paddingTop": "9px", "paddingBottom": "9px",
|
|
1410
|
+
"paddingLeft": "16px", "paddingRight": "16px"}},
|
|
1411
|
+
"hover": {"_": {
|
|
1412
|
+
"backgroundColor": {"token": "--mk-panel"}}},
|
|
1413
|
+
"focus-visible": {"_": dict(FOCUS_RING)}},
|
|
1414
|
+
"children": [
|
|
1415
|
+
T("p", label, fontFamily=CJK, fontSize="13px",
|
|
1416
|
+
fontWeight="500", color={"token": "--mk-ink"}),
|
|
1417
|
+
T("p", blurb, fontFamily=CJK, fontSize="11px",
|
|
1418
|
+
lineHeight="1.6", color={"token": "--mk-muted"},
|
|
1419
|
+
marginTop="3px"),
|
|
1420
|
+
]}
|
|
1421
|
+
for i, (label, href, blurb) in enumerate(CAPABILITIES)
|
|
1422
|
+
]},
|
|
1423
|
+
]}
|
|
1424
|
+
|
|
1425
|
+
|
|
1284
1426
|
HEADER = box("mk-shell-top", {}, [
|
|
1285
1427
|
# a `code` node with insertLocation "head" is the only way to get @keyframes into
|
|
1286
1428
|
# the document - customDeclarations is emitted inside a rule and cannot hold one
|
|
@@ -1298,9 +1440,17 @@ HEADER = box("mk-shell-top", {}, [
|
|
|
1298
1440
|
children=
|
|
1299
1441
|
[wrap("mk-header-in", [
|
|
1300
1442
|
{"type": "div", "data": {"attrID": "mk-header-row"},
|
|
1443
|
+
# The nav grew from four items to seven as the showcase pages were
|
|
1444
|
+
# added, and on a phone the row simply ran off the right edge - the
|
|
1445
|
+
# last two links sat at left=393 and left=436 in a 390px viewport,
|
|
1446
|
+
# which is to say they were unreachable. One line is not a layout
|
|
1447
|
+
# that survives new content, so on `_m` the row wraps and the nav
|
|
1448
|
+
# takes the second line to itself.
|
|
1301
1449
|
"style": bp({"display": "flex", "alignItems": "center",
|
|
1302
1450
|
"justifyContent": "space-between", "columnGap": "40px"},
|
|
1303
|
-
{"columnGap": "24px"},
|
|
1451
|
+
{"columnGap": "24px"},
|
|
1452
|
+
{"columnGap": "12px", "flexWrap": "wrap",
|
|
1453
|
+
"rowGap": "10px"}),
|
|
1304
1454
|
"children": [
|
|
1305
1455
|
# the wordmark is the only route back to the top on a one-pager, so
|
|
1306
1456
|
# it has to be a link. `menu-link` with a url renders a real <a>.
|
|
@@ -1319,7 +1469,7 @@ HEADER = box("mk-shell-top", {}, [
|
|
|
1319
1469
|
# time of day - a page cannot know that without JavaScript, and
|
|
1320
1470
|
# labelling a page-load counter "14:32" would be a small lie told
|
|
1321
1471
|
# in the most trustworthy typeface on the site.
|
|
1322
|
-
box("mk-clock", {}, [
|
|
1472
|
+
box("mk-clock", {}, _m={"display": "none"}, children=[
|
|
1323
1473
|
box("mk-clock-dot", {}, []),
|
|
1324
1474
|
T("p", "T+", color={"token": "--mk-faint"}, fontSize="10px",
|
|
1325
1475
|
fontFamily=MONO, letterSpacing="0.18em"),
|
|
@@ -1331,7 +1481,14 @@ HEADER = box("mk-shell-top", {}, [
|
|
|
1331
1481
|
{"type": "menu", "data": {"attrID": "mk-nav"},
|
|
1332
1482
|
"style": bp({"display": "flex", "columnGap": "30px",
|
|
1333
1483
|
"alignItems": "center"},
|
|
1334
|
-
{"columnGap": "20px"
|
|
1484
|
+
{"columnGap": "20px", "flexWrap": "wrap",
|
|
1485
|
+
"rowGap": "6px"},
|
|
1486
|
+
{"columnGap": "16px", "rowGap": "6px",
|
|
1487
|
+
"flexWrap": "wrap", "width": "100%",
|
|
1488
|
+
"justifyContent": "flex-start",
|
|
1489
|
+
"customDeclarations":
|
|
1490
|
+
"order:3;border-top:1px solid " + RULE
|
|
1491
|
+
+ ";padding-top:10px;"}),
|
|
1335
1492
|
"children": [
|
|
1336
1493
|
{"type": "menu-link",
|
|
1337
1494
|
"data": {"attrID": "mk-nav-%d" % i, "url": href},
|
|
@@ -1341,12 +1498,14 @@ HEADER = box("mk-shell-top", {}, [
|
|
|
1341
1498
|
"fontFamily": MONO,
|
|
1342
1499
|
"transitionAll": "160ms ease",
|
|
1343
1500
|
"cursor": "pointer"},
|
|
1344
|
-
"
|
|
1501
|
+
"_t": {"fontSize": "12px",
|
|
1502
|
+
"paddingTop": "8px",
|
|
1503
|
+
"paddingBottom": "8px"}},
|
|
1345
1504
|
"hover": {"_": {"color": {"token": "--mk-accent"}}},
|
|
1346
1505
|
"focus-visible": {"_": FOCUS_RING}},
|
|
1347
1506
|
"text": text}
|
|
1348
1507
|
for i, (text, href) in enumerate(NAV)
|
|
1349
|
-
]},
|
|
1508
|
+
] + [capability_dropdown()]},
|
|
1350
1509
|
# not a pill: a bracketed link, the way a document cross-references
|
|
1351
1510
|
{"type": "button", "data": {"attrID": "mk-header-cta",
|
|
1352
1511
|
"url": "#contact"},
|
|
@@ -3059,6 +3218,18 @@ if __name__ == "__main__":
|
|
|
3059
3218
|
# helpers it reads are pure functions - two copies produce identical dicts.
|
|
3060
3219
|
import _dialogs
|
|
3061
3220
|
import _sliders
|
|
3221
|
+
import _tokens
|
|
3222
|
+
import _dynamic
|
|
3223
|
+
import _conversion
|
|
3224
|
+
SITE["pages"].append({"slug": "conversion", "post_id": 273,
|
|
3225
|
+
"title": "Elementor 轉換",
|
|
3226
|
+
"tree": apply_type(_conversion.TREE, DISPLAY, CJK, "600")})
|
|
3227
|
+
SITE["pages"].append({"slug": "dynamic", "post_id": 272,
|
|
3228
|
+
"title": "動態內容",
|
|
3229
|
+
"tree": apply_type(_dynamic.TREE, DISPLAY, CJK, "600")})
|
|
3230
|
+
SITE["pages"].append({"slug": "tokens", "post_id": 271,
|
|
3231
|
+
"title": "設計系統",
|
|
3232
|
+
"tree": apply_type(_tokens.TREE, DISPLAY, CJK, "600")})
|
|
3062
3233
|
SITE["pages"].append({"slug": "sliders", "post_id": 270,
|
|
3063
3234
|
"title": "輪播實驗室",
|
|
3064
3235
|
"tree": apply_type(_sliders.TREE, DISPLAY, CJK, "600")})
|