mosaic-headless 1.27.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -4,7 +4,7 @@ description: |
4
4
  Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface with `mo.py`, which joins every source table to the live sweeps so a lookup leads with the measured verdict rather than the declaration (128 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.27.0"
7
+ version: "1.28.0"
8
8
  ---
9
9
 
10
10
  # Headless Mosaic
@@ -530,14 +530,15 @@ so the pattern is in the data, not just in this paragraph.
530
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 |
531
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
532
  | `data/slider-verification.csv` | 30 | **driven live** - two sliders, one autoplaying and one a carousel: advance, arrows, bullets, keyboard, reduced motion |
533
- | `data/navigation-verification.csv` | 36 | **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 |
533
+ | `data/navigation-verification.csv` | 39 | **fetched and opened live** - every menu item is a real `<a href>`, every page of the nineteen answers 200, and no page scrolls sideways at 390 / 768 / 1280 |
534
534
  | `data/page-modal-verification.csv` | 19 | **read live** - one modal per page, and what each one's trigger, run rule, memory scope and dismissal policy actually came out as |
535
535
  | `data/dialog-verification.csv` | 19 | **driven live** - the 1.0.9 modal, its triggers, its run rules and the memory actions, each exercised in a real browser |
536
536
  | `data/dialog-form-verification.csv` | 49 | **driven live** - the same three nodes made into five FORMS (sheet, drawer, corner, takeover, gate), each re-opened at 390px and 768px; also where `pickOne` on a click turned out to be a latch rather than a lottery |
537
537
  | `data/token-verification.csv` | 15 | **driven live** - every `:root` custom property declared once, non-empty, and resolved by something on the page; a token nothing points at is a token that is not working |
538
- | `data/mobile-audit.csv` | 60 | **driven live** - what a sideways-scroll check cannot see: type under the legible floor, controls too small to hit, a grid that never collapsed, content escaping its box |
538
+ | `data/mobile-audit.csv` | 63 | **driven live** - what a sideways-scroll check cannot see: type under the legible floor, controls too small to hit, a grid that never collapsed, content escaping its box |
539
539
  | `data/component-wall-verification.csv` | 13 | **driven live** - tabs switched, an accordion opened, a form's fields and Mosaic's own honeypot, and an OpenStreetMap asserted on the coordinates it was GIVEN rather than the default it falls back to |
540
540
  | `data/form-surface-verification.csv` | 8 | **driven live** - every field type Mosaic ships in one form: select with labelled options, radio and checkbox groups, file upload, and the browser's own validation refusing an empty submit |
541
+ | `data/content-loop-verification.csv` | 10 | **driven live** - a slider, a tab bar, a list and a paginated archive whose children all come from one authored template repeated per post; the pagination is clicked and the rows asserted to CHANGE |
541
542
  | `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 |
542
543
  | `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 |
543
544
  | `data/node-type-notes.csv` | 8 | where a sweep outcome is true but misleading on its own, why. Surfaced by `mo.py type` |
@@ -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.27.0"
23
+ "version": "1.28.0"
24
24
  },
25
25
  "loaderBehaviour": "Upload via Settings -> Skills -> Upload. Claude.ai parses SKILL.md frontmatter and surfaces the skill in your library. The extraction tool (extract-block-schema.php) needs a live WP-CLI connection and won't run in the sandbox; use it from a local terminal against your own site instead.",
26
26
  "uploadSteps": [
@@ -20,7 +20,7 @@
20
20
  "description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
21
21
  "license": "MIT",
22
22
  "author": "moksa (https://moksaweb.com)",
23
- "version": "1.27.0"
23
+ "version": "1.28.0"
24
24
  },
25
25
  "loaderBehaviour": "Auto-loads on session start when SKILL.md frontmatter parses successfully.",
26
26
  "verified": true,
@@ -20,7 +20,7 @@
20
20
  "description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
21
21
  "license": "MIT",
22
22
  "author": "moksa (https://moksaweb.com)",
23
- "version": "1.27.0"
23
+ "version": "1.28.0"
24
24
  },
25
25
  "loaderBehaviour": "Confirmed (2026-07-11): Codex CLI natively supports the SKILL.md spec. Place SKILL.md under .codex/skills/<name>/ (project) or ~/.codex/skills/<name>/ (personal) and Codex loads the name+description at session start, then the full body on demand. A parallel, broader convention .agents/skills/ (searched from cwd up to repo root, then ~/.agents/skills/) also exists across multiple tools - if your Codex CLI version prioritizes that path instead, mirror the same SKILL.md there.",
