mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): restore zoom on timeline text via rem tokens
Cmd +/- zoom scales the root <html> font-size (rem-only by design), but PR #891 converted the message-timeline + thread render path from rem tokens to hardcoded px (text-[15px]/text-[13px], font-size: 15px), freezing that text against zoom. Preserve Kenny's 15px chat sizing intent but express it in rem so it scales: add rem-based `text-chat` (0.9375rem === 15px) and `text-code` (0.8125rem === 13px) Tailwind tokens, and swap the px classes over in MessageRow, markdown, mentionChip, and globals.css (.mention-highlight). Codify it: add `pnpm check:px-text` CI guard flagging new px text in the timeline/thread render path, wired into `pnpm check`, plus an AGENTS.md note steering future agents to rem tokens. Scoped to the regression footprint — no app-wide sweep. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
co-authored by
Taylor Ho
parent
9ee5aeebd9
commit
bfb892c985
@@ -0,0 +1,116 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* Shared "no hardcoded px text size" guard.
|
||||
*
|
||||
* Zoom (Cmd +/-) scales the root <html> font-size, so only **rem**-based text
|
||||
* scales. Hardcoded px text sizes (`text-[15px]`, `font-size: 15px`) freeze
|
||||
* against zoom — that's the timeline regression we fixed. This guard stops new
|
||||
* px text sizes from creeping back in. Use a rem-based Tailwind token instead
|
||||
* (e.g. `text-chat`, `text-code`, `text-sm`).
|
||||
*
|
||||
* It flags:
|
||||
* - Tailwind arbitrary px text utilities: `text-[NNpx]`
|
||||
* - CSS px font sizes: `font-size: NNpx`
|
||||
*
|
||||
* Decorative/chrome exceptions (avatar initials sized to a fixed avatar box,
|
||||
* etc.) live in the `overrides` allowlist supplied by each app.
|
||||
*/
|
||||
|
||||
const TEXT_PX_RE = /\btext-\[\d+(?:\.\d+)?px\]/g;
|
||||
// Match the CSS `font-size` property, but NOT custom properties like
|
||||
// `--font-size:` (third-party widget vars) which merely contain the substring.
|
||||
const FONT_SIZE_PX_RE = /(?<!-)\bfont-size:\s*\d+(?:\.\d+)?px/g;
|
||||
|
||||
async function walkFiles(directory) {
|
||||
const entries = await fs.readdir(directory, { withFileTypes: true });
|
||||
const files = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return walkFiles(fullPath);
|
||||
}
|
||||
return [fullPath];
|
||||
}),
|
||||
);
|
||||
return files.flat();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {string} options.projectRoot Absolute path the rule roots resolve against.
|
||||
* @param {Array<{root: string, extensions: Set<string>}>} options.rules Where to scan.
|
||||
* @param {string} options.label Human label for the failure header.
|
||||
* @param {Set<string>} [options.overrides] Allowlisted "relativePath:lineNumber" entries.
|
||||
* @param {string} options.scriptPath Path mentioned in the failure hint.
|
||||
*/
|
||||
export async function runPxTextCheck({
|
||||
projectRoot,
|
||||
rules,
|
||||
label,
|
||||
overrides = new Set(),
|
||||
scriptPath,
|
||||
}) {
|
||||
const candidateFiles = (
|
||||
await Promise.all(
|
||||
rules.map((rule) => {
|
||||
const dir = path.join(projectRoot, rule.root);
|
||||
return fs
|
||||
.access(dir)
|
||||
.then(() => walkFiles(dir))
|
||||
.catch(() => []);
|
||||
}),
|
||||
)
|
||||
).flat();
|
||||
|
||||
const violations = [];
|
||||
|
||||
for (const filePath of candidateFiles) {
|
||||
const relativePath = path.relative(projectRoot, filePath);
|
||||
const rule = rules.find((r) =>
|
||||
relativePath.startsWith(`${r.root}${path.sep}`),
|
||||
);
|
||||
if (!rule) {
|
||||
continue;
|
||||
}
|
||||
if (!rule.extensions.has(path.extname(relativePath))) {
|
||||
continue;
|
||||
}
|
||||
// Optional per-rule basename allowlist — scopes the scan to specific files.
|
||||
if (rule.files && !rule.files.has(path.basename(relativePath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = await fs.readFile(filePath, "utf8");
|
||||
const lines = content.split(/\r?\n/);
|
||||
lines.forEach((line, index) => {
|
||||
const lineNumber = index + 1;
|
||||
const key = `${relativePath}:${lineNumber}`;
|
||||
if (overrides.has(key)) {
|
||||
return;
|
||||
}
|
||||
const matches = [
|
||||
...(line.match(TEXT_PX_RE) ?? []),
|
||||
...(line.match(FONT_SIZE_PX_RE) ?? []),
|
||||
];
|
||||
for (const match of matches) {
|
||||
violations.push({ relativePath, lineNumber, match });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error(`${label} px-text check failed:`);
|
||||
for (const v of violations) {
|
||||
console.error(`- ${v.relativePath}:${v.lineNumber}: ${v.match}`);
|
||||
}
|
||||
console.error(
|
||||
"Use a rem-based Tailwind text token (e.g. `text-chat`, `text-code`, " +
|
||||
"`text-sm`) so the text scales with Cmd +/- zoom. If this px size is " +
|
||||
"genuinely decorative/chrome (not readable message text), add a " +
|
||||
`narrowly scoped \`relativePath:lineNumber\` exception in \`${scriptPath}\`.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user