mosaic-headless 1.20.1 → 1.21.1

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 CHANGED
@@ -160,6 +160,8 @@ commit の成功はページが動く証拠ではないし、正しいスタイ
160
160
  | **スキル自身** | `claude plugin eval .` — ユーザーが実際に尋ねる 5 問を各 3 回、スキルあり/なしの 2 腕、1 回ごとに LLM 審査 3 名。**あり:5 問すべて 1.00。なし:5 問すべて 0.00。** ベースラインの最善の回答は回答拒否だった |
161
161
  | **ライブ計測** | REST ルート 115、variant 156、条件サブジェクト 59、23 テーブル / 210 カラム |
162
162
  | **1.0.9 の新機能** | modal(`<dialog>`)、OpenStreetMap、exit-intent と scroll-depth のトリガー、run rules、memory アクション、modal ショートハンド — すべて実ページに作り、ブラウザで実際に操作:19 / 19。「離脱時ポップアップを 1 セッション 1 回」が配線なしのオブジェクト 1 つで済む |
163
+ | **slider** | ファクトリが要求する入れ子でブラウザ実操作、19 / 19:宣言どおりの間隔で自走、矢印・ドット・キーボードで駆動、`prefers-reduced-motion` では自ら停止。ドットは実はテンプレートで、スライド数だけ複製され id は全部同じ |
164
+ | **本物のサイト** | 変換済み 19 ページを 1 つのサイトに:実メニューとページごとのモーダル。ナビ 46 / 46、モーダル 19 / 19、トリガー/ルール/記憶/閉じ方の組み合わせは 6 種 |
163
165
  | **カスタムフィールド** | ACF と Meta Box、1 ページに 40 フィールド、`@VAR` / `@LOOP` で配信 HTML から読み戻し:62 のうち 59 が解決、3 つの空は理由付き;複数値フィールドのループはフィールドの行数どおりに描画。`tools/list_fields.php` が Mosaic の実際の登録名を出す |
164
166
  | **ライブ更新** | 1.0.7 → 1.0.8 をプラグイン自身の milestone ルートで wp-admin の外から駆動:6 milestone、4 テーブル改名、出力クラス名は全部変わり、`customStyles` は全部書き換え — その結果に対して上の計測をすべて再実行 |
165
167
 
package/README.ko.md CHANGED
@@ -156,6 +156,8 @@ commit 성공은 페이지가 동작한다는 증거가 아니고, 올바른 스
156
156
  | **스킬 자체** | `claude plugin eval .` — 사용자가 실제로 묻는 5문항을 각 3회, 스킬 있음/없음 두 팔로, 매회 LLM 심사 3명. **있음: 5문항 모두 1.00. 없음: 5문항 모두 0.00.** 베이스라인의 최선은 답변 거부였다 |
157
157
  | **라이브 측정** | REST 라우트 115, variant 156, 조건 subject 59, 23 테이블 / 210 컬럼 |
158
158
  | **1.0.9 신기능** | modal(`<dialog>`), OpenStreetMap, exit-intent·scroll-depth 트리거, run rules, memory 액션, modal 숏핸드를 실제 페이지에 만들고 브라우저에서 직접 조작: 19/19. "이탈 의도 팝업, 세션당 한 번"이 배선 없이 객체 하나 |
159
+ | **slider** | 팩토리가 요구하는 중첩으로 브라우저에서 실제 조작, 19/19: 선언한 간격으로 자동 전진, 화살표·불릿·키보드로 구동, `prefers-reduced-motion`에서 스스로 멈춤. 불릿은 사실 템플릿이라 슬라이드 수만큼 복제되고 id가 전부 같다 |
160
+ | **진짜 사이트** | 변환된 19개 페이지를 하나의 사이트로: 실제 메뉴와 페이지별 모달. 내비 46/46, 모달 19/19, 트리거·규칙·기억·닫기 조합 6종 |
159
161
  | **커스텀 필드** | ACF와 Meta Box, 한 페이지에 40개 필드, `@VAR` / `@LOOP`로 전달된 HTML에서 읽어냄: 62개 중 59개 해석, 빈 3개는 이유 있음; 다중값 필드 루프는 필드의 행 수 그대로 렌더링. `tools/list_fields.php`가 Mosaic이 실제 등록하는 이름을 출력 |
