mosaic-headless 1.27.0 → 1.29.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 +5 -4
- 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/component-wall-verification.csv +6 -0
- package/data/content-loop-verification.csv +12 -0
- package/data/form-surface-verification.csv +4 -0
- package/data/mobile-audit.csv +5 -2
- package/data/navigation-verification.csv +16 -16
- package/package.json +1 -1
- package/references/design-system.md +30 -0
- package/references/dynamic-content.md +54 -0
- package/scripts/check-release.mjs +4 -3
- package/sites/_moksa.py +50 -11
- package/sites/moksa.json +2339 -259
- package/tools/build_page.py +13 -0
- package/tools/verify_components.py +96 -0
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.
|
|
7
|
+
version: "1.29.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Headless Mosaic
|
|
@@ -535,9 +535,10 @@ so the pattern is in the data, not just in this paragraph.
|
|
|
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` |
|
|
539
|
-
| `data/component-wall-verification.csv` |
|
|
540
|
-
| `data/form-surface-verification.csv` |
|
|
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
|
+
| `data/component-wall-verification.csv` | 19 | **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
|
+
| `data/form-surface-verification.csv` | 12 | **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` | 11 | **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.
|
|
23
|
+
"version": "1.29.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.
|
|
23
|
+
"version": "1.29.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.
|
|
23
|
+
"version": "1.29.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.
|
|
23
|
+
"version": "1.29.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.
|
|
23
|
+
"version": "1.29.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,
|
|
@@ -10,5 +10,11 @@ FORM/required,PASS,required is set on 3 of them
|
|
|
10
10
|
FORM/honeypot,PASS,"Mosaic adds its own spam trap: ['mosaic_hp_url', 'mosaic_hp_ts']"
|
|
11
11
|
FORM/option_labels,SKIPPED,no select on this form
|
|
12
12
|
FORM/required_blocks,PASS,"empty form reports invalid: ['name', 'email', 'message']"
|
|
13
|
+
COMPONENT/instances,PASS,"3 instances: ['cs-0-cs-card', 'cs-1-cs-card', 'cs-2-cs-card']"
|
|
14
|
+
COMPONENT/one_definition,PASS,"all share the generated class ['_fm'] - one definition, not 3 copies"
|
|
15
|
+
COMPONENT/content_differs,PASS,"3 instances, 3 distinct bodies - the overrides are per-instance"
|
|
16
|
+
LOOP/slider,SKIPPED,not on this page
|
|
17
|
+
LOOP/tabs,SKIPPED,not on this page
|
|
18
|
+
LOOP/pagination,SKIPPED,not on this page
|
|
13
19
|
MAP/frame,PASS,renders an openstreetmap iframe
|
|
14
20
|
MAP/coordinates_applied,PASS,"marker=24.147700,120.673600"
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
COMPONENT/instances,SKIPPED,fewer than two on this page
|
|
6
|
+
LOOP/slider,PASS,6 slides from one authored slider-slide
|
|
7
|
+
LOOP/tabs,PASS,4 tabs from one authored tabs-tab
|
|
8
|
+
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"
|
|
9
|
+
LOOP/pagination_links,PASS,"3 page links: ['v=16802¬es=1', 'v=16802¬es=2', 'v=16802¬es=3']"
|
|
10
|
+
LOOP/pagination_key_is_safe,PASS,paginationKey does not collide with a WordPress query var
|
|
11
|
+
LOOP/pagination_changes_rows,PASS,page 1 3 -> page 2 3
|
|
12
|
+
MAP,SKIPPED,not on this page
|
|
@@ -6,4 +6,8 @@ FORM/required,PASS,required is set on 8 of them
|
|
|
6
6
|
FORM/honeypot,PASS,"Mosaic adds its own spam trap: ['mosaic_hp_url', 'mosaic_hp_ts']"
|
|
7
7
|
FORM/option_labels,PASS,"5 options, all labelled"
|
|
8
8
|
FORM/required_blocks,PASS,"empty form reports invalid: ['name', 'email', 'kind', 'kind']"
|
|
9
|
+
COMPONENT/instances,SKIPPED,fewer than two on this page
|
|
10
|
+
LOOP/slider,SKIPPED,not on this page
|
|
11
|
+
LOOP/tabs,SKIPPED,not on this page
|
|
12
|
+
LOOP/pagination,SKIPPED,not on this page
|
|
9
13
|
MAP,SKIPPED,not on this page
|
package/data/mobile-audit.csv
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
path,width,finding,element,detail
|
|
2
|
-
forms/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
|
|
3
2
|
components/,390,GRID_NOT_COLLAPSED,cp-tabs-menu,2 columns at 390px
|
|
4
3
|
components/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
|
|
4
|
+
content/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
|
|
5
|
+
forms/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
|
|
5
6
|
dialogs/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
|
|
6
7
|
sliders/,390,SMALL_TARGET,sl-main-nav-dot,34x28:
|
|
7
8
|
sliders/,390,SMALL_TARGET,sl-main-nav-dot,34x28:
|
|
@@ -17,9 +18,11 @@ 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
|
|
20
|
-
forms/,768,LONG_MEASURE,m-text,59 chars/line
|
|
21
21
|
components/,768,LONG_MEASURE,m-text,44 chars/line
|
|
22
22
|
components/,768,LONG_MEASURE,m-text,56 chars/line
|
|
23
|
+
content/,768,LONG_MEASURE,m-text,43 chars/line
|
|
24
|
+
content/,768,LONG_MEASURE,m-text,58 chars/line
|
|
25
|
+
forms/,768,LONG_MEASURE,m-text,59 chars/line
|
|
23
26
|
dialogs/,768,LONG_MEASURE,m-text,46 chars/line
|
|
24
27
|
dialogs/,768,LONG_MEASURE,m-text,45 chars/line
|
|
25
28
|
dialogs/,768,LONG_MEASURE,m-text,45 chars/line
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
check,result,detail
|
|
2
|
-
FRONT_PAGE,PASS,"HTTP 200,
|
|
3
|
-
MENU_IS_LINKS,PASS,
|
|
4
|
-
NAV
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
PAGE
|
|
9
|
-
PAGE
|
|
10
|
-
PAGE
|
|
11
|
-
PAGE
|
|
12
|
-
PAGE
|
|
13
|
-
PAGE
|
|
14
|
-
PAGE
|
|
15
|
-
PAGE
|
|
16
|
-
PAGE sliders,PASS,"HTTP 200, 151524 bytes"
|
|
17
|
-
PAGE dialogs,PASS,"HTTP 200, 206683 bytes"
|
|
2
|
+
FRONT_PAGE,PASS,"HTTP 200, 240831 bytes"
|
|
3
|
+
MENU_IS_LINKS,PASS,1 real <a href> in #mk-nav
|
|
4
|
+
NAV 首頁,PASS,"/ -> HTTP 200, 240830 bytes"
|
|
5
|
+
PAGE moksa,PASS,"HTTP 200, 240830 bytes"
|
|
6
|
+
PAGE my-account,PASS,"HTTP 200, 101036 bytes"
|
|
7
|
+
PAGE intro,PASS,"HTTP 200, 134744 bytes"
|
|
8
|
+
PAGE content,PASS,"HTTP 200, 102378 bytes"
|
|
9
|
+
PAGE forms,PASS,"HTTP 200, 101343 bytes"
|
|
10
|
+
PAGE components,PASS,"HTTP 200, 115524 bytes"
|
|
11
|
+
PAGE conversion,PASS,"HTTP 200, 103202 bytes"
|
|
12
|
+
PAGE dynamic,PASS,"HTTP 200, 112622 bytes"
|
|
13
|
+
PAGE tokens,PASS,"HTTP 200, 125792 bytes"
|
|
14
|
+
PAGE sliders,PASS,"HTTP 200, 151809 bytes"
|
|
15
|
+
PAGE dialogs,PASS,"HTTP 200, 207347 bytes"
|
|
18
16
|
ANCHORS moksa,PASS,every in-page link resolves
|
|
19
17
|
ANCHORS my-account,PASS,every in-page link resolves
|
|
20
18
|
ANCHORS intro,PASS,every in-page link resolves
|
|
19
|
+
ANCHORS content,PASS,every in-page link resolves
|
|
21
20
|
ANCHORS forms,PASS,every in-page link resolves
|
|
22
21
|
ANCHORS components,PASS,every in-page link resolves
|
|
23
22
|
ANCHORS conversion,PASS,every in-page link resolves
|
|
@@ -28,6 +27,7 @@ ANCHORS dialogs,PASS,every in-page link resolves
|
|
|
28
27
|
WIDTH moksa,PASS,"no sideways scroll at 390,768,1280"
|
|
29
28
|
WIDTH my-account,PASS,"no sideways scroll at 390,768,1280"
|
|
30
29
|
WIDTH intro,PASS,"no sideways scroll at 390,768,1280"
|
|
30
|
+
WIDTH content,PASS,"no sideways scroll at 390,768,1280"
|
|
31
31
|
WIDTH forms,PASS,"no sideways scroll at 390,768,1280"
|
|
32
32
|
WIDTH components,PASS,"no sideways scroll at 390,768,1280"
|
|
33
33
|
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.
|
|
3
|
+
"version": "1.29.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",
|
|
@@ -196,3 +196,33 @@ is in the wrong hemisphere. The three coordinate properties take
|
|
|
196
196
|
The element is an `<iframe>` to openstreetmap.org, not a tile layer, so "how many
|
|
197
197
|
tiles loaded" is unanswerable from the parent document — assert on the frame's
|
|
198
198
|
`src`, which carries `marker=lat,lon`.
|
|
199
|
+
|
|
200
|
+
## Component instances: one definition, many uses
|
|
201
|
+
|
|
202
|
+
A component is a theme-level record; a page writes an INSTANCE of it and supplies
|
|
203
|
+
only the slots that differ:
|
|
204
|
+
|
|
205
|
+
```jsonc
|
|
206
|
+
// the definition, once, in the site's `components` map
|
|
207
|
+
"case-card": <tree with slot() text nodes>
|
|
208
|
+
|
|
209
|
+
// an instance, on a page
|
|
210
|
+
{"type": "div", "component": "case-card",
|
|
211
|
+
"data": {"attrID": "cs-0"},
|
|
212
|
+
"overrides": {"cs-kind": {"text": "E-COMMERCE"},
|
|
213
|
+
"cs-name": {"text": "…"}}}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**An instance's ids are prefixed with the instance's own attrID.** The definition's
|
|
217
|
+
`cs-card` becomes `cs-0-cs-card`, `cs-1-cs-card`, and so on, so the definition's
|
|
218
|
+
attrID is a SUFFIX in the delivered page and `getElementById` on it finds nothing.
|
|
219
|
+
Address instances with `[id$="-cs-card"]`.
|
|
220
|
+
|
|
221
|
+
The claim worth verifying is not that three cards rendered. It is that they are the
|
|
222
|
+
same definition, and the evidence is in the class: all three carry the identical
|
|
223
|
+
generated token (`_fm` here) while their text differs. Three copy-pasted sections
|
|
224
|
+
would carry three tokens and three rules. `verify_components.py` asserts exactly
|
|
225
|
+
that — and the first version of the check passed on three `null`s, because its
|
|
226
|
+
regex contained a literal backspace: `\b` inside a non-raw Python string is 0x08,
|
|
227
|
+
not a word boundary. An assertion that cannot fail is worse than no assertion, so
|
|
228
|
+
the check now also refuses a token it could not read.
|
|
@@ -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,9 +101,10 @@ 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":
|
|
105
|
-
"data/component-wall-verification.csv":
|
|
106
|
-
"data/form-surface-verification.csv":
|
|
104
|
+
"data/mobile-audit.csv": 63,
|
|
105
|
+
"data/component-wall-verification.csv": 19,
|
|
106
|
+
"data/form-surface-verification.csv": 12,
|
|
107
|
+
"data/content-loop-verification.csv": 11,
|
|
107
108
|
"data/navigation-verification.csv": 36,
|
|
108
109
|
"data/page-modal-verification.csv": 19,
|
|
109
110
|
"data/interaction-rules-verification.csv": 17,
|
package/sites/_moksa.py
CHANGED
|
@@ -285,8 +285,11 @@ def mask_line(attr, text, tag="h1", **st):
|
|
|
285
285
|
# bare `#services` is therefore a dead link on all seven of the others: the
|
|
286
286
|
# browser looks for that id in the current document, does not find it, and does
|
|
287
287
|
# nothing at all. Absolute, so they mean the same thing from anywhere.
|
|
288
|
-
|
|
289
|
-
|
|
288
|
+
# The header used to be the studio site's own section anchors. On a demo whose
|
|
289
|
+
# whole job is to show what Mosaic can do, those were four links that left the
|
|
290
|
+
# showcase - so the menu IS the showcase now, and the one remaining top-level
|
|
291
|
+
# link goes home.
|
|
292
|
+
NAV = [("首頁", "/")]
|
|
290
293
|
|
|
291
294
|
# Read from the shipped tables rather than typed, so the menu cannot claim a
|
|
292
295
|
# number the evidence no longer supports.
|
|
@@ -302,8 +305,34 @@ TOKENABLE_COUNT = _count("style-properties.csv",
|
|
|
302
305
|
CONVERTED_COUNT = _count("conversion-batch.csv",
|
|
303
306
|
lambda rs: sum(int(r["converted"]) for r in rs))
|
|
304
307
|
|
|
305
|
-
#
|
|
308
|
+
# Two groups, because eight flat links is the thing this menu was already fixed
|
|
309
|
+
# for once. `label, href, one line of what it demonstrates`.
|
|
310
|
+
CAPABILITY_GROUPS = [
|
|
311
|
+
("介面元件", [
|
|
312
|
+
("元件牆", "/components/",
|
|
313
|
+
"分頁、手風琴、表單、地圖——都能直接操作"),
|
|
314
|
+
("表單", "/forms/",
|
|
315
|
+
"下拉、單選、複選、上傳、必填規則與錯誤訊息"),
|
|
316
|
+
("輪播", "/sliders/",
|
|
317
|
+
"四種動畫並排、輪轉模式、鍵盤與減少動態"),
|
|
318
|
+
("彈窗與觸發", "/dialogs/",
|
|
319
|
+
"九種形態、四種觸發、四種執行規則"),
|
|
320
|
+
]),
|
|
321
|
+
("資料與系統", [
|
|
322
|
+
("內容驅動", "/content/",
|
|
323
|
+
"輪播、分頁、存檔——都由文章資料複製"),
|
|
324
|
+
("動態內容", "/dynamic/",
|
|
325
|
+
"迴圈、@VAR 表達式、自訂欄位"),
|
|
326
|
+
("設計系統", "/tokens/",
|
|
327
|
+
"設計代幣、variant 層、%d 個接得住代幣的屬性" % TOKENABLE_COUNT),
|
|
328
|
+
("Elementor 轉換", "/conversion/",
|
|
329
|
+
"19 頁、%s 個元素、每一段字都核過" % "{:,}".format(CONVERTED_COUNT)),
|
|
330
|
+
]),
|
|
331
|
+
]
|
|
332
|
+
|
|
306
333
|
CAPABILITIES = [
|
|
334
|
+
("內容驅動", "/content/",
|
|
335
|
+
"輪播、分頁、存檔——都由文章資料複製"),
|
|
307
336
|
("表單", "/forms/",
|
|
308
337
|
"下拉、單選、複選、上傳、必填規則與錯誤訊息"),
|
|
309
338
|
("元件牆", "/components/",
|
|
@@ -1366,7 +1395,7 @@ NAV_LINK = {"color": {"token": "--mk-ink"}, "fontSize": "13px",
|
|
|
1366
1395
|
"cursor": "pointer"}
|
|
1367
1396
|
|
|
1368
1397
|
|
|
1369
|
-
def capability_dropdown():
|
|
1398
|
+
def capability_dropdown(key, title, entries):
|
|
1370
1399
|
"""The capability pages as one labelled group.
|
|
1371
1400
|
|
|
1372
1401
|
`dropdown` heals in its own `dropdown-toggle` and `dropdown-wrapper`, so only
|
|
@@ -1377,11 +1406,11 @@ def capability_dropdown():
|
|
|
1377
1406
|
table now carries a `takes_children` column and the build guard reads it.
|
|
1378
1407
|
"""
|
|
1379
1408
|
return {"type": "dropdown",
|
|
1380
|
-
"data": {"attrID": "mk-cap", "horizontalAlign": "right"},
|
|
1409
|
+
"data": {"attrID": "mk-cap-" + key, "horizontalAlign": "right"},
|
|
1381
1410
|
"style": {"&": {"_": {"position": "relative"}}},
|
|
1382
1411
|
"children": [
|
|
1383
1412
|
{"type": "dropdown-toggle",
|
|
1384
|
-
"data": {"attrID": "mk-cap-toggle"},
|
|
1413
|
+
"data": {"attrID": "mk-cap-%s-toggle" % key},
|
|
1385
1414
|
"style": {"&": {"_": dict(NAV_LINK, **{
|
|
1386
1415
|
"display": "flex", "alignItems": "center", "columnGap": "6px",
|
|
1387
1416
|
"customDeclarations":
|
|
@@ -1391,14 +1420,14 @@ def capability_dropdown():
|
|
|
1391
1420
|
"hover": {"_": {"color": {"token": "--mk-accent"}}},
|
|
1392
1421
|
"focus-visible": {"_": dict(FOCUS_RING)}},
|
|
1393
1422
|
"children": [
|
|
1394
|
-
T("p",
|
|
1423
|
+
T("p", title, fontFamily=MONO,
|
|
1395
1424
|
fontSize="13px", letterSpacing="0.04em",
|
|
1396
1425
|
color={"token": "--mk-ink"}, _t={"fontSize": "12px"}),
|
|
1397
1426
|
T("p", "+", fontFamily=MONO, fontSize="11px",
|
|
1398
1427
|
color={"token": "--mk-faint"}),
|
|
1399
1428
|
]},
|
|
1400
1429
|
{"type": "dropdown-wrapper",
|
|
1401
|
-
"data": {"attrID": "mk-cap-menu"},
|
|
1430
|
+
"data": {"attrID": "mk-cap-%s-menu" % key},
|
|
1402
1431
|
"style": bp({"backgroundColor": {"token": "--mk-paper"},
|
|
1403
1432
|
"paddingTop": "10px", "paddingBottom": "10px",
|
|
1404
1433
|
"minWidth": "272px",
|
|
@@ -1415,7 +1444,8 @@ def capability_dropdown():
|
|
|
1415
1444
|
"box-shadow:0 18px 40px rgba(22,24,28,.16);"}),
|
|
1416
1445
|
"children": [
|
|
1417
1446
|
{"type": "menu-link",
|
|
1418
|
-
"data": {"attrID": "mk-cap-%d" %
|
|
1447
|
+
"data": {"attrID": "mk-cap-%s-%d" % (key, i),
|
|
1448
|
+
"url": href},
|
|
1419
1449
|
"style": {"&": {"_": {
|
|
1420
1450
|
"display": "block", "cursor": "pointer",
|
|
1421
1451
|
"paddingTop": "9px", "paddingBottom": "9px",
|
|
@@ -1430,7 +1460,7 @@ def capability_dropdown():
|
|
|
1430
1460
|
lineHeight="1.6", color={"token": "--mk-muted"},
|
|
1431
1461
|
marginTop="3px"),
|
|
1432
1462
|
]}
|
|
1433
|
-
for i, (label, href, blurb) in enumerate(
|
|
1463
|
+
for i, (label, href, blurb) in enumerate(entries)
|
|
1434
1464
|
]},
|
|
1435
1465
|
]}
|
|
1436
1466
|
|
|
@@ -1517,7 +1547,8 @@ HEADER = box("mk-shell-top", {}, [
|
|
|
1517
1547
|
"focus-visible": {"_": FOCUS_RING}},
|
|
1518
1548
|
"text": text}
|
|
1519
1549
|
for i, (text, href) in enumerate(NAV)
|
|
1520
|
-
] + [capability_dropdown()
|
|
1550
|
+
] + [capability_dropdown("g%d" % gi, title, entries)
|
|
1551
|
+
for gi, (title, entries) in enumerate(CAPABILITY_GROUPS)]},
|
|
1521
1552
|
# not a pill: a bracketed link, the way a document cross-references
|
|
1522
1553
|
{"type": "button", "data": {"attrID": "mk-header-cta",
|
|
1523
1554
|
"url": "/#contact"},
|
|
@@ -3230,11 +3261,19 @@ if __name__ == "__main__":
|
|
|
3230
3261
|
# helpers it reads are pure functions - two copies produce identical dicts.
|
|
3231
3262
|
import _dialogs
|
|
3232
3263
|
import _sliders
|
|
3264
|
+
import _content
|
|
3265
|
+
SITE["pages"].append({"slug": "content", "post_id": 283,
|
|
3266
|
+
"title": "內容驅動",
|
|
3267
|
+
"tree": apply_type(_content.TREE, DISPLAY, CJK, "600")})
|
|
3233
3268
|
import _forms
|
|
3234
3269
|
SITE["pages"].append({"slug": "forms", "post_id": 282,
|
|
3235
3270
|
"title": "表單",
|
|
3236
3271
|
"tree": apply_type(_forms.TREE, DISPLAY, CJK, "600")})
|
|
3237
3272
|
import _components
|
|
3273
|
+
# the showcase page brings its own component definition; components are a
|
|
3274
|
+
# theme-level record, so they have to join the site's map before build_site
|
|
3275
|
+
# walks it
|
|
3276
|
+
SITE["components"].update(_components.COMPONENTS)
|
|
3238
3277
|
SITE["pages"].append({"slug": "components", "post_id": 281,
|
|
3239
3278
|
"title": "元件",
|
|
3240
3279
|
"tree": apply_type(_components.TREE, DISPLAY, CJK, "600")})
|