26
26
  "verified": true,
@@ -20,7 +20,7 @@
20
20
  "description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
21
21
  "license": "MIT",
22
22
  "author": "moksa (https://moksaweb.com)",
23
- "version": "1.27.0"
23
+ "version": "1.28.0"
24
24
  },
25
25
  "loaderBehaviour": "CHANGED as of 2026-07-11: GitHub Copilot added a proper '.github/skills/' Agent Skills directory (December 2025), alongside the older single-file .github/copilot-instructions.md convention. This config targets the new skills-directory form. If your Copilot version predates this (pre Dec 2025), use the instructions-append fallback instead (see fallback below).",
26
26
  "fallback": {
@@ -20,7 +20,7 @@
20
20
  "description": "Build and modify Mosaic Pro (Nextend) sites by writing the underlying data model directly - no visual editor, no DOM. Query the real surface (122 node types, 181 node properties, 98 style properties, 53 style states, 151 element classes, 74 dynamic variables, 114 REST routes, 23 tables) instead of guessing, with every node type placed on a live site one at a time and asserted against the delivered HTML, every style and node property swept against the compiled CSS and markup, and 569 responsive declarations checked against the stylesheet the site actually served.",
21
21
  "license": "MIT",
22
22
  "author": "moksa (https://moksaweb.com)",
23
- "version": "1.27.0"
23
+ "version": "1.28.0"
24
24
  },
25
25
  "loaderBehaviour": "CHANGED as of 2026-07-11: Gemini CLI now natively supports the same SKILL.md standard as Claude Code and Codex CLI - the same directory-based skill works unmodified. Gemini CLI discovers skills in this precedence order: built-in, extension skills, ~/.gemini/skills/ (personal), .gemini/skills/ (project, shared via version control). At session start Gemini injects each discovered skill's name+description into the system prompt and calls activate_skill when a task matches.",
26
26
  "verified": true,
@@ -0,0 +1,11 @@
1
+ check,result,detail
2
+ TABS,SKIPPED,not on this page
3
+ ACCORDION,SKIPPED,not on this page
4
+ FORM,SKIPPED,not on this page
5
+ LOOP/slider,PASS,6 slides from one authored slider-slide
6
+ LOOP/tabs,PASS,4 tabs from one authored tabs-tab
7
+ LOOP/tab_labels_intact,PASS,"no replacement characters - @substr counts bytes, so a length that is not a multiple of the encoding cuts a glyph in half"
8
+ LOOP/pagination_links,PASS,"3 page links: ['v=5499&notes=1', 'v=5499&notes=2', 'v=5499&notes=3']"
9
+ LOOP/pagination_key_is_safe,PASS,paginationKey does not collide with a WordPress query var
10
+ LOOP/pagination_changes_rows,PASS,page 1 3 -> page 2 3
11
+ MAP,SKIPPED,not on this page
@@ -1,4 +1,5 @@
1
1
  path,width,finding,element,detail
2
+ content/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
2
3
  forms/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
3
4
  components/,390,GRID_NOT_COLLAPSED,cp-tabs-menu,2 columns at 390px
4
5
  components/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
@@ -17,6 +18,8 @@ sliders/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
17
18
  tokens/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
18
19
  dynamic/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
19
20
  conversion/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
21
+ content/,768,LONG_MEASURE,m-text,43 chars/line
22
+ content/,768,LONG_MEASURE,m-text,58 chars/line
20
23
  forms/,768,LONG_MEASURE,m-text,59 chars/line
21
24
  components/,768,LONG_MEASURE,m-text,44 chars/line
22
25
  components/,768,LONG_MEASURE,m-text,56 chars/line
@@ -1,23 +1,25 @@
1
1
  check,result,detail
2
- FRONT_PAGE,PASS,"HTTP 200, 240166 bytes"
2
+ FRONT_PAGE,PASS,"HTTP 200, 240862 bytes"
3
3
  MENU_IS_LINKS,PASS,4 real <a href> in #mk-nav
