mosaic-headless 1.29.0 → 1.29.2
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 -5
- 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 +7 -0
- package/data/content-loop-verification.csv +2 -1
- package/data/form-surface-verification.csv +3 -0
- package/data/mobile-audit.csv +4 -2
- package/data/navigation-verification.csv +11 -11
- package/package.json +1 -1
- package/references/failure-modes.md +45 -0
- package/references/placement.md +31 -0
- package/scripts/check-release.mjs +4 -4
- package/sites/moksa.json +4764 -2224
- package/tools/build_page.py +17 -0
- package/tools/verify_components.py +113 -0
package/tools/build_page.py
CHANGED
|
@@ -172,8 +172,14 @@ class Surface:
|
|
|
172
172
|
any(x in needs for x in (ancestors + ((parent_type,)
|
|
173
173
|
if parent_type else ()))))
|
|
174
174
|
|
|
175
|
+
# ...and the same question the `allow` branch asks below: is this type
|
|
176
|
+
# part of the required internal structure of something we are inside?
|
|
177
|
+
in_ancestor_structure = any(t in self.default_children.get(x, [])
|
|
178
|
+
for x in ancestors)
|
|
179
|
+
|
|
175
180
|
outcome = self.outcome.get(t)
|
|
176
181
|
if (not declared_child and not nested_as_family and not nested_ok
|
|
182
|
+
and not in_ancestor_structure
|
|
177
183
|
and (outcome == "BROKE_PAGE" or (outcome or "").startswith("COMMIT_5"))):
|
|
178
184
|
problems.append("%s is measured %s under a plain container, and %s is not its declared parent"
|
|
179
185
|
% (t, outcome, parent_type or "<root>"))
|
|
@@ -205,6 +211,17 @@ class Surface:
|
|
|
205
211
|
# disagreement, take the rule from the grandparent, which is both
|
|
206
212
|
# types' actual authority.
|
|
207
213
|
# ancestors[-1] IS the parent; the grandparent is one further up
|
|
214
|
+
# A type that is part of the required internal structure of
|
|
215
|
+
# something we are INSIDE is legal wherever that structure puts
|
|
216
|
+
# it. `multi-steps-forms` declares `canBeParentFor` ->
|
|
217
|
+
# `form-multi-steps`, which is its own parent; the real child,
|
|
218
|
+
# `multi-steps-form-step`, is in `form-multi-steps`'s healed
|
|
219
|
+
# default children. Same disagreement as the loop containers.
|
|
220
|
+
if t not in allowed:
|
|
221
|
+
for anc in ancestors:
|
|
222
|
+
if t in self.default_children.get(anc, []):
|
|
223
|
+
allowed = allowed + [t]
|
|
224
|
+
break
|
|
208
225
|
if t not in allowed and "-loop-" in parent_type and len(ancestors) > 1:
|
|
209
226
|
gp = self.rules.get(ancestors[-2], {})
|
|
210
227
|
allowed = allowed + (gp.get("allowed_children") or "").split("|")
|
|
@@ -53,6 +53,14 @@ def main():
|
|
|
53
53
|
help="a loop-pagination-numbers whose children are page links")
|
|
54
54
|
ap.add_argument("--paged-item", default="ct-arch-title",
|
|
55
55
|
help="the repeated attrID whose text should change per page")
|
|
56
|
+
ap.add_argument("--navbar", default="nb",
|
|
57
|
+
help="a navbar; its toggle is <id>-toggle and its nav <id>-nav")
|
|
58
|
+
ap.add_argument("--media", default="md-img",
|
|
59
|
+
help="an image node; icons are looked for beside it")
|
|
60
|
+
ap.add_argument("--shortcode", default="wp-sc",
|
|
61
|
+
help="a wpShortcode node")
|
|
62
|
+
ap.add_argument("--steps", default="ms-panel-%d:3",
|
|
63
|
+
help="multi-step panel attrID pattern and how many")
|
|
56
64
|
ap.add_argument("--instances", default="cs-card",
|
|
57
65
|
help="a component's root attrID; instances are prefixed")
|
|
58
66
|
a = ap.parse_args()
|
|
@@ -167,6 +175,111 @@ def main():
|
|
|
167
175
|
check("FORM/required_blocks", bool(invalid),
|
|
168
176
|
"empty form reports invalid: %s" % invalid[:4])
|
|
169
177
|
|
|
178
|
+
# ---- navbar: the toggle only exists below its breakpoint
|
|
179
|
+
if not present(a.navbar):
|
|
180
|
+
skip("NAVBAR", "not on this page")
|
|
181
|
+
else:
|
|
182
|
+
wide = p.evaluate("""(id) => {
|
|
183
|
+
const t = document.getElementById(id + '-toggle');
|
|
184
|
+
const n = document.getElementById(id + '-nav');
|
|
185
|
+
const vis = e => { const r = e.getBoundingClientRect(),
|
|
186
|
+
s = getComputedStyle(e);
|
|
187
|
+
return r.height > 2 && s.display !== 'none'
|
|
188
|
+
&& s.visibility !== 'hidden'; };
|
|
189
|
+
return {toggle: vis(t), nav: vis(n),
|
|
190
|
+
links: n.querySelectorAll('a').length};
|
|
191
|
+
}""", a.navbar)
|
|
192
|
+
check("NAVBAR/wide", wide["nav"] and not wide["toggle"],
|
|
193
|
+
"at this width the links are shown and the toggle is not "
|
|
194
|
+
"(nav=%s toggle=%s, %d links)"
|
|
195
|
+
% (wide["nav"], wide["toggle"], wide["links"]))
|
|
196
|
+
p.set_viewport_size({"width": 900, "height": 900})
|
|
197
|
+
p.wait_for_timeout(900)
|
|
198
|
+
narrow = p.evaluate("""(id) => {
|
|
199
|
+
const t = document.getElementById(id + '-toggle');
|
|
200
|
+
const n = document.getElementById(id + '-nav');
|
|
201
|
+
const vis = e => { const r = e.getBoundingClientRect(),
|
|
202
|
+
s = getComputedStyle(e);
|
|
203
|
+
return r.height > 2 && s.display !== 'none'; };
|
|
204
|
+
return {toggle: vis(t), nav: vis(n),
|
|
205
|
+
mode: String(document.getElementById(id).className)
|
|
206
|
+
.includes('dropdown-mode')};
|
|
207
|
+
}""", a.navbar)
|
|
208
|
+
check("NAVBAR/narrow", narrow["toggle"] and not narrow["nav"]
|
|
209
|
+
and narrow["mode"],
|
|
210
|
+
"below buttonBreakpoint it collapses behind the toggle "
|
|
211
|
+
"(dropdown-mode=%s)" % narrow["mode"])
|
|
212
|
+
p.click("#%s-toggle" % a.navbar)
|
|
213
|
+
p.wait_for_timeout(800)
|
|
214
|
+
opened = p.evaluate("""(id) => {
|
|
215
|
+
const n = document.getElementById(id + '-nav');
|
|
216
|
+
const r = n.getBoundingClientRect();
|
|
217
|
+
return r.height > 2 && getComputedStyle(n).display !== 'none'; }""",
|
|
218
|
+
a.navbar)
|
|
219
|
+
check("NAVBAR/toggle_opens", opened, "clicking it reveals the links")
|
|
220
|
+
p.set_viewport_size({"width": 1280, "height": 900})
|
|
221
|
+
p.wait_for_timeout(700)
|
|
222
|
+
|
|
223
|
+
# ---- media: an attachment-protocol image and raw-SVG icons
|
|
224
|
+
if not present(a.media):
|
|
225
|
+
skip("MEDIA", "not on this page")
|
|
226
|
+
else:
|
|
227
|
+
img = p.evaluate("""(id) => {
|
|
228
|
+
const e = document.querySelector('#' + id + ' img, img#' + id);
|
|
229
|
+
if (!e) return null;
|
|
230
|
+
return {w: Math.round(e.getBoundingClientRect().width),
|
|
231
|
+
natural: e.naturalWidth, src: (e.currentSrc || e.src || '')};
|
|
232
|
+
}""", a.media)
|
|
233
|
+
# naturalWidth > 0 is the only proof the file actually loaded; a broken
|
|
234
|
+
# src renders an <img> of the right size and no picture
|
|
235
|
+
check("MEDIA/image_loaded",
|
|
236
|
+
bool(img) and img["natural"] > 0 and img["w"] > 0,
|
|
237
|
+
"%dpx wide, natural %spx" % (img["w"], img["natural"]) if img
|
|
238
|
+
else "no <img> rendered")
|
|
239
|
+
icons = p.evaluate("""() => [...document.querySelectorAll('svg')]
|
|
240
|
+
.filter(s => s.closest('[id^=md-i]')).length""")
|
|
241
|
+
check("MEDIA/icons_inline", icons > 0,
|
|
242
|
+
"%d inline <svg> - `icon` takes RAW SVG in its `svg` property, "
|
|
243
|
+
"not a URL or a library name" % icons)
|
|
244
|
+
|
|
245
|
+
# ---- WordPress interop
|
|
246
|
+
if not present(a.shortcode):
|
|
247
|
+
skip("WORDPRESS", "not on this page")
|
|
248
|
+
else:
|
|
249
|
+
n = p.evaluate("""(id) => document.querySelectorAll(
|
|
250
|
+
'#' + id + ' .product, #' + id + ' li').length""", a.shortcode)
|
|
251
|
+
check("WORDPRESS/shortcode_ran", n > 0,
|
|
252
|
+
"%d items from the shortcode - this is WooCommerce's own "
|
|
253
|
+
"output, not a reimplementation" % n)
|
|
254
|
+
|
|
255
|
+
# ---- a multi-step form shows ONE step at a time
|
|
256
|
+
step_pat, n_steps = a.steps.rsplit(":", 1)
|
|
257
|
+
n_steps = int(n_steps)
|
|
258
|
+
if not present(step_pat % 0):
|
|
259
|
+
skip("MULTISTEP", "not on this page")
|
|
260
|
+
else:
|
|
261
|
+
def steps_shown():
|
|
262
|
+
return [i for i in range(n_steps)
|
|
263
|
+
if p.evaluate(VISIBLE, step_pat % i)]
|
|
264
|
+
at_rest = steps_shown()
|
|
265
|
+
# Mosaic ships NO visibility CSS for a form step - the plugin moves
|
|
266
|
+
# `m-form-step--active` and leaves the hiding to the theme's variant.
|
|
267
|
+
# Without that rule every panel is on screen at once and the form
|
|
268
|
+
# looks like one long page with three sets of buttons.
|
|
269
|
+
check("MULTISTEP/one_at_rest", at_rest == [0],
|
|
270
|
+
"visible steps: %s" % at_rest
|
|
271
|
+
+ ("" if at_rest == [0] else
|
|
272
|
+
" - nothing hides the inactive ones; style .m-form-step"))
|
|
273
|
+
if at_rest == [0]:
|
|
274
|
+
p.click("#ms-next-0")
|
|
275
|
+
p.wait_for_timeout(900)
|
|
276
|
+
check("MULTISTEP/next", steps_shown() == [1],
|
|
277
|
+
"NEXT -> %s" % steps_shown())
|
|
278
|
+
p.click("#ms-back-1")
|
|
279
|
+
p.wait_for_timeout(900)
|
|
280
|
+
check("MULTISTEP/back", steps_shown() == [0],
|
|
281
|
+
"BACK -> %s" % steps_shown())
|
|
282
|
+
|
|
170
283
|
# ---- component instances
|
|
171
284
|
#
|
|
172
285
|
# An instance's ids are PREFIXED with the instance's own attrID
|