mosaic-headless 1.22.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -4,7 +4,7 @@ description: |
4
4
  Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface with `mo.py`, which joins every source table to the live sweeps so a lookup leads with the measured verdict rather than the declaration (126 node types, 191 properties, 98 style properties with 20 structured value shapes pinned down, 53 style states, 156 variants, 74 dynamic variables, 15 interaction triggers, 115 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, the design-token and element-class layers verified against compiled CSS, the @VAR() dynamic language verified against rendered output, nine designed pages built through the tables themselves, and the delivered pages re-read in Chromium at three viewports so a rule that is present, correct and still wrong cannot pass. Drives Mosaic's own theme export/import and its plugin data upgrade from outside the editor and holds the copy against the source tree for tree. Measured on Mosaic Pro 1.0.9: the modal (`<dialog>`), OpenStreetMap, the exit-intent and scroll-depth triggers, run rules and the memory actions all built on a live page and read back, and the 1.0.7 -> 1.0.8 -> 1.0.9 migrations driven from outside wp-admin.
5
5
  license: "MIT"
6
6
  author: "moksa (https://moksaweb.com)"
7
- version: "1.22.0"
7
+ version: "1.23.0"
8
8
  ---
9
9
 
10
10
  # Headless Mosaic
@@ -533,6 +533,7 @@ so the pattern is in the data, not just in this paragraph.
533
533
  | `data/navigation-verification.csv` | 46 | **fetched and opened live** - every menu item is a real `<a href>`, every page of the nineteen answers 200, and no page scrolls sideways at 390 / 768 / 1280 |
534
534
  | `data/page-modal-verification.csv` | 19 | **read live** - one modal per page, and what each one's trigger, run rule, memory scope and dismissal policy actually came out as |
535
535
  | `data/dialog-verification.csv` | 19 | **driven live** - the 1.0.9 modal, its triggers, its run rules and the memory actions, each exercised in a real browser |
536
+ | `data/dialog-form-verification.csv` | 49 | **driven live** - the same three nodes made into five FORMS (sheet, drawer, corner, takeover, gate), each re-opened at 390px and 768px; also where `pickOne` on a click turned out to be a latch rather than a lottery |
536
537
  | `data/custom-fields-verification.csv` | 62 | **rendered live** - ACF and Meta Box fields of every common type read back through `@VAR` / `@LOOP` off the delivered page, loops included |
537
538
  | `data/theme-zip-verification.csv` | 22 | **round-tripped live** - Mosaic's own ZIP export imported in test mode and compared to its source, table by table and tree by tree |
538
539
  | `data/node-type-notes.csv` | 8 | where a sweep outcome is true but misleading on its own, why. Surfaced by `mo.py type` |
@@ -807,6 +808,7 @@ post — `build_all.py` resets first for that reason.
807
808
  | `verify_slider.py` | the slider in a real browser: does it advance, do the arrows and bullets drive it, does the keyboard, does it stop when motion is not wanted |
808
809
  | `verify_navigation.py` | a multi-page site: is every menu item a real `<a href>`, does every page answer 200 (a template that was never bound answers 406 with an empty body, which only a logged-out visitor sees), and with `--viewports` does any page scroll SIDEWAYS - the one question no per-page checker asks |
809
810
  | `verify_dialog.py` | the 1.0.9 modal in a real browser: does it open, close, answer Esc, remember, cap - and does an OpenStreetMap carry the coordinates it was given |
811
+ | `verify_dialog_lab.py` | the question after "does the modal work": can three nodes be made into a bottom sheet, a side drawer, a corner notice, a full takeover and a gate with no way out - and does each one still fit at phone width, which the page's own width check cannot see |
810
812
  | `list_fields.php` | every `@VAR` / `@LOOP` name Mosaic registers for one post - custom fields, their derived `__label` / `__url` / `__id` properties, the row variables of each loop - with the value each resolves to (`wp eval-file`) |
811
813
  | `data_upgrade.py` | after a plugin update, run Mosaic's data migration over its own milestone route - the step wp-admin does from a screen - and set the config's version when the editor API is back |
812
814
  | `copy_styles.py` | push one node's style onto others, by attrID or prefix |
@@ -20,7 +20,7 @@
20
20
  "description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
21
21
  "license": "MIT",
22
22
  "author": "moksa (https://moksaweb.com)",
23
- "version": "1.22.0"
23
+ "version": "1.23.0"
24
24
  },
