mosaic-headless 1.30.0 → 1.31.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 (52) hide show
  1. package/README.ja.md +4 -2
  2. package/README.ko.md +4 -2
  3. package/README.md +5 -3
  4. package/README.zh-TW.md +4 -2
  5. package/SKILL.md +83 -39
  6. package/assets/templates/platforms/claude-ai.json +1 -1
  7. package/assets/templates/platforms/claude-code.json +1 -1
  8. package/assets/templates/platforms/codex-cli.json +1 -1
  9. package/assets/templates/platforms/copilot.json +1 -1
  10. package/assets/templates/platforms/gemini-cli.json +1 -1
  11. package/data/browser-verification/moksa.csv +3986 -0
  12. package/data/browser-verification/my-account.csv +148 -0
  13. package/data/component-wall-verification.csv +1 -1
  14. package/data/condition-comparators.csv +2 -2
  15. package/data/content-loop-verification.csv +4 -1
  16. package/data/data-class-hierarchy.csv +1 -0
  17. package/data/dialog-form-verification.csv +2 -2
  18. package/data/dialog-verification.csv +1 -1
  19. package/data/form-surface-verification.csv +3 -0
  20. package/data/interaction-types.csv +1 -0
  21. package/data/intro-verification.csv +16 -16
  22. package/data/mobile-audit.csv +22 -19
  23. package/data/navigation-verification.csv +16 -13
  24. package/data/node-properties.csv +10 -5
  25. package/data/node-types.csv +1 -0
  26. package/data/node-verification.csv +3 -0
  27. package/data/placement-rules.csv +1 -0
  28. package/data/pluggables.csv +16 -4
  29. package/data/popover-verification.csv +38 -0
  30. package/data/style-states.csv +1 -0
  31. package/data/token-verification.csv +2 -2
  32. package/package.json +3 -1
  33. package/references/dialog-and-triggers.md +199 -8
  34. package/references/dynamic-content.md +19 -0
  35. package/references/failure-modes.md +36 -4
  36. package/references/styling.md +9 -0
  37. package/references/upgrading.md +46 -1
  38. package/scripts/check-release.mjs +41 -11
  39. package/sites/_dialogs.py +18 -8
  40. package/sites/_moksa.py +69 -39
  41. package/sites/_popovers.py +570 -0
  42. package/tools/audit_mobile.py +1 -1
  43. package/tools/extract_default_children.py +1 -1
  44. package/tools/extract_node_types.py +1 -1
  45. package/tools/sweep_node_types.py +55 -6
  46. package/tools/verify_browser.py +20 -8
  47. package/tools/verify_conversion.py +6 -2
  48. package/tools/verify_dialog.py +3 -2
  49. package/tools/verify_dialog_lab.py +11 -11
  50. package/tools/verify_intro.py +5 -3
  51. package/tools/verify_popovers.py +348 -0
  52. package/data/browser-verification.csv +0 -4133
@@ -9,8 +9,8 @@ USED/--mk-accent,PASS,"5 element(s), e.g. mk-progress-track, mk-progress, mk-clo
9
9
  USED/--mk-accent-ink,PASS,"2 element(s), e.g. mk-header-cta, tk-chip-mk-accent-ink"
10
10
  USED/--mk-ai,PASS,"1 element(s), e.g. tk-chip-mk-ai"
11
11
  USED/--mk-faint,PASS,"1 element(s), e.g. tk-chip-mk-faint"
12
- USED/--mk-ink,PASS,"76 element(s), e.g. mk-clock-m, mk-clock-m, mk-clock-s"
12
+ USED/--mk-ink,PASS,"66 element(s), e.g. mk-clock-m, mk-clock-m, mk-clock-s"
13
13
  USED/--mk-muted,PASS,"1 element(s), e.g. tk-chip-mk-muted"
14
14
  USED/--mk-panel,PASS,"3 element(s), e.g. tk-sw, tk-chip-mk-panel, tk-props"
15
- USED/--mk-paper,PASS,"7 element(s), e.g. tk-head, tk-chip-mk-paper, tk-pipe"
15
+ USED/--mk-paper,PASS,"9 element(s), e.g. mk-cap-g0-menu, mk-cap-g1-menu, tk-head"
16
16
  USED/--mk-rule,PASS,"1 element(s), e.g. tk-chip-mk-rule"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mosaic-headless",
3
- "version": "1.30.0",
3
+ "version": "1.31.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,6 +33,7 @@
33
33
  "scripts/",
34
34
  "assets/",
35
35
  "data/*.csv",
36
+ "data/browser-verification/*.csv",
36
37
  "references/",
37
38
  "sites/_moksa.py",
38
39
  "sites/_components.py",
@@ -41,6 +42,7 @@
41
42
  "sites/_dialogs.py",
42
43
  "sites/_dynamic.py",
43
44
  "sites/_forms.py",
45
+ "sites/_popovers.py",
44
46
  "sites/_sliders.py",
45
47
  "sites/_tokens.py",
46
48
  "tools/*.py",
@@ -1,12 +1,64 @@
1
- # Modal, OpenStreetMap, the new triggers and run rules (1.0.9), measured
1
+ # Modal, popover, OpenStreetMap, the triggers and run rules (1.0.9, 1.0.10), measured
2
2
 
3
3
  1.0.9 is the first release since this skill began that adds *features* rather than
4
4
  renaming things: four node types, three interaction triggers, a rule engine that
5
5
  decides whether a trigger is allowed to run, two memory actions, and a shorthand
6
6
  that collapses "open this modal when …" into one object.