160
162
  | **라이브 업그레이드** | 1.0.7 → 1.0.8을 플러그인 자체 milestone 라우트로 wp-admin 밖에서 구동: 6 milestone, 테이블 4개 이름 변경, 출력 클래스 이름 전부 변경, `customStyles` 전부 재작성 — 그 결과 위에서 위의 측정을 전부 다시 실행 |
161
163
 
package/README.md CHANGED
@@ -204,6 +204,8 @@ factories, so Pro types register and render regardless.
204
204
  | **the skill itself** | `claude plugin eval .` — five cases a user would ask, three runs each, with and without the skill loaded, three LLM judges a run. **With: 1.00 on all five. Without: 0.00 on all five.** The baseline's best answer was to refuse |
205
205
  | **measured live** | 115 REST routes, 156 variants, 59 condition subjects, 23 tables / 210 columns |
206
206
  | **1.0.9 features** | the modal (`<dialog>`), OpenStreetMap, the exit-intent and scroll-depth triggers, run rules, memory actions and the modal shorthand, each built on a live page and exercised in a real browser: 19 of 19. An exit-intent popup that fires once a session is one object, no wiring |
207
+ | **slider** | the whole family nested as its factories require and driven in a browser, 19 of 19: advances on its own delay, arrows and bullets and the keyboard move it, holds still under `prefers-reduced-motion`. The bullet turns out to be a template repeated per slide - same id on every copy |
208
+ | **a real site** | the nineteen converted Elementor pages assembled into one site with a real menu and a modal per page: 46 of 46 navigation checks, 19 of 19 modals, six distinct trigger / run-rule / memory / dismissal configurations |
207
209
  | **custom fields** | ACF and Meta Box, forty fields on a page, read back through `@VAR` / `@LOOP` off the delivered HTML: 59 of 62 resolve, 3 empties explained; loops over multi-value fields rendered exactly the field's rows. `tools/list_fields.php` prints the names Mosaic actually registers |
208
210
  | **upgraded live** | 1.0.7 -> 1.0.8 over the plugin's own milestone route, from outside wp-admin: 6 milestones, four tables renamed, every emitted class name changed, every `customStyles` rewritten - then every sweep above re-run on the result |
209
211
 
@@ -289,6 +291,7 @@ and heading level — and it earned its place at once: it caught the converter l
289
291
  | `theme_zip.py` / `theme_zip_compare.php` / `theme_delete.php` | Mosaic's own ZIP export/import from outside the editor, the copy held against the source, and a clean delete that refuses the live theme |
290
292
  | `list_fields.php` | every `@VAR` / `@LOOP` name a post's custom fields register, with values |
291
293
  | `verify_dialog.py` | the 1.0.9 modal, triggers, run rules and memory, driven in a browser |
294
+ | `verify_slider.py` / `verify_navigation.py` | does the slider actually move; is every menu item a real link and every page reachable |
292
295
  | `data_upgrade.py` | after a plugin update, Mosaic's data migration over its own milestone route - the editor API is gone until it runs |
293
296
  | `sweep_*.py` / `probe_*.py` | the instruments the tables were made with |
294
297
  | `bootstrap_probe_theme.php` / `mint_session.php` | a licence-free scratch theme and a REST session from WP-CLI |
package/README.zh-TW.md CHANGED
@@ -181,6 +181,8 @@ commit 成功不代表頁面能用,樣式表正確也不代表。這裡每個
181
181
  | **技能本身** | `claude plugin eval .`——五個使用者真的會問的問題,各跑三次,有載技能和沒載各一臂,每次三個 LLM 裁判。**有:五題全 1.00。沒有:五題全 0.00。** 基準線最好的回答是拒答 |
182
182
  | **線上量測** | 115 條 REST 路由、156 個 variant、59 個條件主體、23 張表 / 210 個欄位 |
183
183
  | **1.0.9 新功能** | modal(`<dialog>`)、OpenStreetMap、exit-intent 與 scroll-depth 觸發、run rules、memory action 與 modal shorthand,全部建在真實頁面上並用瀏覽器實際操作:19 之 19。「離開意圖彈窗、每個 session 只出現一次」只需要一個物件,不用接線 |
