From d89c8d10f1038572b262939f2933caee660cb24b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 07:19:06 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20=EB=B0=94?= =?UTF-8?q?=ED=80=B4=20101?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DU1iNPYKhr4f79Z8KHxqj8 From b951920860bc6daa993b848050ae84ad406d984c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 07:27:21 +0000 Subject: [PATCH 2/3] web: drop 23 CSS module classes nothing reads, and test every module for them Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DU1iNPYKhr4f79Z8KHxqj8 --- web/src/app/pattern-lab/labCss.test.ts | 60 +--------- web/src/app/roadmap/Roadmap.module.css | 105 ------------------ web/src/components/community/Atlas.module.css | 9 -- .../components/community/Community.module.css | 102 +---------------- .../components/guide/ConsoleWindow.module.css | 1 - web/src/components/guide/Guide.module.css | 6 - web/src/components/guide/LabWindow.module.css | 1 - web/src/components/guide/desk/Desk.module.css | 10 -- .../components/sections/BuildPanel.module.css | 8 -- web/src/cssModules.test.ts | 31 ++++++ web/test/cssModules.ts | 42 +++++++ 11 files changed, 78 insertions(+), 297 deletions(-) create mode 100644 web/src/cssModules.test.ts create mode 100644 web/test/cssModules.ts diff --git a/web/src/app/pattern-lab/labCss.test.ts b/web/src/app/pattern-lab/labCss.test.ts index 5c45f33e9..0f8f901d0 100644 --- a/web/src/app/pattern-lab/labCss.test.ts +++ b/web/src/app/pattern-lab/labCss.test.ts @@ -9,10 +9,11 @@ // On the same screen Del was 37×28px, the layer eye 20×17px and Push 40×22px, // and the Pixel status bar was 51px, two lines with the mouse gestures. -import { readFileSync, readdirSync } from "node:fs"; +import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { bareCss, readers } from "../../../test/cssModules"; import { NARROW_DOCK_QUERY } from "./panels/registry"; const css = (name: string) => readFileSync(path.resolve(__dirname, name), "utf8"); @@ -182,59 +183,6 @@ describe("Pixel status line on a touch screen", () => { }); }); -/** Every `.tsx` and `.ts` under `src/`, tests left out. */ -const src = path.resolve(__dirname, "../.."); -const sources = (readdirSync(src, { recursive: true }) as string[]) - .filter((file) => /\.tsx?$/.test(file) && !/\.test\.tsx?$/.test(file)) - .map((file) => path.join(src, file)) - .map((file) => ({ file, text: readFileSync(file, "utf8") })); - -/** A stylesheet with its comments and quoted strings taken out. */ -const bareCss = (name: string) => css(name).replace(/\/\*[\s\S]*?\*\//g, "").replace(/"[^"]*"|'[^']*'/g, ""); - -/** Each class of the module at `file` that a file importing it reads as - * `styles.name` or `styles["name"]`, with the files that read it. Null when - * one of them reads the module by a computed name. */ -function readers(file: string): Map> | null { - const read = new Map>(); - for (const { file: from, text } of sources) { - for (const [, local, spec] of text.matchAll(/import (\w+) from "([^"]+\.module\.css)"/g)) { - const target = spec.startsWith("@/") ? path.join(src, spec.slice(2)) : path.resolve(path.dirname(from), spec); - if (target !== file) continue; - if (new RegExp(`\\b${local}\\[(?!["'\`])`).test(text)) return null; - for (const m of text.matchAll(new RegExp(`\\b${local}(?:\\.(\\w+)|\\[["'\`](\\w+)["'\`]\\])`, "g"))) { - const name = m[1] ?? m[2]; - read.set(name, (read.get(name) ?? new Set()).add(from)); - } - } - } - return read; -} - -describe("Lab CSS modules", () => { - // Until 24 October 2026 these files held 13 classes that no component put - // on an element, and 220 lines of rules for them. A class counts as used - // when a file importing its module reads `styles.name` or `styles["name"]`, - // or a rule in the same file composes it. A module read by a computed name - // counts as used whole. - const modules = (readdirSync(__dirname, { recursive: true }) as string[]).filter((file) => file.endsWith(".module.css")); - - it("define no class that no component uses", () => { - const unused: Record = {}; - for (const name of modules) { - const bare = bareCss(name); - const defined = new Set([...bare.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)].map((m) => m[1])); - const used = new Set([...bare.matchAll(/composes:([^;]+);/g)].flatMap((m) => m[1].trim().split(/\s+/))); - const read = readers(path.join(__dirname, name)); - for (const cls of read?.keys() ?? []) used.add(cls); - const dead = read === null ? [] : [...defined].filter((cls) => !used.has(cls)).sort(); - if (dead.length) unused[name] = dead; - } - expect(modules.length).toBeGreaterThan(0); - expect(unused).toEqual({}); - }); -}); - describe("PatternLab.module.css", () => { // The file is for what two or more files draw with. Until 25 October 2026 // it also held 22 rules only the shell read, for the header, and the dark @@ -246,7 +194,7 @@ describe("PatternLab.module.css", () => { expect(read).not.toBeNull(); const by = (cls: string) => [...(read?.get(cls) ?? [])]; const files = new Map>(); - for (const [, selectors] of bareCss("PatternLab.module.css").matchAll(/([^{}]+)\{[^{}]*\}/g)) { + for (const [, selectors] of bareCss(path.resolve(__dirname, "PatternLab.module.css")).matchAll(/([^{}]+)\{[^{}]*\}/g)) { const plain = selectors.split(",").flatMap((part) => /^\s*\.([_a-zA-Z][\w-]*)\s*$/.exec(part)?.[1] ?? []); for (const [, cls] of selectors.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) { const reach = plain.includes(cls) ? plain.flatMap(by) : by(cls); @@ -269,7 +217,7 @@ describe("LabPanels.module.css", () => { const read = readers(path.resolve(__dirname, "LabPanels.module.css")); expect(read).not.toBeNull(); // Only names the file defines: registry.tsx also reads `def.dock.ref`. - const defined = new Set([...bareCss("LabPanels.module.css").matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)].map((m) => m[1])); + const defined = new Set([...bareCss(path.resolve(__dirname, "LabPanels.module.css")).matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)].map((m) => m[1])); const panels = path.join(__dirname, "panels") + path.sep; const alone = [...(read ?? [])] .filter(([cls, from]) => defined.has(cls) && from.size === 1 && [...from].every((file) => file.startsWith(panels))) diff --git a/web/src/app/roadmap/Roadmap.module.css b/web/src/app/roadmap/Roadmap.module.css index 58e1e3825..1a1f776ed 100644 --- a/web/src/app/roadmap/Roadmap.module.css +++ b/web/src/app/roadmap/Roadmap.module.css @@ -103,54 +103,6 @@ } } -.legend { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 20px; -} - -.legendItem { - display: inline-flex; - align-items: center; - gap: 8px; - color: var(--pf-ink-muted); - font-family: var(--pf-mono); - font-size: 12.5px; - letter-spacing: 0.05em; - text-transform: uppercase; -} - -.legendDrag { - color: var(--pf-ink-faint); - font-family: var(--pf-mono); - font-size: 12.5px; - letter-spacing: 0.05em; - text-transform: uppercase; -} - -.swatchDone, -.swatchPlanned { - width: 22px; - height: 14px; - border-radius: 0; -} - -.swatchDone { - border: 1.5px solid var(--pf-ink); - background: #fff; -} - -.swatchPlanned { - border: 1.5px dashed var(--pf-ink-faint); -} - -.swatchDep { - width: 24px; - height: 0; - border-top: 2px dashed var(--pf-ink-faint); -} - /* .viewport does NOT clip, so the popup can never be cut off by the pannable canvas underneath it — only the canvas itself clips. */ .viewport { @@ -182,44 +134,6 @@ will-change: transform; } -.zoomGroup { - display: inline-flex; - align-items: center; - border: 1px solid var(--pf-rule); - border-radius: 0; - overflow: hidden; -} - -.zoomBtn, -.zoomVal { - appearance: none; - border: none; - background: transparent; - color: var(--pf-ink-muted); - font-family: var(--pf-mono); - cursor: pointer; -} - -.zoomBtn { - padding: 7px 14px; - font-size: 16px; - line-height: 1; -} - -.zoomVal { - padding: 9px 10px; - min-width: 56px; - font-size: 12.5px; - letter-spacing: 0.04em; - border-inline: 1px solid var(--pf-rule); -} - -.zoomBtn:hover, -.zoomVal:hover { - color: var(--pf-ink); - background: var(--pf-cream-2); -} - .mapSvg { display: block; overflow: visible; @@ -249,7 +163,6 @@ letter-spacing: -0.01em; } -.gridLine, .gridLineHorizontal { stroke: var(--pf-rule-soft); stroke-width: 1; @@ -508,21 +421,3 @@ text-decoration: underline; text-underline-offset: 3px; } - -.mapCaption { - position: absolute; - left: 16px; - bottom: 12px; - z-index: 5; - max-width: 620px; - margin: 0; - padding: 8px 12px; - border-radius: 0; - background: rgba(251, 248, 242, 0.88); - color: var(--pf-ink-faint); - font-family: var(--pf-mono); - font-size: 12px; - letter-spacing: 0.03em; - line-height: 1.6; - pointer-events: none; -} diff --git a/web/src/components/community/Atlas.module.css b/web/src/components/community/Atlas.module.css index 7d7bcf72b..8759c36f7 100644 --- a/web/src/components/community/Atlas.module.css +++ b/web/src/components/community/Atlas.module.css @@ -361,15 +361,6 @@ margin: 6px 0 14px; } -.panelWell { - position: relative; - width: 140px; - height: 280px; - border: 1px solid var(--pfc-hair); - background: var(--pfc-well); - overflow: hidden; -} - .openLink { display: inline-block; margin-top: 4px; diff --git a/web/src/components/community/Community.module.css b/web/src/components/community/Community.module.css index e5ee8526e..2d1f95dab 100644 --- a/web/src/components/community/Community.module.css +++ b/web/src/components/community/Community.module.css @@ -2766,35 +2766,6 @@ font-weight: 500; } -/* ── Sign-in gate ── - Never a wall: everything plays without an account, so the prompt appears - inline at the moment something needs keeping, with the knobs left alone. */ -.gatePanel { - display: flex; - gap: 14px; - align-items: center; - border: 1px solid rgba(244, 239, 230, 0.16); - background: var(--pfc-panel); - padding: 16px 18px; - max-width: 560px; -} - -.gateText { - display: flex; - flex-direction: column; - gap: 3px; -} - -.gateTitle { - font-size: 14px; - font-weight: 500; -} - -.gateSub { - font-size: 12.5px; - color: var(--pfc-ink-muted); -} - /* ── Firmware build modal ── */ .buildStatusLine { @@ -2876,20 +2847,6 @@ color: var(--pfc-led); } -.sectionLink { - font-family: var(--pf-mono, monospace); - font-size: 11px; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--pfc-ink); - text-decoration: none; - margin-left: auto; -} - -.sectionLink:hover { - color: var(--pfc-led); -} - .deckSection { padding-bottom: 30px; margin-bottom: 28px; @@ -3031,22 +2988,7 @@ /* An anchor that has to sit in a row of buttons and match them. It is a real link rather than a button because it leaves for the device's own origin — middle-click and "open in new tab" should work, and a click handler would - take those away. */ -.btnLink { - composes: btn; - display: inline-flex; - align-items: center; - text-decoration: none; - /* No line-height override: the buttons beside it inherit the body's, and - resetting it here made the link 6px shorter than everything in the row. */ -} - -.btnLink[aria-disabled="true"] { - opacity: 0.5; - pointer-events: none; -} - -/* The accent weight of the same thing — "this leaves the page and touches + take those away. The accent weight says "this leaves the page and touches your board", which is exactly what it does. `composes: btn btnAccent` names both on purpose: composing btnAccent alone emits only two classes here, dropping the btn underneath it, so the fill @@ -3315,27 +3257,6 @@ border-color: var(--pfc-ink); } -/* Fresh from the wall: a single dense row under the decks, so the newest work - is one glance away from the curated shelf. */ -.freshStrip { - display: grid; - grid-template-columns: repeat(8, minmax(0, 1fr)); - gap: 12px; -} - -@media (max-width: 1100px) { - .freshStrip { - grid-template-columns: repeat(5, minmax(0, 1fr)); - } -} - -@media (max-width: 768px), (hover: none) { - .freshStrip { - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; - } -} - /* ── Deck page ── */ .deckHead { @@ -3754,16 +3675,6 @@ color: var(--pfc-ink); } -.introRow { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 12px; - font-size: 13.5px; - color: var(--pfc-ink-muted); - flex: none; -} - .postComposer { display: flex; flex-direction: column; @@ -3826,13 +3737,6 @@ gap: 10px; } -.composerCount { - flex: 1; - font-family: var(--pf-mono, monospace); - font-size: 11px; - color: var(--pfc-ink-faint); -} - .postList { list-style: none; margin: 0; @@ -5735,10 +5639,6 @@ button.legendWalk:hover { gap: 8px; } - .introRow { - font-size: 13px; - } - .feedControls { gap: 12px; } diff --git a/web/src/components/guide/ConsoleWindow.module.css b/web/src/components/guide/ConsoleWindow.module.css index 7ad79f665..0e9fc8dac 100644 --- a/web/src/components/guide/ConsoleWindow.module.css +++ b/web/src/components/guide/ConsoleWindow.module.css @@ -7,7 +7,6 @@ --cw-ink: #efe9dd; --cw-ink-2: rgba(239, 233, 221, 0.72); --cw-ink-3: rgba(239, 233, 221, 0.46); - --cw-hair: rgba(239, 233, 221, 0.1); --cw-hair-2: rgba(239, 233, 221, 0.18); --cw-led: #ff6a3d; /* The console's own ground (the pages' --cream, light by default), so a diff --git a/web/src/components/guide/Guide.module.css b/web/src/components/guide/Guide.module.css index c1f7fe593..21b74629a 100644 --- a/web/src/components/guide/Guide.module.css +++ b/web/src/components/guide/Guide.module.css @@ -250,12 +250,6 @@ transform: translate3d(0, 128%, 0); } } -/* A rule being drawn, from where the reading starts. */ -@keyframes draw { - from { - transform: scaleX(0); - } -} /* The first load. The page's words arrive with the first paint and never wait for the stage; where the device will stand there is, on a first diff --git a/web/src/components/guide/LabWindow.module.css b/web/src/components/guide/LabWindow.module.css index 32ad45dda..04a228985 100644 --- a/web/src/components/guide/LabWindow.module.css +++ b/web/src/components/guide/LabWindow.module.css @@ -12,7 +12,6 @@ before the lab has painted: no flash of anything else. */ --lw-lab: #f4efe6; --lw-chrome: #1b1a18; - --lw-chrome-2: #131210; --lw-field: #0f0e0d; --lw-shadow: 0 22px 60px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.35); } diff --git a/web/src/components/guide/desk/Desk.module.css b/web/src/components/guide/desk/Desk.module.css index 69e1e36bc..f1062e2d4 100644 --- a/web/src/components/guide/desk/Desk.module.css +++ b/web/src/components/guide/desk/Desk.module.css @@ -14,7 +14,6 @@ --d-ink: #efe9dd; --d-ink-2: rgba(239, 233, 221, 0.72); --d-ink-3: rgba(239, 233, 221, 0.46); - --d-hair: rgba(239, 233, 221, 0.1); --d-hair-2: rgba(239, 233, 221, 0.18); --d-led: var(--g-led, #ff6a3d); --d-chrome: #1b1a18; @@ -246,15 +245,6 @@ inset: 0; overflow: hidden; } -.appEmpty { - display: grid; - place-items: center; - height: 100%; - font-family: var(--pf-mono, ui-monospace, monospace); - font-size: 12px; - letter-spacing: 0.08em; - color: var(--d-ink-3); -} /* ── the Lab ─────────────────────────────────────────────────────────────── */ diff --git a/web/src/components/sections/BuildPanel.module.css b/web/src/components/sections/BuildPanel.module.css index 085c81a8e..a4d3dcc23 100644 --- a/web/src/components/sections/BuildPanel.module.css +++ b/web/src/components/sections/BuildPanel.module.css @@ -9,14 +9,6 @@ container-type: inline-size; } -.note { - max-width: 560px; - margin: 0; - color: var(--pf-ink-muted); - font-size: var(--pf-fs-body); - line-height: 1.65; -} - /* ── What it takes ── cost + time, before the steps. Four across, because the decision is "is this worth it", and that is one glance, not four rows. */ .factsBand { diff --git a/web/src/cssModules.test.ts b/web/src/cssModules.test.ts new file mode 100644 index 000000000..8d21e496e --- /dev/null +++ b/web/src/cssModules.test.ts @@ -0,0 +1,31 @@ +// Until 24 October 2026 the Lab's modules held 13 classes that no component +// put on an element, and 220 lines of rules for them. The test that came with +// that cleanup read the Lab alone, and until 25 October 2026 five modules +// outside it held 23 more: the roadmap's legend and zoom buttons, the +// community's sign-in gate, 241 lines with a keyframe and three variables +// nothing read. + +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { bareCss, cssModules, readers, src } from "../test/cssModules"; + +describe("CSS modules", () => { + // A class counts as used when a file importing its module reads + // `styles.name` or `styles["name"]`, or a rule in the same file composes it. + // A module read by a computed name counts as used whole. A class inside + // `:global(…)` is the page's, not the module's. + it("define no class that no component uses", () => { + const unused: Record = {}; + for (const file of cssModules) { + const bare = bareCss(file).replace(/:global\([^)]*\)/g, ""); + const defined = new Set([...bare.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)].map((m) => m[1])); + const used = new Set([...bare.matchAll(/composes:([^;]+);/g)].flatMap((m) => m[1].trim().split(/\s+/))); + const read = readers(file); + for (const cls of read?.keys() ?? []) used.add(cls); + const dead = read === null ? [] : [...defined].filter((cls) => !used.has(cls)).sort(); + if (dead.length) unused[path.relative(src, file)] = dead; + } + expect(cssModules.length).toBeGreaterThan(40); + expect(unused).toEqual({}); + }); +}); diff --git a/web/test/cssModules.ts b/web/test/cssModules.ts new file mode 100644 index 000000000..675d762c6 --- /dev/null +++ b/web/test/cssModules.ts @@ -0,0 +1,42 @@ +// What the CSS module tests read. jsdom applies none of the modules, so a +// test reads the stylesheet and the files that import it. + +import { readFileSync, readdirSync } from "node:fs"; +import path from "node:path"; + +/** `web/src/`. */ +export const src = path.resolve(__dirname, "../src"); + +const files = readdirSync(src, { recursive: true }) as string[]; + +/** Every `*.module.css` under `src/`. */ +export const cssModules = files.filter((file) => file.endsWith(".module.css")).map((file) => path.join(src, file)); + +/** Every `.tsx` and `.ts` under `src/`, tests left out. */ +const sources = files + .filter((file) => /\.tsx?$/.test(file) && !/\.test\.tsx?$/.test(file)) + .map((file) => path.join(src, file)) + .map((file) => ({ file, text: readFileSync(file, "utf8") })); + +/** A stylesheet with its comments and quoted strings taken out. */ +export const bareCss = (file: string) => + readFileSync(file, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/"[^"]*"|'[^']*'/g, ""); + +/** Each class of the module at `file` that a file importing it reads as + * `styles.name` or `styles["name"]`, with the files that read it. Null when + * one of them reads the module by a computed name. */ +export function readers(file: string): Map> | null { + const read = new Map>(); + for (const { file: from, text } of sources) { + for (const [, local, spec] of text.matchAll(/import (\w+) from ["']([^"']+\.module\.css)["']/g)) { + const target = spec.startsWith("@/") ? path.join(src, spec.slice(2)) : path.resolve(path.dirname(from), spec); + if (target !== file) continue; + if (new RegExp(`\\b${local}\\[(?!["'\`])`).test(text)) return null; + for (const m of text.matchAll(new RegExp(`\\b${local}(?:\\.(\\w+)|\\[["'\`]([\\w-]+)["'\`]\\])`, "g"))) { + const name = m[1] ?? m[2]; + read.set(name, (read.get(name) ?? new Set()).add(from)); + } + } + } + return read; +} From 95bcc5fa3db6a310459e46571a52ccae4d83865b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 07:36:13 +0000 Subject: [PATCH 3/3] web: say what the site-wide CSS module test found in its header Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DU1iNPYKhr4f79Z8KHxqj8 --- web/src/cssModules.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/web/src/cssModules.test.ts b/web/src/cssModules.test.ts index 8d21e496e..ea27aef0e 100644 --- a/web/src/cssModules.test.ts +++ b/web/src/cssModules.test.ts @@ -1,9 +1,8 @@ // Until 24 October 2026 the Lab's modules held 13 classes that no component // put on an element, and 220 lines of rules for them. The test that came with -// that cleanup read the Lab alone, and until 25 October 2026 five modules -// outside it held 23 more: the roadmap's legend and zoom buttons, the -// community's sign-in gate, 241 lines with a keyframe and three variables -// nothing read. +// that cleanup read the Lab alone. Until 25 October 2026 five modules outside +// it held 23 more in 30 rules, the roadmap's legend and zoom buttons and the +// community's sign-in gate among them. import path from "node:path"; import { describe, expect, it } from "vitest";