25
25
  "loaderBehaviour": "Upload via Settings -> Skills -> Upload. Claude.ai parses SKILL.md frontmatter and surfaces the skill in your library. The extraction tool (extract-block-schema.php) needs a live WP-CLI connection and won't run in the sandbox; use it from a local terminal against your own site instead.",
26
26
  "uploadSteps": [
@@ -20,7 +20,7 @@
20
20
  "description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
21
21
  "license": "MIT",
22
22
  "author": "moksa (https://moksaweb.com)",
23
- "version": "1.22.0"
23
+ "version": "1.23.0"
24
24
  },
25
25
  "loaderBehaviour": "Auto-loads on session start when SKILL.md frontmatter parses successfully.",
26
26
  "verified": true,
@@ -20,7 +20,7 @@
20
20
  "description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
21
21
  "license": "MIT",
22
22
  "author": "moksa (https://moksaweb.com)",
23
- "version": "1.22.0"
23
+ "version": "1.23.0"
24
24
  },
25
25
  "loaderBehaviour": "Confirmed (2026-07-11): Codex CLI natively supports the SKILL.md spec. Place SKILL.md under .codex/skills/<name>/ (project) or ~/.codex/skills/<name>/ (personal) and Codex loads the name+description at session start, then the full body on demand. A parallel, broader convention .agents/skills/ (searched from cwd up to repo root, then ~/.agents/skills/) also exists across multiple tools - if your Codex CLI version prioritizes that path instead, mirror the same SKILL.md there.",
26
26
  "verified": true,
@@ -20,7 +20,7 @@
20
20
  "description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
21
21
  "license": "MIT",
22
22
  "author": "moksa (https://moksaweb.com)",
23
- "version": "1.22.0"
23
+ "version": "1.23.0"
24
24
  },
25
25
  "loaderBehaviour": "CHANGED as of 2026-07-11: GitHub Copilot added a proper '.github/skills/' Agent Skills directory (December 2025), alongside the older single-file .github/copilot-instructions.md convention. This config targets the new skills-directory form. If your Copilot version predates this (pre Dec 2025), use the instructions-append fallback instead (see fallback below).",
26
26
  "fallback": {
@@ -20,7 +20,7 @@
20
20
  "description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
21
21
  "license": "MIT",
22
22
  "author": "moksa (https://moksaweb.com)",
23
- "version": "1.22.0"
23
+ "version": "1.23.0"
24
24
  },
25
25
  "loaderBehaviour": "CHANGED as of 2026-07-11: Gemini CLI now natively supports the same SKILL.md standard as Claude Code and Codex CLI - the same directory-based skill works unmodified. Gemini CLI discovers skills in this precedence order: built-in, extension skills, ~/.gemini/skills/ (personal), .gemini/skills/ (project, shared via version control). At session start Gemini injects each discovered skill's name+description into the system prompt and calls activate_skill when a task matches.",
26
26
  "verified": true,
