@godxjp/ui 32.9.0 → 32.10.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "32.9.0",
4
- "godxUiMcp": "32.9.0",
3
+ "version": "32.10.1",
4
+ "godxUiMcp": "32.10.1",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
package/scripts/cli.mjs CHANGED
@@ -85,14 +85,17 @@ const HELP = {
85
85
  --dry-run list every change without writing
86
86
  What it cannot decide (a component move that changes props, an AppProvider that relied on the
87
87
  old vi default) is printed as a "note" for you to finish.`,
88
- "visual-audit": `godxjp-ui visual-audit [--format json] [--strict] [--widths WxH,…] <baseUrl> [route …]
88
+ "visual-audit": `godxjp-ui visual-audit [--format json] [--strict] [--widths WxH,…] [--storage-state FILE] <baseUrl> [route …]
89
89
 
90
90
  Runtime audit (Playwright) against an app you are ALREADY running locally, at six widths.
91
91
  baseUrl the running app, e.g. http://localhost:5173
92
92
  route … paths appended to baseUrl (default "/")
93
93
  --widths viewports to audit (default 320x900,390x844,768x1024,1024x900,1280x900,844x390;
94
94
  a bare width uses height 900)
95
- --strict exit non-zero on any finding, not only errors
95
+ --storage-state a Playwright storageState JSON (cookies + localStorage) so signed-in routes
96
+ are audited signed in; save one with
97
+ npx playwright codegen --save-storage=state.json <url>
98
+ --strict exit non-zero on any finding, not only errors
96
99
  --format json machine-readable: findings (viewport, element, threshold + origin), unmapped
97
100
  items (reviewer required), per-width rule status + reviewer checklist
98
101
  --rules print the visual rule catalog as JSON and exit
@@ -3,7 +3,7 @@
3
3
  * godxjp-ui VISUAL audit — the runtime counterpart to the static ui-audit.mjs. Static audit
4
4
  * (ui-audit.mjs) reads SOURCE with regexes — zero deps, fast, runs on every save.
5
5
  */
6
- import { realpathSync } from "node:fs";
6
+ import { existsSync, realpathSync } from "node:fs";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import {
9
9
  VISUAL_RULES,
@@ -212,6 +212,11 @@ export function collectInPage() {
212
212
  "[role=alert], [role=status], [data-slot=alert], [class*=banner], [class*=notification]",
213
213
  )) {
214
214
  if (!visible(el)) continue;
215
+ // A screen-reader live region (`span.sr-only[role=status]`, 1x1 and clipped) is not a banner:
216
+ // nothing is painted, so there is no anatomy to get wrong (gh#1307 — BlockEditor's own status
217
+ // region was reported as "controls stacked vertically"). Its own box OR an ancestor's clip.
218
+ const own = el.getBoundingClientRect();
219
+ if (own.width <= 1 || own.height <= 1 || clippedAway(el)) continue;
215
220
  // The kit's EmptyState is a polite status region, not a banner: it has no Alert anatomy to get
216
221
  // wrong, so the banner checks do not apply to it (gh#1198).
217
222
  if (el.closest("[data-slot=empty-state]")) continue;
@@ -263,7 +268,11 @@ export function collectInPage() {
263
268
  return { layer, ok: v !== bad && v !== "" };
264
269
  };
265
270
  const layers = [
266
- probe("control", "ui-button", "borderRadius", "0px"),
271
+ // gh#1307 — this read `borderRadius` on a bare `.ui-button`, and no rule in control.css gives
272
+ // `.ui-button` a radius (the corner comes from the variant classes), so the probe read 0px
273
+ // with the layer loaded and reported it missing on every consumer page. `display: inline-flex`
274
+ // is what the bare `.ui-button` rule itself declares; a div without the layer stays `block`.
275
+ probe("control", "ui-button", "display", "block"),
267
276
  probe("navigation-layout", "ui-dropdown-menu-content", "borderTopWidth", "0px"),
268
277
  // `.ui-card` is NOT a class the library sets rules on — Card renders `cn("group/card", …)`
269
278
  // and card-layout.css owns `.ui-card-bar` / `.ui-card-cover` / `.ui-card-inset*` /
@@ -490,7 +499,7 @@ export const PROBE_RULES = [
490
499
  category: "layout",
491
500
  standard: `toggle centre within 8px of the declared rail edge at ≥1024px (${RC})`,
492
501
  kind: "probe",
493
- fix: "An edge-mounted rail toggle sits on the rail edge it controls in both desktop states. Declare the pattern with data-audit-role=rail-toggle (+ data-audit-edge=start|end).",
502
+ fix: "An edge-mounted rail toggle sits on the rail edge it controls in both desktop states. Declare the pattern with data-audit-role=rail-toggle (+ data-audit-edge=start|end). A toggle inside `Sidebar action` is the supported placement and reports not-applicable-sidebar-action.",
494
503
  },
495
504
  {
496
505
  id: "c11-search-keyline",
@@ -638,7 +647,8 @@ export function isTrackedUppercaseLabel(l) {
638
647
  *
639
648
  * Status per rule: fail | pass | partial (mapped elements pass, but unmapped candidates remain) |
640
649
  * unmapped (only candidates) | not-found (nothing to measure — the reviewer confirms N/A) |
641
- * not-applicable-at-width | checklist. Only "pass" is a machine pass, and only for what was mapped.
650
+ * not-applicable-at-width | not-applicable-end-aligned (C11) | not-applicable-sidebar-action (C10) |
651
+ * checklist. Only "pass" is a machine pass, and only for what was mapped.
642
652
  */
643
653
  export function evaluateProbes(raw, { url, viewport }) {
644
654
  const T = PROBE_THRESHOLDS;
@@ -825,6 +835,8 @@ export function evaluateProbes(raw, { url, viewport }) {
825
835
  );
826
836
  noteUnmapped("c10-rail-toggle-attached", raw.c10?.candidates);
827
837
  settle("c10-rail-toggle-attached", toggles.length);
838
+ if (rules["c10-rail-toggle-attached"] === "not-found" && raw.c10?.sidebarAction?.length)
839
+ rules["c10-rail-toggle-attached"] = "not-applicable-sidebar-action";
828
840
 
829
841
  // C11 — desktop search keyline.
830
842
  const keylines = applies("c11-search-keyline") ? raw.c11?.mapped || [] : [];
@@ -1117,7 +1129,13 @@ export function collectProbes(opts) {
1117
1129
  reason:
1118
1130
  "looks like a search field but carries no search mapping (SearchInput, role=searchbox, type=search or data-audit-role=search)",
1119
1131
  }));
1120
- const searchTriggers = [...document.querySelectorAll("[data-audit-role=search-trigger]")]
1132
+ // The kit's own TopbarSearch folds to its trigger below `collapseBelow`; the consumer cannot
1133
+ // annotate that internal button, so it is mapped through its own hook (gh#1307).
1134
+ const searchTriggers = [
1135
+ ...document.querySelectorAll(
1136
+ "[data-audit-role=search-trigger], [data-slot=topbar-search] .ui-topbar-search-trigger",
1137
+ ),
1138
+ ]
1121
1139
  .filter(shown)
1122
1140
  .map((el) => ({ element: desc(el), probe: tag(el) }));
1123
1141
 
@@ -1298,6 +1316,7 @@ export function collectProbes(opts) {
1298
1316
  // ── C10: edge-mounted rail toggle ───────────────────────────────────────────────────────
1299
1317
  const toggles = [];
1300
1318
  const railCandidates = [];
1319
+ const sidebarActionToggles = [];
1301
1320
  if (width >= DESKTOP) {
1302
1321
  const rail = [
1303
1322
  ...document.querySelectorAll("[data-audit-role=rail], .app-sidebar, .sb-root"),
@@ -1326,7 +1345,11 @@ export function collectProbes(opts) {
1326
1345
  for (const t of document.querySelectorAll("[aria-controls]")) {
1327
1346
  if (t.matches("[data-audit-role=rail-toggle]") || !shown(t) || !sidebar) continue;
1328
1347
  const target = document.getElementById(t.getAttribute("aria-controls"));
1329
- if (target && sidebar.contains(target))
1348
+ if (!target || !sidebar.contains(target)) continue;
1349
+ // `Sidebar action` is the placement DESIGN.md prescribes for a collapse toggle — inside the
1350
+ // rail, so the edge-mounted pattern C10 measures does not apply to it (gh#1307).
1351
+ if (t.closest("[data-slot=sidebar-action]")) sidebarActionToggles.push({ element: desc(t) });
1352
+ else
1330
1353
  railCandidates.push({
1331
1354
  element: desc(t),
1332
1355
  reason:
@@ -1439,7 +1462,7 @@ export function collectProbes(opts) {
1439
1462
  c04: { mapped: accounts, candidates: accountCandidates },
1440
1463
  c05: { mapped: editors },
1441
1464
  c06: { mapped: groups, candidates: actionCandidates },
1442
- c10: { mapped: toggles, candidates: railCandidates },
1465
+ c10: { mapped: toggles, candidates: railCandidates, sidebarAction: sidebarActionToggles },
1443
1466
  c11: { mapped: keylines, candidates: keylineCandidates, endAligned },
1444
1467
  c12: { mapped: labels, candidates: labelCandidates },
1445
1468
  };
@@ -1547,7 +1570,7 @@ async function loadDeps() {
1547
1570
  * (page/context/browser) is guaranteed via finally on BOTH success and failure.
1548
1571
  */
1549
1572
  /* c8 ignore start — the browser-driving glue; exercised by scripts/visual-audit-smoke.mjs in CI. */
1550
- async function audit(targets, { chromium }, viewports = DEFAULT_VIEWPORTS) {
1573
+ async function audit(targets, { chromium }, viewports = DEFAULT_VIEWPORTS, { storageState } = {}) {
1551
1574
  const findings = [];
1552
1575
  const errors = [];
1553
1576
  const matrix = [];
@@ -1568,7 +1591,7 @@ async function audit(targets, { chromium }, viewports = DEFAULT_VIEWPORTS) {
1568
1591
  "(or set PLAYWRIGHT_CHROMIUM_EXECUTABLE to a system Chromium).";
1569
1592
  throw err;
1570
1593
  }
1571
- context = await browser.newContext({ viewport: viewports[0] });
1594
+ context = await browser.newContext({ viewport: viewports[0], storageState });
1572
1595
  const page = await context.newPage();
1573
1596
 
1574
1597
  for (const url of targets) {
@@ -1813,12 +1836,32 @@ async function main() {
1813
1836
  return;
1814
1837
  }
1815
1838
 
1839
+ // `--storage-state <file>` — a Playwright storageState JSON (cookies + localStorage), so a
1840
+ // signed-in route is audited signed in (gh#1307: the consumer had to patch a copy to get past
1841
+ // its login). Create one with `npx playwright codegen --save-storage=state.json <url>`.
1842
+ const ssIdx = args.indexOf("--storage-state");
1843
+ const storageState = ssIdx === -1 ? undefined : args[ssIdx + 1];
1844
+ if (ssIdx !== -1 && (!storageState || !existsSync(storageState))) {
1845
+ finish(
1846
+ buildBootstrapError(
1847
+ `--storage-state: "${storageState ?? ""}" is not a readable file`,
1848
+ "pass a Playwright storageState JSON, e.g. from `npx playwright codegen --save-storage=state.json <url>`",
1849
+ ),
1850
+ { asJson, strict, pages: 0 },
1851
+ );
1852
+ return;
1853
+ }
1854
+
1816
1855
  const urls = args.filter(
1817
- (a, i) => !a.startsWith("--") && args[i - 1] !== "--format" && args[i - 1] !== "--widths",
1856
+ (a, i) =>
1857
+ !a.startsWith("--") &&
1858
+ args[i - 1] !== "--format" &&
1859
+ args[i - 1] !== "--widths" &&
1860
+ args[i - 1] !== "--storage-state",
1818
1861
  );
1819
1862
  if (urls.length === 0) {
1820
1863
  const msg =
1821
- "no baseUrl given — usage: visual-audit.mjs [--format json] [--strict] [--widths 390x844,1280x900] <baseUrl> [route …] " +
1864
+ "no baseUrl given — usage: visual-audit.mjs [--format json] [--strict] [--widths 390x844,1280x900] [--storage-state state.json] <baseUrl> [route …] " +
1822
1865
  "(baseUrl is the running app; extra args are routes appended to it)";
1823
1866
  finish(buildBootstrapError(msg), { asJson, strict, pages: 0 });
1824
1867
  return;
@@ -1839,7 +1882,7 @@ async function main() {
1839
1882
  }
1840
1883
 
1841
1884
  try {
1842
- const result = await audit(targets, deps, viewports);
1885
+ const result = await audit(targets, deps, viewports, { storageState });
1843
1886
  finish(result, { asJson, strict, pages: targets.length });
1844
1887
  } catch (e) {
1845
1888
  // A launch/setup failure = infrastructure error (never "zero violations").