mosaic-headless 1.20.0 → 1.21.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/README.ja.md +5 -3
- package/README.ko.md +5 -3
- package/README.md +45 -3
- package/README.zh-TW.md +38 -3
- package/SKILL.md +49 -8
- package/assets/templates/platforms/claude-ai.json +1 -1
- package/assets/templates/platforms/claude-code.json +1 -1
- package/assets/templates/platforms/codex-cli.json +1 -1
- package/assets/templates/platforms/copilot.json +1 -1
- package/assets/templates/platforms/gemini-cli.json +1 -1
- package/data/browser-verification.csv +144 -0
- package/data/intro-verification.csv +9 -9
- package/data/navigation-verification.csv +28 -0
- package/data/page-modal-verification.csv +20 -0
- package/data/rwd-verification.csv +2 -0
- package/data/slider-verification.csv +20 -0
- package/package.json +6 -6
- package/references/dialog-and-triggers.md +75 -1
- package/references/slider.md +79 -0
- package/{bin → scripts}/check-release.mjs +25 -8
- package/{bin → scripts}/sync-version.mjs +14 -2
- package/sites/_moksa.py +179 -2
- package/sites/moksa.json +17014 -11914
- package/tools/verify_dialog.py +61 -34
- package/tools/verify_navigation.py +114 -0
- package/tools/verify_slider.py +256 -0
- /package/{bin → scripts}/install.mjs +0 -0
|
@@ -2,25 +2,25 @@ check,result,detail
|
|
|
2
2
|
PLAYS,PASS,43 of 43 intro elements changed state
|
|
3
3
|
COUNTS,PASS,--mk-n reached 100
|
|
4
4
|
ENDS,PASS,veil is out of hit-testing from 8900ms
|
|
5
|
-
NO_TRAP,PASS,"0 content elements still at opacity 0 in the viewport (
|
|
5
|
+
NO_TRAP,PASS,"0 content elements still at opacity 0 in the viewport (1 below the fold and 0 on a scroll timeline awaiting it, 1 mid-blink)"
|
|
6
6
|
CLEARS,PASS,a real click landed on 'mk-caret-row'
|
|
7
|
-
AMBIENT,PASS,"
|
|
7
|
+
AMBIENT,PASS,"7 elements still moving between 11000ms and 13000ms with no input: mk-board-city-s, mk-board-utc-s, mk-clock-s, mk-flap-0-s, mk-flap-1-s, mk-flap-2-s"
|
|
8
8
|
DEGRADES,PASS,with reduced motion the veil is display:none throughout
|
|
9
9
|
READABLE,PASS,0 elements invisible at 120ms with reduced motion
|
|
10
10
|
|
|
11
11
|
t_ms,counter,veil_visibility,veil_clip,veil_covers_centre,hit_centre,invisible_content
|
|
12
12
|
120,,,,False,html.no-js,0
|
|
13
13
|
700,--mk-n=0;--mk-s=0;--mk-m=0,visible,none,True,mk-boot-num,7
|
|
14
|
-
1400,--mk-n=
|
|
15
|
-
2200,--mk-n=
|
|
16
|
-
3000,--mk-n=
|
|
17
|
-
3800,--mk-n=
|
|
18
|
-
4600,--mk-n=
|
|
14
|
+
1400,--mk-n=5;--mk-s=0;--mk-m=0,visible,none,True,mk-boot-num,7
|
|
15
|
+
2200,--mk-n=40;--mk-s=0;--mk-m=0,visible,none,True,mk-boot-num,3
|
|
16
|
+
3000,--mk-n=71;--mk-s=0;--mk-m=0,visible,none,True,mk-boot-num,3
|
|
17
|
+
3800,--mk-n=87;--mk-s=0;--mk-m=0,visible,none,True,mk-boot-num,3
|
|
18
|
+
4600,--mk-n=95;--mk-s=0;--mk-m=0,visible,none,True,mk-boot-num,3
|
|
19
19
|
5400,--mk-n=98;--mk-s=0;--mk-m=0,visible,none,True,mk-boot-num,3
|
|
20
20
|
6200,--mk-n=100;--mk-s=0;--mk-m=0,visible,none,True,mk-boot-num,3
|
|
21
21
|
7000,--mk-n=100;--mk-s=0;--mk-m=0,visible,none,True,mk-boot-num,3
|
|
22
|
-
7800,--mk-n=100;--mk-s=0;--mk-m=0,visible,none,True,
|
|
23
|
-
8300,--mk-n=100;--mk-s=0;--mk-m=0,visible,none,False,mk-caret-row,
|
|
22
|
+
7800,--mk-n=100;--mk-s=0;--mk-m=0,visible,none,True,p.m-text,2
|
|
23
|
+
8300,--mk-n=100;--mk-s=0;--mk-m=0,visible,none,False,mk-caret-row,1
|
|
24
24
|
8900,--mk-n=0;--mk-s=0;--mk-m=0,hidden,none,False,mk-caret-row,0
|
|
25
25
|
11000,--mk-n=0;--mk-s=0;--mk-m=0,hidden,none,False,mk-caret-row,0
|
|
26
26
|
13000,--mk-n=0;--mk-s=0;--mk-m=0,hidden,none,False,mk-caret-row,0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
check,result,detail
|
|
2
|
+
FRONT_PAGE,PASS,"HTTP 200, 112547 bytes"
|
|
3
|
+
MENU_IS_LINKS,PASS,6 real <a href> in #mw-nav
|
|
4
|
+
NAV 首頁,PASS,"// -> HTTP 200, 112546 bytes"
|
|
5
|
+
NAV 服務,PASS,"/services/ -> HTTP 200, 156300 bytes"
|
|
6
|
+
NAV 作品,PASS,"/works/ -> HTTP 200, 108284 bytes"
|
|
7
|
+
NAV 報價,PASS,"/plan/ -> HTTP 200, 181490 bytes"
|
|
8
|
+
NAV 關於,PASS,"/about/ -> HTTP 200, 66218 bytes"
|
|
9
|
+
NAV 聯絡,PASS,"/contact/ -> HTTP 200, 49644 bytes"
|
|
10
|
+
PAGE home,PASS,"HTTP 200, 112547 bytes"
|
|
11
|
+
PAGE services,PASS,"HTTP 200, 156300 bytes"
|
|
12
|
+
PAGE mowp,PASS,"HTTP 200, 103101 bytes"
|
|
13
|
+
PAGE weebly-migration,PASS,"HTTP 200, 96174 bytes"
|
|
14
|
+
PAGE works,PASS,"HTTP 200, 108284 bytes"
|
|
15
|
+
PAGE plan,PASS,"HTTP 200, 181490 bytes"
|
|
16
|
+
PAGE about,PASS,"HTTP 200, 66218 bytes"
|
|
17
|
+
PAGE teams,PASS,"HTTP 200, 67671 bytes"
|
|
18
|
+
PAGE open-source,PASS,"HTTP 200, 115360 bytes"
|
|
19
|
+
PAGE order-notify-guide,PASS,"HTTP 200, 39646 bytes"
|
|
20
|
+
PAGE contact,PASS,"HTTP 200, 49644 bytes"
|
|
21
|
+
PAGE case-drpen,PASS,"HTTP 200, 89043 bytes"
|
|
22
|
+
PAGE case-lyycamp,PASS,"HTTP 200, 87978 bytes"
|
|
23
|
+
PAGE case-em925,PASS,"HTTP 200, 86621 bytes"
|
|
24
|
+
PAGE case-sweetluck,PASS,"HTTP 200, 87483 bytes"
|
|
25
|
+
PAGE case-amclinic,PASS,"HTTP 200, 86760 bytes"
|
|
26
|
+
PAGE case-ruinart,PASS,"HTTP 200, 87000 bytes"
|
|
27
|
+
PAGE case-medical-erp,PASS,"HTTP 200, 88166 bytes"
|
|
28
|
+
PAGE case-igoods,PASS,"HTTP 200, 86214 bytes"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
page,result,trigger,run_rule,remember,memory_name,closedby,aria_modal,accessible_label
|
|
2
|
+
home,PASS,exitIntent,cap,session,mw-home,everything,yes,還沒找到要的東西?
|
|
3
|
+
services,PASS,scrollDepth,cooldown,forever,mw-services,everything,yes,想知道哪一項最適合?
|
|
4
|
+
mowp,PASS,exitIntent,cap,session,mw-mowp,everything,yes,需要同樣的東西嗎?
|
|
5
|
+
weebly-migration,PASS,exitIntent,cap,session,mw-weebly-migration,everything,yes,需要同樣的東西嗎?
|
|
6
|
+
works,PASS,scrollDepth,cap,session,mw-works,clickOutside,yes,想看完整的案例?
|
|
7
|
+
plan,PASS,pageLoad,firstRunWindow,session,mw-plan,esc,yes,報價只是起點
|
|
8
|
+
about,PASS,exitIntent,cap,forever,mw-about,nothing,yes,我們是誰,決定了做法
|
|
9
|
+
teams,PASS,exitIntent,cap,session,mw-teams,everything,yes,需要同樣的東西嗎?
|
|
10
|
+
open-source,PASS,exitIntent,cap,session,mw-open-source,everything,yes,需要同樣的東西嗎?
|
|
11
|
+
order-notify-guide,PASS,exitIntent,cap,session,mw-order-notify-guide,everything,yes,需要同樣的東西嗎?
|
|
12
|
+
contact,PASS,scrollDepth,cap,page,mw-contact,everything,yes,寫信太慢?
|
|
13
|
+
case-drpen,PASS,exitIntent,cap,session,mw-case-drpen,everything,yes,需要同樣的東西嗎?
|
|
14
|
+
case-lyycamp,PASS,exitIntent,cap,session,mw-case-lyycamp,everything,yes,需要同樣的東西嗎?
|
|
15
|
+
case-em925,PASS,exitIntent,cap,session,mw-case-em925,everything,yes,需要同樣的東西嗎?
|
|
16
|
+
case-sweetluck,PASS,exitIntent,cap,session,mw-case-sweetluck,everything,yes,需要同樣的東西嗎?
|
|
17
|
+
case-amclinic,PASS,exitIntent,cap,session,mw-case-amclinic,everything,yes,需要同樣的東西嗎?
|
|
18
|
+
case-ruinart,PASS,exitIntent,cap,session,mw-case-ruinart,everything,yes,需要同樣的東西嗎?
|
|
19
|
+
case-medical-erp,PASS,exitIntent,cap,session,mw-case-medical-erp,everything,yes,需要同樣的東西嗎?
|
|
20
|
+
case-igoods,PASS,exitIntent,cap,session,mw-case-igoods,everything,yes,需要同樣的東西嗎?
|
|
@@ -323,6 +323,8 @@ https://mosaic.moksaweb.com/moksa/,mk-contact-r-2,_m,grid-template-columns,84px
|
|
|
323
323
|
https://mosaic.moksaweb.com/moksa/,mk-contact-r-2,_m,column-gap,12px,12px,verified
|
|
324
324
|
https://mosaic.moksaweb.com/moksa/,mk-contact-r-3,_m,grid-template-columns,84px 1fr,84px 1fr,verified
|
|
325
325
|
https://mosaic.moksaweb.com/moksa/,mk-contact-r-3,_m,column-gap,12px,12px,verified
|
|
326
|
+
https://mosaic.moksaweb.com/moksa/,mk-colophon,_m,padding-left,18px,18px,verified
|
|
327
|
+
https://mosaic.moksaweb.com/moksa/,mk-colophon,_m,padding-right,18px,18px,verified
|
|
326
328
|
https://mosaic.moksaweb.com/moksa/,mk-header,_t,padding-left,28px,28px,verified
|
|
327
329
|
https://mosaic.moksaweb.com/moksa/,mk-header,_t,padding-right,28px,28px,verified
|
|
328
330
|
https://mosaic.moksaweb.com/moksa/,mk-header,_m,padding-left,18px,18px,verified
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
slider,check,result,detail
|
|
2
|
+
slide+autoplay,HOST,PASS,"<mosaic-slider> animation=slide carousel=None, 3 slides"
|
|
3
|
+
slide+autoplay,TRACK,PASS,<mosaic-slider-slides> aria-live=off
|
|
4
|
+
slide+autoplay,HAS_CURRENT,PASS,slide 0 is current at rest
|
|
5
|
+
slide+autoplay,BULLET_TEMPLATE,PASS,"3 bullets for 3 slides, all with id='sl-dot-0' - one authored bullet is repeated per slide, duplicate ids included"
|
|
6
|
+
slide+autoplay,BULLET_ARIA,PASS,"role=button tabindex=0 labels=ONE / 浮世繪, TWO / 版面, THREE / 色票 - taken from each slide's own title"
|
|
7
|
+
slide+autoplay,ADVANCES,PASS,reached 3 of 3 slides unaided
|
|
8
|
+
slide+autoplay,AUTOPLAY_DECLARED,PASS,data-autoplay=true delay=2200
|
|
9
|
+
slide+autoplay,BULLET_GOES,PASS,bullet 0 -> slide 0
|
|
10
|
+
slide+autoplay,ARROW_ENDS,PASS,"on slide 0 prev hides itself (hidden=True), next does not (hidden=False)"
|
|
11
|
+
slide+autoplay,ARROW_ARIA,PASS,role=button tabindex=0 label='下一張投影片'
|
|
12
|
+
slide+autoplay,NEXT,PASS,slide 0 -> 1
|
|
13
|
+
slide+autoplay,PREV,PASS,slide 1 -> 0
|
|
14
|
+
slide+autoplay,KEYBOARD,PASS,"focus bullet 2 taken, Enter -> slide 2"
|
|
15
|
+
slide+autoplay,REDUCED,PASS,under prefers-reduced-motion it holds still (slide 0 -> 0)
|
|
16
|
+
crossFade+carousel,HOST,PASS,"<mosaic-slider> animation=crossFade carousel=true, 4 slides"
|
|
17
|
+
crossFade+carousel,TRACK,PASS,<mosaic-slider-slides> aria-live=polite
|
|
18
|
+
crossFade+carousel,HAS_CURRENT,PASS,slide 0 is current at rest
|
|
19
|
+
crossFade+carousel,STAYS_PUT,PASS,autoplay off: still on slide 0 after 2.6s
|
|
20
|
+
crossFade+carousel,REDUCED,PASS,under prefers-reduced-motion it holds still (slide 0 -> 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mosaic-headless",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.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",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"author": "moksa (https://moksaweb.com)",
|
|
28
28
|
"type": "module",
|
|
29
29
|
"bin": {
|
|
30
|
-
"mosaic-headless": "
|
|
30
|
+
"mosaic-headless": "scripts/install.mjs"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
|
-
"
|
|
33
|
+
"scripts/",
|
|
34
34
|
"assets/",
|
|
35
35
|
"data/*.csv",
|
|
36
36
|
"references/",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"node": ">=18"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
|
-
"install-skill": "node
|
|
54
|
-
"check": "node
|
|
53
|
+
"install-skill": "node scripts/install.mjs",
|
|
54
|
+
"check": "node scripts/check-release.mjs",
|
|
55
55
|
"preversion": "npm run check",
|
|
56
|
-
"version": "node
|
|
56
|
+
"version": "node scripts/sync-version.mjs && git add SKILL.md assets/templates/platforms .claude-plugin/plugin.json",
|
|
57
57
|
"postversion": "git push && git push --tags",
|
|
58
58
|
"prepublishOnly": "npm run check"
|
|
59
59
|
}
|
|
@@ -133,7 +133,7 @@ then runs as a no-op that writes nothing, because the source skips an unnamed
|
|
|
133
133
|
memory rather than inventing a key. The server reshapes the accepted one into
|
|
134
134
|
`rememberOptions.timelines._.name`, which is what the payload shows.
|
|
135
135
|
|
|
136
|
-
## The interaction shorthand — modals only
|
|
136
|
+
## The interaction shorthand — modals only, and three triggers only
|
|
137
137
|
|
|
138
138
|
Every element's data can carry `interactionShorthand`, but `ModalElementMResourceData`
|
|
139
139
|
is the only class that overrides `getInteractionShorthandActionTypes()` (it returns
|
|
@@ -156,6 +156,19 @@ the type, action from the element — and the page's `mosaicInteractions` gains
|
|
|
156
156
|
with the cap attached. No trigger element, no action, no target: an exit-intent
|
|
157
157
|
popup that fires once a session is one object.
|
|
158
158
|
|
|
159
|
+
**Only three interaction types can be a shorthand**, and the list is in the
|
|
160
|
+
source rather than inferable: `InteractionTypesManager::getShorthandInteractionTypes()`
|
|
161
|
+
returns `pageLoad`, `exitIntent`, `scrollDepth`. Those are exactly the three that do
|
|
162
|
+
not need the element to be visible or interactive first — which a modal never is,
|
|
163
|
+
since it is `display:none` until something opens it.
|
|
164
|
+
|
|
165
|
+
Anything else is **dropped in silence**. A modal given
|
|
166
|
+
`interactionShorthand: {"type": "elementScrollIntoView", …}` renders with its
|
|
167
|
+
`aria-modal`, its `aria-label` and its `data-mosaic-modal-closedby` all correct, the
|
|
168
|
+
page's `mosaicInteractions` carries nothing for it, and it never opens. Measured:
|
|
169
|
+
one page of the nineteen-page site was built that way and read back as the only
|
|
170
|
+
modal on the site with no trigger at all.
|
|
171
|
+
|
|
159
172
|
`"type": "manual"` is the stored answer for "nothing opens this automatically" —
|
|
160
173
|
spelled as a word rather than left absent, because absent means *inherit* on a
|
|
161
174
|
component instance.
|
|
@@ -199,3 +212,64 @@ from the CSS, and a checker must not read "no token" as "the node did not render
|
|
|
199
212
|
**`youtube`'s `privacy` property suppresses the element.** Set on its own it emits
|
|
200
213
|
nothing at all, where 1.0.8 rendered the element and recorded NO_EFFECT. One row of
|
|
201
214
|
`data/node-property-verification.csv`, the only `NO_OUTPUT` in the table.
|
|
215
|
+
|
|
216
|
+
## The worked example's own modal
|
|
217
|
+
|
|
218
|
+
`sites/_moksa.py` carries one: a colophon card shown to a reader about to leave —
|
|
219
|
+
paper ground, hairline border, the document's own trim marks at two corners, a mono
|
|
220
|
+
eyebrow, one accent action and one quiet dismissal. It is built entirely from the
|
|
221
|
+
shorthand, so nothing on the page points at it:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
COLOPHON_NODE_ID = str(uuid.uuid4()) # minted per generation, never fixed
|
|
225
|
+
|
|
226
|
+
{"type": "modal", "nodeID": COLOPHON_NODE_ID,
|
|
227
|
+
"data": {"accessibleLabel": "開始一個專案", "closedby": "everything",
|
|
228
|
+
"interactionShorthand": {
|
|
229
|
+
"type": "exitIntent",
|
|
230
|
+
"exitIntentOptions": {
|
|
231
|
+
"settings": {"awayFor": "500ms"},
|
|
232
|
+
"runs": {"rules": [{"uuid": …, "type": "cap",
|
|
233
|
+
"capOptions": {"max": "1", "remember": "session",
|
|
234
|
+
"name": "mk-colophon"}}]}}}},
|
|
235
|
+
"children": [modal-overlay, modal-window → the card]}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
11 of 11 checks against the live page, and two things it taught on the way:
|
|
239
|
+
|
|
240
|
+
- **The card has to be a `modal-window`, not a styled `div`.** A plain div inside the
|
|
241
|
+
dialog renders and looks right, and `verify_dialog.py` STRUCTURE fails it — the
|
|
242
|
+
window is what Mosaic scrolls and focus-traps.
|
|
243
|
+
- **The design audit failed the first draft.** The heading carried
|
|
244
|
+
`letterSpacing: "-0.02em"`, inherited from the page's Latin display sizes, and
|
|
245
|
+
CJK_NEGATIVE_TRACKING caught it at −0.68px on a Chinese heading at two
|
|
246
|
+
breakpoints. A Han glyph sits on a full em body and is already as close as it is
|
|
247
|
+
meant to be; the line is `0.005em` now, matching the masthead.
|
|
248
|
+
|
|
249
|
+
## A modal per page, each configured differently
|
|
250
|
+
|
|
251
|
+
A nineteen-page site was assembled from converted Elementor pages - one shared
|
|
252
|
+
shell, a menu of `menu-link` nodes, one WordPress page each - and every page given
|
|
253
|
+
its own modal. Six distinct configurations came out of it, and each one is a
|
|
254
|
+
different answer to "when, how often, and how do they get out":
|
|
255
|
+
|
|
256
|
+
| page | trigger | run rule | memory | closedby | what it is for |
|
|
257
|
+
|---|---|---|---|---|---|
|
|
258
|
+
| home | `exitIntent` 500ms | `cap` 1 | session | `everything` | the one offer, once a visit |
|
|
259
|
+
| services | `scrollDepth` 70% | `cooldown` 1 day | forever | `everything` | read most of it, then asked - and not again tomorrow |
|
|
260
|
+
| works | `scrollDepth` 40% | `cap` 2 | session | `clickOutside` | earlier, twice, dismissed by clicking away |
|
|
261
|
+
| plan | `pageLoad` | `firstRunWindow` 10 min | session | `esc` | immediate, but only in the first ten minutes of the visit |
|
|
262
|
+
| about | `exitIntent` 800ms | `cap` 1 | forever | `nothing` | once ever, and the only way out is the button inside |
|
|
263
|
+
| contact | `scrollDepth` 50% | `cap` 3 | page | `everything` | three times per page load, cheap and repeatable |
|
|
264
|
+
|
|
265
|
+
All six were read back out of each page's own `mosaicInteractions` payload
|
|
266
|
+
(`data/page-modal-verification.csv`), and three were then driven in a browser:
|
|
267
|
+
`plan` opened by itself on load and Esc closed it; `services` stayed shut at rest
|
|
268
|
+
and opened past 70% scroll; `about` opened on exit intent and Esc did **not** close
|
|
269
|
+
it, which is what `closedby: "nothing"` promises and what a cookie gate or an age
|
|
270
|
+
gate needs.
|
|
271
|
+
|
|
272
|
+
The pattern is worth stating on its own: because the trigger lives in the modal's
|
|
273
|
+
own `interactionShorthand`, a page's popup policy is one object in that page's data.
|
|
274
|
+
Nothing else on the page changes, no trigger element is placed, and two pages of the
|
|
275
|
+
same site can behave completely differently without sharing anything.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# The slider, measured
|
|
2
|
+
|
|
3
|
+
`sweep_node_types.py` records the whole slider family as `COMMIT_500` or
|
|
4
|
+
`BROKE_PAGE`. That is true of a bare `slider` under a plain `div` and says nothing
|
|
5
|
+
about the component — the same kind of row the accordion probe corrected. Nested
|
|
6
|
+
the way its factories require, every piece commits and renders.
|
|
7
|
+
|
|
8
|
+
`data/slider-verification.csv`: 19 checks across two sliders, all passing.
|
|
9
|
+
|
|
10
|
+
## The shape
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{"type": "slider",
|
|
14
|
+
"data": {"animation": "slide", "duration": "650", "easing": "ease",
|
|
15
|
+
"isAutoplay": "1", "autoplayDelay": "2200", "autoplayLimit": "0",
|
|
16
|
+
"isCarousel": "0", "defaultSlideIndex": "0", "ariaLabel": "…"},
|
|
17
|
+
"children": [
|
|
18
|
+
{"type": "slider-slides", "children": [
|
|
19
|
+
{"type": "slider-slide",
|
|
20
|
+
"data": {"image": "wp-attachment://image/5/full/…", "useAsBackground": "1",
|
|
21
|
+
"alt": "…", "title": "ONE", "positionX": "50%", "positionY": "50%"},
|
|
22
|
+
"children": [ … the caption, which rides inside the slide … ]},
|
|
23
|
+
…]},
|
|
24
|
+
{"type": "slider-navigation", "children": [
|
|
25
|
+
{"type": "slider-navigation-bullet"}]}, // ONE - see below
|
|
26
|
+
{"type": "slider-arrow-left"},
|
|
27
|
+
{"type": "slider-arrow-right"}]}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`animation` is `slide` | `crossFade` | `fadeOut` | `fadeOver`. `autoplayLimit` is how
|
|
31
|
+
many rounds autoplay runs before it stops, `0` meaning no limit. `isCarousel` shows
|
|
32
|
+
more than one slide at a time *and* is what lets the ends wrap.
|
|
33
|
+
|
|
34
|
+
## Two things the markup does not announce
|
|
35
|
+
|
|
36
|
+
**A `slider-navigation-bullet` is a TEMPLATE.** You author one; Mosaic repeats it
|
|
37
|
+
once per slide. Every copy carries the **same id** — three `id="sl-dot-0"` in one
|
|
38
|
+
document — and only the `aria-label` differs, which Mosaic takes from each slide's
|
|
39
|
+
own `title`. Author three bullets and you get three templates. So address a bullet
|
|
40
|
+
by position inside `mosaic-slider-navigation`, never by attrID, and do not expect
|
|
41
|
+
the id to identify one.
|
|
42
|
+
|
|
43
|
+
**An arrow hides itself at the end it cannot pass.** `Slider.js` adds
|
|
44
|
+
`m-slider-arrow--hidden` when the index is 0 (prev) or the last (next), unless the
|
|
45
|
+
slider is a carousel. Autoplay on a non-carousel parks the slider on the last slide,
|
|
46
|
+
so by the time a test reaches for `next` it is legitimately gone — go back to the
|
|
47
|
+
first slide before asking an arrow to move anything. The hidden state is a real
|
|
48
|
+
style state (`___arrow_hidden` in `data/style-states.csv`), so it can be styled.
|
|
49
|
+
|
|
50
|
+
A slide's own `attrID` lands on the inner `.m-slide-content`, not on the
|
|
51
|
+
`<mosaic-slider-slide>` host, and a slide using its image as a background puts an
|
|
52
|
+
`<img>` before that div.
|
|
53
|
+
|
|
54
|
+
## What it does, measured
|
|
55
|
+
|
|
56
|
+
| | |
|
|
57
|
+
|---|---|
|
|
58
|
+
| advances | reached all 3 slides unaided at the declared 2200ms |
|
|
59
|
+
| arrows | `role="button" tabindex="0"` with a localised `aria-label`; next and prev each moved it |
|
|
60
|
+
| bullets | `role="button" tabindex="0"`, labelled from the slide titles; clicking one goes to that slide |
|
|
61
|
+
| keyboard | focusing a bullet and pressing Enter moves the slider — the runtime wires it, a `tabindex` alone would not |
|
|
62
|
+
| track | `<mosaic-slider-slides>` with `aria-live` — `off` while autoplaying, `polite` when it is not |
|
|
63
|
+
| reduced motion | **it holds still on its own.** Mosaic stops the autoplay; the author does not have to |
|
|
64
|
+
|
|
65
|
+
The carousel (`crossFade`, `isCarousel: "1"`, autoplay off) stayed on slide 0 for
|
|
66
|
+
2.6s unaided, which is the other half of the same claim: autoplay is the only thing
|
|
67
|
+
that moves a slider by itself.
|
|
68
|
+
|
|
69
|
+
## Verifying one
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
python tools/verify_slider.py --url https://site/page/ --slider sl-main \
|
|
73
|
+
--autoplay 2200 --csv data/slider-verification.csv
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Everything is addressed by position inside the slider, for the reasons above.
|
|
77
|
+
"Which slide is current" is what the tool reads, because `slide` and `crossFade`
|
|
78
|
+
drive transforms and opacities completely differently and that is the one thing
|
|
79
|
+
both agree on.
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Refuse to publish a package that does not hold together.
|
|
4
4
|
*
|
|
5
|
-
* node
|
|
5
|
+
* node scripts/check-release.mjs
|
|
6
6
|
*
|
|
7
7
|
* Runs as `preversion` and again as `prepublishOnly`, so a release cannot be cut or
|
|
8
8
|
* pushed while any of this is false. Exits non-zero with the reason.
|
|
9
9
|
*
|
|
10
|
-
* The version number exists in
|
|
11
|
-
* frontmatter an agent reads,
|
|
12
|
-
* install. Nothing keeps them together on its own,
|
|
13
|
-
* version it is not is worse than one that reports none.
|
|
14
|
-
* sync-version.mjs to write all
|
|
10
|
+
* The version number exists in four places - package.json, the SKILL.md
|
|
11
|
+
* frontmatter an agent reads, the frontmatter each platform template writes on
|
|
12
|
+
* install, and the Claude plugin manifest. Nothing keeps them together on its own,
|
|
13
|
+
* and a skill that reports a version it is not is worse than one that reports none.
|
|
14
|
+
* `npm version` runs sync-version.mjs to write all four; this asserts it worked.
|
|
15
15
|
*
|
|
16
16
|
* It also checks that every path in package.json `files` exists, that the data files
|
|
17
17
|
* the skill's own claims rest on are present and non-trivial, and that the
|
|
@@ -49,6 +49,20 @@ for (const f of fs.readdirSync(platDir).filter((x) => x.endsWith(".json"))) {
|
|
|
49
49
|
fail(`${f} still names another skill`);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// The Claude plugin manifest carries the version too: the plugin directory lists it
|
|
53
|
+
// and Claude Code pins installs to it, so a stale one serves an old version.
|
|
54
|
+
const manPath = path.join(ROOT, ".claude-plugin", "plugin.json");
|
|
55
|
+
if (!fs.existsSync(manPath)) fail(".claude-plugin/plugin.json missing");
|
|
56
|
+
else {
|
|
57
|
+
const man = JSON.parse(fs.readFileSync(manPath, "utf8"));
|
|
58
|
+
if (man.version !== want) fail(`.claude-plugin/plugin.json version ${man.version} != ${want}`);
|
|
59
|
+
if (man.name !== pkg.name) fail(`.claude-plugin/plugin.json name ${man.name} != ${pkg.name}`);
|
|
60
|
+
}
|
|
61
|
+
// A top-level bin/ goes on the Bash PATH while the plugin is enabled, and claude.ai
|
|
62
|
+
// and Cowork refuse to install a plugin that has one. The npm entry points live in
|
|
63
|
+
// scripts/ for that reason.
|
|
64
|
+
if (fs.existsSync(path.join(ROOT, "bin"))) fail("a top-level bin/ makes claude.ai and Cowork refuse the plugin - keep scripts in scripts/");
|
|
65
|
+
|
|
52
66
|
// ---------- everything the package claims to ship actually exists -----------
|
|
53
67
|
|
|
54
68
|
// `files` entries are globs, not paths - a directory shipped whole is the mistake
|
|
@@ -78,13 +92,16 @@ const counts = {
|
|
|
78
92
|
"data/node-verification.csv": 126,
|
|
79
93
|
"data/style-verification.csv": 98,
|
|
80
94
|
"data/node-property-verification.csv": 191,
|
|
81
|
-
"data/rwd-verification.csv":
|
|
82
|
-
"data/browser-verification.csv":
|
|
95
|
+
"data/rwd-verification.csv": 733,
|
|
96
|
+
"data/browser-verification.csv": 4132,
|
|
83
97
|
"data/style-state-verification.csv": 52,
|
|
84
98
|
"data/interaction-verification.csv": 7,
|
|
85
99
|
"data/data-class-hierarchy.csv": 125,
|
|
86
100
|
"data/custom-fields-verification.csv": 62,
|
|
87
101
|
"data/dialog-verification.csv": 19,
|
|
102
|
+
"data/slider-verification.csv": 19,
|
|
103
|
+
"data/navigation-verification.csv": 27,
|
|
104
|
+
"data/page-modal-verification.csv": 19,
|
|
88
105
|
};
|
|
89
106
|
for (const [file, expected] of Object.entries(counts)) {
|
|
90
107
|
if (!fs.existsSync(path.join(ROOT, file))) { fail(`${file} missing`); continue; }
|
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Write package.json's version into every other place that carries one.
|
|
4
4
|
*
|
|
5
|
-
* node
|
|
5
|
+
* node scripts/sync-version.mjs
|
|
6
6
|
*
|
|
7
7
|
* Runs from the `version` npm lifecycle script, after npm has bumped package.json
|
|
8
8
|
* and before it makes the commit and the tag - so the tagged commit already has all
|
|
9
|
-
*
|
|
9
|
+
* four versions agreeing, and `npm version patch` is the only command anyone has to
|
|
10
10
|
* remember.
|
|
11
11
|
*
|
|
12
12
|
* Targets:
|
|
13
13
|
* SKILL.md frontmatter `version:`
|
|
14
14
|
* assets/templates/platforms/*.json frontmatter.version, where present
|
|
15
|
+
* .claude-plugin/plugin.json version (the Claude plugin directory reads it)
|
|
15
16
|
*
|
|
16
17
|
* check-release.mjs asserts afterwards that this worked, so a silent miss here
|
|
17
18
|
* cannot reach npm.
|
|
@@ -60,4 +61,15 @@ for (const f of fs.readdirSync(platDir).filter((x) => x.endsWith(".json"))) {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
// Claude plugin manifest - the directory listing and Claude Code's version pinning read it
|
|
65
|
+
const manPath = path.join(ROOT, ".claude-plugin", "plugin.json");
|
|
66
|
+
if (fs.existsSync(manPath)) {
|
|
67
|
+
const man = JSON.parse(fs.readFileSync(manPath, "utf8"));
|
|
68
|
+
if (man.version !== v) {
|
|
69
|
+
man.version = v;
|
|
70
|
+
fs.writeFileSync(manPath, JSON.stringify(man, null, 2) + "\n");
|
|
71
|
+
touched.push(".claude-plugin/plugin.json");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
console.log(`synced v${v} into ${touched.length} file(s): ${touched.join(", ") || "none"}`);
|