mosaic-headless 1.26.1 → 1.28.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 -6
- 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/content-loop-verification.csv +11 -0
- package/data/form-surface-verification.csv +9 -0
- package/data/mobile-audit.csv +5 -0
- package/data/navigation-verification.csv +20 -14
- package/data/node-types.csv +2 -0
- package/data/placement-rules.csv +129 -127
- package/package.json +1 -1
- package/references/dynamic-content.md +54 -0
- package/references/failure-modes.md +27 -0
- package/references/placement.md +38 -0
- package/scripts/check-release.mjs +5 -3
- package/sites/_moksa.py +12 -0
- package/sites/moksa.json +3616 -104
- package/tools/build_page.py +31 -5
- package/tools/extract_node_types.py +7 -1
- package/tools/extract_placement.py +23 -3
- package/tools/verify_components.py +155 -52
|
@@ -111,3 +111,57 @@ attachment(2) find_image(2) find_link(2) find_video(2) media
|
|
|
111
111
|
`sum`, `avg`, `concat` and `fallback` are variadic. `date()`'s timestamp argument is a
|
|
112
112
|
true GMT Unix timestamp — pair it with `post/publish_timestamp_gmt`, not
|
|
113
113
|
`publish_timestamp`, unless you want the local-time value re-interpreted as UTC.
|
|
114
|
+
|
|
115
|
+
## A loop in place of a component's children
|
|
116
|
+
|
|
117
|
+
Three component families take a loop INSTEAD of hand-authored children, which is
|
|
118
|
+
the thing a page builder needs a plugin per component to do:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
slider-slides > slider-loop-slides > slider-slide one slide per row
|
|
122
|
+
tabs-menu > tabs-loop-tabs > tabs-tab
|
|
123
|
+
tabs-content > tabs-loop-tab-panes > tabs-tab-pane
|
|
124
|
+
list > list-loop-items > list-item
|
|
125
|
+
accordion > accordion-loop-items> accordion-item
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Each loop container carries the same three keys an ordinary `loop` does —
|
|
129
|
+
`loopType`, `loopNamespace` and a `<loopType>Options` group — and the template
|
|
130
|
+
inside reads its row with `@VAR('<namespace>/…')`. You author ONE slide, one tab,
|
|
131
|
+
one row; the count comes from the content. The assertion that matters is therefore
|
|
132
|
+
a COUNT: a template that failed to loop renders exactly once and looks correct.
|
|
133
|
+
|
|
134
|
+
**`canBeParentFor` disagrees with the factory's own default data on these.**
|
|
135
|
+
`SliderLoopSlidesElementTypeFactory::canBeParentFor()` returns true for
|
|
136
|
+
`SliderSlidesElementTypeFactory` — the track, not the slide — while its
|
|
137
|
+
`getDefaultData()` seeds itself with a `slider-slide`. `accordion-loop-items` says
|
|
138
|
+
`accordion` the same way. The structure the editor actually builds is the second
|
|
139
|
+
one. `build_page.py` resolves it by treating a loop container as standing in for
|
|
140
|
+
its parent's children: what `slider-loop-slides` accepts is what `slider-slides`
|
|
141
|
+
accepts.
|
|
142
|
+
|
|
143
|
+
## `loop-pagination`, and the key you must not use
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
loop > loop-pagination > loop-pagination-button-previous
|
|
147
|
+
> loop-pagination-numbers > loop-pagination-number
|
|
148
|
+
> loop-pagination-button-next
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`loop-pagination-number` is a TEMPLATE repeated per page, the same rule as a
|
|
152
|
+
slider bullet — author one. `loop-pagination-numbers` takes `pagesBefore` /
|
|
153
|
+
`pagesAfter` / `pagesStart` / `pagesEnd` to decide how much of a long range to
|
|
154
|
+
show.
|
|
155
|
+
|
|
156
|
+
**Do not set `paginationKey` to `p`.** It is one of WordPress's own reserved query
|
|
157
|
+
vars, meaning "post ID": `?p=2` answers **HTTP 301** to post 2, so the visitor
|
|
158
|
+
leaves the page entirely and the loop renders nothing. Measured. Any other key
|
|
159
|
+
works — `?notes=2` on the demo pages the archive and changes all three rows.
|
|
160
|
+
|
|
161
|
+
## `@substr` counts BYTES
|
|
162
|
+
|
|
163
|
+
`@substr(@VAR('post/title'), 0, 14)` on a Han title cuts the fifth character in
|
|
164
|
+
half and the output ends in U+FFFD. Lengths have to be a multiple of the encoding
|
|
165
|
+
width — 18 for six three-byte characters — or the truncation has to be left to CSS.
|
|
166
|
+
A count-based check cannot see this; `verify_components.py` looks for the
|
|
167
|
+
replacement character directly.
|
|
@@ -372,3 +372,30 @@ Two things come out of it:
|
|
|
372
372
|
The general form: a check that confirms a resource EXISTS is not a check that the
|
|
373
373
|
control WORKS, and anything living in a shared layout needs verifying on every
|
|
374
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,11 @@ 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": 63,
|
|
105
|
+
"data/component-wall-verification.csv": 13,
|
|
106
|
+
"data/form-surface-verification.csv": 8,
|
|
107
|
+
"data/content-loop-verification.csv": 10,
|
|
108
|
+
"data/navigation-verification.csv": 39,
|
|
107
109
|
"data/page-modal-verification.csv": 19,
|
|
108
110
|
"data/interaction-rules-verification.csv": 17,
|
|
109
111
|
"data/dialog-form-verification.csv": 49,
|
package/sites/_moksa.py
CHANGED
|
@@ -304,6 +304,10 @@ CONVERTED_COUNT = _count("conversion-batch.csv",
|
|
|
304
304
|
|
|
305
305
|
# label, href, one line of what it demonstrates
|
|
306
306
|
CAPABILITIES = [
|
|
307
|
+
("內容驅動", "/content/",
|
|
308
|
+
"輪播、分頁、存檔——都由文章資料複製"),
|
|
309
|
+
("表單", "/forms/",
|
|
310
|
+
"下拉、單選、複選、上傳、必填規則與錯誤訊息"),
|
|
307
311
|
("元件牆", "/components/",
|
|
308
312
|
"分頁、手風琴、表單、地圖——都能直接操作"),
|
|
309
313
|
("彈窗與觸發", "/dialogs/",
|
|
@@ -3228,6 +3232,14 @@ if __name__ == "__main__":
|
|
|
3228
3232
|
# helpers it reads are pure functions - two copies produce identical dicts.
|
|
3229
3233
|
import _dialogs
|
|
3230
3234
|
import _sliders
|
|
3235
|
+
import _content
|
|
3236
|
+
SITE["pages"].append({"slug": "content", "post_id": 283,
|
|
3237
|
+
"title": "內容驅動",
|
|
3238
|
+
"tree": apply_type(_content.TREE, DISPLAY, CJK, "600")})
|
|
3239
|
+
import _forms
|
|
3240
|
+
SITE["pages"].append({"slug": "forms", "post_id": 282,
|
|
3241
|
+
"title": "表單",
|
|
3242
|
+
"tree": apply_type(_forms.TREE, DISPLAY, CJK, "600")})
|
|
3231
3243
|
import _components
|
|
3232
3244
|
SITE["pages"].append({"slug": "components", "post_id": 281,
|
|
3233
3245
|
"title": "元件",
|