mosaic-headless 1.25.1 → 1.26.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/SKILL.md +6 -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 +12 -0
- package/data/mobile-audit.csv +4 -0
- package/data/navigation-verification.csv +24 -13
- package/package.json +1 -1
- package/references/design-system.md +36 -0
- package/references/failure-modes.md +28 -0
- package/scripts/check-release.mjs +3 -2
- package/sites/_moksa.py +23 -9
- package/sites/moksa.json +3360 -116
- package/tools/audit_mobile.py +5 -0
- package/tools/verify_components.py +145 -0
- package/tools/verify_navigation.py +33 -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 (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.
|
|
7
|
+
version: "1.26.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Headless Mosaic
|
|
@@ -530,12 +530,13 @@ 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` |
|
|
533
|
+
| `data/navigation-verification.csv` | 33 | **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` |
|
|
538
|
+
| `data/mobile-audit.csv` | 58 | **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` | 11 | **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 |
|
|
539
540
|
| `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 |
|
|
540
541
|
| `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 |
|
|
541
542
|
| `data/node-type-notes.csv` | 8 | where a sweep outcome is true but misleading on its own, why. Surfaced by `mo.py type` |
|
|
@@ -808,10 +809,11 @@ post — `build_all.py` resets first for that reason.
|
|
|
808
809
|
| `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 |
|
|
809
810
|
| `theme_delete.php` | remove a theme completely through the plugin's own routine; refuses the live one |
|
|
810
811
|
| `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 - `--label`/`--append` put several sliders in one table, which is where a carousel and a plain slider can be held against each other |
|
|
811
|
-
| `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 |
|
|
812
|
+
| `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 - and whether every in-page `#fragment` resolves ON THE PAGE THAT CARRIES IT, which an HTTP check cannot see: a bare `#services` in a SHARED header fetches the current page, answers 200 everywhere, and does nothing on every page but the one that has that section |
|
|
812
813
|
| `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 |
|
|
813
814
|
| `verify_dialog_lab.py` | the question after "does the modal work": can three nodes be made into a bottom sheet, a side drawer, a corner notice, a full takeover and a gate with no way out - and does each one still fit at phone width, which the page's own width check cannot see |
|
|
814
815
|
| `verify_tokens.py` | the design tokens as the BROWSER resolved them: every `:root` property declared once (a changed value does not retire the old one), non-empty, and actually pointed at by something - a wrong `skinsData` shape still emits the declaration and serves white |
|
|
816
|
+
| `verify_components.py` | presses the interactive components on a page and asserts the RESULT - a pane actually swapped, an accordion item actually opened, the map carries the coordinates it was given - because "these all work" is worth what the last press proved |
|
|
815
817
|
| `audit_mobile.py` | the RWD defects a sideways-scroll check cannot see - type under the legible floor, controls too small to hit, a grid that never collapsed, content escaping a box that is not clipping it - re-opened at every `--widths` |
|
|
816
818
|
| `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`) |
|
|
817
819
|
| `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.
|
|
23
|
+
"version": "1.26.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.
|
|
23
|
+
"version": "1.26.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.
|
|
23
|
+
"version": "1.26.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.
|
|
23
|
+
"version": "1.26.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.
|
|
23
|
+
"version": "1.26.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,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
check,result,detail
|
|
2
|
+
TABS/one_pane_at_rest,PASS,visible panes at rest: [0]
|
|
3
|
+
TABS/click_switches,PASS,clicked tab 3 -> panes [3]
|
|
4
|
+
TABS/active_label_readable,PASS,"{'bg': 'rgb(22, 24, 28)', 'labels': ['rgba(250, 250, 247, 0.6)', 'rgb(250, 250, 247)']}"
|
|
5
|
+
ACCORDION/shut_at_rest,PASS,open: []
|
|
6
|
+
ACCORDION/click_opens_one,PASS,clicked 1 -> open [1]
|
|
7
|
+
ACCORDION/keyboard_reachable,PASS,title tabindex='0' - the plugin puts it in the tab order
|
|
8
|
+
FORM/fields,PASS,"named fields: ['name', 'email', 'message']"
|
|
9
|
+
FORM/required,PASS,every author field is required
|
|
10
|
+
FORM/honeypot,PASS,"Mosaic adds its own spam trap: ['mosaic_hp_url', 'mosaic_hp_ts']"
|
|
11
|
+
MAP/frame,PASS,renders an openstreetmap iframe
|
|
12
|
+
MAP/coordinates_applied,PASS,"marker=24.147700,120.673600"
|
package/data/mobile-audit.csv
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
path,width,finding,element,detail
|
|
2
|
+
components/,390,GRID_NOT_COLLAPSED,cp-tabs-menu,2 columns at 390px
|
|
3
|
+
components/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
|
|
2
4
|
dialogs/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
|
|
3
5
|
sliders/,390,SMALL_TARGET,sl-main-nav-dot,34x28:
|
|
4
6
|
sliders/,390,SMALL_TARGET,sl-main-nav-dot,34x28:
|
|
@@ -14,6 +16,8 @@ sliders/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
|
|
|
14
16
|
tokens/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
|
|
15
17
|
dynamic/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
|
|
16
18
|
conversion/,390,GRID_NOT_COLLAPSED,mk-f-links,2 columns at 390px
|
|
19
|
+
components/,768,LONG_MEASURE,m-text,44 chars/line
|
|
20
|
+
components/,768,LONG_MEASURE,m-text,56 chars/line
|
|
17
21
|
dialogs/,768,LONG_MEASURE,m-text,46 chars/line
|
|
18
22
|
dialogs/,768,LONG_MEASURE,m-text,45 chars/line
|
|
19
23
|
dialogs/,768,LONG_MEASURE,m-text,45 chars/line
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
check,result,detail
|
|
2
|
-
FRONT_PAGE,PASS,"HTTP 200,
|
|
2
|
+
FRONT_PAGE,PASS,"HTTP 200, 239472 bytes"
|
|
3
3
|
MENU_IS_LINKS,PASS,4 real <a href> in #mk-nav
|
|
4
|
-
NAV 服務,PASS,"
|
|
5
|
-
NAV 作品,PASS,"
|
|
6
|
-
NAV 產品,PASS,"
|
|
7
|
-
NAV 聯絡,PASS,"
|
|
8
|
-
PAGE moksa,PASS,"HTTP 200,
|
|
9
|
-
PAGE my-account,PASS,"HTTP 200,
|
|
10
|
-
PAGE intro,PASS,"HTTP 200,
|
|
11
|
-
PAGE
|
|
12
|
-
PAGE
|
|
13
|
-
PAGE
|
|
14
|
-
PAGE
|
|
15
|
-
PAGE
|
|
4
|
+
NAV 服務,PASS,"/#services -> HTTP 200, 239568 bytes"
|
|
5
|
+
NAV 作品,PASS,"/#works -> HTTP 200, 239568 bytes"
|
|
6
|
+
NAV 產品,PASS,"/#products -> HTTP 200, 239568 bytes"
|
|
7
|
+
NAV 聯絡,PASS,"/#contact -> HTTP 200, 239568 bytes"
|
|
8
|
+
PAGE moksa,PASS,"HTTP 200, 239471 bytes"
|
|
9
|
+
PAGE my-account,PASS,"HTTP 200, 99676 bytes"
|
|
10
|
+
PAGE intro,PASS,"HTTP 200, 133386 bytes"
|
|
11
|
+
PAGE components,PASS,"HTTP 200, 114165 bytes"
|
|
12
|
+
PAGE conversion,PASS,"HTTP 200, 101843 bytes"
|
|
13
|
+
PAGE dynamic,PASS,"HTTP 200, 111263 bytes"
|
|
14
|
+
PAGE tokens,PASS,"HTTP 200, 124433 bytes"
|
|
15
|
+
PAGE sliders,PASS,"HTTP 200, 150450 bytes"
|
|
16
|
+
PAGE dialogs,PASS,"HTTP 200, 205987 bytes"
|
|
17
|
+
ANCHORS moksa,PASS,every in-page link resolves
|
|
18
|
+
ANCHORS my-account,PASS,every in-page link resolves
|
|
19
|
+
ANCHORS intro,PASS,every in-page link resolves
|
|
20
|
+
ANCHORS components,PASS,every in-page link resolves
|
|
21
|
+
ANCHORS conversion,PASS,every in-page link resolves
|
|
22
|
+
ANCHORS dynamic,PASS,every in-page link resolves
|
|
23
|
+
ANCHORS tokens,PASS,every in-page link resolves
|
|
24
|
+
ANCHORS sliders,PASS,every in-page link resolves
|
|
25
|
+
ANCHORS dialogs,PASS,every in-page link resolves
|
|
16
26
|
WIDTH moksa,PASS,"no sideways scroll at 390,768,1280"
|
|
17
27
|
WIDTH my-account,PASS,"no sideways scroll at 390,768,1280"
|
|
18
28
|
WIDTH intro,PASS,"no sideways scroll at 390,768,1280"
|
|
29
|
+
WIDTH components,PASS,"no sideways scroll at 390,768,1280"
|
|
19
30
|
WIDTH conversion,PASS,"no sideways scroll at 390,768,1280"
|
|
20
31
|
WIDTH dynamic,PASS,"no sideways scroll at 390,768,1280"
|
|
21
32
|
WIDTH tokens,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.26.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",
|
|
@@ -160,3 +160,39 @@ so knew nothing about children a type HEALS into itself — modal's overlay, loo
|
|
|
160
160
|
`loop-items`, slider's `slider-slides`, dropdown's own two. Both fixes are the same
|
|
161
161
|
move: the question the table answers was narrower than the question the table's
|
|
162
162
|
name suggests, so read the other source as well and say which is which.
|
|
163
|
+
|
|
164
|
+
## Component states live on the element the selector names
|
|
165
|
+
|
|
166
|
+
Two of Mosaic's component states read as a parent/child pair and only one of them
|
|
167
|
+
is about the component itself:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
___tab--active &.m-tab--active the tab
|
|
171
|
+
___tab--active___descendants .m-tab--active & anything inside it
|
|
172
|
+
___slider_navigation_bullet--active &.m-slider-bullet--active
|
|
173
|
+
___slide--active .m-slide--active > &
|
|
174
|
+
___arrow_hidden &:where(.m-slider-arrow--hidden)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`___tab--active___descendants` compiles with `&` in the DESCENDANT position, so it
|
|
178
|
+
must be committed on the child node. Put it on the tab and it reads "a descendant
|
|
179
|
+
of an active tab that is also that tab", which matches nothing — the plugin
|
|
180
|
+
darkens the active tab, the label keeps the colour you gave it, and the current
|
|
181
|
+
tab is ink on ink. Measured both ways; the fix is to style the label node, not
|
|
182
|
+
the tab.
|
|
183
|
+
|
|
184
|
+
The same shape governs `___slide--active` (`.m-slide--active > &`, so it goes on
|
|
185
|
+
the slide's content) and the slider bullet's own `--active`, which is what turns a
|
|
186
|
+
row of identical hairlines into a position indicator.
|
|
187
|
+
|
|
188
|
+
## What `openstreetmap` is called
|
|
189
|
+
|
|
190
|
+
`latitude`, `longitude`, `zoom`, `layer`, `lazyLoad` — not `lat` / `lon`. An
|
|
191
|
+
unknown property name is dropped without a word and the element falls back to its
|
|
192
|
+
default centre, which is Times Square: a map that renders, looks deliberate, and
|
|
193
|
+
is in the wrong hemisphere. The three coordinate properties take
|
|
194
|
+
`ValidatorDynamicCodeObject`, so the `{"v": "…"}` object form.
|
|
195
|
+
|
|
196
|
+
The element is an `<iframe>` to openstreetmap.org, not a tile layer, so "how many
|
|
197
|
+
tiles loaded" is unanswerable from the parent document — assert on the frame's
|
|
198
|
+
`src`, which carries `marker=lat,lon`.
|
|
@@ -344,3 +344,31 @@ Two rules come out of it:
|
|
|
344
344
|
that lesson: for anything with a visible and an invisible state, a suite that
|
|
345
345
|
only exercises the visible one is testing half a component, and the half it
|
|
346
346
|
skips is the one the visitor sees most of the time.
|
|
347
|
+
|
|
348
|
+
## A fragment link in a SHARED header is dead on every page but one
|
|
349
|
+
|
|
350
|
+
`#services` points at an id in the CURRENT document. Put it in a header that every
|
|
351
|
+
page shares, and on the one page that has that section it works, while on the other
|
|
352
|
+
seven the browser looks for the id, does not find it, and does nothing — no
|
|
353
|
+
navigation, no error, no feedback of any kind. The visitor clicks and the page sits
|
|
354
|
+
there. It is worse than a 404 for exactly that reason.
|
|
355
|
+
|
|
356
|
+
This shipped. The nav verifier had a check for it and the check could not see it:
|
|
357
|
+
it fetched every menu href and asserted HTTP 200, and `#services` resolves to the
|
|
358
|
+
*current* URL, so it returned 200 on all eight pages while being dead on seven.
|
|
359
|
+
"Every link answers 200" and "every link does something" are different claims.
|
|
360
|
+
|
|
361
|
+
Two things come out of it:
|
|
362
|
+
|
|
363
|
+
- **Write shared-shell links absolutely** — `/#services`, not `#services`. A link
|
|
364
|
+
in a component that appears on more than one page has to mean the same thing
|
|
365
|
+
from all of them.
|
|
366
|
+
- **Assert the fragment resolves on the page that carries it.**
|
|
367
|
+
`verify_navigation.py` now opens every page and fails on any `[href^="#"]`
|
|
368
|
+
whose target id is absent from that document. It found the four in the header
|
|
369
|
+
immediately — and then a fifth and sixth in the masthead, which `/intro/` shares
|
|
370
|
+
with the homepage and which I had not thought to look at.
|
|
371
|
+
|
|
372
|
+
The general form: a check that confirms a resource EXISTS is not a check that the
|
|
373
|
+
control WORKS, and anything living in a shared layout needs verifying on every
|
|
374
|
+
page that includes it, not on the page it was designed for.
|
|
@@ -101,8 +101,9 @@ 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/
|
|
104
|
+
"data/mobile-audit.csv": 58,
|
|
105
|
+
"data/component-wall-verification.csv": 11,
|
|
106
|
+
"data/navigation-verification.csv": 33,
|
|
106
107
|
"data/page-modal-verification.csv": 19,
|
|
107
108
|
"data/interaction-rules-verification.csv": 17,
|
|
108
109
|
"data/dialog-form-verification.csv": 49,
|
package/sites/_moksa.py
CHANGED
|
@@ -281,8 +281,12 @@ def mask_line(attr, text, tag="h1", **st):
|
|
|
281
281
|
# studio's own site; everything that demonstrates what Mosaic can DO is one
|
|
282
282
|
# labelled group, which is also the only honest shape - those five pages are a
|
|
283
283
|
# set, not five more sections of the homepage.
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
# These are sections of the HOMEPAGE, and the header is shared by every page. A
|
|
285
|
+
# bare `#services` is therefore a dead link on all seven of the others: the
|
|
286
|
+
# browser looks for that id in the current document, does not find it, and does
|
|
287
|
+
# nothing at all. Absolute, so they mean the same thing from anywhere.
|
|
288
|
+
NAV = [("服務", "/#services"), ("作品", "/#works"),
|
|
289
|
+
("產品", "/#products"), ("聯絡", "/#contact")]
|
|
286
290
|
|
|
287
291
|
# Read from the shipped tables rather than typed, so the menu cannot claim a
|
|
288
292
|
# number the evidence no longer supports.
|
|
@@ -300,6 +304,8 @@ CONVERTED_COUNT = _count("conversion-batch.csv",
|
|
|
300
304
|
|
|
301
305
|
# label, href, one line of what it demonstrates
|
|
302
306
|
CAPABILITIES = [
|
|
307
|
+
("元件牆", "/components/",
|
|
308
|
+
"分頁、手風琴、表單、地圖——都能直接操作"),
|
|
303
309
|
("彈窗與觸發", "/dialogs/",
|
|
304
310
|
"九種形態、四種觸發、四種執行規則"),
|
|
305
311
|
("輪播", "/sliders/",
|
|
@@ -1369,7 +1375,7 @@ def capability_dropdown():
|
|
|
1369
1375
|
table now carries a `takes_children` column and the build guard reads it.
|
|
1370
1376
|
"""
|
|
1371
1377
|
return {"type": "dropdown",
|
|
1372
|
-
"data": {"attrID": "mk-cap", "horizontalAlign": "
|
|
1378
|
+
"data": {"attrID": "mk-cap", "horizontalAlign": "right"},
|
|
1373
1379
|
"style": {"&": {"_": {"position": "relative"}}},
|
|
1374
1380
|
"children": [
|
|
1375
1381
|
{"type": "dropdown-toggle",
|
|
@@ -1398,9 +1404,13 @@ def capability_dropdown():
|
|
|
1398
1404
|
"border:1px solid rgb(22,24,28);"
|
|
1399
1405
|
"box-shadow:0 20px 48px rgba(22,24,28,.14);"},
|
|
1400
1406
|
None,
|
|
1401
|
-
|
|
1407
|
+
# NOT width:100% - that is 100% of the toggle-sized
|
|
1408
|
+
# host, and the panel came out 79px wide and 1124px
|
|
1409
|
+
# tall. A fixed width, right-aligned, fits 390px.
|
|
1410
|
+
{"minWidth": "0px", "width": "296px",
|
|
1402
1411
|
"customDeclarations":
|
|
1403
|
-
"border:1px solid
|
|
1412
|
+
"border:1px solid rgb(22,24,28);"
|
|
1413
|
+
"box-shadow:0 18px 40px rgba(22,24,28,.16);"}),
|
|
1404
1414
|
"children": [
|
|
1405
1415
|
{"type": "menu-link",
|
|
1406
1416
|
"data": {"attrID": "mk-cap-%d" % i, "url": href},
|
|
@@ -1454,7 +1464,7 @@ HEADER = box("mk-shell-top", {}, [
|
|
|
1454
1464
|
"children": [
|
|
1455
1465
|
# the wordmark is the only route back to the top on a one-pager, so
|
|
1456
1466
|
# it has to be a link. `menu-link` with a url renders a real <a>.
|
|
1457
|
-
{"type": "menu-link", "data": {"attrID": "mk-logo", "url": "
|
|
1467
|
+
{"type": "menu-link", "data": {"attrID": "mk-logo", "url": "/"},
|
|
1458
1468
|
"style": {"&": {"_": {"display": "flex", "alignItems": "baseline",
|
|
1459
1469
|
"columnGap": "9px", "cursor": "pointer"}}},
|
|
1460
1470
|
"children":
|
|
@@ -1508,7 +1518,7 @@ HEADER = box("mk-shell-top", {}, [
|
|
|
1508
1518
|
] + [capability_dropdown()]},
|
|
1509
1519
|
# not a pill: a bracketed link, the way a document cross-references
|
|
1510
1520
|
{"type": "button", "data": {"attrID": "mk-header-cta",
|
|
1511
|
-
"url": "
|
|
1521
|
+
"url": "/#contact"},
|
|
1512
1522
|
"style": {"&": {"_m": {"display": "none"},
|
|
1513
1523
|
"_t": {"fontSize": "12px"},
|
|
1514
1524
|
# 13px of the bright orange on the glass header is
|
|
@@ -1690,8 +1700,8 @@ MASTHEAD = section("mk-mast", [wrap("mk-mast-in", [
|
|
|
1690
1700
|
if n == 1
|
|
1691
1701
|
else {"color": {"token": "--mk-accent"}})}},
|
|
1692
1702
|
"text": text}
|
|
1693
|
-
for n, (text, href) in enumerate([("服務項目 →", "
|
|
1694
|
-
("精選作品 →", "
|
|
1703
|
+
for n, (text, href) in enumerate([("服務項目 →", "/#services"),
|
|
1704
|
+
("精選作品 →", "/#works")], start=1)
|
|
1695
1705
|
]},
|
|
1696
1706
|
]),
|
|
1697
1707
|
# the right-hand column: what a spec sheet puts in its header block
|
|
@@ -3218,6 +3228,10 @@ if __name__ == "__main__":
|
|
|
3218
3228
|
# helpers it reads are pure functions - two copies produce identical dicts.
|
|
3219
3229
|
import _dialogs
|
|
3220
3230
|
import _sliders
|
|
3231
|
+
import _components
|
|
3232
|
+
SITE["pages"].append({"slug": "components", "post_id": 281,
|
|
3233
|
+
"title": "元件",
|
|
3234
|
+
"tree": apply_type(_components.TREE, DISPLAY, CJK, "600")})
|
|
3221
3235
|
import _tokens
|
|
3222
3236
|
import _dynamic
|
|
3223
3237
|
import _conversion
|