zz-meridian 0.19.1 → 0.19.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-meridian",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "description": "Bring ZZ Meridian, a dashboard design system, into a Next.js project, or start a new dashboard on it. Copies the files in; nothing depends on this package at runtime.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,6 +2,12 @@
2
2
 
3
3
  Every release of ZZ Meridian, newest first. Versions follow semver: a removed or renamed token, prop or card is major; a new card, token or variant is minor; a corrected value is a patch. Each entry says what breaks and what to do instead.
4
4
 
5
+ ## [0.19.2] · 2026-10-19
6
+
7
+ ### Fixed
8
+
9
+ - **The presses suite finds a control again by what it is** ([#65](https://github.com/zhixuan312/zz-meridian/issues/65)). It tagged each control by its place in the walk and, after opening the route again, pressed whatever stood at that place: a later load that drew the controls in another order, or fewer of them for a while (a table still loading on a busy machine, rows another suite's write changed), pressed the wrong control or reported "was listed but could not be found again" for one that was on the page a moment later. A control is now found by its accessible name, and which of the controls named alike it is, and one not drawn yet is waited for up to the hang bound before it is called gone.
10
+
5
11
  ## [0.19.1] · 2026-10-19
6
12
 
7
13
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-meridian-template",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "private": true,
5
5
  "license": "MIT",
6
6
  "description": "ZZ Meridian: a dashboard design system and starter. Six layers, from DTCG tokens through four layers of React components to product workspaces, and a Design Atlas, in a light and a dark theme.",
@@ -43,24 +43,32 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
43
43
  const HANG_BOUND_MS = 5000;
44
44
 
45
45
  /** Tag every visible, enabled, not-yet-chosen control with its index; return their names (with the hosts they sit in) and the page's links. */
46
- const LIST = `(() => {
46
+ /**
47
+ * The page's controls and how each is named: its accessible name and the roots it sits in. A control is found again by
48
+ * that name (and which of the controls with it), never by its place in the walk: a later load can draw the controls in
49
+ * another order or fewer of them for a while, a table still loading or rows a concurrent write changed (#65).
50
+ */
51
+ const CONTROLS = `
47
52
  const deepClosest = (el, sel) => { for (let n = el; n; n = deepParent(n)) if (n.matches(sel)) return n; return null; };
48
53
  const vis = (el) => { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && !deepClosest(el, '[aria-hidden="true"],[inert],.sr-only'); };
49
- const chosen = (el) => ['aria-pressed', 'aria-checked', 'aria-selected'].some((a) => el.getAttribute(a) === 'true') || el.getAttribute('aria-current') === 'page' || ['on', 'active', 'checked'].includes(el.getAttribute('data-state'));
50
54
  const hosts = (el) => { const out = []; for (let r = el.getRootNode(); r instanceof ShadowRoot; r = r.host.getRootNode()) out.unshift(r.host.localName); return out; };
51
- const every = deepAll(document.body);
52
- every.forEach((n) => n.removeAttribute('data-press'));
53
- const all = every.filter((b) => b.matches('button, [role="button"], [role="tab"], [role="switch"], [role="checkbox"], [role="radio"], [role="menuitem"]'))
55
+ const nameOf = (b) => { const h = hosts(b); return (b.getAttribute('aria-label') || deepText(b) || b.title || '').trim().replace(/\\s+/g, ' ').slice(0, 60) + (h.length ? ' (in ' + h.join(' > ') + ')' : ''); };
56
+ // A control already chosen is not pressed: choosing it again changes nothing, which is not a dead control.
57
+ const chosen = (el) => ['aria-pressed', 'aria-checked', 'aria-selected'].some((a) => el.getAttribute(a) === 'true') || el.getAttribute('aria-current') === 'page' || ['on', 'active', 'checked'].includes(el.getAttribute('data-state'));
58
+ const pressable = (every) => every.filter((b) => b.matches('button, [role="button"], [role="tab"], [role="switch"], [role="checkbox"], [role="radio"], [role="menuitem"]'))
54
59
  .filter((b) => !b.disabled && b.getAttribute('aria-disabled') !== 'true' && vis(b) && !chosen(b));
60
+ `;
61
+ const LIST = `(() => {${CONTROLS}
62
+ const every = deepAll(document.body);
55
63
  return {
56
- controls: all.map((b, i) => { b.setAttribute('data-press', i); const h = hosts(b); return (b.getAttribute('aria-label') || deepText(b) || b.title || '').trim().replace(/\\s+/g, ' ').slice(0, 60) + (h.length ? ' (in ' + h.join(' > ') + ')' : ''); }),
64
+ controls: pressable(every).map(nameOf),
57
65
  links: every.filter((a) => a.matches('a[href]') && vis(a)).map((a) => a.getAttribute('href')),
58
66
  };
59
67
  })()`;
60
68
 
61
69
  /** Scroll the control into view, check nothing covers it, and start counting what changes. */
62
- const ARM = (i: number) => `(() => {
63
- const el = deepAll(document.body).find((n) => n.getAttribute('data-press') === '${i}'); if (!el) return null;
70
+ const ARM = (name: string, nth: number) => `(() => {${CONTROLS}
71
+ const el = pressable(deepAll(document.body)).filter((b) => nameOf(b) === ${JSON.stringify(name)})[${nth}]; if (!el) return null;
64
72
  el.scrollIntoView({ block: 'center', inline: 'nearest' });
65
73
  const r = el.getBoundingClientRect(), x = r.left + r.width / 2, y = r.top + r.height / 2;
66
74
  const hit = document.elementFromPoint(x, y);
@@ -76,7 +84,7 @@ const ARM = (i: number) => `(() => {
76
84
  window.__mo = new MutationObserver((ms) => { for (const m of ms) {
77
85
  if (tipOnly(m.target)) continue;
78
86
  if (m.type === 'childList' && [...m.addedNodes, ...m.removedNodes].every(tipOnly)) continue;
79
- if (m.type === 'attributes' && m.target === el && ['data-state', 'class', 'style', 'data-press', 'aria-describedby'].includes(m.attributeName)) continue;
87
+ if (m.type === 'attributes' && m.target === el && ['data-state', 'class', 'style', 'aria-describedby'].includes(m.attributeName)) continue;
80
88
  window.__changes++;
81
89
  } });
82
90
  setTimeout(() => roots().forEach((r) => window.__mo.observe(r, { subtree: true, childList: true, attributes: true, characterData: true })), 40);
@@ -125,10 +133,19 @@ for (const width of WIDTHS) {
125
133
  if (status >= 400) failures.push(`${route}: link ${href}${path === href ? '' : ` (${path})`} answers ${status}`);
126
134
  }
127
135
  for (let i = 0; i < controls.length; i++) {
136
+ // Which of the controls named alike this is, in the order the first load listed them.
137
+ const nth = controls.slice(0, i).filter((c) => c === controls[i]).length;
138
+ type Armed = { x: number; y: number; covered?: string } | null;
128
139
  // A document replaced since the last read has no helper: that is a page to open again, never a crash (#64).
129
- let armed = await page.eval<{ x: number; y: number; covered?: string } | null>(ARM(i)).catch(() => null);
130
- // Something left open or a toast in the way: start the page again and try once more.
131
- if (!armed || armed.covered) { await fresh(); armed = await page.eval(ARM(i)); }
140
+ const arm = () => page.eval<Armed>(ARM(controls[i], nth)).catch((): Armed => null);
141
+ let armed = await arm();
142
+ // Something left open or a toast in the way: start the page again and try once more. A control not drawn yet on the
143
+ // new load (a list still loading) is waited for up to the hang bound before it is called gone.
144
+ if (!armed || armed.covered) {
145
+ await fresh();
146
+ armed = await arm();
147
+ for (let waited = 0; !armed && waited < HANG_BOUND_MS; waited += 200) { await sleep(200); armed = await arm(); }
148
+ }
132
149
  // Tagged by this suite a moment ago and gone after a fresh load: say so, never skip it.
133
150
  if (!armed) { failures.push(`${route} @${width}: "${controls[i]}" was listed but could not be found again`); continue; }
134
151
  if (armed.covered) { failures.push(`${route} @${width}: "${controls[i]}" is covered by ${armed.covered}`); continue; }