@leonidstern/census 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +189 -2
- package/cli/cdp.mjs +199 -0
- package/cli/census.mjs +227 -0
- package/cli/playwright.mjs +63 -0
- package/cli/read.mjs +59 -0
- package/cli/sourcemap.mjs +110 -0
- package/dist/browser.d.ts +1 -0
- package/dist/census.d.ts +193 -0
- package/dist/census.js +1990 -0
- package/dist/census.mjs +2111 -0
- package/dist/color.d.ts +9 -0
- package/dist/diff.d.ts +15 -0
- package/dist/findings.d.ts +79 -0
- package/dist/index.d.ts +6 -0
- package/package.json +63 -4
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The census inside your own Playwright tests: after your sign-in, setup and steps, read the page you are on.
|
|
2
|
+
//
|
|
3
|
+
// import { census, lines, save } from "census/playwright";
|
|
4
|
+
// const home = await census(page, { rules, sources: true });
|
|
5
|
+
// console.log(lines(home));
|
|
6
|
+
// save("census-out", { home }); // home.txt and census.json, for census diff
|
|
7
|
+
//
|
|
8
|
+
// Playwright isn't a dependency: you pass your own page. Forced hover, pressed and focus colors need Chromium (they
|
|
9
|
+
// go through Chrome's DevTools Protocol); in Firefox and WebKit the census reads them from the CSS rules.
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { forceStates, mapPlaces, pageMapper } from "./read.mjs";
|
|
14
|
+
import { censusLines, mergeCensus } from "../dist/census.mjs";
|
|
15
|
+
|
|
16
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
let script;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The census of the page as it is now (a Census: groups and, for census diff, each element's look).
|
|
21
|
+
* - rules: your design system's values (rules.json's shape)
|
|
22
|
+
* - states: "force" (default; Chromium only, else "rules") or "rules"
|
|
23
|
+
* - sources: true adds "@ file:line" where each part that needs a fix was used (a dev build), mapped through the
|
|
24
|
+
* page's source maps; root: the project folder, to write the paths relative to it (default: the current one)
|
|
25
|
+
* - hide: a selector of elements to hide first (a dev overlay, a cookie banner)
|
|
26
|
+
* - selector: read only this element and what is inside it
|
|
27
|
+
*/
|
|
28
|
+
export async function census(page, { rules = {}, states = "force", sources = false, root, hide, selector } = {}) {
|
|
29
|
+
script ??= readFileSync(join(here, "../dist/census.js"), "utf8");
|
|
30
|
+
// an evaluate runs in the page's own world, past its Content-Security-Policy, and changes nothing but window.census
|
|
31
|
+
if (!(await page.evaluate(() => !!window.census))) await page.evaluate(script);
|
|
32
|
+
if (hide) await page.evaluate((h) => { const s = document.createElement("style"); s.id = "census-hide"; s.textContent = `${h}{display:none!important}`; document.head.append(s); }, hide);
|
|
33
|
+
const cdp = states === "force" ? await page.context().newCDPSession(page).catch(() => null) : null;
|
|
34
|
+
try {
|
|
35
|
+
if (cdp) {
|
|
36
|
+
await cdp.send("DOM.enable"); await cdp.send("CSS.enable");
|
|
37
|
+
let root;
|
|
38
|
+
await forceStates({
|
|
39
|
+
evaluate: (fn, arg) => page.evaluate(fn, arg),
|
|
40
|
+
async nodes(sel) { root ??= (await cdp.send("DOM.getDocument", { depth: 0 })).root.nodeId; return (await cdp.send("DOM.querySelectorAll", { nodeId: root, selector: sel })).nodeIds; },
|
|
41
|
+
force: (ids, classes) => Promise.all(ids.map((nodeId) => cdp.send("CSS.forcePseudoState", { nodeId, forcedPseudoClasses: classes }))),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const data = await page.evaluate(([r, sel]) => window.census.data(r, sel), [{ ...rules, sources, items: true }, selector]);
|
|
45
|
+
if (!sources) return data;
|
|
46
|
+
const origin = new URL(page.url()).origin;
|
|
47
|
+
return mapPlaces(data, pageMapper(page, new Set(/^https?:/.test(origin) ? [origin] : []), root ?? process.cwd()));
|
|
48
|
+
} finally {
|
|
49
|
+
if (hide) await page.evaluate(() => document.getElementById("census-hide")?.remove()).catch(() => {});
|
|
50
|
+
await cdp?.detach().catch(() => {});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A census as text, the way the CLI writes it. */
|
|
55
|
+
export const lines = (data) => censusLines(data).join("\n");
|
|
56
|
+
|
|
57
|
+
/** Writes <name>.txt for each page, all.txt (the pages merged) when there are several, and census.json, as the CLI does. */
|
|
58
|
+
export function save(dir, pages) {
|
|
59
|
+
mkdirSync(dir, { recursive: true });
|
|
60
|
+
for (const [name, data] of Object.entries(pages)) writeFileSync(join(dir, `${name}.txt`), `${lines(data)}\n`);
|
|
61
|
+
if (Object.keys(pages).length > 1) writeFileSync(join(dir, "all.txt"), `${lines(mergeCensus(pages))}\n`);
|
|
62
|
+
writeFileSync(join(dir, "census.json"), JSON.stringify(pages));
|
|
63
|
+
}
|
package/cli/read.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// What a capture does in a page, for any driver: the CLI's own DevTools client and the Playwright helper. A `page`
|
|
2
|
+
// here has evaluate(fn, arg), and for forced states nodes(selector) → protocol node ids and force(nodeIds, classes).
|
|
3
|
+
import { mapSources, sourceMapper } from "./sourcemap.mjs";
|
|
4
|
+
|
|
5
|
+
// Chrome forces each state on each control in view (hover and press on everything above it too, as a real pointer
|
|
6
|
+
// would), with transitions off, and the census reads the control's colors in that state. Controls alike are forced
|
|
7
|
+
// once (census.forceMark). Each step replaces the last forcing, so a control takes 4 reads and 3 forcings. Returns how
|
|
8
|
+
// many controls were forced.
|
|
9
|
+
export async function forceStates(page) {
|
|
10
|
+
await page.evaluate(() => { const s = document.createElement("style"); s.id = "census-still"; s.textContent = "*,*::before,*::after{transition:none!important;animation-play-state:paused!important}"; document.head.append(s); });
|
|
11
|
+
const step = (i, read, next) => page.evaluate(([j, r, n]) => window.census.forceRead(j, r, n), [i, read, next]);
|
|
12
|
+
try {
|
|
13
|
+
const n = await page.evaluate(() => window.census.forceMark());
|
|
14
|
+
for (let i = 0; i < n; i++) {
|
|
15
|
+
const [el] = await page.nodes(`[data-census-f="${i}"]`);
|
|
16
|
+
if (!el) continue;
|
|
17
|
+
const up = await page.nodes(`[data-census-up~="${i}"]`);
|
|
18
|
+
await step(i, null, "hover");
|
|
19
|
+
await page.force([el, ...up], ["hover"]);
|
|
20
|
+
await step(i, "hover", "pressed");
|
|
21
|
+
await page.force([el, ...up], ["hover", "active"]);
|
|
22
|
+
await step(i, "pressed", "focus");
|
|
23
|
+
await page.force(up, []);
|
|
24
|
+
await page.force([el], ["focus", "focus-visible"]);
|
|
25
|
+
await step(i, "focus", null);
|
|
26
|
+
await page.force([el], []);
|
|
27
|
+
}
|
|
28
|
+
return n;
|
|
29
|
+
} finally {
|
|
30
|
+
await page.evaluate(() => { window.census.forceDone(); document.getElementById("census-still")?.remove(); });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A source mapper that fetches maps through the page (its cookies, its cache), only from `origins` (a page's stack
|
|
36
|
+
* frames could name any address), with no redirects and at most 64 MB a file.
|
|
37
|
+
*/
|
|
38
|
+
export function pageMapper(page, origins, root = process.cwd()) {
|
|
39
|
+
return sourceMapper(async (u) => {
|
|
40
|
+
let url;
|
|
41
|
+
try { url = new URL(u); } catch { return null; }
|
|
42
|
+
if (!origins.has(url.origin)) return null;
|
|
43
|
+
return page.evaluate(async (h) => {
|
|
44
|
+
try {
|
|
45
|
+
const r = await fetch(h, { redirect: "error", credentials: "same-origin" });
|
|
46
|
+
if (!r.ok || Number(r.headers.get("content-length") ?? 0) > 64e6) return null;
|
|
47
|
+
const t = await r.text();
|
|
48
|
+
return t.length > 64e6 ? null : t;
|
|
49
|
+
} catch { return null; }
|
|
50
|
+
}, url.href).catch(() => null);
|
|
51
|
+
}, root);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Every " @ {bundle frames}" place in a census (or a map of them) mapped to its file and line. */
|
|
55
|
+
export async function mapPlaces(v, map) {
|
|
56
|
+
return typeof v === "string" ? (v.includes(" @ ") ? mapSources(v, map) : v)
|
|
57
|
+
: Array.isArray(v) ? Promise.all(v.map((x) => mapPlaces(x, map)))
|
|
58
|
+
: v && typeof v === "object" ? Object.fromEntries(await Promise.all(Object.entries(v).map(async ([k, x]) => [k, await mapPlaces(x, map)]))) : v;
|
|
59
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Bundled-code places ("http://host/chunk.js:120:33") back to the app's files ("src/ui/Header.tsx:42"),
|
|
2
|
+
// through the source maps the page serves. Plain source map v3, index maps (sections) included; no dependencies.
|
|
3
|
+
|
|
4
|
+
const B64 = new Map([..."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"].map((c, i) => [c, i]));
|
|
5
|
+
|
|
6
|
+
/** One line of mappings as [generated column, source, line, column] segments (absolute values). */
|
|
7
|
+
function decode(mappings) {
|
|
8
|
+
const lines = [];
|
|
9
|
+
let src = 0, line = 0, col = 0;
|
|
10
|
+
for (const text of mappings.split(";")) {
|
|
11
|
+
const segs = [];
|
|
12
|
+
let gen = 0;
|
|
13
|
+
for (const seg of text.split(",")) {
|
|
14
|
+
if (!seg) continue;
|
|
15
|
+
const v = [];
|
|
16
|
+
for (let i = 0, shift = 0, val = 0; i < seg.length; i++) {
|
|
17
|
+
const d = B64.get(seg[i]);
|
|
18
|
+
val += (d & 31) << shift;
|
|
19
|
+
if (d & 32) shift += 5;
|
|
20
|
+
else { v.push(val & 1 ? -(val >>> 1) : val >>> 1); val = 0; shift = 0; }
|
|
21
|
+
}
|
|
22
|
+
gen += v[0];
|
|
23
|
+
if (v.length >= 4) { src += v[1]; line += v[2]; col += v[3]; segs.push([gen, src, line, col]); }
|
|
24
|
+
}
|
|
25
|
+
lines.push(segs);
|
|
26
|
+
}
|
|
27
|
+
return lines;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Where a 1-based line and column of the generated file came from: { source, line } or null. */
|
|
31
|
+
function lookup(map, line, col) {
|
|
32
|
+
if (map.sections) {
|
|
33
|
+
let best = null;
|
|
34
|
+
for (const s of map.sections) {
|
|
35
|
+
const o = s.offset;
|
|
36
|
+
if (o.line + 1 < line || (o.line + 1 === line && o.column <= col - 1)) best = s;
|
|
37
|
+
else break;
|
|
38
|
+
}
|
|
39
|
+
if (!best) return null;
|
|
40
|
+
const o = best.offset;
|
|
41
|
+
return lookup(best.map, line - o.line, line - 1 === o.line ? col - o.column : col);
|
|
42
|
+
}
|
|
43
|
+
map._lines ??= decode(map.mappings ?? "");
|
|
44
|
+
const segs = map._lines[line - 1];
|
|
45
|
+
if (!segs?.length) return null;
|
|
46
|
+
let hit = null;
|
|
47
|
+
for (const s of segs) { if (s[0] <= col - 1) hit = s; else break; }
|
|
48
|
+
hit ??= segs[0];
|
|
49
|
+
const source = (map.sourceRoot ? map.sourceRoot.replace(/\/?$/, "/") : "") + map.sources[hit[1]];
|
|
50
|
+
return { source, line: hit[2] + 1 };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A source path as the app's own: bundler prefixes off, relative to the project. */
|
|
54
|
+
export function cleanSource(s, root = process.cwd()) {
|
|
55
|
+
let p = decodeURIComponent(s)
|
|
56
|
+
.replace(/^(turbopack|webpack|webpack-internal):\/\/\/?(\[project\]\/|_N_E\/|\(app-pages-browser\)\/)?/, "")
|
|
57
|
+
.replace(/^file:\/\//, "")
|
|
58
|
+
.replace(/\?.*$/, "")
|
|
59
|
+
.replace(/^\.\//, "");
|
|
60
|
+
if (p.startsWith(root + "/")) p = p.slice(root.length + 1);
|
|
61
|
+
return p;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A mapper for one capture: `get(url)` fetches a file as text (through the browser context, so a signed-in
|
|
66
|
+
* page's chunks come too). map("url:line:col") gives "src/x.tsx:42", or null when there is no map.
|
|
67
|
+
*/
|
|
68
|
+
export function sourceMapper(get, root) {
|
|
69
|
+
const maps = new Map();
|
|
70
|
+
const mapOf = async (url) => {
|
|
71
|
+
if (maps.has(url)) return maps.get(url);
|
|
72
|
+
let map = null;
|
|
73
|
+
try {
|
|
74
|
+
const js = await get(url);
|
|
75
|
+
const ref = /\/[/*][#@]\s*sourceMappingURL=([^\s*]+)\s*(?:\*\/)?\s*$/.exec(js ?? "")?.[1];
|
|
76
|
+
if (ref) {
|
|
77
|
+
const text = ref.startsWith("data:") ? Buffer.from(ref.slice(ref.indexOf(",") + 1), /;base64,/.test(ref) ? "base64" : "utf8").toString("utf8") : await get(new URL(ref, url).href);
|
|
78
|
+
map = text ? JSON.parse(text) : null;
|
|
79
|
+
}
|
|
80
|
+
} catch { map = null; }
|
|
81
|
+
maps.set(url, map);
|
|
82
|
+
return map;
|
|
83
|
+
};
|
|
84
|
+
return async (place) => {
|
|
85
|
+
const m = /^(.*):(\d+):(\d+)$/.exec(place);
|
|
86
|
+
if (!m) return null;
|
|
87
|
+
const map = await mapOf(m[1]);
|
|
88
|
+
const hit = map && lookup(map, +m[2], +m[3]);
|
|
89
|
+
return hit ? `${cleanSource(hit.source, root)}:${hit.line}` : null;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The census text with its places mapped: "{frame frame frame}" (React 19's stack at the use site) becomes the
|
|
95
|
+
* first frame that maps into the app's own files; a lone "url:line:col" after " @ " becomes its file and line.
|
|
96
|
+
*/
|
|
97
|
+
export async function mapSources(text, map) {
|
|
98
|
+
const groups = [...new Set(text.match(/ @ \{[^{}]+\}| @ (?:https?|file):\/\/\S+:\d+:\d+/g) ?? [])];
|
|
99
|
+
const said = new Map();
|
|
100
|
+
for (const g of groups) {
|
|
101
|
+
const frames = g.slice(3).replace(/^\{|\}$/g, "").split(" ");
|
|
102
|
+
let out = null;
|
|
103
|
+
for (const f of frames) {
|
|
104
|
+
const s = await map(f);
|
|
105
|
+
if (s && !/node_modules|^\[externals\]|^\[turbopack\]/.test(s)) { out = s; break; }
|
|
106
|
+
}
|
|
107
|
+
said.set(g, out ? ` @ ${out}` : "");
|
|
108
|
+
}
|
|
109
|
+
return text.replace(/ @ \{[^{}]+\}| @ (?:https?|file):\/\/\S+:\d+:\d+/g, (g) => said.get(g) ?? "");
|
|
110
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/census.d.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { type CheckSetting, type Finding } from "./findings";
|
|
2
|
+
export declare const ownId: (el: Element) => string;
|
|
3
|
+
export declare function nameOf(el: Element): string;
|
|
4
|
+
/**
|
|
5
|
+
* The screen as lines, for a reader that can't see it: "window 1440x860",
|
|
6
|
+
* one line per region ("#toolbar 0,0 1440x52"), then what looks wrong:
|
|
7
|
+
* "cut-text" (its words don't fit and are clipped), "overlap" (two texts
|
|
8
|
+
* drawn over each other), "outside" (a region past the window's edge),
|
|
9
|
+
* "wrapped" (a row's items dropped onto another line), "no-edge" (a filled
|
|
10
|
+
* button the color of what's behind it), "faint" (words or an icon too close
|
|
11
|
+
* to the color behind them: contrast from computed styles, no pixels),
|
|
12
|
+
* "faded" (a control see-through but not turned off), "squeezed" (a list
|
|
13
|
+
* meant to scroll given almost no height), "covered" (something solid over a
|
|
14
|
+
* control's middle).
|
|
15
|
+
* Drawn things (a canvas) show only as their box; the page's state says
|
|
16
|
+
* what's in them. Words come only from the app's own controls
|
|
17
|
+
* (buttons, menu items, headings), never from what people wrote; mark
|
|
18
|
+
* anything else private with data-census-private. About 50 elements a millisecond; run it once, on Send.
|
|
19
|
+
*/
|
|
20
|
+
export declare function layoutLines(root?: Element, cap?: number, skip?: (el: Element) => boolean, faint?: number): string[];
|
|
21
|
+
/** What shows behind an element, from computed styles: its own background color, else its ancestors'. For a canvas whose background is CSS and not drawn. */
|
|
22
|
+
export declare function behind(el: Element | null): [number, number, number, number];
|
|
23
|
+
/** A text-align said by the writing direction: start, end, center or justify. */
|
|
24
|
+
export declare function sayAlign(a: string, dir: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* How a box lays out its children, as a key: "row gap 8 pad 8 16 items center", "grid 3 cols gap 0/16", "column gaps
|
|
27
|
+
* 12-16 (margins)", or a filled box's padding, "box pad 16". The gaps are measured between the children as drawn, so
|
|
28
|
+
* margins count too, and the key says when they, not gap, make the space. Null for a box that lays nothing out.
|
|
29
|
+
*/
|
|
30
|
+
export declare function layoutOf(el: Element, cs: CSSStyleDeclaration, r: DOMRect, filled: boolean): {
|
|
31
|
+
key: string;
|
|
32
|
+
kids: string;
|
|
33
|
+
} | null;
|
|
34
|
+
/**
|
|
35
|
+
* A design system's allowed values, to mark what is off it. Colors as CSS
|
|
36
|
+
* colors; sizes in px. `parts`: the design system's part names, so a place
|
|
37
|
+
* is named by its part (Button) rather than the component around it, and a
|
|
38
|
+
* native control that no listed part drew says so. `shadows`: its named
|
|
39
|
+
* box-shadows. `ignore`: a selector for subtrees to leave out. `foreign:
|
|
40
|
+
* false` keeps what no component drew (by default, on a page with component
|
|
41
|
+
* names, such a subtree with controls in it is named once and left out).
|
|
42
|
+
* `closed`: also what pop-ups not opened yet would look like (React only).
|
|
43
|
+
* `sources`: " @ file:line" where a place's part was used, on the places that
|
|
44
|
+
* need a fix (set where used, off the system, a part in two looks).
|
|
45
|
+
* `items`: also each element's own look (Census.items), for diffCensus.
|
|
46
|
+
* `extends`: "recommended@1" turns on the accessibility checks, "best-practice@1" the max check (findings.ts CHECKS);
|
|
47
|
+
* either a name or a list of them. `checks` sets one check's level and number ("off", "rule", a number, or
|
|
48
|
+
* ["rule", number]).
|
|
49
|
+
* `max`: how many distinct values a group may hold (findings.ts MAX_DEFAULTS), by census group name — { "fill": 16 },
|
|
50
|
+
* or "off" for a group. The max check is on by default when rules.json gives no values at all, so a run with no file
|
|
51
|
+
* still says something.
|
|
52
|
+
*/
|
|
53
|
+
export type StyleRules = {
|
|
54
|
+
colors?: Record<string, string>;
|
|
55
|
+
type?: string[];
|
|
56
|
+
radii?: number[];
|
|
57
|
+
parts?: string[];
|
|
58
|
+
shadows?: Record<string, string>;
|
|
59
|
+
ignore?: string;
|
|
60
|
+
foreign?: boolean;
|
|
61
|
+
closed?: boolean;
|
|
62
|
+
sources?: boolean;
|
|
63
|
+
items?: boolean;
|
|
64
|
+
extends?: string | string[];
|
|
65
|
+
checks?: Record<string, CheckSetting>;
|
|
66
|
+
max?: Record<string, number | "off">;
|
|
67
|
+
/** what a box of a role must look like, by ARIA role (dialog, menu, listbox, tooltip) or part name: a color name, a shadow name, a corner in px */
|
|
68
|
+
roles?: Record<string, {
|
|
69
|
+
fill?: string;
|
|
70
|
+
shadow?: string;
|
|
71
|
+
radius?: number;
|
|
72
|
+
}>;
|
|
73
|
+
/** the color names text may use (icons aside) */
|
|
74
|
+
text?: string[];
|
|
75
|
+
/** what a state does to a control's fill: "wash white-5" (that color laid over the resting fill) or a color name */
|
|
76
|
+
states?: Record<string, string>;
|
|
77
|
+
/** parts that must no longer be on screen */
|
|
78
|
+
legacy?: string[];
|
|
79
|
+
/** the text colors each fill takes, by color name */
|
|
80
|
+
pairs?: Record<string, string[]>;
|
|
81
|
+
/** the lowest contrast text may have; 2 when not given */
|
|
82
|
+
contrast?: number;
|
|
83
|
+
};
|
|
84
|
+
/** The census as data: groups of counted values with a few places each. censusLines writes it out; mergeCensus joins pages. */
|
|
85
|
+
export type CensusLook = {
|
|
86
|
+
state: string;
|
|
87
|
+
shape: string;
|
|
88
|
+
skin: string;
|
|
89
|
+
say: string;
|
|
90
|
+
n: number;
|
|
91
|
+
at: string[];
|
|
92
|
+
lo?: number;
|
|
93
|
+
hi?: number;
|
|
94
|
+
};
|
|
95
|
+
export type CensusRow = {
|
|
96
|
+
key: string;
|
|
97
|
+
n: number;
|
|
98
|
+
note?: string;
|
|
99
|
+
at: string[];
|
|
100
|
+
looks?: CensusLook[];
|
|
101
|
+
flag?: boolean;
|
|
102
|
+
};
|
|
103
|
+
export type CensusGroup = {
|
|
104
|
+
name: string;
|
|
105
|
+
what?: string;
|
|
106
|
+
rows: CensusRow[];
|
|
107
|
+
more?: number;
|
|
108
|
+
};
|
|
109
|
+
/** One element's look, for a diff between two captures (rules.items): `id` names it with its words, `loose` without them. */
|
|
110
|
+
export type CensusItem = {
|
|
111
|
+
id: string;
|
|
112
|
+
loose: string;
|
|
113
|
+
at: string;
|
|
114
|
+
box: [number, number, number, number];
|
|
115
|
+
look: Record<string, string>;
|
|
116
|
+
};
|
|
117
|
+
export type Census = {
|
|
118
|
+
head: string;
|
|
119
|
+
groups: CensusGroup[];
|
|
120
|
+
items?: CensusItem[]; /** a second read a moment later, by the capture */
|
|
121
|
+
again?: {
|
|
122
|
+
items?: CensusItem[];
|
|
123
|
+
};
|
|
124
|
+
findings?: Finding[]; /** the accessibility findings are in the text: rules asked for them (extends or checks); without, the text is as it always was */
|
|
125
|
+
report?: boolean;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* A box-shadow as its layers: `rings` are the ones with no offset or blur
|
|
129
|
+
* (a line drawn as a shadow: "#ffffff1a 1px (ring)"), `rest` the real
|
|
130
|
+
* shadows ("0 4px 6px -1px #0000001a"). Layers that draw nothing (see-through,
|
|
131
|
+
* or all zero) are dropped.
|
|
132
|
+
*/
|
|
133
|
+
export declare function shadowLayers(css: string): {
|
|
134
|
+
rings: string[];
|
|
135
|
+
rest: string[];
|
|
136
|
+
};
|
|
137
|
+
/** The state a control says it is in (ARIA, data-state, disabled): "disabled", "selected", "open", or two of them; "" at rest. */
|
|
138
|
+
export declare function stateOf(el: Element): string;
|
|
139
|
+
/** A font stack said against the first one: what it adds and what it leaves out (whole, when they share little). */
|
|
140
|
+
export declare function fontDiff(stack: string, first: string): string;
|
|
141
|
+
/**
|
|
142
|
+
* The component that drew an element as its own root, with its variant
|
|
143
|
+
* props ("Button variant=secondary size=sm"): a `data-part` attribute (and
|
|
144
|
+
* `data-variant`, `data-size`) if the app sets one, else React's own
|
|
145
|
+
* component names, which dev builds keep. A part with no variant of its own
|
|
146
|
+
* takes its family's ("TabsTab of TabsList variant=secondary"), and a part
|
|
147
|
+
* drawn with another names both ("Badge variant=info < Text size=sm").
|
|
148
|
+
* Only short word-like prop values of the PART_PROPS keys, never text. ""
|
|
149
|
+
* when nothing says.
|
|
150
|
+
*/
|
|
151
|
+
export declare function partOf(el: Element, parts?: string[]): string;
|
|
152
|
+
/** A selector's specificity as one number (ids, then classes, attributes and pseudo-classes, then types): :where() counts nothing, :is(), :not() and :has() their most specific branch. */
|
|
153
|
+
export declare function specificity(sel: string): number;
|
|
154
|
+
/** The census written out: one line a value, a group under its heading. */
|
|
155
|
+
export declare function censusLines(c: Census): string[];
|
|
156
|
+
/**
|
|
157
|
+
* Several pages' censuses as one: a value on more than one page is said
|
|
158
|
+
* once, with the pages it is on ("(all)", "(home+cart)"), its counts added
|
|
159
|
+
* and a place from each page, tagged "[home]".
|
|
160
|
+
*/
|
|
161
|
+
export declare function mergeCensus(pages: Record<string, Census>, rules?: StyleRules): Census;
|
|
162
|
+
/**
|
|
163
|
+
* Every style the screen uses, counted, for a reader checking a design
|
|
164
|
+
* system without seeing it: font stacks, text styles ("type 14/500/20 ×38"),
|
|
165
|
+
* text, fill and line colors, corner radii, shadows, fields, control
|
|
166
|
+
* heights, the hover, pressed and focus colors the page's rules give each
|
|
167
|
+
* control, what a part's use site changed ("set where used"), pop-up layers,
|
|
168
|
+
* and each part with its looks by state. Each value names up to 3 places
|
|
169
|
+
* (role and the app's own words, the part that drew it, where, its box).
|
|
170
|
+
* Values nearly the same as a more used one are marked "near"; with rules,
|
|
171
|
+
* a value off them says OFF and names the nearest. Loading placeholders,
|
|
172
|
+
* controls under the pointer or focused when read (they wear a state),
|
|
173
|
+
* things caught mid-animation and what no component drew are counted apart.
|
|
174
|
+
* Then what the layout check finds. Only on request (a design check), never
|
|
175
|
+
* in a feedback bundle by default: it adds one hidden element to the page
|
|
176
|
+
* while it reads and takes it out again.
|
|
177
|
+
*/
|
|
178
|
+
export declare function styleLines(rules?: StyleRules, root?: Element, cap?: number): string[];
|
|
179
|
+
/** styleLines as data (see Census). */
|
|
180
|
+
export declare function styleData(rules?: StyleRules, root?: Element, cap?: number): Census;
|
|
181
|
+
/**
|
|
182
|
+
* Marks the controls in view (data-census-f="i", the ones above them data-census-up="i") and reads their resting look;
|
|
183
|
+
* returns how many to force. A control like one already marked shares its readings and isn't marked.
|
|
184
|
+
*/
|
|
185
|
+
export declare function forceMark(): number;
|
|
186
|
+
/**
|
|
187
|
+
* Control i, between two of Chrome's forcings: reads its look in the state just forced (`read`), takes off the
|
|
188
|
+
* attributes a kit sets from its own pointer handlers, then sets the ones of the state forced next (`next`). A press
|
|
189
|
+
* is read twice: as Chrome forces it, then with data-pressed too ("pressed-attr"), and the page's rules choose.
|
|
190
|
+
*/
|
|
191
|
+
export declare function forceRead(i: number, read: string | null, next: string | null): void;
|
|
192
|
+
/** Takes the marks off. */
|
|
193
|
+
export declare function forceDone(): void;
|