7
7
 
8
+ 1.0.10 adds the other half: `popover`, a dialog surface that does not block the
9
+ page, and it moves the modal onto the same browser API. Both are measured below -
10
+ the 1.0.10 changes to the modal first, because they overturn three things this page
11
+ used to say, then the popover in full.
12
+
8
13
  Everything below was built on the live site and read back out of the delivered page
9
- or out of the browser. `data/dialog-verification.csv`, 19 checks, all passing.
14
+ or out of the browser. `data/dialog-verification.csv`, 19 checks, and
15
+ `data/popover-verification.csv`, 37 checks, all passing on 1.0.10.
16
+
17
+ ## 1.0.10: the modal moved onto the Popover API
18
+
19
+ The host is still a `<dialog>`, but it now carries `popover="manual"` and the
20
+ controller opens it with `showPopover()`, so an open modal is in the browser's
21
+ **top layer**. Measured with the same probe page on both versions:
22
+
23
+ | | 1.0.9 | 1.0.10 |
24
+ |---|---|---|
25
+ | open modal in the top layer | no | **yes** |
26
+ | `dialog.open` while showing | `true` | **`false`**, always |
27
+ | visible state | `[open]` attribute | `:popover-open`, plus `data-mosaic-dialog-state="open"` |
28
+ | closed state | `dialog:not([open]){display:none}` (UA) | `.m-modal:not(:popover-open){display:none !important}` |
29
+ | controller | `el.mosaicDialog.close()` | **no `close()`** - `open()`, `requestClose()` (a Promise), `toggle()` |
30
+
31
+ What each row costs you:
32
+
33
+ - **Top layer means `z-index` no longer decides anything.** A 1.0.10 modal paints
34
+ above a fixed header, a sticky bar, anything - whatever z-index those carry.
35
+ - **Anything that tested `dialog.open` now reads every modal as closed.** Read
36
+ `el.matches(':popover-open')`. The dialog lab's verifier had exactly this bug on
37
+ the upgrade: it decided the page-load notice was shut, never dismissed it, and
38
+ 18 of 33 checks failed behind its overlay. `tools/verify_dialog*.py` read either
39
+ signal now, so they hold on both versions.
40
+ - **To close one from script, `el.mosaicDialog.requestClose()`.** `close()` throws
41
+ on 1.0.10 ("is not a function"); `hidePopover()` bypasses the controller.
42
+ - **`display` on the host is safe now.** The closed state is `!important`, so an
43
+ author's `display:flex` can no longer pin a modal open - the trap the "five forms"
44
+ section below was built around. Measured: a modal whose host sets
45
+ `display:flex; align-items:center; justify-content:center` computes `display:none`
46
+ and 0x0 while closed, and `display:flex` with a centred window when open
47
+ (`FLEXMODAL/*` in `data/popover-verification.csv`). Positioning the window, as
48
+ the forms below do, still works on both versions.
49
+ - **The window keeps Mosaic's default card look** - `border-radius:8px` and a
50
+ three-layer shadow on `:where(.m-modal-window)`. Zero-specificity, so any author
51
+ value wins, but a square-edged site that restyles the border and forgets the
52
+ radius gets rounded corners on a full-screen takeover and an edge-docked sheet.
53
+ Set `border-radius:0` with the border.
54
+
55
+ **`scrollDepth` renamed its action slot.** It used to have one generated slot named
56
+ after the type, `scrollDepth`; 1.0.10 declares two, **`reached`** and **`returned`**
57
+ (scrolled back above the threshold). The data upgrade moves stored actions from
58
+ `scrollDepth` to `reached`, and `completedAfter: "scrollDepth"` with them. Write
59
+ `actionSlots.reached` - a key the type no longer declares is never exported, so an
60
+ action left under `scrollDepth` is stored and silently never runs. The
61
+ `interactionShorthand` form is unaffected: it fills the slot at render.
10
62
 
11
63
  ## Modal — a real `<dialog>`
12
64
 
@@ -47,12 +99,13 @@ delivers
47
99
  Measured on a modal authored with no overlay: the delivered HTML contains
48
100
  `<div class="m-modal-overlay" tabindex="-1">`, the host carries
49
101
  `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`.
102
+ same page scrolls 700px with nothing open. **There is no non-blocking modal.**
103
+ On 1.0.9 a corner notice that must not trap the reader had to be built from
104
+ ordinary nodes; on 1.0.10 it is a `popover` (below).
53
105
  - **`closedby` is `everything` | `clickOutside` | `esc` | `nothing`**, and it is
54
106
  emitted as `data-mosaic-modal-closedby`, NOT as the native `closedby` attribute —
55
- Mosaic opens the host with `show()`, where the native light-dismiss does not
107
+ Mosaic opens the host itself (`show()` on 1.0.9, `showPopover()` on a
108
+ `popover="manual"` host on 1.0.10), where the native light-dismiss does not
56
109
  apply, so its own controller reads the attribute. Measured: with `everything`,
57
110
  Esc closes it. `nothing` is the cookie-gate case, where the only way out is a
58
111
  `modalClose` action you place yourself.
@@ -281,6 +334,143 @@ anywhere. That is the skill's existing rule (`ValidatorDynamicCode` = bare strin
281
334
  `ValidatorDynamicCodeObject` = `{"v": …}`) applying to a new element, and it is also
282
335
  why those three can hold `@VAR('post/meta_lat')` and a custom field can drive the map.
283
336
 