4
- NAV 服務,PASS,"/#services -> HTTP 200, 240262 bytes"
5
- NAV 作品,PASS,"/#works -> HTTP 200, 240262 bytes"
6
- NAV 產品,PASS,"/#products -> HTTP 200, 240262 bytes"
7
- NAV 聯絡,PASS,"/#contact -> HTTP 200, 240262 bytes"
8
- PAGE moksa,PASS,"HTTP 200, 240166 bytes"
9
- PAGE my-account,PASS,"HTTP 200, 100372 bytes"
10
- PAGE intro,PASS,"HTTP 200, 134080 bytes"
11
- PAGE forms,PASS,"HTTP 200, 100299 bytes"
12
- PAGE components,PASS,"HTTP 200, 114860 bytes"
13
- PAGE conversion,PASS,"HTTP 200, 102160 bytes"
14
- PAGE dynamic,PASS,"HTTP 200, 112338 bytes"
15
- PAGE tokens,PASS,"HTTP 200, 124749 bytes"
16
- PAGE sliders,PASS,"HTTP 200, 151524 bytes"
17
- PAGE dialogs,PASS,"HTTP 200, 206683 bytes"
4
+ NAV 服務,PASS,"/#services -> HTTP 200, 240958 bytes"
5
+ NAV 作品,PASS,"/#works -> HTTP 200, 240958 bytes"
6
+ NAV 產品,PASS,"/#products -> HTTP 200, 240958 bytes"
7
+ NAV 聯絡,PASS,"/#contact -> HTTP 200, 240958 bytes"
8
+ PAGE moksa,PASS,"HTTP 200, 240862 bytes"
9
+ PAGE my-account,PASS,"HTTP 200, 101069 bytes"
10
+ PAGE intro,PASS,"HTTP 200, 134776 bytes"
11
+ PAGE content,PASS,"HTTP 200, 102296 bytes"
12
+ PAGE forms,PASS,"HTTP 200, 101374 bytes"
13
+ PAGE components,PASS,"HTTP 200, 115556 bytes"
14
+ PAGE conversion,PASS,"HTTP 200, 103234 bytes"
15
+ PAGE dynamic,PASS,"HTTP 200, 112654 bytes"
16
+ PAGE tokens,PASS,"HTTP 200, 125824 bytes"
17
+ PAGE sliders,PASS,"HTTP 200, 151841 bytes"
18
+ PAGE dialogs,PASS,"HTTP 200, 207378 bytes"
18
19
  ANCHORS moksa,PASS,every in-page link resolves
19
20
  ANCHORS my-account,PASS,every in-page link resolves
20
21
  ANCHORS intro,PASS,every in-page link resolves
22
+ ANCHORS content,PASS,every in-page link resolves
21
23
  ANCHORS forms,PASS,every in-page link resolves
22
24
  ANCHORS components,PASS,every in-page link resolves
23
25
  ANCHORS conversion,PASS,every in-page link resolves
@@ -28,6 +30,7 @@ ANCHORS dialogs,PASS,every in-page link resolves
28
30
  WIDTH moksa,PASS,"no sideways scroll at 390,768,1280"
29
31
  WIDTH my-account,PASS,"no sideways scroll at 390,768,1280"
30
32
  WIDTH intro,PASS,"no sideways scroll at 390,768,1280"
33
+ WIDTH content,PASS,"no sideways scroll at 390,768,1280"
31
34
  WIDTH forms,PASS,"no sideways scroll at 390,768,1280"
32
35
  WIDTH components,PASS,"no sideways scroll at 390,768,1280"
33
36
  WIDTH conversion,PASS,"no sideways scroll at 390,768,1280"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mosaic-headless",
3
- "version": "1.27.0",
3
+ "version": "1.28.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",
@@ -111,3 +111,57 @@ attachment(2) find_image(2) find_link(2) find_video(2) media
111
111
  `sum`, `avg`, `concat` and `fallback` are variadic. `date()`'s timestamp argument is a
112
112
  true GMT Unix timestamp — pair it with `post/publish_timestamp_gmt`, not
113
113
  `publish_timestamp`, unless you want the local-time value re-interpreted as UTC.
