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,174 @@
|
|
|
1
|
+
# Responsive (RWD)
|
|
2
|
+
|
|
3
|
+
Responsive in Mosaic is the **second axis of the style object**, not a separate data
|
|
4
|
+
structure and not a naming convention:
|
|
5
|
+
|
|
6
|
+
```jsonc
|
|
7
|
+
"style": {
|
|
8
|
+
"&": { // state
|
|
9
|
+
"_": {"paddingLeft": "48px"}, // base breakpoint
|
|
10
|
+
"_t": {"paddingLeft": "32px"}, // <=1079px
|
|
11
|
+
"_m": {"paddingLeft": "20px"} // <=767px
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`state → breakpoint → property`. Get the two middle levels the wrong way round and
|
|
17
|
+
the validator sees `'n'/'o'/'n'/'e'` as property names, because it is iterating the
|
|
18
|
+
characters of a value it expected to be a dict.
|
|
19
|
+
|
|
20
|
+
## The breakpoints are rows, not constants
|
|
21
|
+
|
|
22
|
+
They live in `wp_mosaic_breakpoints`, scoped to the theme, and a fresh theme heals
|
|
23
|
+
into exactly two. Read off the live table:
|
|
24
|
+
|
|
25
|
+
| ID | name | width | direction | ordering |
|
|
26
|
+
|---|---|---|---|---|
|
|
27
|
+
| `_t` | Tablet | 1079 | down | `a0` |
|
|
28
|
+
| `_m` | Mobile | 767 | down | `a1` |
|
|
29
|
+
|
|
30
|
+
Both are `direction: down`, so both match on a phone, and **`_m` is ordered after
|
|
31
|
+
`_t`** — which is the only reason a mobile value overrides a tablet one rather than
|
|
32
|
+
losing to it. Custom breakpoints get UUIDs instead of `_t`/`_m`; do not hardcode the
|
|
33
|
+
pair, read the table.
|
|
34
|
+
|
|
35
|
+
## What it compiles to
|
|
36
|
+
|
|
37
|
+
One inline stylesheet per breakpoint, in the head, each wrapped in its own query:
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<style id="mosaic-theme-block-editor-styles_-inline-css"> <!-- base, no query -->
|
|
41
|
+
<style id="mosaic-theme-block-editor-styles_t-inline-css"> <!-- @media (max-width:1079px) -->
|
|
42
|
+
<style id="mosaic-theme-block-editor-styles_m-inline-css"> <!-- @media (max-width:767px) -->
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Rules inside them hang off the generated `.M_EL<n>` class, **never off your
|
|
46
|
+
`attrID`**. The id is in the HTML and the rule is in the CSS, and the only bridge
|
|
47
|
+
between them is `id="<attrID>" class="M_EL<n>"` in the delivered markup. Any tool
|
|
48
|
+
that wants to check a breakpoint value has to read that mapping first.
|
|
49
|
+
|
|
50
|
+
## The trap that has cost the most: an override cannot REMOVE
|
|
51
|
+
|
|
52
|
+
**A breakpoint override can only CHANGE a property. It can never remove one the base
|
|
53
|
+
breakpoint declared.** This is ordinary cascade behaviour and it is still the single
|
|
54
|
+
most repeated bug in this codebase, because the shape of the data invites it:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# WRONG - and it looks right
|
|
58
|
+
"_": {"customStyles": "border-left:1px solid #ddd;"}, # 4-up: rule between cells
|
|
59
|
+
"_m": {"customStyles": ""}, # 1-up: no rule wanted
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The mobile value declares nothing, so nothing overrides, so the desktop
|
|
63
|
+
`border-left` survives into the collapsed layout and draws a vertical rule down the
|
|
64
|
+
middle of a single column. Same for the asymmetric padding that went with it.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# RIGHT - say zero out loud
|
|
68
|
+
"_m": {"customStyles": "border-left:0;"},
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Measured twice on the same page: a 3×2 hairline list that kept a column rule and a
|
|
72
|
+
48px indent on alternate rows at one column, and a 4-up statistics band whose third
|
|
73
|
+
cell opened a row while still carrying the four-up rule. `tools/verify_rwd.py`
|
|
74
|
+
reports every one of these as `RESET-RISK`.
|
|
75
|
+
|
|
76
|
+
## `gridColumnStart` is in the surface and emits nothing
|
|
77
|
+
|
|
78
|
+
`gridColumnStart`, `gridColumnEnd`, `gridRowStart` and `gridRowEnd` are all in
|
|
79
|
+
`data/style-properties.csv` — they are real entries in the plugin's own property
|
|
80
|
+
list, under the `gridChildPosition` group. They compile to nothing.
|
|
81
|
+
|
|
82
|
+
Measured: 16 `gridColumnStart` declarations in a committed spec, **zero** occurrences
|
|
83
|
+
of `grid-column` anywhere in the delivered CSS. No exception, no console warning, no
|
|
84
|
+
change to the commit response. If you need a child in a particular column, change the
|
|
85
|
+
template — do not try to place the child.
|
|
86
|
+
|
|
87
|
+
## A row with more children than columns fills the gutter, not the width
|
|
88
|
+
|
|
89
|
+
This is the trap the CSS checker structurally cannot catch, and it is worth stating
|
|
90
|
+
on its own because every value involved verifies.
|
|
91
|
+
|
|
92
|
+
A three-child row at `84px 1fr 1.35fr` narrowed to `48px 1fr` does not put its third
|
|
93
|
+
child on the full width. Grid puts it in **column 1** — the 48px number gutter — and
|
|
94
|
+
the body text comes out at one or two characters per line. Every declaration in that
|
|
95
|
+
spec is present in the right media query with the right value; `verify_rwd.py`
|
|
96
|
+
reports 251 verified and 0 missing. The stylesheet is right and the page is wrong.
|
|
97
|
+
|
|
98
|
+
Measured twice on the same build: a services row whose body text ran 48px wide, and a
|
|
99
|
+
four-child products row whose status chip landed alone in a 72px gutter on row two at
|
|
100
|
+
tablet, 60px tall for one line of 10px type.
|
|
101
|
+
|
|
102
|
+
The rule that follows: **when a template narrows, count the children.** Either the
|
|
103
|
+
narrow template keeps as many columns as the row has children, or the row collapses
|
|
104
|
+
to a single column, or a child that is really a property of another child moves
|
|
105
|
+
inside it. The products chip took the third option — it describes the product, it was
|
|
106
|
+
never a column of the table.
|
|
107
|
+
|
|
108
|
+
## Three layout traps that only appear at a narrow width
|
|
109
|
+
|
|
110
|
+
**A transform makes an element the containing block for its absolutely positioned
|
|
111
|
+
descendants.** Any element in a scroll-reveal list carries one, so an
|
|
112
|
+
`position:absolute; top:0; bottom:0` child resolves against *it* rather than against
|
|
113
|
+
the section. Measured: a rule meant to span a 483px band came out 299px — the band
|
|
114
|
+
minus its 92px padding, exactly. The fix is to move the absolute child out to the
|
|
115
|
+
positioned ancestor you actually meant.
|
|
116
|
+
|
|
117
|
+
**A flex row with no `nowrap` does not overflow — it shrinks its children until the
|
|
118
|
+
text breaks.** A header row that looked like it was wrapping was actually being
|
|
119
|
+
squeezed, splitting a three-character wordmark across two lines. `white-space:nowrap`
|
|
120
|
+
makes the row hold its size so you can see the real overflow and design for it.
|
|
121
|
+
|
|
122
|
+
**Inherited `line-height` sets element heights, not your padding.** A header measured
|
|
123
|
+
66px with nothing wrapping in it, because every nav link inherited the `2.05`
|
|
124
|
+
line-height set for running text. The design was not choosing that height; the
|
|
125
|
+
cascade was.
|
|
126
|
+
|
|
127
|
+
## Section is `display:flex`
|
|
128
|
+
|
|
129
|
+
A `section` node is a flex container, so a max-width block child shrink-to-fits
|
|
130
|
+
instead of filling: a 1120px wrap came out 415px. Every wrapper needs `width:100%`.
|
|
131
|
+
|
|
132
|
+
## Verify it, do not look at it
|
|
133
|
+
|
|
134
|
+
Resizing the browser checks the part of the page that happens to be on screen. The
|
|
135
|
+
mechanical check reads what the spec declared and what the site actually served, and
|
|
136
|
+
compares them key by key:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
python tools/verify_rwd.py --config sweep.json --site sites/moksa.json --csv rwd.csv
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
It maps each `attrID` to its generated class, parses the `_t` and `_m` stylesheets,
|
|
143
|
+
and asserts every declared property is present in that breakpoint's rule for that
|
|
144
|
+
element. It exits non-zero on any `MISSING` or `RESET-RISK`.
|
|
145
|
+
|
|
146
|
+
`data/rwd-verification.csv` is the current run: **569 responsive declarations across
|
|
147
|
+
two sites and three pages, all `verified`, no `MISSING`, no `unmapped`, no
|
|
148
|
+
`RESET-RISK`.**
|
|
149
|
+
|
|
150
|
+
A verifier that has never failed is not evidence of anything, so it is checked
|
|
151
|
+
against a poisoned copy of the spec — one undeclared property and one un-reset
|
|
152
|
+
border injected — and it must report exactly those two and exit non-zero:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
mk-svc-0,_m,letter-spacing,9px,,MISSING
|
|
156
|
+
mk-svc-0,_m,border-left,set at base,not reset,RESET-RISK
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Rows it cannot judge are labelled `unmapped`, never counted as passes: a tool that
|
|
160
|
+
scores its own blind spots as successes is worse than no tool.
|
|
161
|
+
|
|
162
|
+
**What it does not cover, and this matters:**
|
|
163
|
+
|
|
164
|
+
- **Anything that needs layout rather than CSS.** Grid placement, horizontal
|
|
165
|
+
overflow, clipped text. The two worst bugs on this page both had a completely
|
|
166
|
+
clean verifier run — see the gutter trap above. Every width still needs a real
|
|
167
|
+
browser at 1440 / 1024 / 390.
|
|
168
|
+
- **Nodes with no `attrID`.** The bridge from a spec declaration to a compiled rule
|
|
169
|
+
is `id="<attrID>" class="M_EL<n>"`, so a node that was never given an id is
|
|
170
|
+
invisible to the tool. The services body text was exactly that: it carried a
|
|
171
|
+
responsive declaration the tool never saw. Give an `attrID` to anything whose
|
|
172
|
+
responsive behaviour you want checked.
|
|
173
|
+
|
|
174
|
+
The tool tells you the values arrived. Only the browser tells you they add up.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Styling: how a value becomes CSS
|
|
2
|
+
|
|
3
|
+
## Grouped properties are inert on their own
|
|
4
|
+
|
|
5
|
+
`data/style-properties.csv` has a `group` column. Twenty of the 98 properties have a
|
|
6
|
+
value in it, and **not one of them compiles when set individually** — swept live,
|
|
7
|
+
zero exceptions:
|
|
8
|
+
|
|
9
|
+
| group | members | result |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `borderStyle` | `border{Top,Right,Bottom,Left}{Width,Style,Color}` | 12 / 12 inert |
|
|
12
|
+
| `outlineStyle` | `outlineWidth`, `outlineStyle`, `outlineColor`, `outlineOffset` | 4 / 4 inert |
|
|
13
|
+
| `gridChildPosition` | `gridColumnStart`, `gridColumnEnd`, `gridRowStart`, `gridRowEnd` | 4 / 4 inert |
|
|
14
|
+
|
|
15
|
+
The commit is accepted, the row is stored, and the stylesheet simply has no such
|
|
16
|
+
declaration. Meanwhile every one of the 58 testable ungrouped properties compiled.
|
|
17
|
+
|
|
18
|
+
Use the grouped shape — `border` takes `{"width": "3px", "style": "dashed", "color":
|
|
19
|
+
"rgb(...)"}` and does compile — or drop to `customStyles`. If you need a child in a
|
|
20
|
+
particular grid column, change the template rather than trying to place the child.
|
|
21
|
+
|
|
22
|
+
`data/style-verification.csv` is per-property, with the group beside the result.
|
|
23
|
+
|
|
24
|
+
Measured on a live install by committing a styled `div` and reading what came back
|
|
25
|
+
down the wire.
|
|
26
|
+
|
|
27
|
+
## The shape
|
|
28
|
+
|
|
29
|
+
A node's `data.style` is addressed by three axes — **state, breakpoint, property**:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"style": {
|
|
34
|
+
"states": {
|
|
35
|
+
"&": { "_": { "backgroundColor": "rgb(11, 22, 33)", "paddingTop": "37px" },
|
|
36
|
+
"_m": { "paddingTop": "9px" } },
|
|
37
|
+
"hover": { "_": { "backgroundColor": "rgb(77, 88, 99)" } }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- `states` keys come from `data/style-states.csv` — `&` is the base state, then
|
|
44
|
+
`hover`, `focus`, `focus-visible`, and 45 node-type-specific ones.
|
|
45
|
+
- The next level is the **breakpoint ID**. `_` is the base breakpoint
|
|
46
|
+
(`DocumentBreakpoints::baseBreakpointID`); the others are rows in
|
|
47
|
+
`mosaic_breakpoints` — a fresh theme heals into `_t` (Tablet, ≤1079px) and
|
|
48
|
+
`_m` (Mobile, ≤767px). Custom breakpoints get UUIDs.
|
|
49
|
+
- The leaf keys are the camelCase properties in `data/style-properties.csv`.
|
|
50
|
+
|
|
51
|
+
`style` also carries `elementClass`, `utilityClasses`, `defaultClass` and
|
|
52
|
+
`localStates` alongside `states` — those are the class/token layer, not per-node values.
|
|
53
|
+
|
|
54
|
+
## What comes out
|
|
55
|
+
|
|
56
|
+
That exact payload produced, in the delivered page:
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<div id="styleprobe" class="M_EL4 M_EL_Div">
|
|
60
|
+
|
|
61
|
+
<style id="mosaic-theme-block-editor-styles_-inline-css">
|
|
62
|
+
html{overflow-x:clip}body{opacity:0}html,body{margin:0}
|
|
63
|
+
.M_EL4{padding-top:37px;background-color:rgb(11, 22, 33)}.M_EL4:HOVER{background-color:rgb(77, 88, 99)}
|
|
64
|
+
</style>
|
|
65
|
+
|
|
66
|
+
<style id="mosaic-theme-block-editor-styles_m-inline-css">
|
|
67
|
+
@media only screen and (max-width: 767px){.M_EL4{padding-top:9px}}
|
|
68
|
+
</style>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Four things to take from that:
|
|
72
|
+
|
|
73
|
+
**Mosaic ships no stylesheet file.** Every compiled rule is an inline `<style>` block
|
|
74
|
+
in the head, id `mosaic-theme-block-editor-styles_<breakpointID>-inline-css`, one
|
|
75
|
+
block per breakpoint. Tooling that looks for a `<link rel="stylesheet">` to diff finds
|
|
76
|
+
nothing — and would wrongly conclude the style had no effect. (That bug was in this
|
|
77
|
+
skill's own property sweep until the probe above caught it.)
|
|
78
|
+
|
|
79
|
+
**The selector is the element's generated class**, `.M_EL4`, not the `id` you set.
|
|
80
|
+
`attrID` gives you an `id` attribute for your own use; it is not what styling hangs on.
|
|
81
|
+
The `M_EL<n>` number is assigned per element per document and is **not stable** — do
|
|
82
|
+
not write selectors against it. The companion `M_EL_Div` / `M_EL_Text` / `M_EL_Button`
|
|
83
|
+
class *is* stable and is the type's marker.
|
|
84
|
+
|
|
85
|
+
**States compile through their selector template.** `hover` used
|
|
86
|
+
`&:HOVER` from `style-states.csv` and produced `.M_EL4:HOVER` — uppercase, exactly as
|
|
87
|
+
the template is written. Everything in that CSV compiles the same way, so the template
|
|
88
|
+
column tells you in advance what rule you will get.
|
|
89
|
+
|
|
90
|
+
**Breakpoints compile to `@media`**, derived from the breakpoint row: `_m` has
|
|
91
|
+
`width: 767, direction: down` and produced `@media only screen and (max-width: 767px)`.
|
|
92
|
+
Breakpoints are theme data, so the same key means different things in different themes.
|
|
93
|
+
|
|
94
|
+
Also note `body{opacity:0}` in the base block — the theme reveals itself from
|
|
95
|
+
JavaScript. A page fetched without running scripts is fully styled but invisible;
|
|
96
|
+
screenshot tooling must let the JS run before capturing.
|
|
97
|
+
|
|
98
|
+
## Setting values
|
|
99
|
+
|
|
100
|
+
The `factory` column in `data/style-properties.csv` tells you the value shape:
|
|
101
|
+
|
|
102
|
+
| factory | write |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `CSSPropertyFactory` (29) | a plain CSS value string — `"flex"`, `"center"`, `"700"` |
|
|
105
|
+
| `CSSCollectionVariablePropertyFactory` (26) | a length string, or a reference to a collection variable |
|
|
106
|
+
| `CSSColorPropertyFactory` (2) | `color`, `backgroundColor` — a colour string or variable reference |
|
|
107
|
+
| `CSSGrouppedPropertyFactory` (20) | one leg of `borderStyle`, `outlineStyle` or `gridChildPosition`; the data is keyed under the group |
|
|
108
|
+
| the other 21 | purpose-built shapes — shadow, transform, gradient, filter, mask. Read the factory named in the CSV. |
|
|
109
|
+
|
|
110
|
+
Plain strings were accepted verbatim for the properties probed above. The 34
|
|
111
|
+
token-referencable properties are the interface to the collection/variable layer,
|
|
112
|
+
which is how a design system is meant to be expressed rather than hard-coding values
|
|
113
|
+
on every node.
|
|
114
|
+
|
|
115
|
+
## Where styling should actually live
|
|
116
|
+
|
|
117
|
+
Setting `style` directly on a node works — everything above proves it — but it is the
|
|
118
|
+
lowest layer and it opts that node out of both reuse mechanisms:
|
|
119
|
+
|
|
120
|
+
1. **Element classes** (`data/element-classes.csv`, 151 built in) carry the shared
|
|
121
|
+
look; a node points at one through `style.elementClass`.
|
|
122
|
+
2. **Collections → modes/skins → variables** are the token layer, the thing the
|
|
123
|
+
34 token-referencable properties reference.
|
|
124
|
+
|
|
125
|
+
Per-node `style` is for the exception, not the rule. Build a design by putting values
|
|
126
|
+
in the class and token layers and letting nodes inherit; reach for per-node style when
|
|
127
|
+
one element genuinely differs.
|
|
128
|
+
|
|
129
|
+
## The five structured values
|
|
130
|
+
|
|
131
|
+
Five properties are silently useless if you write a CSS string to them. The string is
|
|
132
|
+
accepted — no rejection, no exception — and the compiled rule comes out as
|
|
133
|
+
`transform:none`, `box-shadow:none`, or simply never appears. Every shape below was
|
|
134
|
+
found by probing and confirmed against the compiled CSS.
|
|
135
|
+
|
|
136
|
+
```jsonc
|
|
137
|
+
// border-radius:18px
|
|
138
|
+
"borderRadius": {"type": "all", "allOptions": {"borderRadiusValue": "18px"}}
|
|
139
|
+
|
|
140
|
+
// transform:translateY(-8px) — one entry per transform, type names come from
|
|
141
|
+
// TransformTypeFactoryManager: translateX/Y/Z, rotateX/Y/Z, skewX/Y, scaleX/Y/Z
|
|
142
|
+
"transform": [{"type": "translateY", "translateYOptions": {"value": "-8px"}, "uuid": "<uuid>"}]
|
|
143
|
+
|
|
144
|
+
// box-shadow:rgb(20, 20, 20) 8px 8px 0px 0px — type is "outside" or "inside",
|
|
145
|
+
// NOT "outset"/"inset"; a wrong type yields box-shadow:none
|
|
146
|
+
"boxShadow": [{"x": "8px", "y": "8px", "blur": "0px", "spread": "0px",
|
|
147
|
+
"color": "rgb(20,20,20)", "type": "outside", "uuid": "<uuid>"}]
|
|
148
|
+
|
|
149
|
+
// transition-property/duration/timing-function/delay
|
|
150
|
+
"transition": [{"transitionProperty": "all", "transitionDuration": "350ms",
|
|
151
|
+
"transitionDelay": "0ms", "transitionTimingFunction": "ease", "uuid": "<uuid>"}]
|
|
152
|
+
|
|
153
|
+
// all twelve border-*-* longhands; there is no shorthand form
|
|
154
|
+
"borderStyle": {"borderTopWidth": "5px", "borderTopStyle": "solid", "borderTopColor": "rgb(...)",
|
|
155
|
+
"borderRightWidth": …, "borderBottomWidth": …, "borderLeftWidth": …}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The `<type>Options` suffix is the general pattern (`BorderRadiusTypeFactoryAbstract::
|
|
159
|
+
getOptionsName()` returns `getType() . 'Options'`), so it applies to `flexSizing` and the
|
|
160
|
+
other type-switched groups too.
|
|
161
|
+
|
|
162
|
+
`scale` as a single transform entry did not compile with the `value` shape — it uses
|
|
163
|
+
`ScaleTransformTypeFactory`, which takes separate axes. Use `scaleX`/`scaleY` instead,
|
|
164
|
+
which are `SingleTransformTypeFactory` and do take `{"value": …}`.
|
|
165
|
+
|
|
166
|
+
**The escape hatch.** `customStyles` accepts raw CSS text and emits it verbatim into
|
|
167
|
+
the rule: `"customStyles": "outline: 2px dashed rgb(7,8,9);"` compiled through
|
|
168
|
+
unchanged. It is the fallback for anything whose structured shape you have not pinned
|
|
169
|
+
down — at the cost of bypassing the collection-variable layer entirely.
|
|
170
|
+
|
|
171
|
+
`tools/build_page.py` wraps the five shapes above as `radius`, `shadow`,
|
|
172
|
+
`transitionAll`, `move` and `border` shorthands so a design spec cannot get them wrong.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Templates, routing and conditions
|
|
2
|
+
|
|
3
|
+
## How Mosaic decides which template renders
|
|
4
|
+
|
|
5
|
+
A Mosaic template row carries `assign`, `path` and `conditions`, and those three
|
|
6
|
+
answer the whole routing question. From
|
|
7
|
+
`LocatedTemplateRenderMResourceManager`:
|
|
8
|
+
|
|
9
|
+
```php
|
|
10
|
+
if ($templateRevisionRecord->getAssign() == 'auto'
|
|
11
|
+
&& in_array($templateRevisionRecord->getPath(), $paths)
|
|
12
|
+
&& $templateRevisionRecord->matchConditions()) { … }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
So there are exactly two routing modes:
|
|
16
|
+
|
|
17
|
+
| `assign` | how it is chosen |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `auto` | `path` must be in the WordPress template hierarchy for the current request, **and** `conditions` must match |
|
|
20
|
+
| `manual` | bound to one specific post through a row in `mosaic_template_assigns`; `conditions` are not consulted |
|
|
21
|
+
|
|
22
|
+
`path` values come from `GET /template-path-dictionary` — `single-page.php`,
|
|
23
|
+
`single-post.php`, `archive-post.php`, `taxonomy-category.php`, `author.php`,
|
|
24
|
+
`single-product.php`, `archive-product.php` and so on. It is the WordPress template
|
|
25
|
+
hierarchy, so a Mosaic template lands wherever the corresponding PHP template would.
|
|
26
|
+
|
|
27
|
+
**`assign` + `path` is the other binding, and it is a real catch-all.** The
|
|
28
|
+
template row carries `assign` (default `auto`) and `path` columns. Commit one with
|
|
29
|
+
`assign:"auto"`, `path:"index.php"` and it binds to a template path rather than to a
|
|
30
|
+
post; `X-Mosaic-Paths` on a 406 names the paths Mosaic looked for, and `index.php` is
|
|
31
|
+
in all of them. Measured A/B: a URL with no template returned 406, was handled once
|
|
32
|
+
the row existed, and returned to 406 when it was deleted.
|
|
33
|
+
|
|
34
|
+
Two limits, both measured: `adminTemplateEditorInstance` does not list auto templates,
|
|
35
|
+
and their document has no `template-internal` root - `heal()` builds that only for
|
|
36
|
+
templates created through `createManualTemplate`, and committing one directly answers
|
|
37
|
+
HTTP 500. So the row stops the 406 but there is no verified route to putting content
|
|
38
|
+
in it.
|
|
39
|
+
|
|
40
|
+
**`post/<id>` is the only resourceQuery `createManualTemplate` accepts.** The grammar in
|
|
41
|
+
`ResourceQuery::create()` is just `explode('/', $s, 2)`, so anything parses - but the
|
|
42
|
+
only resource type any template path registers is `post`
|
|
43
|
+
(`setResourceType('post')` in PathPostTypePage, PathPostTypePost and PathPostTypes).
|
|
44
|
+
`path/single-page.php` and `path/index.php` were both tried against the live
|
|
45
|
+
endpoint and both returned HTTP 500. There is no catch-all: a URL with no template
|
|
46
|
+
of its own gets `status_header(406)` and an empty body.
|
|
47
|
+
|
|
48
|
+
**Manual assignment is a REST call, not a commit.** `POST /templateAssign/createManualTemplate`
|
|
49
|
+
with `resourceQuery=post/<postID>` and `masterID=<masterID>` creates the template row
|
|
50
|
+
*and* the assign row in one step, deriving the path and name from the post. The eight
|
|
51
|
+
pages built by `tools/build_site.py` are all bound this way.
|
|
52
|
+
|
|
53
|
+
One consequence worth knowing: **that endpoint creates a new template every time.**
|
|
54
|
+
Calling it twice for the same post leaves two template rows and one assign row —
|
|
55
|
+
`tools/build_all.py` resets the theme before rebuilding for exactly this reason.
|
|
56
|
+
|
|
57
|
+
## The condition structure
|
|
58
|
+
|
|
59
|
+
The same shape drives template routing, element visibility (`node.data.conditions`),
|
|
60
|
+
interaction gating and form actions — four contexts, one grammar.
|
|
61
|
+
|
|
62
|
+
```jsonc
|
|
63
|
+
"conditions": [ // groups are OR-ed
|
|
64
|
+
{
|
|
65
|
+
"uuid": "<uuid>",
|
|
66
|
+
"evaluationUnits": [ // units within a group are AND-ed
|
|
67
|
+
{
|
|
68
|
+
"uuid": "<uuid>",
|
|
69
|
+
"type": "serverCondition",
|
|
70
|
+
"serverConditionOptions": {
|
|
71
|
+
"subject": {"type": "httpGet",
|
|
72
|
+
"httpGetOptions": {"settings": {"name": "show"}}},
|
|
73
|
+
"comparator": {"type": "text",
|
|
74
|
+
"textOptions": {"operator": "equals",
|
|
75
|
+
"settings": {"value": "yes"}}}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The `<id>Options` suffix is the same convention as everywhere else in Mosaic
|
|
84
|
+
(`RuleTypeAbstract::getOptionsDataName()` returns `getID() . 'Options'`).
|
|
85
|
+
|
|
86
|
+
**An empty or absent `conditions` matches.** `matchConditions()` returns `true` when
|
|
87
|
+
the evaluation yields null, so a template with no conditions always applies and an
|
|
88
|
+
element with none always renders. Conditions subtract, they never add.
|
|
89
|
+
|
|
90
|
+
## What you can test
|
|
91
|
+
|
|
92
|
+
Two tables, both read off the running site rather than the source:
|
|
93
|
+
|
|
94
|
+
- `data/condition-subjects.csv` — 59 subjects across four contexts (`element`,
|
|
95
|
+
`template`, `interaction`, `formAction`), each with its `settingsFields` and the
|
|
96
|
+
group it belongs to (`editor`, `http`, `currentUser`, `post`, `product`, `page`,
|
|
97
|
+
`attachment`).
|
|
98
|
+
- `data/condition-comparators.csv` — 12 comparator rows with their operator sets:
|
|
99
|
+
|
|
100
|
+
| comparator | operators |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `text` | equals, not-equals, contains, not-contains, starts-with, regexp |
|
|
103
|
+
| `number` | equals, not-equals, greater-than, greater-than-or-equal, less-than, less-than-or-equal |
|
|
104
|
+
| `date` | after, before |
|
|
105
|
+
|
|
106
|
+
`date` exists only in `serverCondition`. The `interaction` context additionally has a
|
|
107
|
+
`browserCondition` rule type whose `text` comparator drops `regexp` — browser-side
|
|
108
|
+
evaluation is a reduced subset, so a rule that works in a template will not
|
|
109
|
+
necessarily work as an interaction gate.
|
|
110
|
+
|
|
111
|
+
Most subject settings fields carry `supportDynamic: true`, meaning the value itself
|
|
112
|
+
can be an `@` expression rather than a literal — see `references/dynamic-content.md`.
|
|
113
|
+
|
|
114
|
+
## Verification status
|
|
115
|
+
|
|
116
|
+
The routing rules and the condition grammar above are read from source and from the
|
|
117
|
+
live metas endpoints. **Committing a condition has not been driven end to end** — the
|
|
118
|
+
property sweep's naive string probe on `conditions` returns HTTP 500, which is
|
|
119
|
+
consistent with the nested shape being required but is not proof the shape above is
|
|
120
|
+
accepted. Build one, then read the row back and diff it against what you sent; on this
|
|
121
|
+
data model that diff is the only error message you get
|
|
122
|
+
(`references/interactions.md` explains why).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Mosaic vs Elementor vs Gutenberg, as data models
|
|
2
|
+
|
|
3
|
+
If you already know `elementor-headless` or `gutenberg-headless`, this is the page
|
|
4
|
+
that tells you which of your habits transfer and which will actively mislead you.
|
|
5
|
+
|
|
6
|
+
## The one-line difference
|
|
7
|
+
|
|
8
|
+
| | Elementor | Gutenberg | Mosaic |
|
|
9
|
+
|---|---|---|---|
|
|
10
|
+
| Where the page lives | `wp_postmeta._elementor_data`, one nested JSON blob | `wp_posts.post_content`, serialized HTML comments | **23 own tables**, one row per element in `wp_mosaic_nodes` |
|
|
11
|
+
| Unit of structure | widget in a nested array | block in a comment-delimited string | node row with a `parentID` |
|
|
12
|
+
| Tree encoded by | JSON nesting | comment nesting in a string | a foreign key |
|
|
13
|
+
| Ordering | array index | document order in the string | **fractional-index string** in `ordering` |
|
|
14
|
+
| Scope | the post | the post | the **theme** (`themeID` on every row) |
|
|
15
|
+
| Written by | overwrite the whole meta blob | overwrite `post_content` | checkout → commit, **per row, revision-checked** |
|
|
16
|
+
| Silent-failure mode | unknown control stored and ignored | comment JSON and saved HTML disagree | validator rejection returned as HTTP 200 with an `exceptions` body |
|
|
17
|
+
|
|
18
|
+
## What transfers
|
|
19
|
+
|
|
20
|
+
**The core discipline transfers exactly.** All three builders will accept a payload
|
|
21
|
+
they do not understand and give you a page that looks 90% right. In all three, the
|
|
22
|
+
rule is the same: never write a property name, enum value, or Free/Pro claim from
|
|
23
|
+
memory — look it up in `data/`.
|
|
24
|
+
|
|
25
|
+
**The Free/Pro split is a real axis in both Elementor and Mosaic.** In Mosaic it is
|
|
26
|
+
cleaner: 48 of 122 node types live under `NodeTypes/Pro/` and simply have no factory
|
|
27
|
+
on a Free install (`edition` column in `data/node-types.csv`). There is no
|
|
28
|
+
"partially available" state to reason about the way there is with Elementor Pro
|
|
29
|
+
controls on Free widgets.
|
|
30
|
+
|
|
31
|
+
**Dynamic values exist in all three**, under three names. Elementor calls them
|
|
32
|
+
dynamic tags; Gutenberg has bindings; Mosaic has **evaluator functions** — 19 of
|
|
33
|
+
them (`abs`, `attachment`, `avg`, `calc`, `concat`, `date`, `esc_attr`, `esc_html`,
|
|
34
|
+
`excerpt`, `fallback`, `find_image`, `find_link`, `find_video`, `json_encode`,
|
|
35
|
+
`remove_html`, `round`, `round_up`, `substr`, `sum`) plus 8 dynamic sources, in
|
|
36
|
+
`data/pluggables.csv`. A property accepts one wherever its validator chain includes
|
|
37
|
+
`ValidatorDynamicCodeObject`.
|
|
38
|
+
|
|
39
|
+
## What will mislead you
|
|
40
|
+
|
|
41
|
+
**"Edit the page's JSON."** There is no page JSON. The closest thing to
|
|
42
|
+
`_elementor_data` is a *set of rows*, and the tree is a `parentID` column. Reading a
|
|
43
|
+
Mosaic document means a query, not a `json_decode`.
|
|
44
|
+
|
|
45
|
+
**"Reorder by array index."** `ordering` is a fractional-index *string*. Sorting it
|
|
46
|
+
as a number scrambles the page; renumbering siblings the way you would an array is
|
|
47
|
+
both unnecessary and destructive of other clients' inserts.
|
|
48
|
+
|
|
49
|
+
**"The post is the unit."** In Elementor and Gutenberg, one post = one page's data.
|
|
50
|
+
In Mosaic a post is a *target*: templates are assigned to types via
|
|
51
|
+
`mosaic_template_assigns`, and the same template serves many posts. Editing "the
|
|
52
|
+
page" usually means editing a template, and the blast radius is every post that
|
|
53
|
+
template is assigned to. Check `mosaic_template_assigns` before you edit.
|
|
54
|
+
|
|
55
|
+
**"Style the element."** Elementor puts controls on the widget. Mosaic puts styling
|
|
56
|
+
in a **class system** (151 built-in element classes) layered over a **token system**
|
|
57
|
+
(collections → modes/skins → variables). Writing styles onto individual nodes works
|
|
58
|
+
and is almost always wrong; it opts that node out of both layers.
|
|
59
|
+
|
|
60
|
+
**"Breakpoints are a site setting."** In Mosaic they are theme-scoped rows in
|
|
61
|
+
`mosaic_breakpoints`. Two themes on one site can disagree about what "tablet" means.
|
|
62
|
+
|
|
63
|
+
**"Overwrite and move on."** Elementor and Gutenberg tolerate blind overwrites.
|
|
64
|
+
Mosaic checks the `revision` of every row you claim; a commit built on a stale
|
|
65
|
+
revision is rejected, and the rejection arrives as HTTP 200. See
|
|
66
|
+
`references/write-protocol.md`.
|
|
67
|
+
|
|
68
|
+
## Which skill to reach for
|
|
69
|
+
|
|
70
|
+
- Post content authored as blocks, on a normal WP theme → `gutenberg-headless`.
|
|
71
|
+
- An Elementor site, page-level construction → `elementor-headless`.
|
|
72
|
+
- A Mosaic site → here. The write path and all 74 free node types are measured; the
|
|
73
|
+
48 Pro-only types are extracted from source but not render-verified.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Writing to Mosaic: the checkout / check / commit protocol
|
|
2
|
+
|
|
3
|
+
*Verified end to end against a live install: a master, a template and a text node were
|
|
4
|
+
created through these routes and the result asserted in the delivered HTML. See
|
|
5
|
+
`references/failure-modes.md` for what goes wrong and how it surfaces.*
|
|
6
|
+
|
|
7
|
+
Mosaic has no "save the page" endpoint. Every document you can edit — theme,
|
|
8
|
+
master, template, style guide, component, component node — is reached through an
|
|
9
|
+
**editor instance**, and every editor instance exposes the same four routes:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
GET /mosaic/v<version>/theme/<themeID>/<instance> open (read state)
|
|
13
|
+
POST /mosaic/v<version>/theme/<themeID>/<instance>/checkout claim (take the lock)
|
|
14
|
+
POST /mosaic/v<version>/theme/<themeID>/<instance>/check poll (still in sync?)
|
|
15
|
+
POST /mosaic/v<version>/theme/<themeID>/<instance>/commit write
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
All 114 routes are in `data/rest-routes.csv`; the namespace is
|
|
19
|
+
`/wp-json/mosaic/v<plugin version>` — **the plugin version is in the namespace**, so
|
|
20
|
+
the base URL changes on every Mosaic update. Read it from `mosaicOptions.rest_api_url`
|
|
21
|
+
on any Mosaic admin page rather than composing it yourself.
|
|
22
|
+
|
|
23
|
+
## The envelopes
|
|
24
|
+
|
|
25
|
+
Both `checkout` and `check` take one required string parameter, `syncCheckEnvelopes`
|
|
26
|
+
— a JSON string, not a JSON body — shaped as manager type → list of `[ID, revision]`
|
|
27
|
+
pairs:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{"node": [["<nodeID>", "<revision>"], ...], "elementClass": [[...]]}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The `revision` is the value from the row's `revision` column when you read it. The
|
|
34
|
+
server compares them and returns `syncResponseEnvelopes` for anything that moved
|
|
35
|
+
underneath you. This is **optimistic locking, per row** — not a page-level lock — so
|
|
36
|
+
two callers editing different nodes of the same document do not conflict.
|
|
37
|
+
|
|
38
|
+
`commit` takes `syncCheckEnvelopes` *and* `revisionEnvelopes` (also a JSON string).
|
|
39
|
+
The server runs `doCommit(revisionEnvelopes)` → `pushToDB()` → and then immediately
|
|
40
|
+
does a fresh `doCheckout` so the response hands you the new revisions to use for
|
|
41
|
+
your next write. **Use them.** Reusing the pre-commit revision on the following
|
|
42
|
+
commit is the single most likely way to get stuck in a sync-rejection loop.
|
|
43
|
+
|
|
44
|
+
## Order of operations
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
1. GET <instance> read current state + revisions
|
|
48
|
+
2. POST <instance>/checkout with the revisions you just read
|
|
49
|
+
3. ...build your changes...
|
|
50
|
+
4. POST <instance>/check optional; cheap way to detect drift before committing
|
|
51
|
+
5. POST <instance>/commit with syncCheckEnvelopes + revisionEnvelopes
|
|
52
|
+
6. take the revisions from the response, go to 3
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`isCheckoutAllowed()` / `isCheckAllowed()` / `isCommitAllowed()` gate each step and
|
|
56
|
+
throw a bare `Not allowed!` when they fail — there is no useful error body, so if a
|
|
57
|
+
commit throws, check the lock (`mosaic_locks`) and the capability of the user whose
|
|
58
|
+
cookie you are sending before suspecting your payload.
|
|
59
|
+
|
|
60
|
+
## Authentication
|
|
61
|
+
|
|
62
|
+
These are `wp-json` routes behind a nonce, not application passwords:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Cookie: the logged-in admin cookie
|
|
66
|
+
X-WP-Nonce: mosaicOptions.common.nonces.wp_rest
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Both are on the Mosaic admin page (`admin.php?page=mosaic`) in the `var mosaicOptions`
|
|
70
|
+
blob. That blob is also where you find `mosaicEdition` (`pro` / free),
|
|
71
|
+
`mosaicVersion`, `mosaicDataVersion`, `license.isLicenseActive`, `siteHash`, and the
|
|
72
|
+
`availablePostTypes` map — read it once at the start of a session instead of probing.
|
|
73
|
+
|
|
74
|
+
## What errors look like
|
|
75
|
+
|
|
76
|
+
A failed validator does not return an HTTP error. The response is a
|
|
77
|
+
`RESTJSONExceptionEnvelope`: HTTP 200 with an `exceptions` array in the body. Code
|
|
78
|
+
that only checks the status code will report a clean save on a rejected write.
|
|
79
|
+
Always read `exceptions` before believing a commit landed.
|