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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Dynamic content: the `@` expression language
|
|
2
|
+
|
|
3
|
+
Mosaic's answer to Elementor's dynamic tags. Verified end to end — every example on
|
|
4
|
+
this page was committed to a live site and read back out of the delivered HTML.
|
|
5
|
+
|
|
6
|
+
## The syntax you would not guess
|
|
7
|
+
|
|
8
|
+
A bare identifier does **nothing**. `Evaluator::exec()` handles the `Identifier` node
|
|
9
|
+
by returning the literal string `"Identifiers are not used currently"`. So this:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
@post.title
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
is stored happily, renders as the text `@post.title`, and looks like a typo in your
|
|
16
|
+
content rather than a broken feature.
|
|
17
|
+
|
|
18
|
+
The real form is a **function call named `VAR`, taking a `namespace/name` string**:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
@VAR('post/title') -> Probe lab
|
|
22
|
+
@VAR('post/id') -> 20
|
|
23
|
+
@VAR('post/permalink') -> https://example.com/probe-lab/
|
|
24
|
+
@VAR('site/name') -> example.com
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Expressions compose, and the 19 evaluator functions take `@VAR(...)` results as
|
|
28
|
+
arguments:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
@concat('[', @VAR('post/title'), '] #', @VAR('post/id')) -> [Probe lab] #20
|
|
32
|
+
@substr(@VAR('post/title'), 0, 5) -> Probe
|
|
33
|
+
@fallback(@VAR('post/nope'), 'DEFAULTED') -> DEFAULTED
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A literal `@` in ordinary text needs escaping as `\@`.
|
|
37
|
+
|
|
38
|
+
`@VAR_RAW('…')` is the same lookup **without HTML-escaping**. The evaluator escapes
|
|
39
|
+
untrusted (request-derived) values at the point of interpolation and carries a
|
|
40
|
+
`{value, trusted}` envelope to do it; `VAR_RAW` opts out of that. Use `VAR` unless you
|
|
41
|
+
are deliberately publishing markup from a trusted field.
|
|
42
|
+
|
|
43
|
+
## Where an expression goes
|
|
44
|
+
|
|
45
|
+
**Inline in text** — a `wysiwyg-variable` node inside a `text` node, with the
|
|
46
|
+
expression as a plain string in `dynamicCode`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{"type": "text", "data": {"tagName": "p"},
|
|
50
|
+
"children": [{"type": "wysiwyg-variable", "data": {"dynamicCode": "@VAR('post/title')"}}]}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`wysiwyg-variable` is one of the seven types that **kill the page** when placed under
|
|
54
|
+
a plain container — it needs a wysiwyg parent, and the sweep recorded it as
|
|
55
|
+
`NodeMResourceFilterFunctionInterface parent is missing`.
|
|
56
|
+
|
|
57
|
+
**On a property** whose validator chain includes `ValidatorDynamicCodeObject` — a
|
|
58
|
+
button's `url`, an image's `src`, and so on. There the value is an object, because a
|
|
59
|
+
literal is also legal in that slot and the object is what disambiguates:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
"url": {"v": "@VAR('post/permalink')"}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Two different shapes for the same language, and which one applies is decided by the
|
|
66
|
+
property's validator chain in `data/node-properties.csv`: **`ValidatorDynamicCode`
|
|
67
|
+
means a bare string, `ValidatorDynamicCodeObject` means `{"v": "…"}`**.
|
|
68
|
+
|
|
69
|
+
## The variable namespace
|
|
70
|
+
|
|
71
|
+
`data/dynamic-variables.csv` — 74 variables across 11 namespaces, each row carrying
|
|
72
|
+
the exact `@VAR('…')` expression to paste:
|
|
73
|
+
|
|
74
|
+
| namespace | n | what it needs to resolve |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `post` | 22 | a post/page context — `title`, `id`, `permalink`, `content`, `featuredImage`, `publish_date`, `author_name`, `modified_timestamp`, … |
|
|
77
|
+
| `user` | 13 | the current visitor — `displayName`, `email`, `loggedin`, `user_registered_date`, … |
|
|
78
|
+
| `site` | 7 | always — `name`, `url`, `description`, `admin_email`, `wpurl` |
|
|
79
|
+
| `author` | 7 | a post context |
|
|
80
|
+
| `menu` | 7 | inside a menu loop — `href`, `title`, `hasChildren` |
|
|
81
|
+
| `request` | 4 | always — `ip`, `page_url`, `referrer`, `user_agent` |
|
|
82
|
+
| `slider` | 4 | inside a slider |
|
|
83
|
+
| `wordpress_search` | 4 | a search results page — `searchQuery`, `resultCount` |
|
|
84
|
+
| `wordpress_archive` | 3 | an archive page |
|
|
85
|
+
| `form` | 2 | inside a form action |
|
|
86
|
+
| `__current_theme` | 1 | always |
|
|
87
|
+
| `row` | — | inside a loop; its schema is declared per loop source, not statically |
|
|
88
|
+
|
|
89
|
+
**An unresolvable name is an empty string, never an error.** `@VAR('nope/nothing')`
|
|
90
|
+
renders as nothing at all, and so does a real name in the wrong context — a
|
|
91
|
+
`post/title` on a page with no post context is indistinguishable from a typo. That is
|
|
92
|
+
what `@fallback()` is for, and why a blank spot on a page is the symptom to look for
|
|
93
|
+
rather than a message in a log.
|
|
94
|
+
|
|
95
|
+
The name separator is `/` (`TemplatingContextVariable::VARIABLE_SEPARATOR`), and the
|
|
96
|
+
namespace is everything before the first one.
|
|
97
|
+
|
|
98
|
+
## The functions
|
|
99
|
+
|
|
100
|
+
`data/evaluator-functions.csv` — 19 of them with their arity:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
abs(1) round(1) round_up(2) calc(1) sum(…) avg(…) numeric
|
|
104
|
+
concat(…) substr(3) excerpt(4) remove_html(1) text
|
|
105
|
+
esc_attr(1) esc_html(1) json_encode(1) escaping
|
|
106
|
+
date(3) dates
|
|
107
|
+
fallback(…) defaulting
|
|
108
|
+
attachment(2) find_image(2) find_link(2) find_video(2) media
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`sum`, `avg`, `concat` and `fallback` are variadic. `date()`'s timestamp argument is a
|
|
112
|
+
true GMT Unix timestamp — pair it with `post/publish_timestamp_gmt`, not
|
|
113
|
+
`publish_timestamp`, unless you want the local-time value re-interpreted as UTC.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# How Mosaic fails, measured
|
|
2
|
+
|
|
3
|
+
## `$wpdb->insert()` casts any column named `ID` to an integer
|
|
4
|
+
|
|
5
|
+
WordPress hardcodes `'ID' => '%d'` in `$wpdb->field_types`, because `wp_posts.ID` is
|
|
6
|
+
an integer. Mosaic's `ID` columns are `varchar(36)` — UUIDs, and for breakpoints the
|
|
7
|
+
literals `_t` and `_m`.
|
|
8
|
+
|
|
9
|
+
So `$wpdb->insert($table, $row)` on a Mosaic table stores `_t` as `0`. It happened to
|
|
10
|
+
fail loudly here, because `_m` also becomes `0` and collides on the composite primary
|
|
11
|
+
key — on a table where only one row was affected it would have been silent.
|
|
12
|
+
|
|
13
|
+
Pass an explicit format array on every insert:
|
|
14
|
+
|
|
15
|
+
```php
|
|
16
|
+
$wpdb->insert($table, $row, array_fill(0, count($row), '%s'));
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## WP-CLI eats `--flags` before your script sees them
|
|
20
|
+
|
|
21
|
+
`wp eval-file script.php --active` fails with *unknown --active parameter*: WP-CLI
|
|
22
|
+
parses anything `--`-prefixed as its own option. Script arguments must be bare
|
|
23
|
+
keywords — `wp eval-file script.php active`.
|
|
24
|
+
|
|
25
|
+
## An invalid `ordering` orphans the node, silently
|
|
26
|
+
|
|
27
|
+
`ordering` is a fractional-index STRING. Send something that is not one and the
|
|
28
|
+
commit returns **no exception** — and the row is stored with **both `ordering` and
|
|
29
|
+
`parentID` blanked**. The node has no parent, so it never renders, and anything you
|
|
30
|
+
were measuring through it reads as a clean negative.
|
|
31
|
+
|
|
32
|
+
Measured side by side in one commit, three sibling probes:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
ordering "a6" -> stored: ordering=a6 parentID=a2dd9602-... renders
|
|
36
|
+
ordering "z000" -> stored: ordering='' parentID='' orphan
|
|
37
|
+
ordering "z001" -> stored: ordering='' parentID='' orphan
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This cost a whole style-property sweep: 77 probes committed without error, none
|
|
41
|
+
rendered, and all 77 properties came back ABSENT — including `color` and
|
|
42
|
+
`paddingTop`, which the entire site is built on. The result looked like a finding.
|
|
43
|
+
It was the tool being broken.
|
|
44
|
+
|
|
45
|
+
Use `ordering_for()` in `tools/build_page.py`; never format your own. And when a
|
|
46
|
+
sweep returns a suspiciously total failure, check that the probes rendered before
|
|
47
|
+
believing the measurement — `sweep_style_properties.py` now exits rather than
|
|
48
|
+
reporting a run in which nothing rendered.
|
|
49
|
+
|
|
50
|
+
## Duplicate `ordering` among siblings drops nodes, silently
|
|
51
|
+
|
|
52
|
+
`ordering_for()` in `build_page.py` wraps after 62 entries, so `ordering_for(i % 62)`
|
|
53
|
+
hands two siblings the same index. Mosaic keeps one and discards the other without an
|
|
54
|
+
exception. A sweep of 79 probes left 39 nodes in the database and still printed a
|
|
55
|
+
result table.
|
|
56
|
+
|
|
57
|
+
For more than 62 siblings, generate your own monotonic index - `"a" + ALPHA[i // 62]
|
|
58
|
+
+ ALPHA[i % 62]` gives 3844 lexicographically ordered slots.
|
|
59
|
+
|
|
60
|
+
## A sweep must refuse a contaminated page
|
|
61
|
+
|
|
62
|
+
Probe ids from separate runs share an id space, and two runs can give the same id to
|
|
63
|
+
different node types - `np-101` was a `<select>` from one run and a `<div>` from the
|
|
64
|
+
next, on the same page. Every number read off that page was meaningless.
|
|
65
|
+
|
|
66
|
+
Worse, the cleanup query was `LIKE "%np-0%"`, which silently misses every id from
|
|
67
|
+
`np-100` up, so "0 remaining" was itself wrong. Match the stored shape:
|
|
68
|
+
`LIKE '%"attrID":"np-%'`.
|
|
69
|
+
|
|
70
|
+
`sweep_node_properties.py` now exits unless the page carries exactly the probes this
|
|
71
|
+
run planned, allowing for the ones whose own property relocates them out of the tree.
|
|
72
|
+
|
|
73
|
+
## heal() owns the body's children
|
|
74
|
+
|
|
75
|
+
Do not parent anything directly to a `body` node. `heal()` rebuilds the
|
|
76
|
+
body > three-div skeleton whenever it decides one is missing, and probes hung
|
|
77
|
+
straight off the body do not survive it. Attach to a div inside the body instead.
|
|
78
|
+
|
|
79
|
+
Every failure below was produced on purpose, on a live install, by the sweep in
|
|
80
|
+
`tools/sweep_node_types.py`. None of it is inferred from source.
|
|
81
|
+
|
|
82
|
+
## Mosaic does not validate placement at commit time
|
|
83
|
+
|
|
84
|
+
This is the fact that shapes everything else. Mosaic's commit endpoint will happily
|
|
85
|
+
store a node whose parent makes no structural sense. The rejection happens later, at
|
|
86
|
+
**render** — and it takes the entire page with it.
|
|
87
|
+
|
|
88
|
+
Placing `accordion-item` directly under a `div`:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
commit -> HTTP 200, syncResponseEnvelopes with action "create". The row is in the DB.
|
|
92
|
+
render -> HTTP 200, 54 bytes, body: "AccordionElementMResource instance required"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Not a 500. Not a WordPress error page. A **200 with a plain string where the site used
|
|
96
|
+
to be**. Three of the 74 free types do this from a plain `div` parent:
|
|
97
|
+
|
|
98
|
+
| type | what the dead page says |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `accordion-item` | `AccordionElementMResource instance required` |
|
|
101
|
+
| `accordion-content` | `AccordionItemElementMResource instance required` |
|
|
102
|
+
| `wysiwyg-variable` | `NodeMResourceFilterFunctionInterface parent is missing` |
|
|
103
|
+
|
|
104
|
+
The consequence for anything automated: **a successful commit is not evidence of a
|
|
105
|
+
working page.** Fetch the page and check its size after writing. A monitor that only
|
|
106
|
+
watches status codes will report a healthy site that is serving 54 bytes.
|
|
107
|
+
|
|
108
|
+
It also means a batch write is dangerous in a way it is not in Elementor or Gutenberg:
|
|
109
|
+
one bad node does not degrade its own corner of the page, it deletes the page. Write
|
|
110
|
+
one subtree, verify, then write the next.
|
|
111
|
+
|
|
112
|
+
## Some types kill the commit request itself
|
|
113
|
+
|
|
114
|
+
Five free types return **HTTP 500** from `/commit` when placed under a `div`:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
component-instance loop loop-items loop-pagination-numbers loop-pagination-number
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
These are the types that require a resolved context — a component to instantiate, a
|
|
121
|
+
query to iterate — and the constructor throws before Mosaic can turn the problem into
|
|
122
|
+
a normal validation response. Nothing is written, so a 500 here is *safer* than the
|
|
123
|
+
silent 200 above, but it is still a PHP fatal in the error log rather than an
|
|
124
|
+
error message you can show a user.
|
|
125
|
+
|
|
126
|
+
`document` produced a 502 on one run and a 500 on another: the same fatal, sometimes
|
|
127
|
+
surfacing as a gateway timeout instead. Treat 5xx from `/commit` as one class.
|
|
128
|
+
|
|
129
|
+
## Validator rejections arrive as HTTP 200
|
|
130
|
+
|
|
131
|
+
Separate from both cases above: when Mosaic *does* reject a value cleanly, it answers
|
|
132
|
+
`RESTJSONExceptionEnvelope` — HTTP **200** with an `exceptions` array in the body.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
resp = commit(...) # 200
|
|
136
|
+
resp["response"]["exceptions"] # <- the actual verdict lives here
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
So there are three distinct outcomes and only one of them changes the status code:
|
|
140
|
+
|
|
141
|
+
| what happened | status | how you detect it |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| clean validator rejection | 200 | `exceptions` in the body |
|
|
144
|
+
| PHP fatal during commit | 500/502 | status code |
|
|
145
|
+
| structurally invalid node accepted | 200 | **nothing, until you fetch the page** |
|
|
146
|
+
|
|
147
|
+
## Commit has side effects beyond the rows you sent
|
|
148
|
+
|
|
149
|
+
The first commit against a fresh theme came back having created things nobody asked
|
|
150
|
+
for:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
create breakpoint _t
|
|
154
|
+
create breakpoint _m
|
|
155
|
+
create collection 9083a14b-…
|
|
156
|
+
create collectionMode 9a674a81-…
|
|
157
|
+
create collectionSkin e4beb80a-…
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`heal()` runs as part of the commit path, so the response's `syncResponseEnvelopes`
|
|
161
|
+
can contain resources from managers you never touched. Read the whole envelope list
|
|
162
|
+
and take every revision in it — assuming the response only describes your own writes
|
|
163
|
+
will leave you holding stale revisions for the rest of the session.
|
|
164
|
+
|
|
165
|
+
Composite types heal aggressively too: a naive batch that placed all 74 types once
|
|
166
|
+
produced **802 node rows**, because types like `accordion` and `navbar` build their
|
|
167
|
+
required children on commit.
|
|
168
|
+
|
|
169
|
+
## `modified_gmt` is server-assigned
|
|
170
|
+
|
|
171
|
+
A record committed with `"modified_gmt": "2026-09-05 17:00:00"` came back stored as
|
|
172
|
+
`16:59:16`. The server overwrites it. Do not use a value you sent as a local cache key.
|
|
173
|
+
|
|
174
|
+
## Reproducing all of this
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
wp eval-file tools/bootstrap_probe_theme.php # licence-free scratch theme
|
|
178
|
+
python tools/sweep_node_types.py --config sweep.json --setup
|
|
179
|
+
python tools/sweep_node_types.py --config sweep.json --sweep
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The sweep is destructive by design and must only be pointed at a scratch site.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Interactions: Mosaic's JavaScript animation system
|
|
2
|
+
|
|
3
|
+
There are two ways to animate in Mosaic and they are completely separate.
|
|
4
|
+
|
|
5
|
+
**The CSS path** — a `transition` array plus a `hover` (or any other) state — is fully
|
|
6
|
+
verified and is what `sites/_moksa.py` uses. It covers hover, focus and
|
|
7
|
+
every node-type state in `data/style-states.csv`. Reach for it first;
|
|
8
|
+
`references/styling.md` has the shapes.
|
|
9
|
+
|
|
10
|
+
**The interaction path** is this document: scroll-driven and event-driven animation
|
|
11
|
+
with keyframes, run by Mosaic's own JavaScript. It is **partially verified**, and this
|
|
12
|
+
page is explicit about where the verified part ends.
|
|
13
|
+
|
|
14
|
+
## What is confirmed
|
|
15
|
+
|
|
16
|
+
The envelope, the trigger types and the action/timeline structure all reach the
|
|
17
|
+
browser. A node carries:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
"interactions": [{
|
|
21
|
+
"type": "scrollIntoView",
|
|
22
|
+
"scrollIntoViewOptions": {
|
|
23
|
+
"name": "Reveal",
|
|
24
|
+
"ID": "<uuid>",
|
|
25
|
+
"actionSlots": {
|
|
26
|
+
"scrollIntoView": {
|
|
27
|
+
"actions": [{
|
|
28
|
+
"type": "animation",
|
|
29
|
+
"uuid": "<uuid>",
|
|
30
|
+
"animationOptions": {
|
|
31
|
+
"propertyMetas": [ … ],
|
|
32
|
+
"initial": { … },
|
|
33
|
+
"keyframes": [{"uuid": "<uuid>",
|
|
34
|
+
"progressData": {"delay": 0, "duration": 100}}]
|
|
35
|
+
}
|
|
36
|
+
}]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
and the page then emits, in a `<script>` before the frontend bundle:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
var mosaicInteractionTypes={"progress":[{"id":"scrollIntoView",
|
|
47
|
+
"defaultSettings":{"allowBackward":"1","smoothing":"60","startWhen":"middleOfTheScreen",
|
|
48
|
+
"startOffset":"0px","endWhen":"middleOfTheScreen","endOffset":"0px"},
|
|
49
|
+
"supportsLivePreview":true,"timelineKeys":["_"]}]};
|
|
50
|
+
var mosaicInteractions=[{"type":"scrollIntoView","triggerSelector":".M_EL3",
|
|
51
|
+
"action":{"scrollIntoView":{"actions":[{"type":"animation","data":{},
|
|
52
|
+
"animationOptions":{"timelines":{"_":{"keyframes":[
|
|
53
|
+
{"progressData":{"delay":0,"duration":100},"easing":"ease"}]}}}}]}}}];
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
That payload is the oracle for anything still unknown: commit a shape, fetch the page,
|
|
57
|
+
read `var mosaicInteractions`. Whatever survives into it is what Mosaic accepted.
|
|
58
|
+
|
|
59
|
+
Confirmed from it:
|
|
60
|
+
|
|
61
|
+
- **The action-slot key is the interaction's own ID** when the type declares no
|
|
62
|
+
explicit slots (`TimedInteractionTypeFactory` falls back to `$this->reader->getId()`).
|
|
63
|
+
- **The array inside a slot is `actions`**, not the slot name.
|
|
64
|
+
- **The keyframe array is `keyframes`**, not the timeline key — the timeline key
|
|
65
|
+
(`_` for a single-timeline type) is applied by the server, which reshapes
|
|
66
|
+
`animationOptions` into `{"timelines": {"_": {"keyframes": [...]}}}`.
|
|
67
|
+
- **`progressData` uses floats 0–100**, not durations, for progress-family triggers —
|
|
68
|
+
it is a percentage of the scroll range, not milliseconds.
|
|
69
|
+
- **`easing` is defaulted to `ease`** by the server.
|
|
70
|
+
|
|
71
|
+
## What is NOT solved
|
|
72
|
+
|
|
73
|
+
**Property binding.** `propertyMetas` and per-keyframe `properties` did not survive
|
|
74
|
+
into the payload in any shape tried: flat array of metas, metas nested one level
|
|
75
|
+
deeper (which made the whole interaction vanish), properties as an object, properties
|
|
76
|
+
as an array. The keyframes arrive with timing but with nothing to animate.
|
|
77
|
+
|
|
78
|
+
Do not ship an interaction animation on the strength of this page. Use the CSS path,
|
|
79
|
+
or drive the editor once by hand and read the stored row back out of `wp_mosaic_nodes`
|
|
80
|
+
— that row is the authoritative example, and one look at it would settle the shape.
|
|
81
|
+
|
|
82
|
+
## The failure mode that makes this hard
|
|
83
|
+
|
|
84
|
+
**Invalid sub-structures are silently pruned.** Committing the full interaction above
|
|
85
|
+
returned HTTP 200, no `exceptions`, and a normal `create` envelope — and the row
|
|
86
|
+
stored was:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
[{"type":"scrollIntoView","scrollIntoViewOptions":{"name":"Reveal"},"uuid":"…"}]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`ID`, `actionSlots`, the action and the whole animation were dropped without a word.
|
|
93
|
+
Mosaic also *added* a `uuid` of its own. So on this data model:
|
|
94
|
+
|
|
95
|
+
- a successful commit says nothing about whether your structure was understood;
|
|
96
|
+
- **read the row back** (`GET …/masterDocumentInstance/<masterID>`) and compare it to
|
|
97
|
+
what you sent — the diff is the error message Mosaic never gives you.
|
|
98
|
+
|
|
99
|
+
That check is worth applying to any deeply-nested Mosaic write, not just interactions.
|
|
100
|
+
|
|
101
|
+
## The surface, extracted
|
|
102
|
+
|
|
103
|
+
`data/interaction-types.csv` — 12 trigger types in two families:
|
|
104
|
+
|
|
105
|
+
| family | how it runs | types |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `timed` | its own clock, fired by an event | `pageLoad`, `click`, `hover`, `formSubmit`, `tabChange`, `slideChange`, `dropdownChange`, `accordionItemChange`, `elementScrollIntoView`, `scrollDirectionChange` |
|
|
108
|
+
| `progress` | driven by a scalar, scrubbable both ways | `scrollIntoView`, `pointerMove` |
|
|
109
|
+
|
|
110
|
+
`data/animatable-properties.csv` — the 22 properties a keyframe can drive. **This is
|
|
111
|
+
not the same set as the 98 style properties**, and ten of them animate through a CSS
|
|
112
|
+
custom property rather than the property itself:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
transform translateX/Y/Z (--mosaic-translate-*) scale, scaleX/Y/Z (--mosaic-scale*)
|
|
116
|
+
rotateX/Y/Z (--mosaic-rotate-*) opacity color backgroundColor borderColor
|
|
117
|
+
boxShadow textShadow width height filter backdropFilter transformOrigin
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Animating through `--mosaic-translate-y` rather than `translate` is why a Mosaic
|
|
121
|
+
interaction can move an element that already has a CSS `translate` set: the two
|
|
122
|
+
compose instead of one clobbering the other.
|
|
123
|
+
|
|
124
|
+
`data/pluggables.csv` lists the 11 non-animation action types an interaction can run —
|
|
125
|
+
`link`, `script`, `setCookie`, `player*`, `interaction*` — for triggering behaviour
|
|
126
|
+
rather than motion.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Placement: the rule the API will not enforce for you
|
|
2
|
+
|
|
3
|
+
Mosaic decides whether an element may sit inside another element in **two** places,
|
|
4
|
+
and neither of them is the REST commit endpoint.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
canBeParentFor($childFactory) on the PARENT type - "will I take this child?"
|
|
8
|
+
canBeNestedChildFor($target, $br) on the CHILD type - "am I allowed in this ancestry?"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The visual editor calls both before it lets you drop anything. `POST …/commit` calls
|
|
12
|
+
neither. A node committed into a parent that would have refused it is stored, returns
|
|
13
|
+
HTTP 200 with a normal `syncResponseEnvelopes`, and the **public page then dies**,
|
|
14
|
+
serving a bare string like `AccordionElementMResource instance required` as a 200 with
|
|
15
|
+
a ~54 byte body.
|
|
16
|
+
|
|
17
|
+
That is the whole reason this table exists. Writing Mosaic headlessly means being the
|
|
18
|
+
component that checks placement, because nothing downstream will.
|
|
19
|
+
|
|
20
|
+
## The parent side: `data/placement-rules.csv`
|
|
21
|
+
|
|
22
|
+
One row per node type, extracted from every `canBeParentFor` in the source (122 types):
|
|
23
|
+
|
|
24
|
+
| rule | count | meaning |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `none` | 59 | leaf. Refuses every child — the default on `NodeTypeFactoryAbstract`. |
|
|
27
|
+
| `any` | 34 | takes anything. `div`, `body`, `section`, `button`, `menu`, `slider-slide`, `tabs-tab-pane`, … |
|
|
28
|
+
| `allow` | 26 | takes only the types in `allowed_children`. |
|
|
29
|
+
| `complex` | 3 | the body is not a plain `instanceof` chain — `fieldset`, `select-input`, `styleguide-entry-content-text-quote`. Read the file named in `declared_in`. |
|
|
30
|
+
|
|
31
|
+
The `allow` rows are the component families, and they are strict. A few worth knowing
|
|
32
|
+
by heart because they are the ones people get wrong:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
accordion -> accordion-item | accordion-loop-items
|
|
36
|
+
accordion-item -> accordion-item | accordion-loop-items
|
|
37
|
+
list -> list-item | list-loop-items
|
|
38
|
+
tabs -> tabs-content | tabs-menu
|
|
39
|
+
tabs-menu -> tabs-loop-tabs | tabs-tab
|
|
40
|
+
tabs-content -> tabs-loop-tab-panes | tabs-tab-pane
|
|
41
|
+
slider-slides -> slider-loop-slides | slider-slide
|
|
42
|
+
slider-navigation -> slider-navigation-bullet
|
|
43
|
+
form-wrapper -> form | success-screen
|
|
44
|
+
submit-button -> submit-label | submit-loading
|
|
45
|
+
loop-items -> loop-item
|
|
46
|
+
wp-menu -> list-item
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Note `accordion-item -> accordion-item`: an accordion item's children are further
|
|
50
|
+
items, not arbitrary content. The content goes in `accordion-title` and
|
|
51
|
+
`accordion-content`, both of which are `rule = any`.
|
|
52
|
+
|
|
53
|
+
## The child side: `nested_rule`
|
|
54
|
+
|
|
55
|
+
**55 of the 122 types also implement `canBeNestedChildFor`** — a runtime condition on
|
|
56
|
+
the *ancestry*, not just the immediate parent. A static table cannot resolve these,
|
|
57
|
+
so the CSV flags them and names the file.
|
|
58
|
+
|
|
59
|
+
`accordion-item` is the worked example:
|
|
60
|
+
|
|
61
|
+
```php
|
|
62
|
+
// legal only if the target is an accordion / accordion-loop-items,
|
|
63
|
+
// or an accordion appears somewhere up the branch
|
|
64
|
+
if (!$inAccordion && !$elementFactoryNode->hasFactoryOnBranch(AccordionElementTypeFactory::class)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`div` says `any`, so the parent side waves `accordion-item` through. The child side
|
|
70
|
+
would have refused — but only the editor asks it. Commit does not, and the page dies.
|
|
71
|
+
This is exactly the measured `BROKE_PAGE` case.
|
|
72
|
+
|
|
73
|
+
## The rule to write into your own code
|
|
74
|
+
|
|
75
|
+
Before committing a node of type `C` under a parent of type `P`:
|
|
76
|
+
|
|
77
|
+
1. Look up `P` in `placement-rules.csv`. If `rule` is `none`, stop. If `allow`, `C`
|
|
78
|
+
must be in `allowed_children`. If `complex`, read `declared_in`.
|
|
79
|
+
2. Look up `C`. If `nested_rule` is `yes`, its required ancestry must genuinely be
|
|
80
|
+
present — the parent chain, not just the immediate parent.
|
|
81
|
+
3. Commit, then **fetch the page and check its size**. Step 3 is not optional; steps
|
|
82
|
+
1 and 2 are a static approximation of two runtime methods.
|
|
83
|
+
|
|
84
|
+
Types with `rule = any` are the safe scaffolding: `div`, `section`, `body`,
|
|
85
|
+
`loop-item`, `tabs-tab-pane`, `slider-slide`, `styleguide-entry-content`. Build with
|
|
86
|
+
those and place specialised children only inside the family that declares them.
|
|
87
|
+
|
|
88
|
+
## What this table does NOT tell you
|
|
89
|
+
|
|
90
|
+
It does not predict which types are unsafe to drop into a plain container. That was
|
|
91
|
+
tested rather than assumed, and it failed:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
predictor tp / fp / fn precision recall
|
|
95
|
+
nested_rule == yes 9 / 46 / 13 0.16 0.41
|
|
96
|
+
rule == allow 11 / 15 / 11 0.42 0.50
|
|
97
|
+
rule in (allow, complex) 11 / 18 / 11 0.38 0.50
|
|
98
|
+
rule == allow AND nested_rule 5 / 12 / 17 0.29 0.23
|
|
99
|
+
rule == allow OR nested_rule 15 / 49 / 7 0.23 0.68
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
22 of the 122 types break the page or fault the commit when placed under a bare `div`,
|
|
103
|
+
and they are spread across every rule value - 11 are `allow`, 6 are `none`, 5 are
|
|
104
|
+
`any`. No flag in this table separates them, and the best combination still misses a
|
|
105
|
+
third of them while flagging 49 types that are perfectly fine.
|
|
106
|
+
|
|
107
|
+
So the two questions are different, and only one of them is settled by source:
|
|
108
|
+
|
|
109
|
+
| question | authority |
|
|
110
|
+
|---|---|
|
|
111
|
+
| which children does parent P accept? | `placement-rules.csv` - reliable, this is literally `canBeParentFor` |
|
|
112
|
+
| is child C safe under a plain container? | `node-verification.csv` - **measured**, nothing else predicts it |
|
|
113
|
+
|
|
114
|
+
Reach for the measured table when you are deciding what to build with, and this one
|
|
115
|
+
when you are deciding what may go inside what. `tools/check_placement_predicts.py`
|
|
116
|
+
re-scores the numbers above after any re-sweep, so the claim stays honest if the
|
|
117
|
+
plugin changes.
|