337
+ ## Popover (1.0.10)
338
+
339
+ A dialog surface that does **not** block the page: a menu, an info card, a tip, a
340
+ notice, a tour step. It appears next to whatever opened it, next to an element you
341
+ name, or in a fixed cell of the viewport, and the visitor can keep using everything
342
+ else while it is open. `data/popover-verification.csv` (37 checks, all passing) is
343
+ the popover lab, `/popovers/` on the demo site, built from `sites/_popovers.py`.
344
+
345
+ ```json
346
+ {"type": "popover", "nodeID": "<uuid you mint>",
347
+ "data": {"accessibleLabel": "服務項目", "role": "menu", "closedby": "everything",
348
+ "positioning": {"type": "element",
349
+ "elementOptions": {"side": "block-end", "align": "start",
350
+ "keepInView": "1"}}},
351
+ "children": [ … anything … ]}
352
+ ```
353
+
354
+ delivers
355
+
356
+ ```html
357
+ <div class="m-popover M_Dialog M_IJ _x" id="pp-menu" popover="manual" role="menu"
358
+ aria-label="服務項目" data-mosaic-popover-closedby="everything"
359
+ data-mosaic-popover-placement="block-end start"
360
+ data-mosaic-popover-keep-in-view="1"> … </div>
361
+ ```
362
+
363
+ - **It has no required children.** Unlike `modal` there is no window and no
364
+ overlay - and that is the point: no overlay, nothing blocking, no `aria-modal`.
365
+ Measured with a page-load popover showing: the centre of the viewport hit-tests to
366
+ the page, a wheel event scrolls it 600px, and a button elsewhere on the page opens
367
+ a second popover while the first stays up. It may sit anywhere (`placement-rules`
368
+ says `any`), and dialogs may nest inside each other.
369
+ - **`closedby` defaults to `nothing`.** A popover expects an explicit close: a
370
+ `popoverClose` / `popoverToggle` action, usually a button inside it. Measured:
371
+ `nothing` ignores Esc; `clickOutside` ignores Esc and closes on an outside click;
372
+ `everything` closes on both.
373
+ - **`role` has no default** (dialog / menu / listbox / tooltip are the suggested
374
+ values); **`ariaLive`** is `off` | `polite` | `assertive` and is emitted only when
375
+ it announces; **`accessibleLabel`** always emits `aria-label`, as on the modal.
376
+ - **The default look is a card**: white, `border-radius:8px`, shadow, `padding:24px`,
377
+ `max-width:384px`, `display:flex; flex-direction:column; row-gap:8px` while open -
378
+ all on `:where()`, so any style you set wins. The closed state is
379
+ `display:none !important`, so `display` on a popover is safe too.
380
+
381
+ ### Where it sits: `positioning`
382
+
383
+ One group, `type` plus that type's options. Both option groups always exist in the
384
+ schema; only the chosen one is read.
385
+
386
+ | `type` | options | what it does |
387
+ |---|---|---|
388
+ | `screen` (default) | `screenOptions.position`: `top-left` … `bottom-right`, nine cells; default `bottom-right` | pure CSS, right in the first painted frame, no JavaScript. Physical directions: a viewport corner stays that corner in RTL |
389
+ | `element` | `elementOptions.side`: `block-start` / `block-end` / `inline-start` / `inline-end` (default `block-end`); `align`: `start` / `center` / `end` (default `center`); `keepInView`: `"1"` / `"0"` (default `"0"`); `anchor`: a target (`{"type": "element", "uuid": "<node id>"}`, or a class / variant) plus optional `anchorModifiers` | positioned with native CSS anchor positioning. Logical directions, so RTL mirrors them. **No anchor means "whatever opened it"** |
390
+
391
+ Measured, in Chromium at 1280x900:
392
+
393
+ - an unanchored menu sits on its trigger's block-end edge, start-aligned, to the
394
+ pixel - and with `keepInView` it flips above when the 404px menu would not fit
395
+ below;
396
+ - an anchor set to another element's node id puts the popover on THAT element
397
+ (bottom edge on the card's top edge, centres within 1px), whatever opened it;
398
+ - all nine screen cells dock where their names say.
399
+
400
+ **`keepInView` is the flip, and `___popover--flipped` is how it looks flipped.**
401
+ With `keepInView: "1"` and no room on the requested side, the popover moves to the
402
+ opposite side and carries `data-mosaic-popover-flipped`; the style state
403
+ `___popover--flipped` (selector
404
+ `&:where(.m-popover[data-mosaic-popover-flipped],.m-popover[data-mosaic-popover-flipped] &)`)
405
+ applies to it and everything inside it. Two measured surprises:
406
+
407
+ - **`keepInView: "0"` does not overflow.** Asked for `inline-end` with no room on
408
+ the right, the popover did not run off-screen - the browser shifted it back inside
409
+ the viewport, **on top of its own trigger** (popover 1020-1280px, trigger
410
+ 1080-1231px). If covering the trigger is wrong, keepInView is not optional.
411
+ - **On a phone, neither side may fit, and then nothing flips.** At 390px both
412
+ `inline-*` sides are too narrow for a 260px card: `flip` is armed, `flipped` never
413
+ set, and the card covers the trigger. Anchor to `block-start` / `block-end` for
414
+ anything that has to work at phone width.
415
+
416
+ **A screen popover is glued to the viewport edge.** Mosaic places it with `inset:0`
417
+ plus auto margins, zeroing the margin on the docked side, so `bottom-left` touches
418
+ the bottom-left corner exactly. `top/right/bottom/left: 12px` on the popover moves
419
+ every edge in at once and keeps the cell.
420
+
421
+ ### Opening and closing: three actions, one trigger
422
+
423
+ Actions, aimed at the popover's **node id** like the modal's:
424
+ `popoverOpen`, `popoverClose`, `popoverToggle`. The popover's
425
+ `interactionShorthand` action is `popoverOpen`, so `{"type": "pageLoad", …}` on the
426
+ popover itself is a self-opening notice (with run rules, exactly as on a modal).
427
+
428
+ A new trigger, **`popover`** ("Popover visibility change", group Element events),
429
+ goes ON the popover and has three slots: **`show`**, **`beforeClose`** (the native
430
+ hide waits for it to finish), **`afterClose`** (the popover is already hidden):
431
+
432
+ ```json
433
+ "interactions": [{"type": "popover", "uuid": "<uuid>",
434
+ "popoverOptions": {"name": "tour-1", "ID": "<uuid>",
435
+ "actionSlots": {"afterClose": {"actions": [
436
+ {"type": "popoverOpen", "uuid": "<uuid>",
437
+ "settings": {"target": {"type": "element",
438
+ "uuid": "<next step>"}}}]}}}}]
439
+ ```
440
+
441
+ ### Two runtime rules that decide how a sequence has to be built
442
+
443
+ Found by building a three-step tour whose "next" button did nothing:
444
+
445
+ 1. **A dialog opened by a trigger inside another dialog becomes its child, and a
446
+ parent closing closes its children.** The controller takes the dialog the
447
+ trigger sits in as the parent. So a "next" button inside step 1 that runs
448
+ `popoverOpen step 2` + `popoverClose step 1` opens step 2 as step 1's child and
449
+ closes both: measured, nothing open 200ms later.
450
+ 2. **Closing a popover suspends the interactions of every element inside it.** So
451
+ with the order reversed - close first, then open - the open never runs at all.
452
+
453
+ The sequence therefore has to be advanced by **`afterClose`**: it runs once the step
454
+ is already closed, so the step it opens has no open parent to be taken down with.
455
+ The "next" button only closes its own step. And because afterClose fires on EVERY
456
+ close, Esc included, a step that should be leavable must not take Esc
457
+ (`closedby: "nothing"`); leaving is a button that runs `interactionSuspend` on the
458
+ step first (its own afterClose then does not run) and then `popoverClose`. The
459
+ start button runs `interactionResume` on each step before opening the first, so a
460
+ tour ended halfway can be started again. Measured: start > next > next > done walks
461
+ step 1 > 2 > 3 > the closing card; "end" on step 2 leaves nothing open; a restart
462
+ advances normally; Esc on a step leaves it open.
463
+
464
+ `interactionSuspend` / `interactionResume` take a `target` and an optional
465
+ `settings.interactionID`; with none, every interaction on the target.
466
+
467
+ ### The modal, flex-centred
468
+
469
+ The lab's last demo is the 1.0.10 modal change made visible: a modal whose host is
470
+ `display:flex` + centred, which on 1.0.9 would have pinned it open. It hides when
471
+ closed and centres its window when open, and it sits in the top layer above the
472
+ fixed header.
473
+
284
474
  ## The rest of 1.0.9, briefly
285
475
 
286
476
  Not everything in the release is a node you place. Four things that change how the
@@ -387,7 +577,7 @@ own `interactionShorthand`, a page's popup policy is one object in that page's d
387
577
  Nothing else on the page changes, no trigger element is placed, and two pages of the
388
578
  same site can behave completely differently without sharing anything.
389
579
 
390
- ## Five forms out of three nodes — and the one property you must not set
580
+ ## Five forms out of three nodes — and, on 1.0.9, the one property you must not set
391
581
 
392
582
  "A modal" in most builders means one centred card, because the builder owns the
393
583
  chrome. Mosaic owns none of it: `.m-modal` is a `position:fixed; inset:0` layer
@@ -395,7 +585,8 @@ with `pointer-events:none`, `.m-modal-overlay` is a second fixed layer that take
395
585
  pointer events back, and `.m-modal-window` is an ordinary child of the first. The
396
586
  form is decided by where the window is pinned inside that layer.
397
587
 
398
- **Never set `display` on the `modal` host.** A `<dialog>`'s open/closed visibility
588
+ **On 1.0.9, never set `display` on the `modal` host** (1.0.10 made it safe - see
589
+ the top of this page). A `<dialog>`'s open/closed visibility
399
590
  *is* its display property — the UA stylesheet hides it with
400
591
  `dialog:not([open]) { display: none }` — so an author-level `display:flex` on the
401
592
  host wins the cascade and every modal on the page is permanently on screen. It is
@@ -165,3 +165,22 @@ half and the output ends in U+FFFD. Lengths have to be a multiple of the encodin
165
165
  width — 18 for six three-byte characters — or the truncation has to be left to CSS.
166
166
  A count-based check cannot see this; `verify_components.py` looks for the
167
167
  replacement character directly.
168
+
169
+ ## Date conditions (1.0.10)
170
+
171
+ The `date` comparator - element and form-action server conditions, and the post /
172
+ user loop filters - has four operators now: `after`, `after-or-equal`, `before`,
173
+ `before-or-equal` (1.0.9 had the first and third). The value is any text, read by
174
+ `DateComparison` when it compares rather than validated when it is saved:
175
+
176
+ - a year first: `2026-09-24`, `2026/9/24`, `2026-09`, `2026`;
177
+ - a year last: `28/09/2026`, `9-28-2026` - a number above 12 is the day; when both
178
+ could be, the **site's date format** decides which comes first;
179
+ - `Ymd` (ACF's date picker storage) and `YmdHis`; any shorter digit run is a Unix
180
+ timestamp in the site timezone;
181
+ - a time after the date or on its own (`09:16`, `9:16 pm`), with an optional zone.
182
+
183
+ The less precise side sets the precision: a date against a date-time compares the
184
+ two days. Whatever cannot be read as a date matches **no** operator, so a typo is a
185
+ condition that is never true rather than an error. Loop filters compare in SQL
186
+ (`post_date_gmt` / `post_modified_gmt` as UTC, the others as the site's wall clock).
@@ -319,7 +319,12 @@ The general shape, worth carrying beyond this repo: a derived artifact checked i
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
321
 
322
- ## Setting `display` on a `<dialog>` pins it open, and open-state checks cannot see it
322
+ ## Setting `display` on a `<dialog>` pins it open, and open-state checks cannot see it (1.0.9)
323
+
324
+ **Fixed in 1.0.10**: the closed modal is `.m-modal:not(:popover-open){display:none !important}`,
325
+ which no author `display` beats - measured, a flex-centred modal host hides when closed
326
+ (`references/dialog-and-triggers.md`). Everything below is the 1.0.9 behaviour, and still
327
+ the reason the checks it produced exist.
323
328
 
324
329
  The modal host is a `<dialog>`. Its open/closed visibility is not a class or an
325
330
  attribute Mosaic manages — it is the UA stylesheet rule
@@ -340,7 +345,8 @@ Two rules come out of it:
340
345
  edges inside the fixed, `inset:0` host), never by making the host a flex or grid
341
346
  container.
342
347
  - **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
348
+ first and fails if any closed modal (not `:popover-open` and not `[open]`) still
349
+ has a box. The general form of
344
350
  that lesson: for anything with a visible and an invisible state, a suite that
345
351
  only exercises the visible one is testing half a component, and the half it
346
352
  skips is the one the visitor sees most of the time.
@@ -402,8 +408,9 @@ labels and that an empty required form is actually refused.
402
408
 
403
409
  ## `display` on an element whose visibility the plugin owns — twice now
404
410
 
405
- A `<dialog>`'s open state is its `display`, so `display:flex` on a modal host pins
406
- every modal open. The same mistake, in a different family: `multi-steps-form-step`
411
+ On 1.0.9 a `<dialog>`'s open state was its `display`, so `display:flex` on a modal host
412
+ pinned every modal open (1.0.10 made the closed state `!important`). The same mistake,
413
+ in a family 1.0.10 did not touch: `multi-steps-form-step`
407
414
  is shown and hidden by the plugin moving `m-form-step--active`, and a `display:flex`
408
415
  on the step wins the cascade, so all three steps sit on the page at once with three
409
416
  sets of Next buttons.
@@ -444,3 +451,28 @@ On a theme that cannot use the variant layer, a `code` node with
444
451
  verbatim**: it does not wrap anything, so the `<style>` tags are yours to write.
445
452
  Without them the rule lands in `<head>` as bare text between `</style>` and
446
453
  `</head>`, renders nothing, and reads as a CSS specificity problem.
454
+
455
+ ## A "next" button inside a popover cannot open the next popover
456
+
457
+ Measured on 1.0.10, building a three-step tour: the button in step 1 ran
458
+ `popoverOpen` (step 2) and `popoverClose` (step 1), and nothing was open 200ms later.
459
+ Two runtime rules combine:
460
+
461
+ - a dialog opened by a trigger that sits inside another dialog becomes that dialog's
462
+ CHILD, and closing a parent closes its children - step 2 went down with step 1;
463
+ - closing a popover suspends the interactions of every element inside it - with the
464
+ actions reversed, the `popoverOpen` after the close never ran.
465
+
466
+ Advance a sequence on the step's own `afterClose` (it runs after the close, with no
467
+ open parent left), make the button close only its own step, and remember afterClose
468
+ also fires on Esc. The full pattern is in `references/dialog-and-triggers.md`.
469
+
470
+ ## A plugin folder swapped in by hand serves every asset as 406
471
+
472
+ Installing 1.0.10 by unzipping it next to the old folder and renaming: the zip's
473
+ directories came out `drwx------`, the web server could not traverse them, and every
474
+ `public/Frontend/*.js` and the theme `style.css` answered **406**. The page still
475
+ rendered - server-side output was fine - but with no front-end runtime every dropdown
476
+ sat open and no modal could open. `find mosaic -type d -exec chmod 775 {} +` (files
477
+ 664), matching the other plugin folders, and purge the page cache. A REST-level check
478
+ cannot see this; only fetching an asset can.
@@ -227,3 +227,12 @@ Both of these are refused at build time by `build_page.py` rather than silently
227
227
  dropped, which is the only reason they cost minutes instead of a debugging session
228
228
  — a property that is not in the registry cannot be set at all, and Mosaic's own
229
229
  API accepts the write and discards it.
230
+
231
+ ## Gradient stops take units (1.0.10)
232
+
233
+ A gradient's `dots[].position` - background and mask alike - used to be a bare float
234
+ 0-100. 1.0.10 validates it as a length-percentage: `"50%"`, `"120px"`, a `calc()`, or a
235
+ collection variable, with `%` as the fallback unit and 0-100 enforced only on
236
+ percentages. A bare number still validates (it is read as a percentage), but the
237
+ values Mosaic itself now writes are strings with a unit - the built-in background
238
+ collection's stops changed from `10` / `50` to `"10%"` / `"50%"`.
@@ -1,4 +1,4 @@
1
- # Upgrading the plugin: what 1.0.7 → 1.0.8 did to the data, measured
1
+ # Upgrading the plugin: what 1.0.7 → 1.0.8 → 1.0.10 did to the data, measured
2
2
 
3
3
  A Mosaic plugin update is two events, not one. The files change when WordPress
4
4
  installs the package; the DATA changes only when Mosaic's own upgrade flow has
@@ -180,3 +180,48 @@ The order that worked, and why each step is there:
180
180
  five-minute upstream limit during `createTemplates`; at 25 masters and 8,608
181
181
  nodes it finished in under two minutes and `theme_zip_compare.php` passed
182
182
  22 of 22, the four renamed tables included.
183
+
184
+ ## 1.0.9 → 1.0.10
185
+
186
+ **Getting the package.** The update shows in wp-admin, but its package is
187
+ `private-mosaic-pro://releases/stable/Mosaic-pro-plugin-stable-1.0.10.zip`, which
188
+ `wp plugin update mosaic` could not fetch on an unlicensed install ("No plugins
189
+ updated (1 failed)", and a second run claiming "already updated" while the version
190
+ still read 1.0.9). Two downloads labelled as the new release were 1.0.9 byte for
191
+ byte - one only the inner `Mosaic/` namespace folder. **Check `Version:` in
192
+ `mosaic.php` before installing anything**, and diff the tree against the old one: a
193
+ real 1.0.10 has `Mosaic/NodeTypes/Popover/`. The real package unpacked to a folder
194
+ named `mosaic-next/`; it is installed by renaming it to `mosaic` in place of the old
195
+ folder, then **chmod** (directories 775, files 664 - the zip's `drwx------` served
196
+ every asset as 406), then `tools/data_upgrade.py`, whose six milestones took about
197
+ seven minutes on this theme.
198
+
199
+ **What the migration rewrote.** One thing: scroll-depth interactions. The type
200
+ used to have a single generated action slot named after itself, `scrollDepth`; it
201
+ now declares `reached` and `returned`, and the migration moves stored actions (and
202
+ a `completedAfter: "scrollDepth"`) to `reached` across `mosaic_nodes` and the four
203
+ class tables. No table changed shape (`Schema-1.0.10.php` lists the same tables).
204
+
205
+ **What changed in the delivered page.** The modal host gained `popover="manual"`
206
+ and opens into the top layer; `dialog.open` is always false and the controller has
207
+ `requestClose()` instead of `close()`. Every tool that decided "is it open?" from
208
+ `dialog.open` read every modal as closed afterwards - the dialog lab reported 18 of
209
+ 33 failures that were all the verifier's. `references/dialog-and-triggers.md` has
210
+ the measured table.
211
+
212
+ **Re-extracted from the 1.0.10 source and diffed against 1.0.9:** one node type
213
+ (`popover`, 129), five node properties (196), one data class (126), one placement
214
+ rule (129), one style state (`___popover--flipped`, 54), one interaction trigger
215
+ (`popover`, 16), twelve pluggable IDs (261 - the three popover actions and their
216
+ groups, the trigger and its slots, scroll depth's two new slots), the date
217
+ comparator's operators (`after-or-equal`, `before-or-equal`) and gradient stop
218
+ positions accepting units (`references/styling.md`). The 115 editor routes are the
219
+ same 115. Every demo verifier was re-run on the result and passes; the full node
220
+ sweep was run for the types 1.0.10 added or that had never been swept
221
+ (`popover`, `fieldset-legend`, `fieldset-content`, all RENDERED), the other rows
222
+ are the 1.0.9 sweep.
223
+
224
+ **The sweep had stopped measuring anything, and nothing said so.** It read `/`,
225
+ which stopped being the post archive when the site got a static front page; every
226
+ type, a plain `div` included, came back COMMITTED. It now reads `sweepPath`
227
+ (default `?post_type=post`) and refuses to run unless a plain div renders there.
@@ -26,9 +26,37 @@ import { fileURLToPath } from "node:url";
26
26
 
27
27
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
28
28
  const read = (p) => fs.readFileSync(path.join(ROOT, p), "utf8");
29
+ // A table split one file per page (data/browser-verification/) is still one table:
30
+ // its CSVs, in name order, each with its own header.
31
+ const tableFiles = (p) => fs.statSync(path.join(ROOT, p)).isDirectory()
32
+ ? fs.readdirSync(path.join(ROOT, p)).filter((f) => f.endsWith(".csv")).sort()
33
+ .map((f) => `${p}/${f}`)
34
+ : [p];
29
35
  const problems = [];
30
36
  const fail = (m) => problems.push(m);
31
37
 
38
+ // ---------- the plugin directory's limits -----------------------------------
39
+ //
40
+ // The Claude plugin directory reads the repository root, every tracked file of it,
41
+ // and holds a submission for a reviewer when any file that is not an image or font
42
+ // is over 256 KiB, or when there are more than 512 files. A 400 KB CSV and a 2 MB
43
+ // generated site file did exactly that. Checked here, so it fails on this machine
44
+ // rather than in a review queue a week later.
45
+ try {
46
+ const tracked = execSync("git ls-files -z", { cwd: ROOT }).toString().split("\0").filter(Boolean);
47
+ if (tracked.length > 512) fail(`${tracked.length} tracked files - the plugin directory allows 512`);
48
+ for (const f of tracked) {
49
+ if (/\.(png|jpe?g|gif|webp|svg|ico|woff2?|ttf|otf)$/i.test(f)) continue;
50
+ const full = path.join(ROOT, f);
51
+ if (!fs.existsSync(full)) continue;
52
+ const size = fs.statSync(full).size;
53
+ if (size > 256 * 1024)
54
+ fail(`${f} is ${(size / 1024).toFixed(0)} KiB - the plugin directory holds any non-image file over 256 KiB`);
55
+ }
56
+ } catch {
57
+ // not a git checkout (an unpacked tarball): nothing to measure against
58
+ }
59
+
32
60
  // ---------- versions agree --------------------------------------------------
33
61
 
34
62
  const pkg = JSON.parse(read("package.json"));
@@ -91,28 +119,30 @@ if (!fs.existsSync(path.join(ROOT, pkg.bin["mosaic-headless"])))
91
119
 
92
120
  // ---------- the numbers in the docs are counted, not trusted ----------------
93
121
 
94
- const rows = (p) => read(p).trim().split("\n").length - 1; // minus the header
122
+ const rows = (p) => tableFiles(p)
123
+ .reduce((n, f) => n + read(f).trim().split("\n").length - 1, 0); // minus each header
95
124
  const counts = {
96
- "data/node-verification.csv": 126,
125
+ "data/node-verification.csv": 129,
97
126
  "data/style-verification.csv": 98,
98
127
  "data/node-property-verification.csv": 191,
99
128
  "data/rwd-verification.csv": 733,
100
- "data/browser-verification.csv": 4132,
129
+ "data/browser-verification": 4132,
101
130
  "data/style-state-verification.csv": 52,
102
131
  "data/interaction-verification.csv": 7,
103
- "data/data-class-hierarchy.csv": 125,
132
+ "data/data-class-hierarchy.csv": 126,
104
133
  "data/custom-fields-verification.csv": 62,
105
134
  "data/dialog-verification.csv": 19,
106
135
  "data/slider-verification.csv": 30,
107
136
  "data/token-verification.csv": 15,
108
- "data/mobile-audit.csv": 65,
137
+ "data/mobile-audit.csv": 68,
109
138
  "data/component-wall-verification.csv": 26,
110
- "data/form-surface-verification.csv": 15,
111
- "data/content-loop-verification.csv": 12,
112
- "data/navigation-verification.csv": 36,
139
+ "data/form-surface-verification.csv": 18,
140
+ "data/content-loop-verification.csv": 15,
141
+ "data/navigation-verification.csv": 39,
113
142
  "data/page-modal-verification.csv": 19,
114
143
  "data/interaction-rules-verification.csv": 17,
115
144
  "data/dialog-form-verification.csv": 49,
145
+ "data/popover-verification.csv": 37,
116
146
  "data/value-coercion-verification.csv": 12,
117
147
  };
118
148
  for (const [file, expected] of Object.entries(counts)) {
@@ -131,7 +161,7 @@ if (!style.includes("SKIPPED")) fail("style-verification.csv has no SKIPPED rows
131
161
 
132
162
  // The browser pass has the same obligation, under a different word: a declaration it
133
163
  // cannot soundly compare must say so rather than be counted as agreement.
134
- const browser = read("data/browser-verification.csv");
164
+ const browser = tableFiles("data/browser-verification").map(read).join("\n");
135
165
  if (!browser.includes("not-comparable"))
136
166
  fail("browser-verification.csv has no not-comparable rows - a computed-value check "
137
167
  + "that claims to compare everything is comparing things it cannot");
@@ -190,13 +220,13 @@ if (/,FAIL,/.test(zip)) fail("data/theme-zip-verification.csv carries a failed c
190
220
  // rwd and browser counts, and must not carry the previous ones.
191
221
  {
192
222
  const fmt = (n) => n.toLocaleString("en-US");
193
- const want = [rows("data/rwd-verification.csv"), rows("data/browser-verification.csv")];
223
+ const want = [rows("data/rwd-verification.csv"), rows("data/browser-verification")];
194
224
  for (const readme of ["README.md", "README.zh-TW.md", "README.ja.md", "README.ko.md"]) {
195
225
  const text = read(readme);
196
226
  for (const n of want)
197
227
  if (!text.includes(fmt(n)) && !text.includes(String(n)))
198
228
  fail(`${readme} does not quote ${fmt(n)} - it is quoting a stale count`);
199
- if (/0 findings|指摘 0 件|지적 0건|0 項發現/.test(text))
229
+ if (/\b0 findings\b|指摘 0 件|지적 0건|0 項發現/.test(text))
200
230
  fail(`${readme} still claims a design audit with 0 findings`);
201
231
  }
202
232
  }
package/sites/_dialogs.py CHANGED
@@ -139,6 +139,8 @@ def make_modal(key, num, title, lead, body, specs, host, window,
139
139
  "paddingLeft": "30px", "paddingRight": "30px",
140
140
  "customDeclarations": "border:1px solid rgb(22,24,28);"}
141
141
  win_style.update(window)
142
+ win_style["customDeclarations"] = (win_style.get("customDeclarations", "")
143
+ + "border-radius:0;")
142
144
 
143
145
  children = []
144
146
  if overlay:
@@ -210,9 +212,10 @@ def M(key, num, title, lead, body, specs, host, window, **kw):
210
212
  CENTRE_MODAL = M(
211
213
  "centre", 1, "置中對話框",
212
214
  "最直接的一種,也是其他八種的基準。",
213
- "Mosaic 的 modal 是真正的 &lt;dialog&gt;,由外掛自己的控制器用 show() 開啟。"
214
- "因為是 show() 而不是 showModal(),瀏覽器原生的 light-dismiss 不會生效,"
215
- "所以關閉方式寫在 data-mosaic-modal-closedby 屬性上,由控制器讀取。"
215
+ "Mosaic 的 modal 是真正的 &lt;dialog&gt;,1.0.10 起帶 popover=\"manual\","
216
+ "由外掛自己的控制器用 showPopover() 開啟,進入瀏覽器的 top layer——"
217
+ "所以它一定蓋在固定 header 之上,z-index 管不到它。manual 代表瀏覽器不會自己關它,"
218
+ "關閉方式寫在 data-mosaic-modal-closedby 屬性上,由控制器讀取。"
216
219
  "另外 aria-modal=\"true\" 不是你寫的——它在 modal 帶有 overlay 子節點時才出現,"
217
220
  "因為真正擋住頁面的是 overlay。",
218
221
  [("觸發", "click → modalOpen"), ("執行規則", "無"), ("記憶", "無"),
@@ -225,8 +228,8 @@ SHEET_MODAL = M(
225
228
  "modal 這層是 position:fixed; inset:0 的全視窗圖層,pointer-events:none;"
226
229
  "modal-window 只是它的一個 flex 子元素。所以「抽屜」不是另一種元件,"
227
230
  "只是把 window 絕對定位到 left:0; right:0; bottom:0——沒有新的節點型別,也沒有自訂 CSS。"
228
- "特別不能把 host 做成 flex 容器:&lt;dialog&gt; 是靠 display 決定開關的,"
229
- "在 host 上寫 display 會讓所有彈窗永遠關不起來。"
231
+ "1.0.9 時 host 上不能寫 display——那會讓彈窗永遠關不起來;"
232
+ "1.0.10 把關閉狀態改成 display:none !important,這條限制消失了。"
230
233
  "手機上這是比置中卡片更合手的形態,拇指搆得到。",
231
234
  [("觸發", "click → modalOpen"), ("執行規則", "無"), ("記憶", "無"),
232
235
  ("關閉", "clickOutside(只有點遮罩)"), ("排版", "window: left/right/bottom = 0")],
@@ -261,7 +264,8 @@ TOAST_MODAL = M(
261
264
  "所以 aria-modal=\"true\" 實際上是拿不掉的,"
262
265
  "實測也確認:這個彈窗開著時,滑鼠滾輪同樣捲不動頁面。"
263
266
  "角落通知是一種排版,不是一種「不阻擋」的 modal——"
264
- "1.0.9 沒有不阻擋的 modal。真正不擋人的通知要用普通節點做。",
267
+ "modal 沒有不阻擋的版本。1.0.10 起,真正不擋人的通知用 Popover 做——"
268
+ "浮動層頁的第五個示範就是同一張通知,換成 popover 之後頁面照樣能用。",
265
269
  [("觸發", "pageLoad(interactionShorthand)"), ("執行規則", "cap max 1"),
266
270
  ("記憶", "session(sessionStorage,每個分頁各自計算)"),
267
271
  ("關閉", "everything"),
@@ -272,6 +276,8 @@ TOAST_MODAL = M(
272
276
  "paddingLeft": "20px", "paddingRight": "20px",
273
277
  "customDeclarations": "border:1px solid rgb(22,24,28);"
274
278
  "box-shadow:0 18px 44px rgba(22,24,28,.18);"},
279
+ window_m={"top": "72px", "left": "0px", "right": "0px", "width": "auto",
280
+ "maxWidth": "none", "maxHeight": "calc(100% - 72px)"},
275
281
  shorthand={"type": "pageLoad",
276
282
  "pageLoadOptions": {"runs": {"rules": [
277
283
  {"uuid": U(), "type": "cap",
@@ -350,6 +356,8 @@ DEPTH_MODAL = M(
350
356
  ("關閉", "clickOutside"), ("紀錄", "mos:v1:r:dl-depth")],
351
357
  {},
352
358
  {"left": "24px", "bottom": "24px", "maxWidth": "380px", "width": "100%"},
359
+ window_m={"left": "0px", "right": "0px", "bottom": "0px", "width": "auto",
360
+ "maxWidth": "none"},
353
361
  closedby="clickOutside",
354
362
  shorthand={"type": "scrollDepth",
355
363
  "scrollDepthOptions": {
@@ -559,8 +567,10 @@ NOTE = section("dl-note", [wrap("dl-note-w", [
559
567
  "Mosaic 沒有那層外殼——modal、modal-overlay、modal-window "
560
568
  "是三個可以自由排版的節點,所以上面九種形態全部來自同一組資料,"
561
569
  "差別只在 window 被釘在哪裡。順帶一個踩過的坑:"
562
- "host 不能設 display——&lt;dialog&gt; 是靠 display 決定開關的,"
563
- "寫了 display:flex 就等於把每一個彈窗永遠打開。",
570
+ "在 1.0.9,host 不能設 display——&lt;dialog&gt; 是靠 display 決定開關的,"
571
+ "寫了 display:flex 就等於把每一個彈窗永遠打開。"
572
+ "1.0.10 把關閉狀態改成 display:none !important,這個坑填平了;"
573
+ "這裡的九種形態仍然用定位排版,好讓同一份資料在兩個版本都對。",
564
574
  fontFamily=CJK, fontSize="15px", lineHeight="1.95",
565
575
  color={"token": "--mk-ink"}, marginTop="16px", maxWidth="680px"),
566
576
  T("p",