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.
@@ -1,19 +1,19 @@
1
1
  slider,check,result,detail
2
- "hero, 4 slides, plain",HOST,PASS,"<mosaic-slider> animation=slide carousel=None, 4 slides"
3
- "hero, 4 slides, plain",TRACK,PASS,<mosaic-slider-slides> aria-live=off
4
- "hero, 4 slides, plain",HAS_CURRENT,PASS,slide 0 is current at rest
5
- "hero, 4 slides, plain",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, plain",BULLET_ARIA,PASS,"role=button tabindex=0 labels=一、輪播不是相簿, 二、四種動畫, 三、箭頭會自己消失, 四、不想動就不動 - taken from each slide's own title"
7
- "hero, 4 slides, plain",ADVANCES,PASS,reached 4 of 4 slides unaided
8
- "hero, 4 slides, plain",AUTOPLAY_DECLARED,PASS,data-autoplay=true delay=3600
9
- "hero, 4 slides, plain",BULLET_GOES,PASS,bullet 0 -> slide 0
10
- "hero, 4 slides, plain",ARROW_ENDS,PASS,"on slide 0 prev hides itself (hidden=True), next does not (hidden=False)"
11
- "hero, 4 slides, plain",TRACK_LAYOUT,PASS,"grid ""slide"", every slide one track wide ([1200] vs 1200)"
12
- "hero, 4 slides, plain",ARROW_ARIA,PASS,role=button tabindex=0 label='下一張投影片'
13
- "hero, 4 slides, plain",NEXT,PASS,slide 0 -> 1
14
- "hero, 4 slides, plain",PREV,PASS,slide 1 -> 0
15
- "hero, 4 slides, plain",KEYBOARD,PASS,"focus bullet 3 taken, Enter -> slide 3"
16
- "hero, 4 slides, plain",REDUCED,PASS,under prefers-reduced-motion it holds still (slide 0 -> 0)
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.24.0",
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.
@@ -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.
@@ -201,9 +201,23 @@ type-switched group, `flexSizing`:
201
201
  "flexSizing": {"type": "custom", "customOptions": {…}}
202
202
  ```
203
203
 
204
- `none` is what you want for a label column that must not be squeezed, and `grow`
205
- for the cell that takes the slack — the pair that `flex-shrink:0` / `flex-grow:1`
206
- would have been.
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/navigation-verification.csv": 46,
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
- """Sections carry no vertical padding here - the document's rhythm is set by the
115
- rules between clauses, not by a stack of padded bands."""
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({"backgroundColor": {"token": bg},
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"), ("彈窗", "/dialogs/"), ("輪播", "/sliders/"),
252
- ("聯絡", "#contact")]
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"}, {"columnGap": "12px"}),
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"}, {"columnGap": "15px"}),
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
- "_m": {"fontSize": "12px"}},
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")})