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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +94 -0
  3. package/README.md +133 -0
  4. package/README.zh-CN.md +87 -0
  5. package/README.zh-TW.md +87 -0
  6. package/SKILL.md +353 -0
  7. package/assets/templates/platforms/claude-ai.json +34 -0
  8. package/assets/templates/platforms/claude-code.json +28 -0
  9. package/assets/templates/platforms/codex-cli.json +33 -0
  10. package/assets/templates/platforms/continue.json +31 -0
  11. package/assets/templates/platforms/copilot.json +46 -0
  12. package/assets/templates/platforms/cursor.json +36 -0
  13. package/assets/templates/platforms/gemini-cli.json +33 -0
  14. package/assets/templates/platforms/windsurf.json +34 -0
  15. package/bin/check-release.mjs +143 -0
  16. package/bin/install.mjs +452 -0
  17. package/bin/sync-version.mjs +63 -0
  18. package/data/animatable-properties.csv +23 -0
  19. package/data/condition-comparators.csv +13 -0
  20. package/data/condition-subjects.csv +60 -0
  21. package/data/db-columns.csv +207 -0
  22. package/data/default-children.csv +11 -0
  23. package/data/dynamic-variables.csv +75 -0
  24. package/data/element-classes.csv +152 -0
  25. package/data/evaluator-functions.csv +20 -0
  26. package/data/interaction-types.csv +13 -0
  27. package/data/node-properties.csv +182 -0
  28. package/data/node-property-verification.csv +182 -0
  29. package/data/node-types.csv +123 -0
  30. package/data/node-verification.csv +123 -0
  31. package/data/placement-rules.csv +123 -0
  32. package/data/pluggables.csv +208 -0
  33. package/data/property-verification.csv +171 -0
  34. package/data/rest-routes.csv +115 -0
  35. package/data/rwd-verification.csv +570 -0
  36. package/data/style-properties.csv +99 -0
  37. package/data/style-states.csv +54 -0
  38. package/data/style-value-shapes.csv +23 -0
  39. package/data/style-verification.csv +99 -0
  40. package/package.json +59 -0
  41. package/references/data-model.md +95 -0
  42. package/references/design-system.md +118 -0
  43. package/references/dynamic-content.md +113 -0
  44. package/references/failure-modes.md +182 -0
  45. package/references/interactions.md +126 -0
  46. package/references/placement.md +117 -0
  47. package/references/responsive.md +174 -0
  48. package/references/styling.md +172 -0
  49. package/references/templates-and-conditions.md +122 -0
  50. package/references/vs-elementor-gutenberg.md +73 -0
  51. package/references/write-protocol.md +79 -0
  52. package/sites/_moksa.py +1165 -0
  53. package/sites/moksa.json +8685 -0
  54. package/tools/bootstrap_probe_theme.php +68 -0
  55. package/tools/build_all.py +55 -0
  56. package/tools/build_page.py +352 -0
  57. package/tools/build_report.py +221 -0
  58. package/tools/build_site.py +174 -0
  59. package/tools/capture_live.py +130 -0
  60. package/tools/check_placement_predicts.py +72 -0
  61. package/tools/copy_styles.py +204 -0
  62. package/tools/extract_default_children.py +94 -0
  63. package/tools/extract_dynamic_variables.py +104 -0
  64. package/tools/extract_interactions.py +98 -0
  65. package/tools/extract_node_types.py +165 -0
  66. package/tools/extract_placement.py +135 -0
  67. package/tools/extract_pluggables.py +90 -0
  68. package/tools/extract_style_properties.py +163 -0
  69. package/tools/mint_session.php +52 -0
  70. package/tools/probe.py +144 -0
  71. package/tools/sweep_node_properties.py +271 -0
  72. package/tools/sweep_node_types.py +318 -0
  73. package/tools/sweep_properties.py +215 -0
  74. package/tools/sweep_style_properties.py +254 -0
  75. package/tools/theme_export.php +91 -0
  76. package/tools/theme_import.php +113 -0
  77. 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.