184
+ | **slider** | 整組照工廠要求嵌套後在瀏覽器裡實際操作,19 之 19:會自己按宣告的間隔前進、箭頭與圓點與鍵盤都能驅動、`prefers-reduced-motion` 下會自己停。圓點其實是樣板,依 slide 數複製,每個複製品 id 相同 |
185
+ | **真正的網站** | 19 個轉換頁組成一個站:真實選單、每頁一個彈窗。導覽 46 之 46、彈窗 19 之 19,六種不同的觸發/規則/記憶/關閉設定 |
184
186
  | **自訂欄位** | ACF 與 Meta Box,一頁四十個欄位,透過 `@VAR` / `@LOOP` 從送達的 HTML 讀回:62 個裡 59 個解析正確、3 個空值有解釋;多值欄位的迴圈剛好渲染出欄位的列數。`tools/list_fields.php` 列出 Mosaic 真正註冊的名字 |
185
187
  | **線上升級** | 1.0.7 → 1.0.8 走外掛自己的 milestone 路由、在 wp-admin 之外驅動:6 個 milestone、四張表改名、每個輸出 class 名稱都變、每個 `customStyles` 都重寫——然後上面每一項掃描在結果上重跑 |
186
188
 
@@ -256,6 +258,7 @@ button / html / icon-list / divider / image 佔了全部元素的 99.6%。長尾
256
258
  | `theme_zip.py` / `theme_zip_compare.php` / `theme_delete.php` | 在編輯器外驅動 Mosaic 自己的 ZIP 匯出匯入、副本對來源逐樹比對、以及拒絕刪 live 主題的乾淨刪除 |
257
259
  | `list_fields.php` | 一篇文章的自訂欄位註冊了哪些 `@VAR` / `@LOOP` 名字,連值一起列出 |
258
260
  | `verify_dialog.py` | 在瀏覽器裡實際操作 1.0.9 的 modal、觸發、run rules 與 memory |
261
+ | `verify_slider.py` / `verify_navigation.py` | slider 真的會動嗎;選單每一項是不是真連結、每頁是不是都到得了 |
259
262
  | `data_upgrade.py` | 外掛更新後,走 Mosaic 自己的 milestone 路由跑資料遷移——沒跑完之前編輯器 API 是不存在的 |
260
263
  | `sweep_*.py` / `probe_*.py` | 那些表格是用這些儀器量出來的 |
261
264
  | `bootstrap_probe_theme.php` / `mint_session.php` | 免授權的實驗主題,以及從 WP-CLI 鑄出 REST session |
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.20.1"
7
+ version: "1.21.1"
8
8
  ---
9
9
 
10
10
  # Headless Mosaic
@@ -172,6 +172,28 @@ DESIGN SYS variants and collection variables both verified against compiled
172
172
  :root{--brand: rgb(9, 99, 199)} with background-color:var(--brand).
173
173
  references/design-system.md
174
174
 
175
+ SLIDER the slider family nested as its factories require, driven in a
176
+ browser, 19 of 19 across two configurations: it advances unaided on
177
+ its declared delay, the arrows and bullets move it, Enter on a
178
+ focused bullet moves it, and under prefers-reduced-motion it holds
179
+ still on its own. The sweep calls the whole family COMMIT_500 /
180
+ BROKE_PAGE, which is what a bare `slider` does under a plain div and
181
+ nothing about the component - the same kind of row the accordion
182
+ probe corrected. Two facts no markup announces: a
183
+ slider-navigation-bullet is a TEMPLATE, repeated once per slide with
184
+ the SAME id on every copy and an aria-label taken from each slide's
185
+ own title; and an arrow hides ITSELF at the end it cannot pass.
186
+ data/slider-verification.csv
187
+
188
+ SITE the nineteen converted Elementor pages assembled into one real site:
189
+ a shared shell, a menu of `menu-link` nodes that render real
190
+ <a href>, a WordPress page each, and a modal on every page - six
191
+ distinct configurations across pageLoad / exitIntent / scrollDepth,
192
+ cap / cooldown / firstRunWindow, page / session / forever memory and
193
+ four closedby policies. 46 of 46 navigation checks (including no page scrolling sideways at three widths), 19 of 19 pages
194
+ carrying a working modal. data/navigation-verification.csv,
195
+ data/page-modal-verification.csv
196
+
175
197
  DIALOG 1.0.9's features built on a live page and read back out of the