@@ -0,0 +1,50 @@
1
+ check,result,detail
2
+ HOST/centre,PASS,"['dialog', 'everything']"
3
+ HOST/sheet,PASS,"['dialog', 'clickOutside']"
4
+ HOST/drawer,PASS,"['dialog', 'esc']"
5
+ HOST/toast,PASS,"['dialog', 'everything']"
6
+ HOST/takeover,PASS,"['dialog', 'everything']"
7
+ HOST/gate,PASS,"['dialog', 'nothing']"
8
+ HOST/exit,PASS,"['dialog', 'everything']"
9
+ HOST/depth,PASS,"['dialog', 'clickOutside']"
10
+ HOST/aba,PASS,"['dialog', 'everything']"
11
+ HOST/abb,PASS,"['dialog', 'everything']"
12
+ TOAST/pageLoad,PASS,opened on load
13
+ TOAST/overlay-synthesised,PASS,authored none; heal inserted one
14
+ TOAST/aria-modal-unavoidable,PASS,aria-modal='true' despite no authored overlay
15
+ TOAST/top-right,PASS,x=911 y=114 w=330 vw=1280
16
+ TOAST/wheel-blocked,PASS,"a corner toast is a LAYOUT, not a non-blocking modal"
17
+ CLOSED_IS_HIDDEN,PASS,every closed modal is display:none
18
+ CENTRE/centred,PASS,x=385 w=520 vw=1280
19
+ CENTRE/aria-modal,PASS,aria-modal='true' (overlay=True)
20
+ CENTRE/esc-closes,PASS,closedby=everything
21
+ SHEET/at-bottom,PASS,y+h=900 vh=900
22
+ SHEET/wide,PASS,w=720
23
+ SHEET/esc-does-nothing,PASS,"closedby=clickOutside, so Esc must not close it"
24
+ DRAWER/right-edge,PASS,x+w=1265 layer right=1265
25
+ DRAWER/full-height,PASS,h=900 vh=900
26
+ DRAWER/esc-closes,PASS,closedby=esc
27
+ TAKEOVER/fills-viewport,PASS,1265x900 vs layer 1265x900
28
+ GATE/esc-refused,PASS,closedby=nothing
29
+ GATE/outside-refused,PASS,clicking the overlay must not close it
30
+ GATE/button-closes,PASS,the modalClose inside is the only exit
31
+ DEPTH/shut-at-rest,PASS,
32
+ DEPTH/opens-past-75,PASS,scrollDepth threshold 75%
33
+ EXIT/opens,PASS,exitIntent is desktop-only and synthetic leave is approximate
34
+ LATCH/record-abb,PASS,"{""counter"":1,""firstAt"":1791091416059,""lastAt"":1791091416059,""lastID"":""2c17cec6-3"
35
+ LATCH/B-first-wins,PASS,"6/6 as predicted, 0 lost clicks {'abb>abb,aba>none': 6}"
36
+ LATCH/record-aba,PASS,"{""counter"":1,""firstAt"":1791091466323,""lastAt"":1791091466323,""lastID"":""031d462e-7"
37
+ LATCH/A-first-wins,PASS,"6/6 as predicted, 0 lost clicks {'aba>aba,abb>none': 6}"
38
+ LATCH/order-decides,PASS,"the winner flips with click order, so it is the first EVALUATED, not the first authored"
39
+ FIT390/centre,PASS,scrollW=349/349 childOverflow=0 pageScrollsSideways=False
40
+ FIT390/sheet,PASS,scrollW=373/373 childOverflow=0 pageScrollsSideways=False
41
+ FIT390/drawer,PASS,scrollW=350/350 childOverflow=0 pageScrollsSideways=False
42
+ FIT390/takeover,PASS,scrollW=375/375 childOverflow=0 pageScrollsSideways=False
43
+ FIT390/gate,PASS,scrollW=349/349 childOverflow=0 pageScrollsSideways=False
44
+ FIT390/aba,PASS,scrollW=349/349 childOverflow=0 pageScrollsSideways=False
45
+ FIT768/centre,PASS,scrollW=518/518 childOverflow=0 pageScrollsSideways=False
46
+ FIT768/sheet,PASS,scrollW=718/718 childOverflow=0 pageScrollsSideways=False
47
+ FIT768/drawer,PASS,scrollW=399/399 childOverflow=0 pageScrollsSideways=False
48
+ FIT768/takeover,PASS,scrollW=753/753 childOverflow=0 pageScrollsSideways=False
49
+ FIT768/gate,PASS,scrollW=458/458 childOverflow=0 pageScrollsSideways=False
50
+ FIT768/aba,PASS,scrollW=438/438 childOverflow=0 pageScrollsSideways=False
@@ -4,6 +4,10 @@ pickOne,"8 reloads of one tab, remember=page",PASS,re-drawn on every load: 6/1/1
4
4
  pickOne,"8 reloads of one tab, remember=forever",PASS,the same contender all 8 times
