diff --git a/CHANGELOG.md b/CHANGELOG.md
index d06c0f8..c66e04b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## 1.0.1
+
+### Fixed
+- **Themes no longer slow X down.** A few theme rules made the browser restyle most of the page every time X added something (about 440 elements per change; now about 2). Scrolling with any theme on is smooth again. A test now fails any theme that does this.
+- **Windows 98 and Classic bars show every item.** X made each menu item as wide as the whole menu, so a bottom or top bar showed only Home.
+- **The app menu opens from each pane's wB button** at the top right (Alt+F, F10 and the account menu still open it). The titlebar logo is a plain logo again.
+- **A restored window always fits its monitor**, including when a second monitor is smaller or gone.
+- **macOS: Cmd+F always opens find** for the page in front, even when the app's own tab was active.
+- The "help translate" link points at the public repository.
+
## 1.0.0
The first public release. wayBetter for X is the first app on the open
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 60c712a..ec416ee 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "@better-twttr/desktop",
- "version": "1.0.0",
+ "version": "1.0.1",
"private": true,
"license": "GPL-3.0-or-later",
"type": "module",
diff --git a/apps/desktop/scripts/theme-matrix.mjs b/apps/desktop/scripts/theme-matrix.mjs
index ec28538..bfe4ddb 100644
--- a/apps/desktop/scripts/theme-matrix.mjs
+++ b/apps/desktop/scripts/theme-matrix.mjs
@@ -163,8 +163,20 @@ function inspect() {
sideScroll: scroller.scrollWidth - scroller.clientWidth,
};
}
+ // 5. The rail (and a theme's bar made of it): every visible item answers a click at its center.
+ const navMiss = [];
+ for (const item of document.querySelectorAll('header[role="banner"] nav[role="navigation"] > :is(a,button), header[role="banner"] [data-testid="SideNav_NewTweet_Button"]')) {
+ const r = item.getBoundingClientRect();
+ // Off-window items count: a bar that pushes items out of the window loses them.
+ if (r.width < 2 || r.height < 2 || dimmed(item)) continue;
+ const x = r.left + r.width / 2, y = r.top + r.height / 2;
+ const hit = x >= 0 && y >= 0 && x < innerWidth && y < innerHeight ? document.elementFromPoint(x, y) : null;
+ // X's own layers (a modal's mask, a menu) cover the rail on purpose.
+ if (hit?.closest('#layers')) continue;
+ if (!hit || !item.contains(hit)) navMiss.push({ name: item.getAttribute('data-testid') || item.getAttribute('aria-label') || item.tagName, over: hit ? (hit.getAttribute('data-testid') || hit.tagName + (hit.id ? '#' + hit.id : '') + '.' + String(hit.className).slice(0, 40)) : 'outside the window' });
+ }
return {
- chat, short: texts.filter(t => t.need && t.ratio < t.need).slice(0, 4),
+ navMiss, chat, short: texts.filter(t => t.need && t.ratio < t.need).slice(0, 4),
iconCount: icons.length, faintIcons: icons.filter(i => i.ratio < 3).length, worstIcon: icons[0] ?? null,
layers,
texts: texts.slice(0, 6), low: texts.filter(t => t.ratio < 4.5).length, lowLinks: texts.filter(t => t.tag === 'A' && t.ratio < 4.5).length, fail: texts.filter(t => t.ratio < 3).length, textCount: texts.length,
@@ -173,12 +185,36 @@ function inspect() {
};
}
+/** Elements the browser restyles per DOM insertion (and removal) in the page's main list: a theme rule with a
+ * page-wide :has() makes it the whole page (1.0.0: about 440 on x.com, against 1.5 without a theme). */
+const RESTYLE_BUDGET = +(args.restyleBudget ?? 5);
+async function restylePerInsertion(page, cdp) {
+ const events = [];
+ const collect = event => events.push(...event.value);
+ cdp.on('Tracing.dataCollected', collect);
+ await cdp.send('Tracing.start', { traceConfig: { includedCategories: ['devtools.timeline'] }, transferMode: 'ReportEvents' });
+ const ops = await page.evaluate(() => {
+ const host = document.querySelector('[data-testid="cellInnerDiv"]')?.parentElement ?? document.querySelector('[data-testid="primaryColumn"]') ?? document.querySelector('main') ?? document.body;
+ document.body.offsetHeight;
+ for (let i = 0; i < 20; i++) { const d = document.createElement('div'); host.appendChild(d); d.offsetHeight; d.remove(); document.body.offsetHeight; }
+ return 40;
+ });
+ const complete = new Promise(resolve => cdp.once('Tracing.tracingComplete', resolve));
+ await cdp.send('Tracing.end');
+ await complete;
+ cdp.off('Tracing.dataCollected', collect);
+ let elements = 0;
+ for (const event of events) if (event.name === 'UpdateLayoutTree' && event.args?.elementCount) elements += event.args.elementCount;
+ return +(elements / ops).toFixed(1);
+}
+
const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || undefined });
const results = [];
const failures = [];
const baselineWarnings = [];
try {
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
+ const cdp = await page.context().newCDPSession(page);
// Measure settled colors: theme motion only runs without a reduced-motion preference.
await page.emulateMedia({ reducedMotion: 'reduce' });
const errors = [];
@@ -201,6 +237,8 @@ try {
await page.evaluate(preset => window.__bt.apply({ preset }), theme);
await page.evaluate(() => new Promise(requestAnimationFrame));
const check = await page.evaluate(inspect);
+ // Style cost of the page changing (once per cell, theme and mode).
+ if (width === widthsFor(cell)[0]) check.restyle = await restylePerInsertion(page, cdp);
if (theme === 'x') baseline = check;
const problems = [];
for (const layer of check.layers) if (!layer.opaque) problems.push(`layer ${layer.name} is see-through (${layer.color})`);
@@ -215,6 +253,8 @@ try {
if (!check.contentVisible) problems.push('primary content not visible');
if (baseline && check.contentWidth < Math.min(baseline.contentWidth * 0.85, baseline.contentWidth - 90)) problems.push(`content squeezed to ${check.contentWidth}px (X: ${baseline.contentWidth}px)`);
if (check.overflow > 1) problems.push(`page overflows the pane by ${check.overflow}px`);
+ if (check.restyle !== undefined && check.restyle > RESTYLE_BUDGET) problems.push(`each DOM insertion restyles ${check.restyle} elements (budget ${RESTYLE_BUDGET}; X: ${baseline?.restyle ?? '?'}): a page-wide selector such as :has() over the page`);
+ if (check.navMiss.length) problems.push(`${check.navMiss.length} rail item(s) not clickable (${check.navMiss.slice(0, 3).map(m => m.name + ' under ' + m.over).join(', ')})`);
if (check.chat) {
const c = check.chat, x = baseline?.chat;
const same = (a, b) => (!a && !b) || (!!a && !!b && Math.abs(a.left - b.left) <= 4 && Math.abs(a.width - b.width) <= 4); // a theme's column frame may take its border (up to 2px a side)
diff --git a/apps/desktop/scripts/theme-matrix/fixtures.mjs b/apps/desktop/scripts/theme-matrix/fixtures.mjs
index 95ca8de..45a2ed6 100644
--- a/apps/desktop/scripts/theme-matrix/fixtures.mjs
+++ b/apps/desktop/scripts/theme-matrix/fixtures.mjs
@@ -172,14 +172,14 @@ function nav(m) {
const glyph = ``;
const item = ([testid, href, label]) => {
const tag = href ? 'a' : 'button';
- return `<${tag} ${href ? `href="${href}" role="link"` : 'type="button" role="button"'}${testid ? ` data-testid="${testid}"` : ''} aria-label="${label}" class="css-175oi2r" style="text-decoration: none; border: 0; background: transparent; padding: 4px 0;">