176
198
  browser, 19 of 19: the modal renders a real <dialog> (aria-modal
177
199
  appears only with an overlay child), a modalOpen action opens it and
@@ -469,12 +491,14 @@ so the pattern is in the data, not just in this paragraph.
469
491
  10. `references/interactions.md` — the JavaScript animation system, and how far it is
470
492
  verified.
471
493
  11. `references/vs-elementor-gutenberg.md` — which builder habits transfer.
472
- 12. `references/custom-fields.md` — ACF and Meta Box fields as `@VAR` / `@LOOP`:
494
+ 12. `references/slider.md` — the slider nested as its factories require, and the
495
+ two things about it the markup does not announce.
496
+ 13. `references/custom-fields.md` — ACF and Meta Box fields as `@VAR` / `@LOOP`:
473
497
  the names, what each field type resolves to, and the loop element that walks
474
498
  a multi-value field. Measured, 62 rows.
475
- 13. `references/dialog-and-triggers.md` — 1.0.9's modal, OpenStreetMap, the
499
+ 14. `references/dialog-and-triggers.md` — 1.0.9's modal, OpenStreetMap, the
476
500
  exit-intent / scroll-depth triggers, run rules, memory and the shorthand.
477
- 14. `references/upgrading.md` — what a plugin update does to the data and to the
501
+ 15. `references/upgrading.md` — what a plugin update does to the data and to the
478
502
  delivered page, measured on 1.0.7 -> 1.0.8; how to drive the migration and what
479
503
  to re-verify afterwards.
480
504
 
@@ -505,6 +529,9 @@ so the pattern is in the data, not just in this paragraph.
505
529
  | `data/conversion-verification.csv` | 8 | **converted then checked live** - an Elementor page rebuilt as Mosaic and held against its source (text, images, links, heading levels), then put through rwd, browser and the design audit with every finding classified inherited-or-introduced |
506
530
  | `data/conversion-batch.csv` | 19 | **converted, built and checked live, one page after another** - every Elementor page of a production site through the converter, with per-page element and content counts |
507
531
  | `data/token-benchmark.csv` | 6 | **measured with tiktoken** - the same six lookups priced three ways: reading the plugin source, loading every table, querying `mo.py`. 71-99.5% fewer tokens than the source and 99.6%+ fewer than the tables, which total 259,539 - never load them, query them |
532
+ | `data/slider-verification.csv` | 19 | **driven live** - two sliders, one autoplaying and one a carousel: advance, arrows, bullets, keyboard, reduced motion |
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
+ | `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 |
508
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 |
509
536
  | `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 |