5
5
  pickOne,"6 fresh browser contexts, remember=forever",PASS,draws again in a new context: 1/3/2 - the record is per browser profile
6
6
  pickOne,storage record,PASS,"mos:v1:r:<name> gains lastID, naming the contender the draw chose"
7
+ pickOne,"two buttons, group dl-ab, click B then A",PASS,"B opens, A then does nothing - 6/6 fresh visitors"
8
+ pickOne,"same two buttons, click A then B",PASS,"A opens, B then does nothing - 6/6; the winner flips with the ORDER"
9
+ pickOne,both interactions on ONE button,PASS,the same contender 14/14 - one evaluates first and records itself
10
+ pickOne,what the rule actually does,PASS,"draws among contenders that evaluate TOGETHER; a click delivers one at a time, so click+pickOne is a latch, not a lottery"
7
11
  targetModifiers,plain element,PASS,"[{""type"":""all"",""selector"":""._e""}]"
8
12
  targetModifiers,plain variant,PASS,"[{""type"":""all"",""selector"":""h2,.m-heading-2""}] - a variant resolves to its own selectors"
9
13
  targetModifiers,children(variant),PASS,"appends {""type"":""children"",""selector"":""h2,.m-heading-2""}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mosaic-headless",
3
- "version": "1.22.0",
3
+ "version": "1.23.0",
4
4
  "description": "AI-agent skill: build Mosaic Pro (Nextend) WordPress sites by writing the underlying data model directly - 23 custom tables, no visual editor, no DOM. Every node type, style property and node property swept against a live install and asserted on the delivered HTML and compiled CSS. Installs into Claude Code, Cursor, Codex CLI, Gemini CLI, Copilot, Continue, Windsurf and Claude.ai.",
