mosaic-headless 1.31.1 → 1.31.3

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
@@ -5,7 +5,7 @@ description: |
5
5
  license: "MIT"
6
6
  metadata:
7
7
  author: "moksa (https://moksaweb.com)"
8
- version: "1.31.1"
8
+ version: "1.31.3"
9
9
  homepage: "https://github.com/Moksa1123/mosaic-headless"
10
10
  ---
11
11
 
@@ -817,6 +817,12 @@ post — `build_all.py` resets first for that reason.
817
817
  default - it is a `<span>`, a label rather than a control. Measured all three.
818
818
  `menu-link` and `wysiwyg-link` still render `<span>` without a `url`.
819
819
  - **`text` renders as `<div>` by default** — set `tagName` for `<h1>`, `<p>` and so on.
820
+ - **`youtube` renders `<mosaic-youtube><iframe>` with no `title`** - the iframe gets
821
+ only `loading`, `src`, `allow` and an id, so an accessible name has to be added
822
+ some other way. `youtubeUrl` is `{"v": "https://www.youtube.com/watch?v=…"}`;
823
+ `lazyLoad` adds `loading="lazy"` and there is no click-to-load facade. Sized by
824
+ `width`/`height: 100%` inside an `aspect-ratio: 16/9` box. `privacy` alone still
825
+ emits nothing (NO_OUTPUT).
820
826
  - **Eight types emit custom elements**: `<mosaic-dropdown>`, `<mosaic-navbar>`,
821
827
  `<mosaic-tabs>`, `<mosaic-accordion>`, `<mosaic-vimeo>`, `<mosaic-youtube>` and
822
828
  friends. A selector written against `div`/`nav` misses all of them.
@@ -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.31.1"
23
+ "version": "1.31.3"
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.31.1"
23
+ "version": "1.31.3"
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.31.1"
23
+ "version": "1.31.3"
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.31.1"
23
+ "version": "1.31.3"
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.31.1"
23
+ "version": "1.31.3"
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mosaic-headless",
3
- "version": "1.31.1",
3
+ "version": "1.31.3",
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",
@@ -66,6 +66,14 @@ Two different shapes for the same language, and which one applies is decided by
66
66
  property's validator chain in `data/node-properties.csv`: **`ValidatorDynamicCode`
67
67
  means a bare string, `ValidatorDynamicCodeObject` means `{"v": "…"}`**.
68
68
 
69
+ Getting it wrong on a URL is silent and looks like a broken link rather than a broken
70
+ expression. `menu-link`'s `url` chain is `ValidatorDynamicCodeObject → ValidatorURL →
71
+ ValidatorUrlObject`; given a bare `"@concat('/contact/?ref=', @VAR('post/title'), '#consult')"`
72
+ it commits without complaint and delivers
73
+ `href="http://concat('/contact/?ref=%27…"` - the leading `@` dropped and the rest taken
74
+ as a URL (measured on a client build on 1.0.10). As `{"v": "@concat(…)"}` it delivers the evaluated link.
75
+
76
+
69
77
  ## The variable namespace
70
78
 
71
79
  `data/dynamic-variables.csv` — 74 variables across 11 namespaces, each row carrying
@@ -108,7 +116,18 @@ fallback(…) defaulting
108
116
  attachment(2) find_image(2) find_link(2) find_video(2) media
109
117
  ```
110
118
 
111
- `sum`, `avg`, `concat` and `fallback` are variadic. `date()`'s timestamp argument is a
119
+ `sum`, `avg`, `concat` and `fallback` are variadic.
120
+
121
+ **`@concat` drops any argument that evaluates to `'0'`** (and `''`). Each argument is
122
+ `strval()`-ed and appended only `if ($item)` (`EvaluatorFunctionConcat.php`), and
123
+ `'0'` is the one non-empty string PHP calls false. So a number that can be zero must
124
+ not be built into a sentence with `@concat`:
125
+ `@concat('SEARCH · ', @VAR('wordpress_search/resultCount'), ' 件作品')` delivers
126
+ `SEARCH · 件作品` on an empty search - the count vanishes, the spaces stay. Put the
127
+ number in its own `wysiwyg-variable` between two text runs instead; a bare
128
+ `@VAR(…)` prints `0`. The same goes for a price, a stock count or a loop index that
129
+ can be 0. `@fallback` has the mirror-image rule: it treats `'0'` as empty, and returns
130
+ its fallback only if that is truthy, so `'0'` can never be the fallback. `date()`'s timestamp argument is a
112
131
  true GMT Unix timestamp — pair it with `post/publish_timestamp_gmt`, not
