zz-meridian 0.19.1 → 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 +1 -1
- package/payload/CHANGELOG.md +12 -0
- package/payload/package.json +1 -1
- package/payload/scripts/interactions.ts +41 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zz-meridian",
|
|
3
|
-
"version": "0.19.
|
|
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",
|
package/payload/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
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
|
+
|
|
11
|
+
## [0.19.2] · 2026-10-19
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **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.
|
|
16
|
+
|
|
5
17
|
## [0.19.1] · 2026-10-19
|
|
6
18
|
|
|
7
19
|
### Fixed
|
package/payload/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zz-meridian-template",
|
|
3
|
-
"version": "0.19.
|
|
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.",
|
|
@@ -43,24 +43,40 @@ 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
|
-
|
|
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). 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).
|
|
51
|
+
*/
|
|
52
|
+
const CONTROLS = `
|
|
47
53
|
const deepClosest = (el, sel) => { for (let n = el; n; n = deepParent(n)) if (n.matches(sel)) return n; return null; };
|
|
48
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'); };
|
|
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
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; };
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const
|
|
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);
|
|
62
|
+
// A control already chosen is not pressed: choosing it again changes nothing, which is not a dead control.
|
|
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'));
|
|
64
|
+
const pressable = (every) => every.filter((b) => b.matches('button, [role="button"], [role="tab"], [role="switch"], [role="checkbox"], [role="radio"], [role="menuitem"]'))
|
|
54
65
|
.filter((b) => !b.disabled && b.getAttribute('aria-disabled') !== 'true' && vis(b) && !chosen(b));
|
|
66
|
+
`;
|
|
67
|
+
const LIST = `(() => {${CONTROLS}
|
|
68
|
+
const every = deepAll(document.body);
|
|
55
69
|
return {
|
|
56
|
-
controls:
|
|
70
|
+
controls: pressable(every).map(nameOf),
|
|
71
|
+
keys: pressable(every).map(keyOf),
|
|
57
72
|
links: every.filter((a) => a.matches('a[href]') && vis(a)).map((a) => a.getAttribute('href')),
|
|
58
73
|
};
|
|
59
74
|
})()`;
|
|
60
75
|
|
|
61
76
|
/** Scroll the control into view, check nothing covers it, and start counting what changes. */
|
|
62
|
-
const ARM = (
|
|
63
|
-
const
|
|
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;
|
|
64
80
|
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
65
81
|
const r = el.getBoundingClientRect(), x = r.left + r.width / 2, y = r.top + r.height / 2;
|
|
66
82
|
const hit = document.elementFromPoint(x, y);
|
|
@@ -76,7 +92,7 @@ const ARM = (i: number) => `(() => {
|
|
|
76
92
|
window.__mo = new MutationObserver((ms) => { for (const m of ms) {
|
|
77
93
|
if (tipOnly(m.target)) continue;
|
|
78
94
|
if (m.type === 'childList' && [...m.addedNodes, ...m.removedNodes].every(tipOnly)) continue;
|
|
79
|
-
if (m.type === 'attributes' && m.target === el && ['data-state', 'class', 'style', '
|
|
95
|
+
if (m.type === 'attributes' && m.target === el && ['data-state', 'class', 'style', 'aria-describedby'].includes(m.attributeName)) continue;
|
|
80
96
|
window.__changes++;
|
|
81
97
|
} });
|
|
82
98
|
setTimeout(() => roots().forEach((r) => window.__mo.observe(r, { subtree: true, childList: true, attributes: true, characterData: true })), 40);
|
|
@@ -112,8 +128,8 @@ for (const width of WIDTHS) {
|
|
|
112
128
|
const touch = width < 600;
|
|
113
129
|
for (const route of ROUTES) {
|
|
114
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.
|
|
115
|
-
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); };
|
|
116
|
-
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();
|
|
117
133
|
// A link is followed as a browser follows it: its href as the page writes it, resolved against the page. Skipping every
|
|
118
134
|
// href that did not start with "/" let a relative link that answered 404 pass (/system/system/contributing).
|
|
119
135
|
for (const href of new Set(links)) {
|
|
@@ -125,10 +141,21 @@ for (const width of WIDTHS) {
|
|
|
125
141
|
if (status >= 400) failures.push(`${route}: link ${href}${path === href ? '' : ` (${path})`} answers ${status}`);
|
|
126
142
|
}
|
|
127
143
|
for (let i = 0; i < controls.length; i++) {
|
|
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.
|
|
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;
|
|
148
|
+
type Armed = { x: number; y: number; covered?: string } | null;
|
|
128
149
|
// A document replaced since the last read has no helper: that is a page to open again, never a crash (#64).
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
const arm = () => page.eval<Armed>(ARM(controls[i], nth, keys[i], keyNth)).catch((): Armed => null);
|
|
151
|
+
let armed = await arm();
|
|
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
|
|
153
|
+
// new load (a list still loading) is waited for up to the hang bound before it is called gone.
|
|
154
|
+
if (!armed || armed.covered) {
|
|
155
|
+
await fresh();
|
|
156
|
+
armed = await arm();
|
|
157
|
+
for (let waited = 0; !armed && waited < HANG_BOUND_MS; waited += 200) { await sleep(200); armed = await arm(); }
|
|
158
|
+
}
|
|
132
159
|
// Tagged by this suite a moment ago and gone after a fresh load: say so, never skip it.
|
|
133
160
|
if (!armed) { failures.push(`${route} @${width}: "${controls[i]}" was listed but could not be found again`); continue; }
|
|
134
161
|
if (armed.covered) { failures.push(`${route} @${width}: "${controls[i]}" is covered by ${armed.covered}`); continue; }
|