5
5
  "keywords": [
6
6
  "wordpress",
@@ -33,9 +33,23 @@ delivers
33
33
  </dialog>
34
34
  ```
35
35
 
36
- - **`aria-modal="true"` is emergent, not authored.** It appears exactly when the
37
- modal has an overlay child, because the overlay is what actually blocks the page.
38
- No overlay, no `aria-modal`.
36
+ - **`aria-modal="true"` is emergent, not authored — and you cannot shed it.**
37
+ The render sets it exactly when the modal has an overlay child, because the
38
+ overlay is what actually blocks the page. An earlier version of this page drew
39
+ the obvious conclusion — *no overlay, no `aria-modal`* — and that conclusion is
40
+ **wrong**, because the overlay-less modal is unreachable. A modal committed with
41
+ no `modal-overlay` child comes back from the server with one: `onHeal()` inserts
42
+ it at the end of the child order, and the source says why — *"the overlay is a
43
+ required structural part — non-deletable, one per Modal"*. The synthetic one
44
+ carries no style token and no id, which is the only way to tell it from an
45
+ authored one.
46
+
47
+ Measured on a modal authored with no overlay: the delivered HTML contains
48
+ `<div class="m-modal-overlay" tabindex="-1">`, the host carries
49
+ `aria-modal="true"`, and a real wheel event over the page moves it 0px while the
50
+ same page scrolls 700px with nothing open. **There is no non-blocking modal in
51
+ 1.0.9.** A corner notice that must not trap the reader has to be built from
52
+ ordinary nodes, not from `modal`.
39
53
  - **`closedby` is `everything` | `clickOutside` | `esc` | `nothing`**, and it is
40
54
  emitted as `data-mosaic-modal-closedby`, NOT as the native `closedby` attribute —
41
55
  Mosaic opens the host with `show()`, where the native light-dismiss does not
@@ -146,6 +160,36 @@ back in a test without inspecting which modal is open.
146
160
 
147
161
  `data/interaction-rules-verification.csv` has the runs.
148
162
 
163
+ ### …and what `pickOne` does on a trigger that is NOT `pageLoad`
164
+
165
+ Run rules apply to any `timed` interaction, and `click` is one, so the obvious
166
+ next question is whether a click-driven `pickOne` is also a lottery. It is not,
167
+ and the difference matters enough to state as a rule:
168
+
169
+ > **`pickOne` draws among the contenders that evaluate TOGETHER.**
170
+
171
+ `pageLoad` schedules every member of the group in the same pass, so the draw has
172
+ a real field and a real winner. A click delivers one contender at a time: the
173
+ first one to evaluate records itself and every other member of the group is then
174
+ suppressed for as long as `remember` holds.
175
+
176
+ Measured with two buttons, one modal each, both carrying the group `dl-ab`, six
177
+ fresh visitors each way:
178
+
179
+ | order clicked | result |
180
+ |---|---|
181
+ | B then A | B opens, then A does nothing — 6/6 |
182
+ | A then B | A opens, then B does nothing — 6/6 |
183
+
184
+ The winner flips with the click order, so it is the first *evaluated*, not the
185
+ first authored. An earlier attempt with both interactions on ONE button drew the
186
+ same contender 14 times out of 14, which is the same fact seen from a worse angle.
187
+
188
+ So click + `pickOne` is a **latch**, not an A/B test: *whichever of these the
189
+ visitor takes first, the others stop offering.* That is genuinely useful — one of
190
+ several calls to action, a first-choice-wins offer — but if you want to split an
191
+ audience, the rule has to sit on `pageLoad`.
192
+
149
193
  ## Memory: remember / forget
150
194
 
151
195
  Two actions that write and clear the same records a run rule reads, so one
@@ -330,3 +374,49 @@ The pattern is worth stating on its own: because the trigger lives in the modal'
330
374
  own `interactionShorthand`, a page's popup policy is one object in that page's data.
331
375
  Nothing else on the page changes, no trigger element is placed, and two pages of the
332
376
  same site can behave completely differently without sharing anything.
377
+
378
+ ## Five forms out of three nodes — and the one property you must not set
379
+
380
+ "A modal" in most builders means one centred card, because the builder owns the
381
+ chrome. Mosaic owns none of it: `.m-modal` is a `position:fixed; inset:0` layer
382
+ with `pointer-events:none`, `.m-modal-overlay` is a second fixed layer that takes
383
+ pointer events back, and `.m-modal-window` is an ordinary child of the first. The
384
+ form is decided by where the window is pinned inside that layer.
385
+
386
+ **Never set `display` on the `modal` host.** A `<dialog>`'s open/closed visibility
387
+ *is* its display property — the UA stylesheet hides it with
388
+ `dialog:not([open]) { display: none }` — so an author-level `display:flex` on the
389
+ host wins the cascade and every modal on the page is permanently on screen. It is
390
+ a tempting thing to write, because flex centring is the obvious way to place the
391
+ window, and it fails in a way that no open-state check can see: a page built this
392
+ way passed 36 of 36 modal checks while all ten of its dialogs sat open in the
393
+ document. Lay the forms out by positioning the WINDOW instead, which leaves
394
+ `display` alone:
395
+
396
+ | form | host | window (`position:absolute`) |
397
+ |---|---|---|
398
+ | centred card | padding only | `top:50%; left:50%` + `translateX(-50%) translateY(-50%)`, `max-width` |
399
+ | bottom sheet | no side/bottom padding | `left:0; right:0; bottom:0`, `margin-inline:auto`, `max-width` |
400
+ | side drawer | no top/bottom/right padding | `top:0; right:0; bottom:0`, `max-width` |
401
+ | corner notice | no padding | `top:90px; right:24px`, a small `max-width` |
402
+ | full takeover | no padding | `top:0; right:0; bottom:0; left:0`, `max-width:none` |
403
+
404
+ `transform` is a structured value, so the centring pull-back is two entries
405
+ (`translateX`, `translateY`), not a string — `references/styling.md` has the shape.
406
+
407
+ All five were built on one page and driven in a browser — 49 checks in
408
+ `data/dialog-form-verification.csv`, written by `tools/verify_dialog_lab.py`,
409
+ whose first assertion is now that a CLOSED modal has no box at all. Three more
410
+ things that only showed up by building them:
411
+
412
+ - **Measure the window against the LAYER, not the viewport.** `.m-modal` is inset
413
+ into the viewport *minus the scrollbar*, so a drawer flush against the right
414
+ edge sits at `innerWidth - 15` and a naive check calls it a 15px gap. Compare
415
+ the window's rect to the host's rect.
416
+ - **Give `modal-window` `overflow: auto` and `max-height: 100%`.** Mosaic has no
417
+ `overflowX`/`overflowY` — only the shorthand — and without it a tall dialog
418
+ grows past the viewport instead of scrolling inside itself.
419
+ - **A page that passes a width check says nothing about its modals.** They are a
420
+ layer that only exists once something opens them, so the document underneath can
421
+ measure clean at 390px while a takeover with desktop padding overflows. The
422
+ verifier re-opens every form at each `--viewports` width for this reason.
@@ -318,3 +318,29 @@ everyone remembers.
318
318
  The general shape, worth carrying beyond this repo: a derived artifact checked into
319
319
  the tree is only as fresh as the last time someone re-derived it, and if the deriving
320
320
  input is not in the tree, no amount of CI will notice.
321
+
322
+ ## Setting `display` on a `<dialog>` pins it open, and open-state checks cannot see it
323
+
324
+ The modal host is a `<dialog>`. Its open/closed visibility is not a class or an
325
+ attribute Mosaic manages — it is the UA stylesheet rule
326
+ `dialog:not([open]) { display: none }`. Any author-level `display` on that element
327
+ wins the cascade, and every modal in the document is then on screen permanently,
328
+ stacked over the page in DOM order.
329
+
330
+ `display:flex` on the host is exactly what you reach for when you want to centre
331
+ the window, so this is not an exotic mistake. What makes it worth its own section
332
+ is that **it survives a thorough test suite**: 36 checks drove the modals' triggers,
333
+ run rules, dismissal policies and geometry, every one of them PASSED, and the page
334
+ they passed against had all ten of its dialogs open at once. Every check asked
335
+ "when this is open, is it right?" — none asked "when it is closed, is it gone?"
336
+
337
+ Two rules come out of it:
338
+
339
+ - **Lay a modal's form out by positioning the window** (`position:absolute` plus
340
+ edges inside the fixed, `inset:0` host), never by making the host a flex or grid
341
+ container.
342
+ - **Assert the closed state.** `tools/verify_dialog_lab.py` now closes everything
343
+ first and fails if any `dialog:not([open])` still has a box. The general form of
344
+ that lesson: for anything with a visible and an invisible state, a suite that
345
+ only exercises the visible one is testing half a component, and the half it
346
+ skips is the one the visitor sees most of the time.
@@ -184,3 +184,32 @@ down — at the cost of bypassing the collection-variable layer entirely.
184
184
 
185
185
  `tools/build_page.py` wraps the five shapes above as `radius`, `shadow`,
186
186
  `transitionAll`, `move` and `border` shorthands so a design spec cannot get them wrong.
187
+
188
+ ## Properties that exist in CSS but not in Mosaic
189
+
190
+ `data/style-properties.csv` is the whole registry, and the fastest way to lose an
191
+ hour is to assume a property is in it because CSS has it. Two that catch people,
192
+ both found by writing a page rather than by reading the table:
193
+
194
+ **`flexGrow` and `flexShrink` do not exist.** Mosaic models both as one
195
+ type-switched group, `flexSizing`:
196
+
197
+ ```jsonc
198
+ "flexSizing": {"type": "grow"} // flex-grow:1; flex-shrink:0
199
+ "flexSizing": {"type": "shrink"}
200
+ "flexSizing": {"type": "none"} // the fixed-width column in a flex row
201
+ "flexSizing": {"type": "custom", "customOptions": {…}}
202
+ ```
203
+
204
+ `none` is what you want for a label column that must not be squeezed, and `grow`
205
+ for the cell that takes the slack — the pair that `flex-shrink:0` / `flex-grow:1`
206
+ would have been.
207
+
208
+ **There is no `overflowX` or `overflowY`** — only the shorthand `overflow`, with
209
+ `visible | hidden | clip | scroll | auto`. One axis at a time is not expressible
210
+ through the style system; it needs `customDeclarations`.
211
+
212
+ Both of these are refused at build time by `build_page.py` rather than silently
213
+ dropped, which is the only reason they cost minutes instead of a debugging session
214
+ — a property that is not in the registry cannot be set at all, and Mosaic's own
215
+ API accepts the write and discards it.
@@ -102,7 +102,8 @@ const counts = {
102
102
  "data/slider-verification.csv": 19,
103
103
  "data/navigation-verification.csv": 46,
104
104
  "data/page-modal-verification.csv": 19,
105
- "data/interaction-rules-verification.csv": 13,
105
+ "data/interaction-rules-verification.csv": 17,
106
+ "data/dialog-form-verification.csv": 49,
106
107
  "data/value-coercion-verification.csv": 12,
107
108
  };
108
109
  for (const [file, expected] of Object.entries(counts)) {
package/sites/_moksa.py CHANGED
@@ -26,6 +26,7 @@ Run from this directory: python _moksa.py
26
26
  """
27
27
  import json
28
28
  import os
29
+ import sys
29
30
  import uuid
30
31
 
31
32
  # Monospace leads. The grotesque is the display face and appears at three sizes only;
@@ -247,7 +248,8 @@ def mask_line(attr, text, tag="h1", **st):
247
248
 
248
249
  # ── content, all of it Moksa Web's own ───────────────────────────────────────
249
250
  NAV = [("服務", "#services"), ("作品", "#works"),
250
- ("產品", "#products"), ("聯絡", "#contact")]
251
+ ("產品", "#products"), ("彈窗", "/dialogs/"),
252
+ ("聯絡", "#contact")]
251
253
 
252
254
  # key / figure / caption, read as a datasheet rather than as four big numbers
253
255
  SPEC = [
@@ -3050,6 +3052,15 @@ def clean(node):
3050
3052
 
3051
3053
  if __name__ == "__main__":
3052
3054
  here = os.path.dirname(os.path.abspath(__file__))
3055
+ sys.path.insert(0, here)
3056
+ # Deferred on purpose. `_dialogs` imports this module's helpers, so importing
3057
+ # it at the top would be circular; down here the file re-imports cleanly as
3058
+ # `_moksa` (this copy is `__main__`), its own main block does not run, and the
3059
+ # helpers it reads are pure functions - two copies produce identical dicts.
3060
+ import _dialogs
3061
+ SITE["pages"].append({"slug": "dialogs", "post_id": 268,
3062
+ "title": "彈窗實驗室",
3063
+ "tree": apply_type(_dialogs.TREE, DISPLAY, CJK, "600")})
3053
3064
  path = os.path.join(here, "moksa.json")
3054
3065
  with open(path, "w", encoding="utf-8") as fh:
3055
3066
  json.dump(clean(SITE), fh, indent=1, ensure_ascii=False)