zz-meridian 0.19.2 → 0.19.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-meridian",
3
- "version": "0.19.2",
3
+ "version": "0.19.3",
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.3] · 2026-10-19
6
+
7
+ ### Fixed
8
+
9
+ - **The presses suite finds a control again when its name has gained a live count** ([#66](https://github.com/zhixuan312/zz-meridian/issues/66)). `ShellTools` names its button `Alerts`, then `Alerts, 3 new` once its read answers; on a loaded machine the first load was listed before that read and the fresh load after it, so 0.19.2's find-by-name looked for `Alerts`, waited out the hang bound and reported "was listed but could not be found again". When the exact name is gone, the suite now finds the control by the part of its name before the first comma, and which of the controls that share it it is. Names stay as they are: a count in the accessible name is still how a screen reader hears it.
10
+
5
11
  ## [0.19.2] · 2026-10-19
6
12
 
7
13
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-meridian-template",
3
- "version": "0.19.2",
3
+ "version": "0.19.3",
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.",
@@ -46,13 +46,19 @@ const HANG_BOUND_MS = 5000;
46
46
  /**
47
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
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).
49
+ * another order or fewer of them for a while, a table still loading or rows a concurrent write changed (#65). A name that
50
+ * carries a live part after a comma ("Alerts, 3 new") is found again by the part before it once the exact name is gone (#66).
50
51
  */
51
52
  const CONTROLS = `
52
53
  const deepClosest = (el, sel) => { for (let n = el; n; n = deepParent(n)) if (n.matches(sel)) return n; return null; };
53
54
  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'); };
54
55
  const hosts = (el) => { const out = []; for (let r = el.getRootNode(); r instanceof ShadowRoot; r = r.host.getRootNode()) out.unshift(r.host.localName); return out; };
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
+ const label = (b) => (b.getAttribute('aria-label') || deepText(b) || b.title || '').trim().replace(/\\s+/g, ' ').slice(0, 60);
57
+ const within = (b) => { const h = hosts(b); return h.length ? ' (in ' + h.join(' > ') + ')' : ''; };
58
+ const nameOf = (b) => label(b) + within(b);
59
+ // What the control is without what it says about now: the name before its first comma, so "Alerts, 3 new" is still the
60
+ // "Alerts" listed before the read answered (#66).
61
+ const keyOf = (b) => label(b).split(',')[0].trim() + within(b);
56
62
  // A control already chosen is not pressed: choosing it again changes nothing, which is not a dead control.
57
63
  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
64
  const pressable = (every) => every.filter((b) => b.matches('button, [role="button"], [role="tab"], [role="switch"], [role="checkbox"], [role="radio"], [role="menuitem"]'))
@@ -62,13 +68,15 @@ const LIST = `(() => {${CONTROLS}
62
68
  const every = deepAll(document.body);
63
69
  return {
64
70
  controls: pressable(every).map(nameOf),
71
+ keys: pressable(every).map(keyOf),
65
72
  links: every.filter((a) => a.matches('a[href]') && vis(a)).map((a) => a.getAttribute('href')),
66
73
  };
67
74
  })()`;
68
75
 
69
76
  /** Scroll the control into view, check nothing covers it, and start counting what changes. */
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;
77
+ const ARM = (name: string, nth: number, key: string, keyNth: number) => `(() => {${CONTROLS}
78
+ const all = pressable(deepAll(document.body));
79
+ const el = all.filter((b) => nameOf(b) === ${JSON.stringify(name)})[${nth}] ?? all.filter((b) => keyOf(b) === ${JSON.stringify(key)})[${keyNth}]; if (!el) return null;
72
80
  el.scrollIntoView({ block: 'center', inline: 'nearest' });
73
81
  const r = el.getBoundingClientRect(), x = r.left + r.width / 2, y = r.top + r.height / 2;
74
82
  const hit = document.elementFromPoint(x, y);
@@ -120,8 +128,8 @@ for (const width of WIDTHS) {
120
128
  const touch = width < 600;
121
129
  for (const route of ROUTES) {
122
130
  // The page's late chrome first (scripts/lib/settle.ts): a control that arrives after the list is taken is never pressed, and the count reads as if it had been.
123
- const fresh = async () => { await page.open(base + route, { width, theme: 'dark', cookie }); await inject(route); await settle(page); return page.eval<{ controls: string[]; links: string[] }>(LIST); };
124
- const { controls, links } = await fresh();
131
+ const fresh = async () => { await page.open(base + route, { width, theme: 'dark', cookie }); await inject(route); await settle(page); return page.eval<{ controls: string[]; keys: string[]; links: string[] }>(LIST); };
132
+ const { controls, keys, links } = await fresh();
125
133
  // A link is followed as a browser follows it: its href as the page writes it, resolved against the page. Skipping every
126
134
  // href that did not start with "/" let a relative link that answered 404 pass (/system/system/contributing).
127
135
  for (const href of new Set(links)) {
@@ -133,11 +141,13 @@ for (const width of WIDTHS) {
133
141
  if (status >= 400) failures.push(`${route}: link ${href}${path === href ? '' : ` (${path})`} answers ${status}`);
134
142
  }
135
143
  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.
144
+ // Which of the controls named alike this is, in the order the first load listed them; and, for a name that has
145
+ // changed since (a count that arrived), which of the controls that are the same thing.
137
146
  const nth = controls.slice(0, i).filter((c) => c === controls[i]).length;
147
+ const keyNth = keys.slice(0, i).filter((k) => k === keys[i]).length;
138
148
  type Armed = { x: number; y: number; covered?: string } | null;
139
149
  // A document replaced since the last read has no helper: that is a page to open again, never a crash (#64).
140
- const arm = () => page.eval<Armed>(ARM(controls[i], nth)).catch((): Armed => null);
150
+ const arm = () => page.eval<Armed>(ARM(controls[i], nth, keys[i], keyNth)).catch((): Armed => null);
141
151
  let armed = await arm();
142
152
  // 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
153
  // new load (a list still loading) is waited for up to the hang bound before it is called gone.