waybetter 0.6.0 → 0.7.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/README.md +1 -2
- package/bin/waybetter.mjs +2 -5
- package/cli/buildenv.mjs +1 -1
- package/cli/release.mjs +11 -18
- package/cli/releasecheck.mjs +15 -0
- package/cli/remote.mjs +4 -4
- package/cli/store.mjs +80 -28
- package/cli/strings.mjs +2 -0
- package/dist/runtime/stringPackHook.js +2 -2
- package/dist/runtime/stringPackHook.js.map +1 -1
- package/dist/runtime/stringPackTranslate.d.ts +3 -1
- package/dist/runtime/stringPackTranslate.js +6 -3
- package/dist/runtime/stringPackTranslate.js.map +1 -1
- package/dist/runtime/stringPacks.d.ts +53 -5
- package/dist/runtime/stringPacks.js +161 -25
- package/dist/runtime/stringPacks.js.map +1 -1
- package/dist/runtime/themeBar.d.ts +28 -0
- package/dist/runtime/themeBar.js +82 -0
- package/dist/runtime/themeBar.js.map +1 -0
- package/dist/runtime/themeComponents.js +16 -3
- package/dist/runtime/themeComponents.js.map +1 -1
- package/dist/runtime/themeDecor.js +3 -1
- package/dist/runtime/themeDecor.js.map +1 -1
- package/dist/runtime/themeDerive.d.ts +69 -0
- package/dist/runtime/themeDerive.js +117 -0
- package/dist/runtime/themeDerive.js.map +1 -0
- package/dist/runtime/themeDoctor.d.ts +44 -3
- package/dist/runtime/themeDoctor.js +86 -25
- package/dist/runtime/themeDoctor.js.map +1 -1
- package/dist/runtime/themeGuards.d.ts +60 -0
- package/dist/runtime/themeGuards.js +235 -0
- package/dist/runtime/themeGuards.js.map +1 -0
- package/dist/runtime/themeIcons.d.ts +1 -1
- package/dist/runtime/themeIcons.js +13 -5
- package/dist/runtime/themeIcons.js.map +1 -1
- package/dist/runtime/themeInk.d.ts +37 -0
- package/dist/runtime/themeInk.js +111 -0
- package/dist/runtime/themeInk.js.map +1 -0
- package/dist/runtime/themeInvariants.d.ts +39 -0
- package/dist/runtime/themeInvariants.js +273 -0
- package/dist/runtime/themeInvariants.js.map +1 -0
- package/dist/runtime/themeRuntime.d.ts +15 -1
- package/dist/runtime/themeRuntime.js +78 -5
- package/dist/runtime/themeRuntime.js.map +1 -1
- package/dist/runtime/themeSpec.d.ts +5 -1
- package/dist/runtime/themeSpec.js +9 -13
- package/dist/runtime/themeSpec.js.map +1 -1
- package/dist/shell/InstallDialog.js +14 -8
- package/dist/shell/InstallDialog.js.map +1 -1
- package/dist/shell/StoreView.d.ts +12 -0
- package/dist/shell/StoreView.js +9 -4
- package/dist/shell/StoreView.js.map +1 -1
- package/dist/shell/frame/AppMenu.d.ts +26 -3
- package/dist/shell/frame/AppMenu.js +106 -27
- package/dist/shell/frame/AppMenu.js.map +1 -1
- package/dist/shell/frame/FindBar.js +1 -1
- package/dist/shell/frame/FindBar.js.map +1 -1
- package/dist/shell/frame/Shell.d.ts +1 -0
- package/dist/shell/frame/Shell.js +10 -2
- package/dist/shell/frame/Shell.js.map +1 -1
- package/dist/shell/frame/Titlebar.d.ts +2 -1
- package/dist/shell/frame/Titlebar.js +5 -3
- package/dist/shell/frame/Titlebar.js.map +1 -1
- package/dist/shell/frame/WebPanel.js +4 -1
- package/dist/shell/frame/WebPanel.js.map +1 -1
- package/dist/shell/frame/accountSwitcher.css +10 -10
- package/dist/shell/frame/appMenu.css +19 -13
- package/dist/shell/frame/boot.js +2 -0
- package/dist/shell/frame/boot.js.map +1 -1
- package/dist/shell/frame/chromeTheme.d.ts +19 -1
- package/dist/shell/frame/chromeTheme.js +51 -14
- package/dist/shell/frame/chromeTheme.js.map +1 -1
- package/dist/shell/frame/deck.css +30 -30
- package/dist/shell/frame/edges.css +159 -0
- package/dist/shell/frame/index.d.ts +2 -2
- package/dist/shell/frame/index.js +1 -1
- package/dist/shell/frame/index.js.map +1 -1
- package/dist/shell/frame/mediaModel.d.ts +13 -0
- package/dist/shell/frame/mediaModel.js +14 -0
- package/dist/shell/frame/mediaModel.js.map +1 -1
- package/dist/shell/frame/menuMessages.en.json +43 -1
- package/dist/shell/frame/menuModel.d.ts +42 -0
- package/dist/shell/frame/menuModel.js +8 -0
- package/dist/shell/frame/menuModel.js.map +1 -1
- package/dist/shell/frame/nativeMenu.d.ts +5 -0
- package/dist/shell/frame/nativeMenu.js +178 -0
- package/dist/shell/frame/nativeMenu.js.map +1 -0
- package/dist/shell/frame/nativeMenuModel.d.ts +102 -0
- package/dist/shell/frame/nativeMenuModel.js +186 -0
- package/dist/shell/frame/nativeMenuModel.js.map +1 -0
- package/dist/shell/frame/paletteModel.js +3 -1
- package/dist/shell/frame/paletteModel.js.map +1 -1
- package/dist/shell/frame/paneTheme.css +1 -1
- package/dist/shell/frame/reportModel.d.ts +1 -1
- package/dist/shell/frame/reportModel.js +1 -1
- package/dist/shell/frame/reportModel.js.map +1 -1
- package/dist/shell/frame/shell.css +56 -51
- package/dist/shell/frame/state.d.ts +2 -0
- package/dist/shell/frame/state.js +3 -1
- package/dist/shell/frame/state.js.map +1 -1
- package/dist/shell/frame/themeStore.js +21 -6
- package/dist/shell/frame/themeStore.js.map +1 -1
- package/dist/shell/frame/workspaces.css +12 -12
- package/dist/shell/installController.d.ts +16 -1
- package/dist/shell/installController.js +31 -5
- package/dist/shell/installController.js.map +1 -1
- package/dist/shell/panel/Appearance.js +12 -3
- package/dist/shell/panel/Appearance.js.map +1 -1
- package/dist/shell/panel/Settings.d.ts +1 -1
- package/dist/shell/panel/Settings.js +4 -3
- package/dist/shell/panel/Settings.js.map +1 -1
- package/dist/shell/panel/StoreSection.d.ts +2 -1
- package/dist/shell/panel/StoreSection.js +31 -1
- package/dist/shell/panel/StoreSection.js.map +1 -1
- package/dist/shell/panel/StringPacks.d.ts +12 -3
- package/dist/shell/panel/StringPacks.js +33 -8
- package/dist/shell/panel/StringPacks.js.map +1 -1
- package/dist/shell/panel/backup.css +1 -1
- package/dist/shell/panel/builder.css +48 -40
- package/dist/shell/panel/make.css +8 -7
- package/dist/shell/panel/panel.css +401 -401
- package/dist/shell/panel/panelMessages.d.ts +41 -1
- package/dist/shell/panel/panelMessages.js +13 -1
- package/dist/shell/panel/panelMessages.js.map +1 -1
- package/dist/shell/panel/plugins.css +5 -5
- package/dist/shell/panel/strings.css +1 -1
- package/dist/shell/store.css +34 -14
- package/dist/shell/storeMessages.d.ts +24 -0
- package/dist/shell/storeMessages.js +24 -0
- package/dist/shell/storeMessages.js.map +1 -1
- package/dist/shell/storeModel.d.ts +31 -7
- package/dist/shell/storeModel.js +15 -2
- package/dist/shell/storeModel.js.map +1 -1
- package/dist/shell/stringPacksModel.js +7 -2
- package/dist/shell/stringPacksModel.js.map +1 -1
- package/icons.schema.json +9 -0
- package/locales/ar.json +109 -2
- package/locales/bg.json +109 -2
- package/locales/bn.json +109 -2
- package/locales/ca.json +109 -2
- package/locales/cs.json +109 -2
- package/locales/da.json +109 -2
- package/locales/de.json +109 -2
- package/locales/el.json +109 -2
- package/locales/en.json +108 -2
- package/locales/es.json +109 -2
- package/locales/eu.json +109 -2
- package/locales/fa.json +109 -2
- package/locales/fi.json +109 -2
- package/locales/fil.json +109 -2
- package/locales/fr.json +109 -2
- package/locales/ga.json +109 -2
- package/locales/gl.json +109 -2
- package/locales/gu.json +109 -2
- package/locales/ha.json +109 -2
- package/locales/he.json +109 -2
- package/locales/hi.json +109 -2
- package/locales/hr.json +109 -2
- package/locales/hu.json +110 -2
- package/locales/id.json +109 -2
- package/locales/ig.json +109 -2
- package/locales/it.json +109 -2
- package/locales/ja.json +109 -2
- package/locales/kn.json +109 -2
- package/locales/ko.json +109 -2
- package/locales/mr.json +109 -2
- package/locales/ms.json +109 -2
- package/locales/nb.json +109 -2
- package/locales/nl.json +109 -2
- package/locales/pl.json +109 -2
- package/locales/pt.json +109 -2
- package/locales/ro.json +109 -2
- package/locales/ru.json +109 -2
- package/locales/sk.json +109 -2
- package/locales/sr.json +109 -2
- package/locales/sv.json +109 -2
- package/locales/ta.json +109 -2
- package/locales/th.json +109 -2
- package/locales/tr.json +109 -2
- package/locales/uk.json +109 -2
- package/locales/ur.json +109 -2
- package/locales/vi.json +109 -2
- package/locales/yo.json +109 -2
- package/locales/zh-Hans.json +109 -2
- package/locales/zh-Hant.json +109 -2
- package/package.json +2 -3
- package/store-catalog.schema.json +18 -5
- package/theme.schema.json +9 -0
- package/windows/installer.nsi +22 -2
- package/cli/privacy.mjs +0 -768
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// Theme invariants (theme engine v3, plan 6.2): a pure measurement over a rendered document that
|
|
2
|
+
// says, with numbers, whether a theme is correct. Runs in the page (fixtures over Playwright, the live
|
|
3
|
+
// app over CDP); it never changes the page. Grew out of verification/theme-qa/probe.js.
|
|
4
|
+
//
|
|
5
|
+
// I1 text contrast 4.5:1 (3:1 large; 7:1 with access high) of ink over its painted backdrop
|
|
6
|
+
// I1b non-text contrast icons (svg, masked glyphs) 3:1 over their backdrop
|
|
7
|
+
// I2 X colour leakage computed colours equal to an X palette key that is not whitelisted
|
|
8
|
+
// I3 truncated labels in bar, nav, tabs, buttons and menu rows (X's own long-name truncation allowed)
|
|
9
|
+
// I4 bar geometry a horizontal bar: equal item widths with labels, gaps <= one item, hit-testable
|
|
10
|
+
// I5 icon provenance in pack contexts: X's own line paths still visible (xLine)
|
|
11
|
+
// I6 empty frames a framed box (edge or own fill) with nothing rendered inside, taller than 2px
|
|
12
|
+
// I7 frame nesting a framed box holding another framed box of the same fill, or a whole column
|
|
13
|
+
// I8 popover bounds menus, hover cards, dropdowns, tooltips inside the viewport minus the bar inset
|
|
14
|
+
// I10 targets interactive elements in bar, action bar, composer >= 24px (44 with access high)
|
|
15
|
+
// I9 (forced :hover) needs CDP and I11 is static; both live in the runners.
|
|
16
|
+
export const INVARIANT_IDS = ['I1', 'I1b', 'I2', 'I3', 'I4', 'I5', 'I6', 'I7', 'I8', 'I10'];
|
|
17
|
+
/** X's own palette (Default and Lights out neutrals, X blue): the leak list until the engine exports X_COLOUR_KEYS. */
|
|
18
|
+
export const DEFAULT_X_KEYS = ['rgb(29, 155, 240)', 'rgb(26, 140, 216)', 'rgb(15, 20, 25)', 'rgb(83, 100, 113)', 'rgb(113, 118, 123)', 'rgb(239, 243, 244)',
|
|
19
|
+
'rgb(247, 249, 249)', 'rgb(231, 233, 234)', 'rgb(22, 24, 28)', 'rgb(32, 35, 39)', 'rgb(47, 51, 54)', 'rgb(20, 20, 20)', 'rgb(207, 217, 222)', 'rgb(51, 54, 57)'];
|
|
20
|
+
const parse = (c) => { const m = /rgba?\(([\d.]+),? ([\d.]+),? ([\d.]+)(?:,? ?\/? ?([\d.]+))?/.exec(c || ''); return m ? [+m[1], +m[2], +m[3], m[4] === undefined ? 1 : +m[4]] : null; };
|
|
21
|
+
const lum = ([r, g, b]) => { const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; }; return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); };
|
|
22
|
+
export const contrast = (a, b) => { const [x, y] = [lum(a), lum(b)].sort((m, n) => n - m); return (x + 0.05) / (y + 0.05); };
|
|
23
|
+
const blend = (top, under) => top[3] >= 1 ? top : [0, 1, 2].map(i => top[i] * top[3] + under[i] * (1 - top[3])).concat(1);
|
|
24
|
+
const css = (c) => `rgb(${c.slice(0, 3).map(Math.round).join(', ')})`;
|
|
25
|
+
export function runInvariants(doc = document, opts = {}) {
|
|
26
|
+
const win = doc.defaultView;
|
|
27
|
+
const vw = win.innerWidth, vh = win.innerHeight;
|
|
28
|
+
const keep = opts.keep ?? 12;
|
|
29
|
+
const high = opts.access === 'high';
|
|
30
|
+
const counts = Object.fromEntries(INVARIANT_IDS.map(id => [id, 0]));
|
|
31
|
+
const findings = [];
|
|
32
|
+
const notes = { unknownBackdrop: 0, allowedTruncation: 0, bar: 'none' };
|
|
33
|
+
const cs = (e) => win.getComputedStyle(e);
|
|
34
|
+
const up = (e) => e.parentElement ?? (e.parentNode?.host ?? null);
|
|
35
|
+
const rectOf = (e) => { const r = e.getBoundingClientRect(); return [Math.round(r.left), Math.round(r.top), Math.round(r.width), Math.round(r.height)]; };
|
|
36
|
+
const sel = (el) => {
|
|
37
|
+
const p = [];
|
|
38
|
+
for (let e = el; e && e !== doc.body && p.length < 4; e = up(e)) {
|
|
39
|
+
let s = e.tagName.toLowerCase();
|
|
40
|
+
const t = e.getAttribute('data-testid');
|
|
41
|
+
if (t) {
|
|
42
|
+
p.unshift(`${s}[data-testid="${t}"]`);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
if (e.id) {
|
|
46
|
+
p.unshift(`#${e.id}`);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
const c = [...e.classList].filter(c => !/^(css|r)-/.test(c)).slice(0, 2);
|
|
50
|
+
if (c.length)
|
|
51
|
+
s += '.' + c.join('.');
|
|
52
|
+
const role = e.getAttribute('role');
|
|
53
|
+
if (role)
|
|
54
|
+
s += `[role="${role}"]`;
|
|
55
|
+
p.unshift(s);
|
|
56
|
+
}
|
|
57
|
+
return p.join(' > ');
|
|
58
|
+
};
|
|
59
|
+
const add = (id, el, detail, value) => {
|
|
60
|
+
counts[id]++;
|
|
61
|
+
if (findings.filter(f => f.id === id).length < keep)
|
|
62
|
+
findings.push({ id, sel: sel(el), rect: rectOf(el), detail, ...(value === undefined ? {} : { value: +value.toFixed(2) }) });
|
|
63
|
+
};
|
|
64
|
+
const hidden = (el) => { for (let e = el; e; e = up(e)) {
|
|
65
|
+
const s = cs(e);
|
|
66
|
+
if (s.display === 'none' || s.visibility === 'hidden' || +s.opacity < 0.05)
|
|
67
|
+
return true;
|
|
68
|
+
} return false; };
|
|
69
|
+
const onScreen = (el) => { const r = el.getBoundingClientRect(); return r.width > 1 && r.height > 1 && r.bottom > 0 && r.top < vh && r.right > 0 && r.left < vw; };
|
|
70
|
+
const visible = (el) => onScreen(el) && !hidden(el);
|
|
71
|
+
const pageColor = () => { for (const e of [doc.body, doc.documentElement]) {
|
|
72
|
+
const c = parse(cs(e).backgroundColor);
|
|
73
|
+
if (c && c[3] > 0)
|
|
74
|
+
return blend(c, [255, 255, 255, 1]);
|
|
75
|
+
} return [255, 255, 255, 1]; };
|
|
76
|
+
/** The painted backdrop under an element: the alpha stack up to the first opaque fill; `image` when an image or gradient paints first. */
|
|
77
|
+
const backdrop = (el) => {
|
|
78
|
+
const stack = [];
|
|
79
|
+
for (let e = el; e && e !== doc.documentElement; e = up(e)) {
|
|
80
|
+
const s = cs(e);
|
|
81
|
+
if (s.backgroundImage !== 'none' && !/^linear-gradient\(rgba?\([^)]*\), rgba?\([^)]*\)\)$/.test(s.backgroundImage))
|
|
82
|
+
return { color: pageColor(), image: true };
|
|
83
|
+
const c = parse(s.backgroundColor);
|
|
84
|
+
if (c && c[3] > 0) {
|
|
85
|
+
stack.push(c);
|
|
86
|
+
if (c[3] >= 1)
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let base = pageColor();
|
|
91
|
+
for (const c of stack.reverse())
|
|
92
|
+
base = blend(c, base);
|
|
93
|
+
return { color: base, image: false };
|
|
94
|
+
};
|
|
95
|
+
const ownText = (el) => [...el.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join('').trim();
|
|
96
|
+
const all = [...doc.querySelectorAll('body *')];
|
|
97
|
+
// I1 text contrast, I2 leakage, I3 truncation
|
|
98
|
+
const xKeys = new Set(opts.xColourKeys ?? DEFAULT_X_KEYS);
|
|
99
|
+
const whitelisted = (el) => !!el.closest('[data-testid="icon-verified"],[data-testid="unlike"],[data-testid="unretweet"],[data-testid="removeBookmark"],img,video,canvas');
|
|
100
|
+
const LABEL_CONTEXT = 'nav[role="navigation"], header[role="banner"], [role="tablist"], [role="tab"], button, [role="button"], [role="menuitem"], .dv-tab';
|
|
101
|
+
const ALLOWED_TRUNC = '[data-testid="User-Name"], [data-testid="trend"], .bt-exact-time, [data-testid="UserName"], [data-testid="UserCell"]';
|
|
102
|
+
for (const el of all) {
|
|
103
|
+
if (el.closest('[aria-hidden="true"]') || el.closest('svg') && el.tagName.toLowerCase() !== 'svg')
|
|
104
|
+
continue;
|
|
105
|
+
const text = ownText(el);
|
|
106
|
+
if (!visible(el))
|
|
107
|
+
continue;
|
|
108
|
+
const st = cs(el);
|
|
109
|
+
if (!opts.exemptLeak && !whitelisted(el)) {
|
|
110
|
+
for (const prop of ['color', 'backgroundColor', 'borderTopColor', 'borderBottomColor', 'fill', 'outlineColor']) {
|
|
111
|
+
const v = st[prop];
|
|
112
|
+
if (!xKeys.has(v))
|
|
113
|
+
continue;
|
|
114
|
+
if (prop.startsWith('border') && parseFloat(prop === 'borderTopColor' ? st.borderTopWidth : st.borderBottomWidth) === 0)
|
|
115
|
+
continue;
|
|
116
|
+
if (prop === 'outlineColor' && (st.outlineStyle === 'none' || parseFloat(st.outlineWidth) === 0))
|
|
117
|
+
continue;
|
|
118
|
+
if (prop === 'color' && !text)
|
|
119
|
+
continue;
|
|
120
|
+
if (prop === 'fill' && el.tagName.toLowerCase() !== 'svg')
|
|
121
|
+
continue;
|
|
122
|
+
add('I2', el, `${v} as ${prop}`);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!text)
|
|
127
|
+
continue;
|
|
128
|
+
const fg = parse(st.color);
|
|
129
|
+
if (!fg)
|
|
130
|
+
continue;
|
|
131
|
+
const back = backdrop(el);
|
|
132
|
+
if (back.image)
|
|
133
|
+
notes.unknownBackdrop++;
|
|
134
|
+
else {
|
|
135
|
+
const size = parseFloat(st.fontSize), bold = +st.fontWeight >= 700;
|
|
136
|
+
const large = size >= 24 || (size >= 18.66 && bold);
|
|
137
|
+
const need = high ? (large ? 4.5 : 7) : (large ? 3 : 4.5);
|
|
138
|
+
const ink = blend([fg[0], fg[1], fg[2], fg[3] * Math.min(1, +st.opacity || 1)], back.color);
|
|
139
|
+
const r = contrast(ink, back.color);
|
|
140
|
+
if (r < need - 0.005)
|
|
141
|
+
add('I1', el, `${r.toFixed(2)}:1 < ${need} ${st.color} on ${css(back.color)} "${text.slice(0, 24)}"`, r);
|
|
142
|
+
}
|
|
143
|
+
if (el.closest(LABEL_CONTEXT)) {
|
|
144
|
+
const cut = el.scrollWidth > el.clientWidth + 1 && (st.textOverflow === 'ellipsis' || st.overflow === 'hidden' || st.overflowX === 'hidden');
|
|
145
|
+
const barLabel = !!el.closest('nav[role="navigation"]') && st.textOverflow === 'ellipsis' && el.scrollWidth > el.clientWidth + 1;
|
|
146
|
+
if (cut || barLabel) {
|
|
147
|
+
if (el.closest(ALLOWED_TRUNC))
|
|
148
|
+
notes.allowedTruncation++;
|
|
149
|
+
else
|
|
150
|
+
add('I3', el, `"${text.slice(0, 24)}" shown in ${el.clientWidth}px of ${el.scrollWidth}px`, el.clientWidth);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// I1b non-text contrast and I5 icon provenance
|
|
155
|
+
const PACK_CONTEXT = 'nav[role="navigation"], header[role="banner"], [role="group"], [data-testid="toolBar"], #layers [role="menu"], [data-testid="GrokDrawer"], [data-testid="DMDrawer"], [data-testid="app-bar-back"], [data-testid="app-bar-close"]';
|
|
156
|
+
for (const svg of doc.querySelectorAll('svg')) {
|
|
157
|
+
if (!visible(svg) || svg.closest('[data-testid="icon-verified"]'))
|
|
158
|
+
continue;
|
|
159
|
+
const s = cs(svg);
|
|
160
|
+
const painted = [...svg.querySelectorAll('path, circle, rect, polygon, ellipse, line')].filter(p => { const ps = cs(p); return +ps.opacity > 0.05 && (ps.fill !== 'none' || ps.stroke !== 'none'); });
|
|
161
|
+
const masked = (e) => { const es = cs(e); return (es.maskImage && es.maskImage !== 'none') || (es.webkitMaskImage ?? 'none') !== 'none' || es.backgroundImage !== 'none'; };
|
|
162
|
+
if (opts.pack && svg.closest(PACK_CONTEXT) && painted.length && !masked(svg) && !(svg.parentElement && masked(svg.parentElement)))
|
|
163
|
+
add('I5', svg, `X's own drawing in ${svg.closest(PACK_CONTEXT).tagName.toLowerCase()}`);
|
|
164
|
+
const ink = parse(masked(svg) ? s.backgroundColor : s.color);
|
|
165
|
+
if (!ink || ink[3] === 0 || (!painted.length && !masked(svg)))
|
|
166
|
+
continue;
|
|
167
|
+
const back = backdrop(svg.parentElement ?? svg);
|
|
168
|
+
if (back.image)
|
|
169
|
+
continue;
|
|
170
|
+
const r = contrast(blend(ink, back.color), back.color);
|
|
171
|
+
if (r < 3 - 0.005)
|
|
172
|
+
add('I1b', svg, `${r.toFixed(2)}:1 < 3 icon ${css(ink)} on ${css(back.color)}`, r);
|
|
173
|
+
}
|
|
174
|
+
// I6 empty frames, I7 nesting
|
|
175
|
+
const framed = (el) => {
|
|
176
|
+
const s = cs(el), r = el.getBoundingClientRect();
|
|
177
|
+
if (r.width < 60 || r.height <= 2 || s.position === 'fixed' && r.width >= vw - 2 && r.height >= vh - 2)
|
|
178
|
+
return false;
|
|
179
|
+
if (s.backgroundImage !== 'none')
|
|
180
|
+
return false;
|
|
181
|
+
const edge = ['Top', 'Right', 'Bottom', 'Left'].filter(k => parseFloat(s[`border${k}Width`]) > 0 && s[`border${k}Style`] !== 'none' && (parse(s[`border${k}Color`])?.[3] ?? 0) > 0).length >= 3 || /inset/.test(s.boxShadow);
|
|
182
|
+
const fill = parse(s.backgroundColor);
|
|
183
|
+
const parent = up(el);
|
|
184
|
+
const own = !!fill && fill[3] > 0.05 && !!parent && css(blend(fill, backdrop(parent).color)) !== css(backdrop(parent).color);
|
|
185
|
+
return edge || own;
|
|
186
|
+
};
|
|
187
|
+
const MEDIA = 'img, svg, video, canvas, input, textarea, select, iframe, [role="img"], [role="textbox"], [role="progressbar"]';
|
|
188
|
+
const frames = [];
|
|
189
|
+
for (const el of all) {
|
|
190
|
+
if (!visible(el) || el.closest('#layers [data-testid="mask"]') || el.matches('[data-testid="mask"]'))
|
|
191
|
+
continue;
|
|
192
|
+
if (el.matches(MEDIA) || !framed(el))
|
|
193
|
+
continue;
|
|
194
|
+
frames.push(el);
|
|
195
|
+
const content = (el.textContent ?? '').trim() || [...el.querySelectorAll(MEDIA)].some(m => visible(m)) || [...el.querySelectorAll('*')].some(c => cs(c).backgroundImage !== 'none' && visible(c));
|
|
196
|
+
if (!content)
|
|
197
|
+
add('I6', el, `empty frame ${rectOf(el)[2]}x${rectOf(el)[3]} ${cs(el).backgroundColor}`, rectOf(el)[3]);
|
|
198
|
+
}
|
|
199
|
+
for (const el of frames) {
|
|
200
|
+
const fill = cs(el).backgroundColor;
|
|
201
|
+
const column = el.querySelector('[data-testid="sidebarColumn"], [data-testid="primaryColumn"]');
|
|
202
|
+
// Controls (a bevelled button on a bevelled bar, a pill in a card) are not frames: only boxes nest wrongly.
|
|
203
|
+
const inner = frames.find(f => f !== el && el.contains(f) && !f.closest('a, button, [role="button"], [role="link"], [role="tab"], input, label') && cs(f).backgroundColor === fill && parse(fill)[3] > 0 && /solid|inset/.test(cs(f).borderTopStyle + cs(f).boxShadow));
|
|
204
|
+
if (column)
|
|
205
|
+
add('I7', el, `frame holds the ${column.getAttribute('data-testid')}`);
|
|
206
|
+
else if (inner)
|
|
207
|
+
add('I7', el, `frame holds a frame of the same fill (${sel(inner)})`);
|
|
208
|
+
}
|
|
209
|
+
// I4 bar geometry and I10 targets
|
|
210
|
+
const nav = doc.querySelector('header[role="banner"] nav[role="navigation"]');
|
|
211
|
+
if (nav && visible(nav)) {
|
|
212
|
+
const items = [...nav.children].filter(c => visible(c));
|
|
213
|
+
const rects = items.map(i => i.getBoundingClientRect());
|
|
214
|
+
const horizontal = rects.length > 2 && rects.every(r => Math.abs(r.top - rects[0].top) < 6);
|
|
215
|
+
notes.bar = horizontal ? 'horizontal' : 'rail';
|
|
216
|
+
const labelled = items.some(i => [...i.querySelectorAll('span, div')].some(s => ownText(s) && visible(s)));
|
|
217
|
+
if (horizontal) {
|
|
218
|
+
notes.barMode = labelled ? 'labels' : 'icons';
|
|
219
|
+
const widths = rects.map(r => r.width);
|
|
220
|
+
if (labelled && Math.max(...widths) - Math.min(...widths) > 2)
|
|
221
|
+
add('I4', nav, `item widths ${Math.round(Math.min(...widths))}..${Math.round(Math.max(...widths))}px`, Math.max(...widths) - Math.min(...widths));
|
|
222
|
+
const sorted = [...rects].sort((a, b) => a.left - b.left);
|
|
223
|
+
const unit = Math.max(...widths);
|
|
224
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
225
|
+
const gap = sorted[i].left - sorted[i - 1].right;
|
|
226
|
+
if (gap > unit + 1) {
|
|
227
|
+
add('I4', nav, `gap ${Math.round(gap)}px > one item ${Math.round(unit)}px`, gap);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const [i, item] of items.entries()) {
|
|
233
|
+
const r = rects[i];
|
|
234
|
+
const x = r.left + r.width / 2, y = r.top + r.height / 2;
|
|
235
|
+
if (x < 0 || y < 0 || x > vw || y > vh) {
|
|
236
|
+
add('I4', item, 'item off screen');
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const hit = doc.elementFromPoint(x, y);
|
|
240
|
+
// A layer over the bar (modal mask, menu, hover card) is I8's business, not the bar's.
|
|
241
|
+
if (hit?.closest('#layers'))
|
|
242
|
+
continue;
|
|
243
|
+
if (!hit || !(item === hit || item.contains(hit)))
|
|
244
|
+
add('I4', item, `item not hit-testable (under ${hit ? sel(hit) : 'nothing'})`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const minTarget = high ? 44 : 24;
|
|
248
|
+
const TARGET_CONTEXT = 'header[role="banner"] nav[role="navigation"] > *, header[role="banner"] [data-testid="SideNav_NewTweet_Button"], header[role="banner"] [data-testid="SideNav_AccountSwitcher_Button"], article [role="group"] button, article [role="group"] a, [data-testid="toolBar"] button, [data-testid="toolBar"] [role="button"]';
|
|
249
|
+
for (const el of doc.querySelectorAll(TARGET_CONTEXT)) {
|
|
250
|
+
if (!visible(el))
|
|
251
|
+
continue;
|
|
252
|
+
const r = el.getBoundingClientRect();
|
|
253
|
+
if (r.width < minTarget - 0.5 || r.height < minTarget - 0.5)
|
|
254
|
+
add('I10', el, `${Math.round(r.width)}x${Math.round(r.height)} < ${minTarget}`, Math.min(r.width, r.height));
|
|
255
|
+
}
|
|
256
|
+
// I8 popover bounds
|
|
257
|
+
const root = cs(doc.documentElement);
|
|
258
|
+
const insetTop = parseFloat(root.getPropertyValue('--wb-bar-inset-top')) || 0, insetBottom = parseFloat(root.getPropertyValue('--wb-bar-inset-bottom')) || 0;
|
|
259
|
+
for (const el of doc.querySelectorAll('[data-testid="hoverCardParent"], [role="menu"], [role="listbox"], [data-testid="HoverCard"], [data-testid="Dropdown"], [data-testid="HoverLabel"]')) {
|
|
260
|
+
if (!visible(el) && !(el.getBoundingClientRect().width > 1 && !hidden(el)))
|
|
261
|
+
continue;
|
|
262
|
+
if (el.parentElement?.closest('[data-testid="hoverCardParent"], [role="menu"], [data-testid="HoverCard"]'))
|
|
263
|
+
continue;
|
|
264
|
+
const r = el.getBoundingClientRect();
|
|
265
|
+
if (r.width < 2 || r.height < 2)
|
|
266
|
+
continue;
|
|
267
|
+
const out = Math.max(-r.left, -(r.top - insetTop), r.right - vw, r.bottom - (vh - insetBottom));
|
|
268
|
+
if (out > 1)
|
|
269
|
+
add('I8', el, `${Math.round(out)}px outside the viewport`, out);
|
|
270
|
+
}
|
|
271
|
+
return { counts, findings, notes, viewport: [vw, vh] };
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=themeInvariants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"themeInvariants.js","sourceRoot":"","sources":["../../../runtime/src/themeInvariants.ts"],"names":[],"mappings":"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","sourcesContent":["// Theme invariants (theme engine v3, plan 6.2): a pure measurement over a rendered document that\n// says, with numbers, whether a theme is correct. Runs in the page (fixtures over Playwright, the live\n// app over CDP); it never changes the page. Grew out of verification/theme-qa/probe.js.\n//\n// I1 text contrast 4.5:1 (3:1 large; 7:1 with access high) of ink over its painted backdrop\n// I1b non-text contrast icons (svg, masked glyphs) 3:1 over their backdrop\n// I2 X colour leakage computed colours equal to an X palette key that is not whitelisted\n// I3 truncated labels in bar, nav, tabs, buttons and menu rows (X's own long-name truncation allowed)\n// I4 bar geometry a horizontal bar: equal item widths with labels, gaps <= one item, hit-testable\n// I5 icon provenance in pack contexts: X's own line paths still visible (xLine)\n// I6 empty frames a framed box (edge or own fill) with nothing rendered inside, taller than 2px\n// I7 frame nesting a framed box holding another framed box of the same fill, or a whole column\n// I8 popover bounds menus, hover cards, dropdowns, tooltips inside the viewport minus the bar inset\n// I10 targets interactive elements in bar, action bar, composer >= 24px (44 with access high)\n// I9 (forced :hover) needs CDP and I11 is static; both live in the runners.\n\nexport type InvariantId = 'I1' | 'I1b' | 'I2' | 'I3' | 'I4' | 'I5' | 'I6' | 'I7' | 'I8' | 'I10';\nexport const INVARIANT_IDS: InvariantId[] = ['I1', 'I1b', 'I2', 'I3', 'I4', 'I5', 'I6', 'I7', 'I8', 'I10'];\n\nexport type Rect = [number, number, number, number];\nexport interface Finding { id: InvariantId; sel: string; rect: Rect; detail: string; value?: number }\nexport interface InvariantOptions {\n /** Colours (`rgb(r, g, b)`) that are X's own palette; default: X's light and dark neutrals and blue. */\n xColourKeys?: string[];\n /** The X default theme keeps X's colours: I2 off. */\n exemptLeak?: boolean;\n /** The theme draws an icon pack: I5 on. */\n pack?: boolean;\n access?: 'standard' | 'high';\n /** Cap of findings kept per invariant (counts are always complete). */\n keep?: number;\n}\nexport interface InvariantReport {\n counts: Record<InvariantId, number>;\n findings: Finding[];\n /** Measured but not failures: text over images, X icons with no slot, X's allowed truncation. */\n notes: { unknownBackdrop: number; allowedTruncation: number; bar: 'none' | 'rail' | 'horizontal'; barMode?: 'labels' | 'icons' };\n viewport: [number, number];\n}\n\n/** X's own palette (Default and Lights out neutrals, X blue): the leak list until the engine exports X_COLOUR_KEYS. */\nexport const DEFAULT_X_KEYS = ['rgb(29, 155, 240)', 'rgb(26, 140, 216)', 'rgb(15, 20, 25)', 'rgb(83, 100, 113)', 'rgb(113, 118, 123)', 'rgb(239, 243, 244)',\n 'rgb(247, 249, 249)', 'rgb(231, 233, 234)', 'rgb(22, 24, 28)', 'rgb(32, 35, 39)', 'rgb(47, 51, 54)', 'rgb(20, 20, 20)', 'rgb(207, 217, 222)', 'rgb(51, 54, 57)'];\n\ntype RGBA = [number, number, number, number];\nconst parse = (c: string | null | undefined): RGBA | null => { const m = /rgba?\\(([\\d.]+),? ([\\d.]+),? ([\\d.]+)(?:,? ?\\/? ?([\\d.]+))?/.exec(c || ''); return m ? [+m[1], +m[2], +m[3], m[4] === undefined ? 1 : +m[4]] : null; };\nconst lum = ([r, g, b]: RGBA) => { const f = (v: number) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; }; return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); };\nexport const contrast = (a: RGBA, b: RGBA) => { const [x, y] = [lum(a), lum(b)].sort((m, n) => n - m); return (x + 0.05) / (y + 0.05); };\nconst blend = (top: RGBA, under: RGBA): RGBA => top[3] >= 1 ? top : [0, 1, 2].map(i => top[i] * top[3] + under[i] * (1 - top[3])).concat(1) as RGBA;\nconst css = (c: RGBA) => `rgb(${c.slice(0, 3).map(Math.round).join(', ')})`;\n\nexport function runInvariants(doc: Document = document, opts: InvariantOptions = {}): InvariantReport {\n const win = doc.defaultView as Window & typeof globalThis;\n const vw = win.innerWidth, vh = win.innerHeight;\n const keep = opts.keep ?? 12;\n const high = opts.access === 'high';\n const counts = Object.fromEntries(INVARIANT_IDS.map(id => [id, 0])) as Record<InvariantId, number>;\n const findings: Finding[] = [];\n const notes: InvariantReport['notes'] = { unknownBackdrop: 0, allowedTruncation: 0, bar: 'none' };\n const cs = (e: Element) => win.getComputedStyle(e);\n const up = (e: Element): Element | null => e.parentElement ?? ((e.parentNode as ShadowRoot | null)?.host ?? null);\n const rectOf = (e: Element): Rect => { const r = e.getBoundingClientRect(); return [Math.round(r.left), Math.round(r.top), Math.round(r.width), Math.round(r.height)]; };\n const sel = (el: Element) => {\n const p: string[] = [];\n for (let e: Element | null = el; e && e !== doc.body && p.length < 4; e = up(e)) {\n let s = e.tagName.toLowerCase(); const t = e.getAttribute('data-testid');\n if (t) { p.unshift(`${s}[data-testid=\"${t}\"]`); break; }\n if (e.id) { p.unshift(`#${e.id}`); break; }\n const c = [...e.classList].filter(c => !/^(css|r)-/.test(c)).slice(0, 2); if (c.length) s += '.' + c.join('.');\n const role = e.getAttribute('role'); if (role) s += `[role=\"${role}\"]`;\n p.unshift(s);\n }\n return p.join(' > ');\n };\n const add = (id: InvariantId, el: Element, detail: string, value?: number) => {\n counts[id]++;\n if (findings.filter(f => f.id === id).length < keep) findings.push({ id, sel: sel(el), rect: rectOf(el), detail, ...(value === undefined ? {} : { value: +value.toFixed(2) }) });\n };\n const hidden = (el: Element) => { for (let e: Element | null = el; e; e = up(e)) { const s = cs(e); if (s.display === 'none' || s.visibility === 'hidden' || +s.opacity < 0.05) return true; } return false; };\n const onScreen = (el: Element) => { const r = el.getBoundingClientRect(); return r.width > 1 && r.height > 1 && r.bottom > 0 && r.top < vh && r.right > 0 && r.left < vw; };\n const visible = (el: Element) => onScreen(el) && !hidden(el);\n const pageColor = (): RGBA => { for (const e of [doc.body, doc.documentElement]) { const c = parse(cs(e).backgroundColor); if (c && c[3] > 0) return blend(c, [255, 255, 255, 1]); } return [255, 255, 255, 1]; };\n /** The painted backdrop under an element: the alpha stack up to the first opaque fill; `image` when an image or gradient paints first. */\n const backdrop = (el: Element): { color: RGBA; image: boolean } => {\n const stack: RGBA[] = [];\n for (let e: Element | null = el; e && e !== doc.documentElement; e = up(e)) {\n const s = cs(e);\n if (s.backgroundImage !== 'none' && !/^linear-gradient\\(rgba?\\([^)]*\\), rgba?\\([^)]*\\)\\)$/.test(s.backgroundImage)) return { color: pageColor(), image: true };\n const c = parse(s.backgroundColor);\n if (c && c[3] > 0) { stack.push(c); if (c[3] >= 1) break; }\n }\n let base = pageColor();\n for (const c of stack.reverse()) base = blend(c, base);\n return { color: base, image: false };\n };\n const ownText = (el: Element) => [...el.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join('').trim();\n const all = [...doc.querySelectorAll('body *')];\n\n // I1 text contrast, I2 leakage, I3 truncation\n const xKeys = new Set(opts.xColourKeys ?? DEFAULT_X_KEYS);\n const whitelisted = (el: Element) => !!el.closest('[data-testid=\"icon-verified\"],[data-testid=\"unlike\"],[data-testid=\"unretweet\"],[data-testid=\"removeBookmark\"],img,video,canvas');\n const LABEL_CONTEXT = 'nav[role=\"navigation\"], header[role=\"banner\"], [role=\"tablist\"], [role=\"tab\"], button, [role=\"button\"], [role=\"menuitem\"], .dv-tab';\n const ALLOWED_TRUNC = '[data-testid=\"User-Name\"], [data-testid=\"trend\"], .bt-exact-time, [data-testid=\"UserName\"], [data-testid=\"UserCell\"]';\n for (const el of all) {\n if (el.closest('[aria-hidden=\"true\"]') || el.closest('svg') && el.tagName.toLowerCase() !== 'svg') continue;\n const text = ownText(el);\n if (!visible(el)) continue;\n const st = cs(el);\n if (!opts.exemptLeak && !whitelisted(el)) {\n for (const prop of ['color', 'backgroundColor', 'borderTopColor', 'borderBottomColor', 'fill', 'outlineColor'] as const) {\n const v = st[prop] as string; if (!xKeys.has(v)) continue;\n if (prop.startsWith('border') && parseFloat(prop === 'borderTopColor' ? st.borderTopWidth : st.borderBottomWidth) === 0) continue;\n if (prop === 'outlineColor' && (st.outlineStyle === 'none' || parseFloat(st.outlineWidth) === 0)) continue;\n if (prop === 'color' && !text) continue;\n if (prop === 'fill' && el.tagName.toLowerCase() !== 'svg') continue;\n add('I2', el, `${v} as ${prop}`); break;\n }\n }\n if (!text) continue;\n const fg = parse(st.color); if (!fg) continue;\n const back = backdrop(el);\n if (back.image) notes.unknownBackdrop++;\n else {\n const size = parseFloat(st.fontSize), bold = +st.fontWeight >= 700;\n const large = size >= 24 || (size >= 18.66 && bold);\n const need = high ? (large ? 4.5 : 7) : (large ? 3 : 4.5);\n const ink = blend([fg[0], fg[1], fg[2], fg[3] * Math.min(1, +st.opacity || 1)], back.color);\n const r = contrast(ink, back.color);\n if (r < need - 0.005) add('I1', el, `${r.toFixed(2)}:1 < ${need} ${st.color} on ${css(back.color)} \"${text.slice(0, 24)}\"`, r);\n }\n if (el.closest(LABEL_CONTEXT)) {\n const cut = el.scrollWidth > el.clientWidth + 1 && (st.textOverflow === 'ellipsis' || st.overflow === 'hidden' || st.overflowX === 'hidden');\n const barLabel = !!el.closest('nav[role=\"navigation\"]') && st.textOverflow === 'ellipsis' && el.scrollWidth > el.clientWidth + 1;\n if (cut || barLabel) {\n if (el.closest(ALLOWED_TRUNC)) notes.allowedTruncation++;\n else add('I3', el, `\"${text.slice(0, 24)}\" shown in ${el.clientWidth}px of ${el.scrollWidth}px`, el.clientWidth);\n }\n }\n }\n\n // I1b non-text contrast and I5 icon provenance\n const PACK_CONTEXT = 'nav[role=\"navigation\"], header[role=\"banner\"], [role=\"group\"], [data-testid=\"toolBar\"], #layers [role=\"menu\"], [data-testid=\"GrokDrawer\"], [data-testid=\"DMDrawer\"], [data-testid=\"app-bar-back\"], [data-testid=\"app-bar-close\"]';\n for (const svg of doc.querySelectorAll('svg')) {\n if (!visible(svg) || svg.closest('[data-testid=\"icon-verified\"]')) continue;\n const s = cs(svg);\n const painted = [...svg.querySelectorAll('path, circle, rect, polygon, ellipse, line')].filter(p => { const ps = cs(p); return +ps.opacity > 0.05 && (ps.fill !== 'none' || ps.stroke !== 'none'); });\n const masked = (e: Element) => { const es = cs(e); return (es.maskImage && es.maskImage !== 'none') || ((es as unknown as Record<string, string>).webkitMaskImage ?? 'none') !== 'none' || es.backgroundImage !== 'none'; };\n if (opts.pack && svg.closest(PACK_CONTEXT) && painted.length && !masked(svg) && !(svg.parentElement && masked(svg.parentElement))) add('I5', svg, `X's own drawing in ${svg.closest(PACK_CONTEXT)!.tagName.toLowerCase()}`);\n const ink = parse(masked(svg) ? s.backgroundColor : s.color);\n if (!ink || ink[3] === 0 || (!painted.length && !masked(svg))) continue;\n const back = backdrop(svg.parentElement ?? svg);\n if (back.image) continue;\n const r = contrast(blend(ink, back.color), back.color);\n if (r < 3 - 0.005) add('I1b', svg, `${r.toFixed(2)}:1 < 3 icon ${css(ink)} on ${css(back.color)}`, r);\n }\n\n // I6 empty frames, I7 nesting\n const framed = (el: Element) => {\n const s = cs(el), r = el.getBoundingClientRect();\n if (r.width < 60 || r.height <= 2 || s.position === 'fixed' && r.width >= vw - 2 && r.height >= vh - 2) return false;\n if (s.backgroundImage !== 'none') return false;\n const edge = ['Top', 'Right', 'Bottom', 'Left'].filter(k => parseFloat(s[`border${k}Width` as 'borderTopWidth']) > 0 && s[`border${k}Style` as 'borderTopStyle'] !== 'none' && (parse(s[`border${k}Color` as 'borderTopColor'])?.[3] ?? 0) > 0).length >= 3 || /inset/.test(s.boxShadow);\n const fill = parse(s.backgroundColor); const parent = up(el);\n const own = !!fill && fill[3] > 0.05 && !!parent && css(blend(fill, backdrop(parent).color)) !== css(backdrop(parent).color);\n return edge || own;\n };\n const MEDIA = 'img, svg, video, canvas, input, textarea, select, iframe, [role=\"img\"], [role=\"textbox\"], [role=\"progressbar\"]';\n const frames: Element[] = [];\n for (const el of all) {\n if (!visible(el) || el.closest('#layers [data-testid=\"mask\"]') || el.matches('[data-testid=\"mask\"]')) continue;\n if (el.matches(MEDIA) || !framed(el)) continue;\n frames.push(el);\n const content = (el.textContent ?? '').trim() || [...el.querySelectorAll(MEDIA)].some(m => visible(m)) || [...el.querySelectorAll('*')].some(c => cs(c).backgroundImage !== 'none' && visible(c));\n if (!content) add('I6', el, `empty frame ${rectOf(el)[2]}x${rectOf(el)[3]} ${cs(el).backgroundColor}`, rectOf(el)[3]);\n }\n for (const el of frames) {\n const fill = cs(el).backgroundColor;\n const column = el.querySelector('[data-testid=\"sidebarColumn\"], [data-testid=\"primaryColumn\"]');\n // Controls (a bevelled button on a bevelled bar, a pill in a card) are not frames: only boxes nest wrongly.\n const inner = frames.find(f => f !== el && el.contains(f) && !f.closest('a, button, [role=\"button\"], [role=\"link\"], [role=\"tab\"], input, label') && cs(f).backgroundColor === fill && parse(fill)![3] > 0 && /solid|inset/.test(cs(f).borderTopStyle + cs(f).boxShadow));\n if (column) add('I7', el, `frame holds the ${column.getAttribute('data-testid')}`);\n else if (inner) add('I7', el, `frame holds a frame of the same fill (${sel(inner)})`);\n }\n\n // I4 bar geometry and I10 targets\n const nav = doc.querySelector('header[role=\"banner\"] nav[role=\"navigation\"]');\n if (nav && visible(nav)) {\n const items = [...nav.children].filter(c => visible(c)) as HTMLElement[];\n const rects = items.map(i => i.getBoundingClientRect());\n const horizontal = rects.length > 2 && rects.every(r => Math.abs(r.top - rects[0].top) < 6);\n notes.bar = horizontal ? 'horizontal' : 'rail';\n const labelled = items.some(i => [...i.querySelectorAll('span, div')].some(s => ownText(s) && visible(s)));\n if (horizontal) {\n notes.barMode = labelled ? 'labels' : 'icons';\n const widths = rects.map(r => r.width);\n if (labelled && Math.max(...widths) - Math.min(...widths) > 2) add('I4', nav, `item widths ${Math.round(Math.min(...widths))}..${Math.round(Math.max(...widths))}px`, Math.max(...widths) - Math.min(...widths));\n const sorted = [...rects].sort((a, b) => a.left - b.left);\n const unit = Math.max(...widths);\n for (let i = 1; i < sorted.length; i++) { const gap = sorted[i].left - sorted[i - 1].right; if (gap > unit + 1) { add('I4', nav, `gap ${Math.round(gap)}px > one item ${Math.round(unit)}px`, gap); break; } }\n }\n for (const [i, item] of items.entries()) {\n const r = rects[i]; const x = r.left + r.width / 2, y = r.top + r.height / 2;\n if (x < 0 || y < 0 || x > vw || y > vh) { add('I4', item, 'item off screen'); continue; }\n const hit = doc.elementFromPoint(x, y);\n // A layer over the bar (modal mask, menu, hover card) is I8's business, not the bar's.\n if (hit?.closest('#layers')) continue;\n if (!hit || !(item === hit || item.contains(hit))) add('I4', item, `item not hit-testable (under ${hit ? sel(hit) : 'nothing'})`);\n }\n }\n const minTarget = high ? 44 : 24;\n const TARGET_CONTEXT = 'header[role=\"banner\"] nav[role=\"navigation\"] > *, header[role=\"banner\"] [data-testid=\"SideNav_NewTweet_Button\"], header[role=\"banner\"] [data-testid=\"SideNav_AccountSwitcher_Button\"], article [role=\"group\"] button, article [role=\"group\"] a, [data-testid=\"toolBar\"] button, [data-testid=\"toolBar\"] [role=\"button\"]';\n for (const el of doc.querySelectorAll(TARGET_CONTEXT)) {\n if (!visible(el)) continue;\n const r = el.getBoundingClientRect();\n if (r.width < minTarget - 0.5 || r.height < minTarget - 0.5) add('I10', el, `${Math.round(r.width)}x${Math.round(r.height)} < ${minTarget}`, Math.min(r.width, r.height));\n }\n\n // I8 popover bounds\n const root = cs(doc.documentElement);\n const insetTop = parseFloat(root.getPropertyValue('--wb-bar-inset-top')) || 0, insetBottom = parseFloat(root.getPropertyValue('--wb-bar-inset-bottom')) || 0;\n for (const el of doc.querySelectorAll('[data-testid=\"hoverCardParent\"], [role=\"menu\"], [role=\"listbox\"], [data-testid=\"HoverCard\"], [data-testid=\"Dropdown\"], [data-testid=\"HoverLabel\"]')) {\n if (!visible(el) && !(el.getBoundingClientRect().width > 1 && !hidden(el))) continue;\n if (el.parentElement?.closest('[data-testid=\"hoverCardParent\"], [role=\"menu\"], [data-testid=\"HoverCard\"]')) continue;\n const r = el.getBoundingClientRect();\n if (r.width < 2 || r.height < 2) continue;\n const out = Math.max(-r.left, -(r.top - insetTop), r.right - vw, r.bottom - (vh - insetBottom));\n if (out > 1) add('I8', el, `${Math.round(out)}px outside the viewport`, out);\n }\n\n return { counts, findings, notes, viewport: [vw, vh] };\n}\n"]}
|
|
@@ -60,7 +60,7 @@ export type ThemeRuntimeSite<Mode extends string = string> = {
|
|
|
60
60
|
/** The site's color mode as the page paints it now (read before the theme's own override: e.g. the body's inline style). */
|
|
61
61
|
readMode(win: Window): Mode;
|
|
62
62
|
/** The theme's CSS for the site's current `rules` (the engine's own sheet excluded) in `mode`; `vars` are `siteVars` as the site sets them. */
|
|
63
|
-
css(rules: RuleLike[], choice: ThemeChoice, mode: Mode, vars: Record<string, string
|
|
63
|
+
css(rules: RuleLike[], choice: ThemeChoice, mode: Mode, vars: Record<string, string>, flags: readonly string[]): string;
|
|
64
64
|
/** The active theme resolved for plugins and the UI kit (the site's own palette when the theme keeps its colors). */
|
|
65
65
|
resolved(choice: ThemeChoice, mode: Mode): ResolvedTheme;
|
|
66
66
|
/** Custom properties of the site's root read with the theme's sheet disabled (their unthemed values). */
|
|
@@ -78,7 +78,21 @@ export type ThemeRuntimeSite<Mode extends string = string> = {
|
|
|
78
78
|
styleId?: string;
|
|
79
79
|
/** Name in console errors. */
|
|
80
80
|
name?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Page states the theme's CSS keys on, as `html[data-wb-<name>]` attributes kept by the engine:
|
|
83
|
+
* set while `selector` matches in the page or one of its open shadow roots (domHub `shadowHosts`).
|
|
84
|
+
* A theme rule gated by `html[data-wb-chat]` costs nothing when the page changes; the same gate
|
|
85
|
+
* as `html:has(...)` makes the browser re-match the whole page on every insertion (seen on x.com:
|
|
86
|
+
* 10-100 ms per inserted node under a theme instead of 0.1 ms).
|
|
87
|
+
*/
|
|
88
|
+
flags?: Readonly<Record<string, string>>;
|
|
81
89
|
};
|
|
90
|
+
/** The attribute a page flag sets on `<html>`. */
|
|
91
|
+
export declare const flagAttribute: (name: string) => string;
|
|
92
|
+
/** Which of `flags` hold in `roots` now (pure, for tests). */
|
|
93
|
+
export declare function pageFlags(roots: ReadonlyArray<{
|
|
94
|
+
querySelector(selector: string): unknown;
|
|
95
|
+
}>, flags: Readonly<Record<string, string>>): string[];
|
|
82
96
|
export type ThemeHandle<Mode extends string = string> = {
|
|
83
97
|
apply(choice: unknown): void;
|
|
84
98
|
current(): ThemeChoice;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// theme from the page's first frame (a snapshot in the page's storage). What the CSS is comes from
|
|
5
5
|
// the site adapter (`ThemeRuntimeSite`): X re-declares its own stylesheet rules with the theme's
|
|
6
6
|
// colors (`buildThemeCss`) and adds type/shape/layout rules over its selectors.
|
|
7
|
-
import { domRoots } from "./domHub.js";
|
|
7
|
+
import { domRoots, watchDom } from "./domHub.js";
|
|
8
8
|
import { FONT_ROLE_NAMES, themeKey, themeVarsCss } from "./themeApi.js";
|
|
9
9
|
import { bundledFontsUsed, isNeutral, resolveFontRoles } from "./themeSpec.js";
|
|
10
10
|
/** The font roles as the plugin API reports them in a site's page; an inheriting interface uses `siteStack` (the site's own font). */
|
|
@@ -162,6 +162,20 @@ export function writeSnapshot({ session, local }, snapshot) {
|
|
|
162
162
|
catch { /* Quota or blocked storage: the next document just starts unthemed. */ }
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
+
/** The attribute a page flag sets on `<html>`. */
|
|
166
|
+
export const flagAttribute = (name) => `data-wb-${name}`;
|
|
167
|
+
/** Which of `flags` hold in `roots` now (pure, for tests). */
|
|
168
|
+
export function pageFlags(roots, flags) {
|
|
169
|
+
const on = [];
|
|
170
|
+
for (const [name, selector] of Object.entries(flags)) {
|
|
171
|
+
try {
|
|
172
|
+
if (roots.some(root => !!root.querySelector(selector)))
|
|
173
|
+
on.push(name);
|
|
174
|
+
}
|
|
175
|
+
catch { /* A bad selector is never on. */ }
|
|
176
|
+
}
|
|
177
|
+
return on;
|
|
178
|
+
}
|
|
165
179
|
const VARS_ID = 'wb-theme-vars';
|
|
166
180
|
/** Keeps the site's page in the owner's theme (see the top of this file). */
|
|
167
181
|
export function installThemeRuntime(win, site, options = {}) {
|
|
@@ -252,6 +266,63 @@ export function installThemeRuntime(win, site, options = {}) {
|
|
|
252
266
|
let staticKey = '';
|
|
253
267
|
let staticText = '';
|
|
254
268
|
let siteVars = {};
|
|
269
|
+
// Page flags (`site.flags`): checked once per hub frame after the page changed, on every page (heavy ones too).
|
|
270
|
+
const flagNames = Object.keys(site.flags ?? {});
|
|
271
|
+
let flagWatch = null;
|
|
272
|
+
// The flags that hold now; the theme's CSS is rebuilt when they change (a rule that only applies to a
|
|
273
|
+
// signed-out page can then be left out of a signed-in one instead of being gated by `:has()`).
|
|
274
|
+
let flagState = [];
|
|
275
|
+
const syncFlags = () => {
|
|
276
|
+
const root = doc.documentElement;
|
|
277
|
+
if (!root || !flagNames.length)
|
|
278
|
+
return;
|
|
279
|
+
let on = [];
|
|
280
|
+
try {
|
|
281
|
+
on = pageFlags(domRoots(win), site.flags);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
on = [];
|
|
285
|
+
}
|
|
286
|
+
for (const name of flagNames) {
|
|
287
|
+
const attr = flagAttribute(name);
|
|
288
|
+
const want = on.includes(name);
|
|
289
|
+
if (root.hasAttribute(attr) !== want) {
|
|
290
|
+
if (want)
|
|
291
|
+
root.setAttribute(attr, '');
|
|
292
|
+
else
|
|
293
|
+
root.removeAttribute(attr);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const changed = on.join(' ') !== flagState.join(' ');
|
|
297
|
+
flagState = on;
|
|
298
|
+
return changed;
|
|
299
|
+
};
|
|
300
|
+
const flagsOn = (on) => {
|
|
301
|
+
if (!flagNames.length)
|
|
302
|
+
return;
|
|
303
|
+
if (on) {
|
|
304
|
+
syncFlags();
|
|
305
|
+
if (!flagWatch) {
|
|
306
|
+
try {
|
|
307
|
+
flagWatch = watchDom(win, () => { if (syncFlags()) {
|
|
308
|
+
try {
|
|
309
|
+
render();
|
|
310
|
+
}
|
|
311
|
+
catch { /* Never break the site. */ }
|
|
312
|
+
} }, { offOnHeavy: false });
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
flagWatch = null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
flagWatch?.dispose();
|
|
321
|
+
flagWatch = null;
|
|
322
|
+
for (const name of flagNames)
|
|
323
|
+
doc.documentElement?.removeAttribute(flagAttribute(name));
|
|
324
|
+
}
|
|
325
|
+
};
|
|
255
326
|
// Open shadow roots the site renders a part of itself in (domHub `shadowHosts`; X's chat embed since
|
|
256
327
|
// 2026-10-01): the document's sheets do not reach inside, so each root adopts the theme built from
|
|
257
328
|
// its own rules plus the static sheet, kept last among its sheets, rebuilt when its rules change and
|
|
@@ -288,7 +359,7 @@ export function installThemeRuntime(win, site, options = {}) {
|
|
|
288
359
|
}
|
|
289
360
|
rootSheets.set(root, item);
|
|
290
361
|
}
|
|
291
|
-
const key = `${snapshotKey}|${rootRuleCount(root, item.sheet)}|${staticText.length}`;
|
|
362
|
+
const key = `${snapshotKey}|${flagState.join(' ')}|${rootRuleCount(root, item.sheet)}|${staticText.length}`;
|
|
292
363
|
if (force || item.key !== key) {
|
|
293
364
|
item.key = key;
|
|
294
365
|
// The site's primitives as the root sees them (its own sheets may declare them; the document may not), read with our sheet off.
|
|
@@ -309,7 +380,7 @@ export function installThemeRuntime(win, site, options = {}) {
|
|
|
309
380
|
}
|
|
310
381
|
let css = '';
|
|
311
382
|
try {
|
|
312
|
-
css = site.css(collectRootRules(root, item.sheet, win.CSS), choice, mode, vars);
|
|
383
|
+
css = site.css(collectRootRules(root, item.sheet, win.CSS), choice, mode, vars, flagState);
|
|
313
384
|
}
|
|
314
385
|
catch (error) {
|
|
315
386
|
console.error(`[${name}] theme (shadow root) failed`, error);
|
|
@@ -350,6 +421,7 @@ export function installThemeRuntime(win, site, options = {}) {
|
|
|
350
421
|
staticKey = '';
|
|
351
422
|
dropEarly();
|
|
352
423
|
syncRoots(false, '', false);
|
|
424
|
+
flagsOn(false);
|
|
353
425
|
if (savedCss) {
|
|
354
426
|
savedCss = '';
|
|
355
427
|
writeSnapshot(storage, null);
|
|
@@ -358,6 +430,7 @@ export function installThemeRuntime(win, site, options = {}) {
|
|
|
358
430
|
}
|
|
359
431
|
if (!doc.body)
|
|
360
432
|
return;
|
|
433
|
+
flagsOn(true);
|
|
361
434
|
const snapshotKey = `${JSON.stringify(choice)}|${mode}`;
|
|
362
435
|
if (early && snapshot?.key !== snapshotKey)
|
|
363
436
|
dropEarly();
|
|
@@ -385,7 +458,7 @@ export function installThemeRuntime(win, site, options = {}) {
|
|
|
385
458
|
fixed = null;
|
|
386
459
|
}
|
|
387
460
|
}
|
|
388
|
-
const key = `${JSON.stringify(choice)}|${mode}|${ruleCount(doc) - (style?.sheet?.cssRules.length ?? 0) - (fixed?.sheet?.cssRules.length ?? 0)}`;
|
|
461
|
+
const key = `${JSON.stringify(choice)}|${mode}|${flagState.join(' ')}|${ruleCount(doc) - (style?.sheet?.cssRules.length ?? 0) - (fixed?.sheet?.cssRules.length ?? 0)}`;
|
|
389
462
|
if (!force && key === lastKey) {
|
|
390
463
|
placeStatic(fixed, style);
|
|
391
464
|
syncRoots(true, snapshotKey, false);
|
|
@@ -408,7 +481,7 @@ export function installThemeRuntime(win, site, options = {}) {
|
|
|
408
481
|
if (style.sheet)
|
|
409
482
|
style.sheet.disabled = false;
|
|
410
483
|
siteVars = vars;
|
|
411
|
-
const css = site.css(collectRules(doc, [style, fixed]), choice, mode, vars);
|
|
484
|
+
const css = site.css(collectRules(doc, [style, fixed]), choice, mode, vars, flagState);
|
|
412
485
|
if (style.textContent !== css)
|
|
413
486
|
style.textContent = css;
|
|
414
487
|
// Last in <head> so equal-specificity `!important` rules win over the site's; the static sheet after it.
|