113
132
  `publish_timestamp`, unless you want the local-time value re-interpreted as UTC.
114
133
 
@@ -140,6 +159,42 @@ one. `build_page.py` resolves it by treating a loop container as standing in for
140
159
  its parent's children: what `slider-loop-slides` accepts is what `slider-slides`
141
160
  accepts.
142
161
 
162
+ ## Filtering and ordering a post loop
163
+
164
+ A `wpPostType<Type>` loop takes `filters` (the condition structure of
165
+ `references/templates-and-conditions.md`: units in one `evaluationUnits` group are
166
+ AND-ed, groups are OR-ed) and `orderByClauses`. The filters are evaluated in the
167
+ context the loop sits in, so inside a single-post template `@VAR('post/…')` is the
168
+ post being viewed. Subject IDs are built from WordPress names, camel-cased after
169
+ every `-`, `_` or space (source: `FilterSubjectPostTypes.php`):
170
+
171
+ | subject | id | operators | value |
172
+ |---|---|---|---|
173
+ | the posts themselves | `postType<Type>` (`postTypeWork`) | `includes` / `excludes` | a list of post ids; dynamic |
174
+ | a taxonomy | `taxonomy<Taxonomy>` (`taxonomyWorkCategory`) | `includes` / `excludes` | term ids; dynamic |
175
+ | a meta key | `postType<Type>PostMeta`, `settings.key` | the comparator you pick: `text`, `number`, `date` | per comparator |
176
+
177
+ A dynamic filter value is `{"v": …}` - the field declares `SupportDynamic`, which the
178
+ reader validates with `ValidatorDynamicCodeObject`. "More work in the same category,
179
+ not this one", measured on a client build on 1.0.10:
180
+
181
+ ```json
182
+ {"uuid": "<uuid>", "type": "filter", "filterOptions": {"subject": {
183
+ "type": "postTypeWork", "postTypeWorkOptions": {"settings": {}, "comparator": {
184
+ "type": "postTypeWork", "postTypeWorkOptions": {
185
+ "operator": "excludes", "settings": {"value": {"v": "@VAR('post/id')"}}}}}}}}
186
+ ```
187
+
188
+ with a second unit in the same group on `taxonomyWorkCategory`, operator `includes`,
189
+ value `{"v": "@LOOP('post/work_category', 1, 'term_id')"}`. A Meta Box checkbox
190
+ (stored `"1"`) filters as subject `postTypeWorkPostMeta` with
191
+ `settings: {"key": "work_featured"}` and a `text` comparator
192
+ `{"operator": "equals", "settings": {"value": "1"}}` - six posts back, in order.
193
+
194
+ `orderByClauses` accepts `random`: `{"type": "random", "uuid": "<uuid>",
195
+ "randomOptions": {"settings": {}}}`, a different order on every uncached render. Under
196
+ a page cache that is one order per cache lifetime, not per visitor.
197
+
143
198
  ## `loop-pagination`, and the key you must not use
144
199
 