510
537
  | `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 |
@@ -673,10 +700,32 @@ post — `build_all.py` resets first for that reason.
673
700
  rows and `theme_zip_compare.php` reports a tree that does not match - delete
674
701
  nodes with a missing parent REPEATEDLY until none are left, because each pass
675
702
  orphans the next level down.
703
+ - **A width copied out of Elementor needs `maxWidth: "100%"` beside it.**
704
+ Elementor caps every widget with `max-width:100%`, so a px width there is a
705
+ desktop intent its own CSS already constrains; carried across bare it keeps the
706
+ full 600px and the page scrolls sideways on a phone. `verify_rwd` cannot see it
707
+ - the declaration did reach the stylesheet - and nineteen converted pages were
708
+ green on every per-page check while all nineteen overflowed at 390px.
709
+ - **Only three interaction types can be an `interactionShorthand`**: `pageLoad`,
710
+ `exitIntent`, `scrollDepth` (`InteractionTypesManager::getShorthandInteractionTypes`).
711
+ Any other is dropped in silence - the modal renders with its aria and its
712
+ `closedby`, the page carries no interaction, and nothing ever opens it. The
713
+ three that remain are exactly the ones that do not need the element to be
714
+ visible first, which a modal never is.
715
+ - **A `slider-navigation-bullet` is a template, not a list item.** One authored
716
+ bullet is repeated once per slide, every copy carrying the SAME id - writing
717
+ three gets three templates and a document full of duplicate ids. The
718
+ `aria-label` is Mosaic's, taken from each slide's own `title`. An arrow hides
719
+ itself at the end it cannot pass unless the slider is a carousel, so a test
720
+ that clicks `next` after autoplay has parked the slider finds nothing there.
676
721
  - **An interaction that targets another element stores that element's NODE id**,
677
- not its attrID, so both have to be written in one pass (`nodeID` on a spec
678
- node pins it). The id must be unique in the THEME - `wp_mosaic_nodes`' primary
679
- key is `(ID, themeID)` - so mint it per run rather than hard-coding one.
722
+ not its attrID, so both have to be written in one pass (`nodeID` on a spec node
723
+ pins it). The pin means "these two halves must agree", never "this uuid must be
724
+ the one in the database" - `wp_mosaic_nodes`' key is `(ID, themeID)`, so a fixed
725
+ id makes the spec single-use and the SECOND build dies on a duplicate key with
726
+ the page half written. `build_page.refresh_node_ids()` now rewrites every pin and
727
+ every reference to it at build time, so a spec with pins rebuilds as many times
728
+ as you like.
680
729
  - **Since 1.0.9 the three per-breakpoint stylesheets are one.**
681
730
  `mosaic-theme-document-styles-inline-css` carries the base rules and the
682
731
  `@media` blocks together; the old `mosaic-theme-block-editor-styles_<bp>-inline-css`
@@ -749,6 +798,8 @@ post — `build_all.py` resets first for that reason.
749
798
  | `theme_zip.py` | drive Mosaic's OWN export/import - the ZIP the editor makes, attachments included, over the milestone protocol; import lands in test mode unless told `--activate` |
750
799
  | `theme_zip_compare.php` | hold an imported copy against its source, tree for tree - every scoped table, the (parentType, type) shape, which ids survive, and orphans named rather than counted |
751
800
  | `theme_delete.php` | remove a theme completely through the plugin's own routine; refuses the live one |
801
+ | `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 |
802
+ | `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 |
752
803
  | `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 |
753
804
  | `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`) |
