mosaic-headless 1.26.0 → 1.27.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/SKILL.md +8 -7
- package/assets/templates/platforms/claude-ai.json +1 -1
- package/assets/templates/platforms/claude-code.json +1 -1
- package/assets/templates/platforms/codex-cli.json +1 -1
- package/assets/templates/platforms/copilot.json +1 -1
- package/assets/templates/platforms/gemini-cli.json +1 -1
- package/data/component-wall-verification.csv +4 -2
- package/data/form-surface-verification.csv +9 -0
- package/data/mobile-audit.csv +2 -0
- package/data/navigation-verification.csv +26 -14
- package/data/node-types.csv +2 -0
- package/data/placement-rules.csv +129 -127
- package/package.json +1 -1
- package/references/failure-modes.md +55 -0
- package/references/placement.md +38 -0
- package/scripts/check-release.mjs +4 -3
- package/sites/_moksa.py +16 -6
- package/sites/moksa.json +1996 -114
- package/tools/build_page.py +18 -5
- package/tools/extract_node_types.py +7 -1
- package/tools/extract_placement.py +23 -3
- package/tools/verify_components.py +95 -52
- package/tools/verify_navigation.py +33 -0
|
@@ -344,3 +344,58 @@ Two rules come out of it:
|
|
|
344
344
|
that lesson: for anything with a visible and an invisible state, a suite that
|
|
345
345
|
only exercises the visible one is testing half a component, and the half it
|
|
346
346
|
skips is the one the visitor sees most of the time.
|
|
347
|
+
|
|
348
|
+
## A fragment link in a SHARED header is dead on every page but one
|
|
349
|
+
|
|
350
|
+
`#services` points at an id in the CURRENT document. Put it in a header that every
|
|
351
|
+
page shares, and on the one page that has that section it works, while on the other
|
|
352
|
+
seven the browser looks for the id, does not find it, and does nothing — no
|
|
353
|
+
navigation, no error, no feedback of any kind. The visitor clicks and the page sits
|
|
354
|
+
there. It is worse than a 404 for exactly that reason.
|
|
355
|
+
|
|
356
|
+
This shipped. The nav verifier had a check for it and the check could not see it:
|
|
357
|
+
it fetched every menu href and asserted HTTP 200, and `#services` resolves to the
|
|
358
|
+
*current* URL, so it returned 200 on all eight pages while being dead on seven.
|
|
359
|
+
"Every link answers 200" and "every link does something" are different claims.
|
|
360
|
+
|
|
361
|
+
Two things come out of it:
|
|
362
|
+
|
|
363
|
+
- **Write shared-shell links absolutely** — `/#services`, not `#services`. A link
|
|
364
|
+
in a component that appears on more than one page has to mean the same thing
|
|
365
|
+
from all of them.
|
|
366
|
+
- **Assert the fragment resolves on the page that carries it.**
|
|
367
|
+
`verify_navigation.py` now opens every page and fails on any `[href^="#"]`
|
|
368
|
+
whose target id is absent from that document. It found the four in the header
|
|
369
|
+
immediately — and then a fifth and sixth in the masthead, which `/intro/` shares
|
|
370
|
+
with the homepage and which I had not thought to look at.
|
|
371
|
+
|
|
372
|
+
The general form: a check that confirms a resource EXISTS is not a check that the
|
|
373
|
+
control WORKS, and anything living in a shared layout needs verifying on every
|
|
374
|
+
page that includes it, not on the page it was designed for.
|
|
375
|
+
|
|
376
|
+
## Three ways a form comes back empty, none of them an error
|
|
377
|
+
|
|
378
|
+
A form page built in one pass rendered two fieldsets, one stray label and **zero
|
|
379
|
+
controls**. The commit succeeded, the response carried no exception, and the HTML
|
|
380
|
+
was valid. Every field had been dropped on the way in, for three different reasons:
|
|
381
|
+
|
|
382
|
+
- **`label` and the inputs need a `field` above them.** They declare
|
|
383
|
+
`canBeNestedChildFor` and walk the parent chain looking for one; committed under
|
|
384
|
+
a plain div they are refused and discarded. `placement-rules.csv` now carries a
|
|
385
|
+
`nested_under` column naming exactly what each type is looking for — 54 types
|
|
386
|
+
have one, and the names rarely share a prefix (`radio-input` wants `choice`,
|
|
387
|
+
`label` wants `field`, `loop-pagination` wants `loop`).
|
|
388
|
+
- **`fieldset` does not take fields.** It renders `fieldset-legend` +
|
|
389
|
+
`fieldset-content`, and direct children are dropped. Neither type was in
|
|
390
|
+
`node-types.csv` at all, because they are declared by an
|
|
391
|
+
`*ElementMResourceFactory.php` and every extractor here walked
|
|
392
|
+
`*ElementTypeFactory.php` only. Two types were missing from the catalog; the
|
|
393
|
+
walk now covers both and the count went 126 → 128.
|
|
394
|
+
- **`select-option` is a leaf and its label is the `text` PROPERTY.** Give it a
|
|
395
|
+
`wysiwyg-text` child and the option renders with an empty label: a dropdown that
|
|
396
|
+
opens onto five blank lines, which looks like a styling problem and is not.
|
|
397
|
+
|
|
398
|
+
The through-line is the one this file keeps recording: Mosaic accepts a write it
|
|
399
|
+
cannot use and says nothing. The defence is to read the delivered page back and
|
|
400
|
+
count what arrived — `verify_components.py` now asserts the control count, the option
|
|
401
|
+
labels and that an empty required form is actually refused.
|
package/references/placement.md
CHANGED
|
@@ -115,3 +115,41 @@ Reach for the measured table when you are deciding what to build with, and this
|
|
|
115
115
|
when you are deciding what may go inside what. `tools/check_placement_predicts.py`
|
|
116
116
|
re-scores the numbers above after any re-sweep, so the claim stays honest if the
|
|
117
117
|
plugin changes.
|
|
118
|
+
|
|
119
|
+
## `nested_under`: what a type is looking for above it
|
|
120
|
+
|
|
121
|
+
`rule` is the `canBeParentFor()` answer — "will the editor let me drop this here".
|
|
122
|
+
Many types also implement `canBeNestedChildFor()`, which walks the parent chain at
|
|
123
|
+
runtime looking for ONE ancestor class, and until now this table only recorded
|
|
124
|
+
that such a rule existed. The `nested_under` column now names it. 54 of 128 types
|
|
125
|
+
have one, and the names usually share no prefix with the type, so there is no way
|
|
126
|
+
to infer it:
|
|
127
|
+
|
|
128
|
+
| type | needs above it |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `radio-input`, `checkbox-input`, `choice-label` | `choice` |
|
|
131
|
+
| `choice`, | `choice-field` |
|
|
132
|
+
| `label`, `fileupload-input` | `field` |
|
|
133
|
+
| `choice-field`, `fieldset`, `field`, `hidden-input` | `choice-field` \| `field` \| `form` |
|
|
134
|
+
| `loop-item` | `loop-items` |
|
|
135
|
+
| `loop-pagination` | `loop` |
|
|
136
|
+
| `list-item` | `list` \| `list-loop-items` \| `wp-menu` |
|
|
137
|
+
| `accordion-item` | `accordion` \| `accordion-loop-items` |
|
|
138
|
+
|
|
139
|
+
A node committed without its required ancestor is **accepted and discarded**. The
|
|
140
|
+
commit returns no exception, the page renders, and the element is simply not
|
|
141
|
+
there — which is how a form came back with two fieldsets and no fields.
|
|
142
|
+
`build_page.py` reads this column and now carries the ancestor chain down the walk,
|
|
143
|
+
so a correct placement is allowed and an incorrect one is refused before the write
|
|
144
|
+
rather than silently lost after it.
|
|
145
|
+
|
|
146
|
+
Two other things the table was short of, both fixed the same way — by reading the
|
|
147
|
+
source the column was derived from rather than the one it was convenient to read:
|
|
148
|
+
|
|
149
|
+
- **`takes_children`**, from the factory's `getDefaultData($data, $children)`
|
|
150
|
+
signature. `dropdown-wrapper` answers `none` to `canBeParentFor` and holds the
|
|
151
|
+
menu's links regardless.
|
|
152
|
+
- **two missing types.** `fieldset-legend` and `fieldset-content` are declared by
|
|
153
|
+
an `*ElementMResourceFactory.php`; every extractor walked
|
|
154
|
+
`*ElementTypeFactory.php` only, so they were absent from the catalog and a
|
|
155
|
+
fieldset appeared to have no inner structure.
|
|
@@ -101,9 +101,10 @@ const counts = {
|
|
|
101
101
|
"data/dialog-verification.csv": 19,
|
|
102
102
|
"data/slider-verification.csv": 30,
|
|
103
103
|
"data/token-verification.csv": 15,
|
|
104
|
-
"data/mobile-audit.csv":
|
|
105
|
-
"data/component-wall-verification.csv":
|
|
106
|
-
"data/
|
|
104
|
+
"data/mobile-audit.csv": 60,
|
|
105
|
+
"data/component-wall-verification.csv": 13,
|
|
106
|
+
"data/form-surface-verification.csv": 8,
|
|
107
|
+
"data/navigation-verification.csv": 36,
|
|
107
108
|
"data/page-modal-verification.csv": 19,
|
|
108
109
|
"data/interaction-rules-verification.csv": 17,
|
|
109
110
|
"data/dialog-form-verification.csv": 49,
|
package/sites/_moksa.py
CHANGED
|
@@ -281,8 +281,12 @@ def mask_line(attr, text, tag="h1", **st):
|
|
|
281
281
|
# studio's own site; everything that demonstrates what Mosaic can DO is one
|
|
282
282
|
# labelled group, which is also the only honest shape - those five pages are a
|
|
283
283
|
# set, not five more sections of the homepage.
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
# These are sections of the HOMEPAGE, and the header is shared by every page. A
|
|
285
|
+
# bare `#services` is therefore a dead link on all seven of the others: the
|
|
286
|
+
# browser looks for that id in the current document, does not find it, and does
|
|
287
|
+
# nothing at all. Absolute, so they mean the same thing from anywhere.
|
|
288
|
+
NAV = [("服務", "/#services"), ("作品", "/#works"),
|
|
289
|
+
("產品", "/#products"), ("聯絡", "/#contact")]
|
|
286
290
|
|
|
287
291
|
# Read from the shipped tables rather than typed, so the menu cannot claim a
|
|
288
292
|
# number the evidence no longer supports.
|
|
@@ -300,6 +304,8 @@ CONVERTED_COUNT = _count("conversion-batch.csv",
|
|
|
300
304
|
|
|
301
305
|
# label, href, one line of what it demonstrates
|
|
302
306
|
CAPABILITIES = [
|
|
307
|
+
("表單", "/forms/",
|
|
308
|
+
"下拉、單選、複選、上傳、必填規則與錯誤訊息"),
|
|
303
309
|
("元件牆", "/components/",
|
|
304
310
|
"分頁、手風琴、表單、地圖——都能直接操作"),
|
|
305
311
|
("彈窗與觸發", "/dialogs/",
|
|
@@ -1460,7 +1466,7 @@ HEADER = box("mk-shell-top", {}, [
|
|
|
1460
1466
|
"children": [
|
|
1461
1467
|
# the wordmark is the only route back to the top on a one-pager, so
|
|
1462
1468
|
# it has to be a link. `menu-link` with a url renders a real <a>.
|
|
1463
|
-
{"type": "menu-link", "data": {"attrID": "mk-logo", "url": "
|
|
1469
|
+
{"type": "menu-link", "data": {"attrID": "mk-logo", "url": "/"},
|
|
1464
1470
|
"style": {"&": {"_": {"display": "flex", "alignItems": "baseline",
|
|
1465
1471
|
"columnGap": "9px", "cursor": "pointer"}}},
|
|
1466
1472
|
"children":
|
|
@@ -1514,7 +1520,7 @@ HEADER = box("mk-shell-top", {}, [
|
|
|
1514
1520
|
] + [capability_dropdown()]},
|
|
1515
1521
|
# not a pill: a bracketed link, the way a document cross-references
|
|
1516
1522
|
{"type": "button", "data": {"attrID": "mk-header-cta",
|
|
1517
|
-
"url": "
|
|
1523
|
+
"url": "/#contact"},
|
|
1518
1524
|
"style": {"&": {"_m": {"display": "none"},
|
|
1519
1525
|
"_t": {"fontSize": "12px"},
|
|
1520
1526
|
# 13px of the bright orange on the glass header is
|
|
@@ -1696,8 +1702,8 @@ MASTHEAD = section("mk-mast", [wrap("mk-mast-in", [
|
|
|
1696
1702
|
if n == 1
|
|
1697
1703
|
else {"color": {"token": "--mk-accent"}})}},
|
|
1698
1704
|
"text": text}
|
|
1699
|
-
for n, (text, href) in enumerate([("服務項目 →", "
|
|
1700
|
-
("精選作品 →", "
|
|
1705
|
+
for n, (text, href) in enumerate([("服務項目 →", "/#services"),
|
|
1706
|
+
("精選作品 →", "/#works")], start=1)
|
|
1701
1707
|
]},
|
|
1702
1708
|
]),
|
|
1703
1709
|
# the right-hand column: what a spec sheet puts in its header block
|
|
@@ -3224,6 +3230,10 @@ if __name__ == "__main__":
|
|
|
3224
3230
|
# helpers it reads are pure functions - two copies produce identical dicts.
|
|
3225
3231
|
import _dialogs
|
|
3226
3232
|
import _sliders
|
|
3233
|
+
import _forms
|
|
3234
|
+
SITE["pages"].append({"slug": "forms", "post_id": 282,
|
|
3235
|
+
"title": "表單",
|
|
3236
|
+
"tree": apply_type(_forms.TREE, DISPLAY, CJK, "600")})
|
|
3227
3237
|
import _components
|
|
3228
3238
|
SITE["pages"].append({"slug": "components", "post_id": 281,
|
|
3229
3239
|
"title": "元件",
|