145
200
  ```
@@ -184,3 +239,40 @@ The less precise side sets the precision: a date against a date-time compares th
184
239
  two days. Whatever cannot be read as a date matches **no** operator, so a typo is a
185
240
  condition that is never true rather than an error. Loop filters compare in SQL
186
241
  (`post_date_gmt` / `post_modified_gmt` as UTC, the others as the site's wall clock).
242
+
243
+ ## The search results page
244
+
245
+ A `search.php` auto template (`references/templates-and-conditions.md`) has a
246
+ `wordpress_search` namespace. Measured on 1.0.10, searching a term with four matches
247
+ and one with none:
248
+
249
+ | expression | 4 results | none |
250
+ |---|---|---|
251
+ | `@VAR('wordpress_search/resultCount')` | `4` | `0` |
252
+ | `@VAR('wordpress_search/searchQuery')` | the term | the term |
253
+ | `@fallback(@VAR('wordpress_search/resultCount'), 'none')` | `4` | `none` |
254
+ | `@fallback(@VAR('wordpress_search/resultCount'), '0')` | `4` | `0` |
255
+
256
+ `resultCount` is `number_format_i18n(found_posts)` - a formatted string, so `1,234`
257
+ past a thousand, and `0` (not empty) when nothing matched. Do not wrap it in
258
+ `@concat`: a `0` there is dropped (above), which is exactly how "the count is empty
259
+ on a search with no results" was first reported from a client build. `@fallback` treats `''`,
260
+ `'0'` and `'NaN'` as empty, and returns its second argument only if THAT is truthy in
261
+ PHP, so a fallback of `'0'` can never be chosen; any other text can.
262
+
263
+ The results are a loop: `loopType: "localContext"`, `loopSource:
264
+ {"v": "@VAR('wordpress_search/results')"}` (a `@VAR`, as every loop source must be).
265
+ **Inside it each result is `item`, unless you rename it**:
266
+
267
+ | per-result expression | result |
268
+ |---|---|
269
+ | `@VAR('item/title')`, default namespace | the title |
270
+ | `@VAR('post/title')`, default namespace | empty - `post` is the page's own context, and a search page has none |
271
+ | `@VAR('post/title')` with `loopNamespace: "post"` on the loop | the title |
272
+
273
+ With `loopNamespace: "post"` every per-post expression works per result - meta,
274
+ `@LOOP('post/<taxonomy>', 1, 'term_name')`, the permalink. WordPress's own query
275
+ arguments narrow the search (`/?s=…&post_type=work&work_category=<slug>`), so a
276
+ filter bar is a set of `menu-link`s whose `url` is
277
+ `{"v": "@concat('/?post_type=work&work_category=<slug>&s=', @VAR('wordpress_search/searchQuery'))"}`
278
+ - the term survives the switch.
@@ -31,11 +31,31 @@ post; `X-Mosaic-Paths` on a 406 names the paths Mosaic looked for, and `index.ph
31
31
  in all of them. Measured A/B: a URL with no template returned 406, was handled once
32
32
  the row existed, and returned to 406 when it was deleted.
33
33
 
34
- Two limits, both measured: `adminTemplateEditorInstance` does not list auto templates,
35
- and their document has no `template-internal` root - `heal()` builds that only for
36
- templates created through `createManualTemplate`, and committing one directly answers
37
- HTTP 500. So the row stops the 406 but there is no verified route to putting content
38
- in it.
34
+ **On 1.0.10 an auto template is an ordinary, writable template.** Earlier releases
35
+ measured two limits here - `adminTemplateEditorInstance` did not list auto templates,
36
+ and their document had no `template-internal` root, so there was no route to putting
37
+ content in one. Re-measured on 1.0.10, both are gone:
38
+
39
+ ```
40
+ POST theme/<themeID>/adminTemplateEditorInstance/commit
41
+ revisionEnvelopes.template = [{"newRevisionRecord": {
42
+ "ID": "<uuid you mint>", "parentType": "", "parentID": "", "ordering": "a0",
43
+ "status": "publish", "revision": "", "version": "", "name": "Search",
44
+ "masterID": "<master>", "assign": "auto", "path": "search.php",
45
+ "conditions": []}, "originalRevisionRecord": null}]
46
+ ```
47
+
48
+ answers 200; the template's document already holds one node, the
49
+ `template-internal` root the server created; `build_site.build_page_document()`
50
+ writes a tree under it like any page's; and `/?s=…` renders it. The template is
51
+ listed by `adminTemplateEditorInstance` and is deleted the same way, with
52
+ `status: "delete"`. This is the commit the editor's own "add template" dialog sends -
53
+ `createAutoTemplate` is the post-type case only. Any path from the hierarchy works the
54
+ same way: `search.php`, `404.php`, `archive-<type>.php`, `taxonomy-<tax>.php`.
55
+
56
+ An auto template is not in `template_assigns` - it binds by path - so a clean-up that
57
+ deletes "templates nothing is assigned to" deletes it. Recreate it after any such
58
+ prune.
39
59
 
40
60
  **`post/<id>` is the only resourceQuery `createManualTemplate` accepts.** The grammar in
41
61
  `ResourceQuery::create()` is just `explode('/', $s, 2)`, so anything parses - but the