754
805
  | `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 |
@@ -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.20.1"
23
+ "version": "1.21.1"
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.20.1"
23
+ "version": "1.21.1"
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.20.1"
23
+ "version": "1.21.1"
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.20.1"
23
+ "version": "1.21.1"
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.20.1"
23
+ "version": "1.21.1"
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,
@@ -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 (60 below the fold and 1 on a scroll timeline awaiting it, 1 mid-blink)"
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,"9 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"
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=6;--mk-s=0;--mk-m=0,visible,none,True,mk-boot-num,7
15
- 2200,--mk-n=41;--mk-s=0;--mk-m=0,visible,none,True,mk-boot-num,3
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
16
  3000,--mk-n=71;--mk-s=0;--mk-m=0,visible,none,True,mk-boot-num,3
17
- 3800,--mk-n=88;--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
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
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,0
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,47 @@
1
+ check,result,detail
2
+ FRONT_PAGE,PASS,"HTTP 200, 112365 bytes"
3
+ MENU_IS_LINKS,PASS,6 real <a href> in #mw-nav
4
+ NAV 首頁,PASS,"// -> HTTP 200, 112360 bytes"
5
+ NAV 服務,PASS,"/services/ -> HTTP 200, 156950 bytes"
6
+ NAV 作品,PASS,"/works/ -> HTTP 200, 109070 bytes"
7
+ NAV 報價,PASS,"/plan/ -> HTTP 200, 181775 bytes"
8
+ NAV 關於,PASS,"/about/ -> HTTP 200, 66293 bytes"
9
+ NAV 聯絡,PASS,"/contact/ -> HTTP 200, 49689 bytes"
10
+ PAGE home,PASS,"HTTP 200, 112741 bytes"
11
+ PAGE services,PASS,"HTTP 200, 156570 bytes"
12
+ PAGE mowp,PASS,"HTTP 200, 103205 bytes"
13
+ PAGE weebly-migration,PASS,"HTTP 200, 96324 bytes"
14
+ PAGE works,PASS,"HTTP 200, 109452 bytes"
15
+ PAGE plan,PASS,"HTTP 200, 182156 bytes"
16
+ PAGE about,PASS,"HTTP 200, 65912 bytes"
17
+ PAGE teams,PASS,"HTTP 200, 68157 bytes"
18
+ PAGE open-source,PASS,"HTTP 200, 115144 bytes"
19
+ PAGE order-notify-guide,PASS,"HTTP 200, 40027 bytes"
20
+ PAGE contact,PASS,"HTTP 200, 49689 bytes"
21
+ PAGE case-drpen,PASS,"HTTP 200, 89163 bytes"
22
+ PAGE case-lyycamp,PASS,"HTTP 200, 88098 bytes"
23
+ PAGE case-em925,PASS,"HTTP 200, 86740 bytes"
24
+ PAGE case-sweetluck,PASS,"HTTP 200, 87603 bytes"
25
+ PAGE case-amclinic,PASS,"HTTP 200, 86880 bytes"
26
+ PAGE case-ruinart,PASS,"HTTP 200, 87120 bytes"
27
+ PAGE case-medical-erp,PASS,"HTTP 200, 88285 bytes"
28
+ PAGE case-igoods,PASS,"HTTP 200, 86334 bytes"
29
+ WIDTH home,PASS,"no sideways scroll at 390,768,1280"
30
+ WIDTH services,PASS,"no sideways scroll at 390,768,1280"
31
+ WIDTH mowp,PASS,"no sideways scroll at 390,768,1280"
32
+ WIDTH weebly-migration,PASS,"no sideways scroll at 390,768,1280"
33
+ WIDTH works,PASS,"no sideways scroll at 390,768,1280"
34
+ WIDTH plan,PASS,"no sideways scroll at 390,768,1280"
35
+ WIDTH about,PASS,"no sideways scroll at 390,768,1280"
36
+ WIDTH teams,PASS,"no sideways scroll at 390,768,1280"
37
+ WIDTH open-source,PASS,"no sideways scroll at 390,768,1280"
38
+ WIDTH order-notify-guide,PASS,"no sideways scroll at 390,768,1280"
39
+ WIDTH contact,PASS,"no sideways scroll at 390,768,1280"
40
+ WIDTH case-drpen,PASS,"no sideways scroll at 390,768,1280"
41
+ WIDTH case-lyycamp,PASS,"no sideways scroll at 390,768,1280"
42
+ WIDTH case-em925,PASS,"no sideways scroll at 390,768,1280"
43
+ WIDTH case-sweetluck,PASS,"no sideways scroll at 390,768,1280"
44
+ WIDTH case-amclinic,PASS,"no sideways scroll at 390,768,1280"
45
+ WIDTH case-ruinart,PASS,"no sideways scroll at 390,768,1280"
46
+ WIDTH case-medical-erp,PASS,"no sideways scroll at 390,768,1280"
47
+ WIDTH case-igoods,PASS,"no sideways scroll at 390,768,1280"
@@ -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,需要同樣的東西嗎?
@@ -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.20.1",
3
+ "version": "1.21.1",
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",
@@ -68,8 +68,11 @@ which the page resolves to `{"targetSelectorData": [{"type": "all", "selector":
68
68
  `nodeID` on any spec node for exactly this.
69
69
  - **That id must be unique in the THEME, not the document.** `wp_mosaic_nodes`'
70
70
  primary key is `(ID, themeID)`, so a hard-coded id collides with whatever an
71
- earlier build left behind — the commit answers HTTP 500 on a duplicate key. Mint
72
- it per run.
71
+ earlier build left behind — the commit answers HTTP 500 on a duplicate key, with
72
+ the page already half written. `build_page.refresh_node_ids()` rewrites every
73
+ pinned `nodeID` and every reference to it as the tree is built, so the pin keeps
74
+ doing its real job (two halves of one spec agreeing) without claiming the uuid
75
+ itself, and a spec carrying pins can be rebuilt any number of times.
73
76
 
74
77
  `type` accepts `element`, `variant` or `universalClass` — the last two aim an action
75
78
  at every element of a class.
@@ -133,7 +136,7 @@ then runs as a no-op that writes nothing, because the source skips an unnamed
133
136
  memory rather than inventing a key. The server reshapes the accepted one into
134
137
  `rememberOptions.timelines._.name`, which is what the payload shows.
135
138
 
136
- ## The interaction shorthand — modals only
139
+ ## The interaction shorthand — modals only, and three triggers only
137
140
 
138
141
  Every element's data can carry `interactionShorthand`, but `ModalElementMResourceData`
139
142
  is the only class that overrides `getInteractionShorthandActionTypes()` (it returns
@@ -156,6 +159,19 @@ the type, action from the element — and the page's `mosaicInteractions` gains
156
159
  with the cap attached. No trigger element, no action, no target: an exit-intent
157
160
  popup that fires once a session is one object.
158
161
 
162
+ **Only three interaction types can be a shorthand**, and the list is in the
163
+ source rather than inferable: `InteractionTypesManager::getShorthandInteractionTypes()`
164
+ returns `pageLoad`, `exitIntent`, `scrollDepth`. Those are exactly the three that do
165
+ not need the element to be visible or interactive first — which a modal never is,
166
+ since it is `display:none` until something opens it.
167
+
168
+ Anything else is **dropped in silence**. A modal given
169
+ `interactionShorthand: {"type": "elementScrollIntoView", …}` renders with its
170
+ `aria-modal`, its `aria-label` and its `data-mosaic-modal-closedby` all correct, the
171
+ page's `mosaicInteractions` carries nothing for it, and it never opens. Measured:
172
+ one page of the nineteen-page site was built that way and read back as the only
173
+ modal on the site with no trigger at all.
174
+
159
175
  `"type": "manual"` is the stored answer for "nothing opens this automatically" —
160
176
  spelled as a word rather than left absent, because absent means *inherit* on a
161
177
  component instance.
@@ -232,3 +248,31 @@ COLOPHON_NODE_ID = str(uuid.uuid4()) # minted per generation, never fixed
232
248
  CJK_NEGATIVE_TRACKING caught it at −0.68px on a Chinese heading at two
233
249
  breakpoints. A Han glyph sits on a full em body and is already as close as it is
234
250
  meant to be; the line is `0.005em` now, matching the masthead.
251
+
252
+ ## A modal per page, each configured differently
253
+
254
+ A nineteen-page site was assembled from converted Elementor pages - one shared
255
+ shell, a menu of `menu-link` nodes, one WordPress page each - and every page given
256
+ its own modal. Six distinct configurations came out of it, and each one is a
257
+ different answer to "when, how often, and how do they get out":
258
+
259
+ | page | trigger | run rule | memory | closedby | what it is for |
260
+ |---|---|---|---|---|---|
261
+ | home | `exitIntent` 500ms | `cap` 1 | session | `everything` | the one offer, once a visit |
262
+ | services | `scrollDepth` 70% | `cooldown` 1 day | forever | `everything` | read most of it, then asked - and not again tomorrow |
263
+ | works | `scrollDepth` 40% | `cap` 2 | session | `clickOutside` | earlier, twice, dismissed by clicking away |
264
+ | plan | `pageLoad` | `firstRunWindow` 10 min | session | `esc` | immediate, but only in the first ten minutes of the visit |
265
+ | about | `exitIntent` 800ms | `cap` 1 | forever | `nothing` | once ever, and the only way out is the button inside |
266
+ | contact | `scrollDepth` 50% | `cap` 3 | page | `everything` | three times per page load, cheap and repeatable |
267
+
268
+ All six were read back out of each page's own `mosaicInteractions` payload
269
+ (`data/page-modal-verification.csv`), and three were then driven in a browser:
270
+ `plan` opened by itself on load and Esc closed it; `services` stayed shut at rest
271
+ and opened past 70% scroll; `about` opened on exit intent and Esc did **not** close
272
+ it, which is what `closedby: "nothing"` promises and what a cookie gate or an age
273
+ gate needs.
274
+
275
+ The pattern is worth stating on its own: because the trigger lives in the modal's
276
+ own `interactionShorthand`, a page's popup policy is one object in that page's data.
277
+ Nothing else on the page changes, no trigger element is placed, and two pages of the
278
+ 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.
@@ -71,3 +71,22 @@ revision is rejected, and the rejection arrives as HTTP 200. See
71
71
  - An Elementor site, page-level construction → `elementor-headless`.
72
72
  - A Mosaic site → here. The write path and all 74 free node types are measured; the
73
73
  48 Pro-only types are extracted from source but not render-verified.
74
+
75
+ ## One thing the converter has to add, not copy
76
+
77
+ Elementor's own stylesheet caps every widget with `max-width: 100%`. So a width
78
+ set in the editor - `_element_custom_width: {size: 600, unit: px}`, with no mobile
79
+ value beside it - is a DESKTOP INTENT: Elementor renders it as 346px inside a 346px
80
+ column on a phone, and the author never sees a problem.
81
+
82
+ Copy that width across on its own and the element keeps all 600px. Nineteen
83
+ converted pages did exactly that: at 390px the document was 662px wide and every
84
+ page scrolled sideways, while the Elementor original at the same viewport did not.
85
+ Nothing in the per-page checks caught it, and that is the instructive part -
86
+ `verify_rwd` asks whether a declaration reached the stylesheet, and a 640px width
87
+ on a phone reaches it perfectly.
88
+
89
+ `from_elementor.py` now emits `maxWidth: "100%"` beside any px `width` it carries
90
+ over, after the element handlers have had their say so a width a handler supplies
91
+ is capped too. `verify_navigation.py --viewports 390,768,1280` opens every page of
92
+ a site and fails the one that scrolls sideways, naming the widest element in it.
@@ -99,6 +99,9 @@ const counts = {
99
99
  "data/data-class-hierarchy.csv": 125,
100
100
  "data/custom-fields-verification.csv": 62,
101
101
  "data/dialog-verification.csv": 19,
102
+ "data/slider-verification.csv": 19,
103
+ "data/navigation-verification.csv": 46,
104
+ "data/page-modal-verification.csv": 19,
102
105
  };
103
106
  for (const [file, expected] of Object.entries(counts)) {
104
107
  if (!fs.existsSync(path.join(ROOT, file))) { fail(`${file} missing`); continue; }
package/sites/_moksa.py CHANGED
@@ -2694,8 +2694,15 @@ COLOPHON = {
2694
2694
  }
2695
2695
 
2696
2696
 
2697
+ INTRO_TREE = {"type": "div", "data": {"attrID": "mk-intro-page"},
2698
+ "children": [BOOT, box("mk-doc", {}, [MARKS, MASTHEAD, TICKER_BAND,
2699
+ BOARD, CONTACT])]}
2700
+
2701
+
2697
2702
  HOME_TREE = {"type": "div", "data": {"attrID": "mk-home"},
2698
- "children": [BOOT, box("mk-doc", {}, [
2703
+ # The entrance sequence is withdrawn for now. BOOT and its CSS are
2704
+ # still here, unreferenced: putting it back is adding it to this list.
2705
+ "children": [box("mk-doc", {}, [
2699
2706
  MARKS, INDEX, CUE, LOOP_WINDOW, box("mk-sweep", {}, []),
2700
2707
  # paper, panel, paper, ink, paper, ink, paper - the page
2701
2708
  # changes ground five times so it reads as chapters
@@ -3021,6 +3028,13 @@ SITE = {
3021
3028
  "tree": apply_type(HOME_TREE, DISPLAY, CJK, "600")},
3022
3029
  {"slug": "my-account", "post_id": 9, "title": "My account",
3023
3030
  "tree": apply_type(ACCOUNT_TREE, DISPLAY, CJK, "600")},
3031
+ # The entrance sequence, on a page of its own. It was the homepage's and
3032
+ # cost every visitor eight seconds before the document appeared, which is
3033
+ # the wrong trade for a page people come back to; withdrawing it entirely
3034
+ # would also have retired a measurement that is still true, so it lives
3035
+ # here instead and verify_intro.py still has something to measure.
3036
+ {"slug": "intro", "post_id": 245, "title": "Entrance sequence",
3037
+ "tree": apply_type(INTRO_TREE, DISPLAY, CJK, "600")},
3024
3038
  ],
3025
3039
  }
3026
3040