114
+
115
+ ## A loop in place of a component's children
116
+
117
+ Three component families take a loop INSTEAD of hand-authored children, which is
118
+ the thing a page builder needs a plugin per component to do:
119
+
120
+ ```
121
+ slider-slides > slider-loop-slides > slider-slide one slide per row
122
+ tabs-menu > tabs-loop-tabs > tabs-tab
123
+ tabs-content > tabs-loop-tab-panes > tabs-tab-pane
124
+ list > list-loop-items > list-item
125
+ accordion > accordion-loop-items> accordion-item
126
+ ```
127
+
128
+ Each loop container carries the same three keys an ordinary `loop` does —
129
+ `loopType`, `loopNamespace` and a `<loopType>Options` group — and the template
130
+ inside reads its row with `@VAR('<namespace>/…')`. You author ONE slide, one tab,
131
+ one row; the count comes from the content. The assertion that matters is therefore
132
+ a COUNT: a template that failed to loop renders exactly once and looks correct.
133
+
134
+ **`canBeParentFor` disagrees with the factory's own default data on these.**
135
+ `SliderLoopSlidesElementTypeFactory::canBeParentFor()` returns true for
136
+ `SliderSlidesElementTypeFactory` — the track, not the slide — while its
137
+ `getDefaultData()` seeds itself with a `slider-slide`. `accordion-loop-items` says
138
+ `accordion` the same way. The structure the editor actually builds is the second
139
+ one. `build_page.py` resolves it by treating a loop container as standing in for
140
+ its parent's children: what `slider-loop-slides` accepts is what `slider-slides`
141
+ accepts.
142
+
143
+ ## `loop-pagination`, and the key you must not use
144
+
145
+ ```
146
+ loop > loop-pagination > loop-pagination-button-previous
147
+ > loop-pagination-numbers > loop-pagination-number
148
+ > loop-pagination-button-next
149
+ ```
150
+
151
+ `loop-pagination-number` is a TEMPLATE repeated per page, the same rule as a
152
+ slider bullet — author one. `loop-pagination-numbers` takes `pagesBefore` /
153
+ `pagesAfter` / `pagesStart` / `pagesEnd` to decide how much of a long range to
154
+ show.
155
+
156
+ **Do not set `paginationKey` to `p`.** It is one of WordPress's own reserved query
157
+ vars, meaning "post ID": `?p=2` answers **HTTP 301** to post 2, so the visitor
158
+ leaves the page entirely and the loop renders nothing. Measured. Any other key
159
+ works — `?notes=2` on the demo pages the archive and changes all three rows.
160
+
161
+ ## `@substr` counts BYTES
162
+
163
+ `@substr(@VAR('post/title'), 0, 14)` on a Han title cuts the fifth character in
164
+ half and the output ends in U+FFFD. Lengths have to be a multiple of the encoding
165
+ width — 18 for six three-byte characters — or the truncation has to be left to CSS.
166
+ A count-based check cannot see this; `verify_components.py` looks for the
167
+ replacement character directly.
@@ -101,10 +101,11 @@ const counts = {
101
101
  "data/dialog-verification.csv": 19,
102
102
  "data/slider-verification.csv": 30,
103
103
  "data/token-verification.csv": 15,
104
- "data/mobile-audit.csv": 60,
104
+ "data/mobile-audit.csv": 63,
105
105
  "data/component-wall-verification.csv": 13,
106
106
  "data/form-surface-verification.csv": 8,
107
- "data/navigation-verification.csv": 36,
107
+ "data/content-loop-verification.csv": 10,
108
+ "data/navigation-verification.csv": 39,
108
109
  "data/page-modal-verification.csv": 19,
109
110
  "data/interaction-rules-verification.csv": 17,
110
111
  "data/dialog-form-verification.csv": 49,
package/sites/_moksa.py CHANGED
@@ -304,6 +304,8 @@ CONVERTED_COUNT = _count("conversion-batch.csv",
304
304
 
305
305
  # label, href, one line of what it demonstrates
306
306
  CAPABILITIES = [
307
+ ("內容驅動", "/content/",
308
+ "輪播、分頁、存檔——都由文章資料複製"),
307
309
  ("表單", "/forms/",
308
310
  "下拉、單選、複選、上傳、必填規則與錯誤訊息"),
309
311
  ("元件牆", "/components/",
@@ -3230,6 +3232,10 @@ if __name__ == "__main__":
3230
3232
  # helpers it reads are pure functions - two copies produce identical dicts.
3231
3233
  import _dialogs
3232
3234
  import _sliders
3235
+ import _content
3236
+ SITE["pages"].append({"slug": "content", "post_id": 283,
3237
+ "title": "內容驅動",
3238
+ "tree": apply_type(_content.TREE, DISPLAY, CJK, "600")})
3233
3239
  import _forms
3234
3240
  SITE["pages"].append({"slug": "forms", "post_id": 282,
3235
3241
  "title": "表單",