diff --git a/web/src/app/pattern-lab/LabPanels.module.css b/web/src/app/pattern-lab/LabPanels.module.css index d70c4142f..7c5382e8e 100644 --- a/web/src/app/pattern-lab/LabPanels.module.css +++ b/web/src/app/pattern-lab/LabPanels.module.css @@ -1,7 +1,7 @@ /* ── Layered lab: dock header + panel chrome ───────────────────────────────── The dock with its header and menus, and the chrome every panel shares. A - panel's own classes go in panels/.module.css, as Layers' did on - 2026-10-25; the Preview, Pixel, Ramp and Knobs rules below are still here. + panel's own classes go in panels/.module.css: Layers' went there on + 2026-10-25, and Preview's, Pixel's, Ramp's and Knobs' the same day. Modal styles are in PatternLab.module.css. */ .dockShell { @@ -436,107 +436,7 @@ color: #f7f3ea; } -/* ── Preview panel ── */ - -.previewBody { - flex: 1 1 auto; - min-height: 0; - display: flex; - flex-direction: column; - gap: 8px; - padding: 10px; - overflow: auto; -} - -.previewCanvasBox { - flex: 1 1 auto; - min-height: 120px; - display: flex; - align-items: center; - justify-content: center; - min-width: 0; - overflow: hidden; -} - -.previewFrame { - background: #060505; - border: 1px solid #171512; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04); - max-width: 100%; - max-height: 100%; - padding: 10px; - box-sizing: border-box; - display: flex; - align-items: center; - justify-content: center; -} - -.previewFrame canvas { - image-rendering: pixelated; - width: 100%; - height: 100%; - object-fit: contain; -} - -/* Camera view frames arrive at output resolution — smooth them down, the - way the export will actually look, instead of nearest-neighbor grit. */ -.previewFrame canvas.cameraCanvas { - image-rendering: auto; -} - -@media (hover: none) { - /* A finger has no Alt key, right button or middle button. On a 390×844 - phone this line wrapped the Pixel status bar to 51px, which left the - picture a 372×151 viewport and fitted it at 1×. Without it the viewport - is 372×177, and 2× needs 168. */ - .pixelMouseHint { - display: none; - } -} - -/* ── Pixel editor panel ── */ - -.pixelViewport { - flex: 1 1 auto; - min-height: 0; - overflow: auto; - display: flex; - background: #eee8d8; - padding: 16px; -} - -.pixelStage { - position: relative; - flex: 0 0 auto; - /* auto margins center when small AND keep every edge scrollable when zoomed */ - margin: auto; - background: repeating-conic-gradient(#c9c2b1 0% 25%, #b5ad9a 0% 50%) 0 0 / 12px 12px; - box-shadow: 0 0 0 1px rgba(23, 21, 18, 0.5); -} - -.pixelStage canvas { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - image-rendering: pixelated; - display: block; -} - -.pixelStatus { - display: flex; - align-items: center; - gap: 10px; - padding: 4px 8px; - border-top: 1px solid rgba(23, 21, 18, 0.12); - font-family: var(--font-inter), system-ui, sans-serif; - font-size: 11px; - color: rgba(23, 21, 18, 0.6); - text-transform: none; - flex: 0 0 auto; - flex-wrap: wrap; -} - +/* Color inputs in the Pixel, Text and Graphic Export bars. */ .colorWell { width: 26px; height: 22px; @@ -546,71 +446,6 @@ cursor: pointer; } -.swatchRow { - display: inline-flex; - gap: 3px; -} - -.swatch { - width: 16px; - height: 16px; - border: 1px solid rgba(23, 21, 18, 0.3); - padding: 0; - cursor: pointer; - background: none; -} - -.panelBar .swatch { - width: 22px; - height: 22px; - min-height: 22px; - padding: 0; - border-radius: 3px; -} - -.toolSep { - width: 1px; - align-self: stretch; - background: rgba(23, 21, 18, 0.15); - margin: 0 2px; -} - -/* ── Ramp panel (dock variant) ── */ - -.rampDock { - container: ramp-controls / inline-size; - padding: 10px; - display: flex; - flex-direction: column; - gap: 8px; -} - -.rampAlphaRow { - display: flex; - align-items: center; - gap: 8px; - font-family: var(--font-inter), system-ui, sans-serif; - font-size: 11px; - text-transform: none; - color: rgba(23, 21, 18, 0.65); -} - -.rampAlphaRow input[type="range"] { - flex: 1 1 auto; - min-width: 0; - accent-color: #171512; -} - -.rampAlphaValue { - width: 36px; - text-align: right; -} - -/* Knobs panel: dock padding around the shared .controls block */ -.knobsDock { - padding: 10px; -} - /* Compact scrollbars inside panels */ .panel ::-webkit-scrollbar { width: 10px; @@ -629,8 +464,3 @@ outline: 2px solid #9b4827; outline-offset: 2px; } - -/* Scrolling keeps the ramp and controls at their intended height. */ -.rampDock > * { - flex-shrink: 0; -} diff --git a/web/src/app/pattern-lab/labCss.test.ts b/web/src/app/pattern-lab/labCss.test.ts index 95d8925dc..5c45f33e9 100644 --- a/web/src/app/pattern-lab/labCss.test.ts +++ b/web/src/app/pattern-lab/labCss.test.ts @@ -172,13 +172,13 @@ describe("Layers and Knobs on a touch screen", () => { }); describe("Pixel status line on a touch screen", () => { - const dock = css("LabPanels.module.css"); + const own = css("panels/PixelPanel.module.css"); const pixel = css("panels/PixelPanel.tsx"); it("drops the mouse gestures, and nothing else", () => { - expect(mediaRule(dock, "(hover: none)", ".pixelMouseHint")).toMatch(/display: none;/); - expect(pixel).toContain("alt-click picks · right-drag erases · middle-drag pans"); - expect(mediaRule(dock, "(hover: none)", ".pixelStatus")).toBe(""); + expect(mediaRule(own, "(hover: none)", ".pixelMouseHint")).toMatch(/display: none;/); + expect(pixel).toContain("alt-click picks · right-drag erases · middle-drag pans"); + expect(mediaRule(own, "(hover: none)", ".pixelStatus")).toBe(""); }); }); @@ -258,3 +258,24 @@ describe("PatternLab.module.css", () => { expect(alone).toEqual([]); }); }); + +describe("LabPanels.module.css", () => { + // The dock and the chrome the panels share. Until 25 October 2026 it also + // held 20 rules that one panel file read, for Preview's body and frame, + // Pixel's viewport and toolbars, Color Ramp's body and Knobs' padding. A + // class one file under panels/ reads belongs in that panel's stylesheet. + // The shell's header is the dock, so a class only the shell reads stays. + it("holds no class that only one panel file reads", () => { + 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 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))) + .map(([cls]) => cls) + .sort(); + expect(defined.size).toBeGreaterThan(0); + expect(alone).toEqual([]); + }); +}); diff --git a/web/src/app/pattern-lab/panels/KnobsPanel.module.css b/web/src/app/pattern-lab/panels/KnobsPanel.module.css index b3a336b04..e54596b4a 100644 --- a/web/src/app/pattern-lab/panels/KnobsPanel.module.css +++ b/web/src/app/pattern-lab/panels/KnobsPanel.module.css @@ -1,5 +1,6 @@ /* ── KnobsPanel: its own classes ───────────────────────────────────────── - Moved out of PatternLab.module.css on 2026-09; only this file uses them. */ + Moved out of PatternLab.module.css on 2026-09, and knobsDock out of + LabPanels.module.css on 2026-10-25; only this file uses them. */ .controls { container: knob-controls / inline-size; @@ -8,6 +9,11 @@ margin-top: 2px; } +/* The dock's padding around .controls. */ +.knobsDock { + padding: 10px; +} + /* Two lines per knob: name · value · push on top, then a full-width slider flanked by its min/max fields — long enough to tune precisely. */ .knobLine { diff --git a/web/src/app/pattern-lab/panels/KnobsPanel.tsx b/web/src/app/pattern-lab/panels/KnobsPanel.tsx index f19ea70e6..67aef0ee2 100644 --- a/web/src/app/pattern-lab/panels/KnobsPanel.tsx +++ b/web/src/app/pattern-lab/panels/KnobsPanel.tsx @@ -243,7 +243,7 @@ export default function KnobsPanel() {
-
+
{knobs.map((value, index) => (
diff --git a/web/src/app/pattern-lab/panels/PixelPanel.module.css b/web/src/app/pattern-lab/panels/PixelPanel.module.css new file mode 100644 index 000000000..250c3723f --- /dev/null +++ b/web/src/app/pattern-lab/panels/PixelPanel.module.css @@ -0,0 +1,89 @@ +/* ── PixelPanel: its own classes ───────────────────────────────────────── + Moved out of LabPanels.module.css on 2026-10-25. PixelPanel.tsx and its + toolbars (pixel/PixelToolbars.tsx) are the only files that use them. */ + +.pixelViewport { + flex: 1 1 auto; + min-height: 0; + overflow: auto; + display: flex; + background: #eee8d8; + padding: 16px; +} + +.pixelStage { + position: relative; + flex: 0 0 auto; + /* auto margins center when small AND keep every edge scrollable when zoomed */ + margin: auto; + background: repeating-conic-gradient(#c9c2b1 0% 25%, #b5ad9a 0% 50%) 0 0 / 12px 12px; + box-shadow: 0 0 0 1px rgba(23, 21, 18, 0.5); +} + +.pixelStage canvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + image-rendering: pixelated; + display: block; +} + +.pixelStatus { + display: flex; + align-items: center; + gap: 10px; + padding: 4px 8px; + border-top: 1px solid rgba(23, 21, 18, 0.12); + font-family: var(--font-inter), system-ui, sans-serif; + font-size: 11px; + color: rgba(23, 21, 18, 0.6); + text-transform: none; + flex: 0 0 auto; + flex-wrap: wrap; +} + +@media (hover: none) { + /* A finger has no Alt key, right button or middle button. On a 390×844 + phone this line wrapped the Pixel status bar to 51px, which left the + picture a 372×151 viewport and fitted it at 1×. Without it the viewport + is 372×177, and 2× needs 168. */ + .pixelMouseHint { + display: none; + } +} + +/* ── Toolbars ── */ + +.swatchRow { + display: inline-flex; + gap: 3px; +} + +.swatch { + width: 16px; + height: 16px; + border: 1px solid rgba(23, 21, 18, 0.3); + padding: 0; + cursor: pointer; + background: none; +} + +/* Was `.panelBar .swatch`, when this sat in the same file as `.panelBar`. + Every swatch is in the row, and the row is in a panel bar. Two classes + beat `.panelBar button` in LabPanels.module.css whichever file the + bundle puts first. */ +.swatchRow .swatch { + width: 22px; + height: 22px; + min-height: 22px; + padding: 0; + border-radius: 3px; +} + +.toolSep { + width: 1px; + align-self: stretch; + background: rgba(23, 21, 18, 0.15); + margin: 0 2px; +} diff --git a/web/src/app/pattern-lab/panels/PixelPanel.tsx b/web/src/app/pattern-lab/panels/PixelPanel.tsx index 9a2430d29..5418dd1de 100644 --- a/web/src/app/pattern-lab/panels/PixelPanel.tsx +++ b/web/src/app/pattern-lab/panels/PixelPanel.tsx @@ -28,6 +28,7 @@ import { usePixelSelection } from "./pixel/usePixelSelection"; import { usePixelToolState } from "./pixel/usePixelToolState"; import { usePixelViewport, ZOOM_MIN } from "./pixel/usePixelViewport"; import dock from "../LabPanels.module.css"; +import local from "./PixelPanel.module.css"; // What the panel says when another kind of layer is selected. A text layer is // pixels too, but the Text panel redraws them from its words. @@ -165,12 +166,12 @@ export default function PixelPanel() {
-
+
{pixel.width}×{pixel.height} @@ -222,7 +223,7 @@ export default function PixelPanel() { )} - alt-click picks · right-drag erases · middle-drag pans + alt-click picks · right-drag erases · middle-drag pans
{importOpen && ( diff --git a/web/src/app/pattern-lab/panels/PreviewPanel.module.css b/web/src/app/pattern-lab/panels/PreviewPanel.module.css index 116730b5b..f8711ced7 100644 --- a/web/src/app/pattern-lab/panels/PreviewPanel.module.css +++ b/web/src/app/pattern-lab/panels/PreviewPanel.module.css @@ -1,5 +1,6 @@ /* ── PreviewPanel: its own classes ───────────────────────────────────────── - Moved out of PatternLab.module.css on 2026-09; only this file uses them. */ + Moved out of PatternLab.module.css on 2026-09, and the body and frame out + of LabPanels.module.css on 2026-10-25; only this file uses them. */ /* Direct W×H entry, sitting next to the frame picker. */ .customMatrix { @@ -72,3 +73,51 @@ text-transform: none; cursor: help; } + +/* ── Body: the frame and anything under it ── */ + +.previewBody { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px; + overflow: auto; +} + +.previewCanvasBox { + flex: 1 1 auto; + min-height: 120px; + display: flex; + align-items: center; + justify-content: center; + min-width: 0; + overflow: hidden; +} + +.previewFrame { + background: #060505; + border: 1px solid #171512; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04); + max-width: 100%; + max-height: 100%; + padding: 10px; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; +} + +.previewFrame canvas { + image-rendering: pixelated; + width: 100%; + height: 100%; + object-fit: contain; +} + +/* Camera view frames arrive at output resolution — smooth them down, the + way the export will actually look, instead of nearest-neighbor grit. */ +.previewFrame canvas.cameraCanvas { + image-rendering: auto; +} diff --git a/web/src/app/pattern-lab/panels/PreviewPanel.tsx b/web/src/app/pattern-lab/panels/PreviewPanel.tsx index cd4b4c7ca..17813392e 100644 --- a/web/src/app/pattern-lab/panels/PreviewPanel.tsx +++ b/web/src/app/pattern-lab/panels/PreviewPanel.tsx @@ -273,10 +273,10 @@ export default function PreviewPanel() { )}
-
-
+
+
* { + flex-shrink: 0; +} .rampToggle { display: inline-flex; @@ -215,6 +231,27 @@ background: #f0eae1; } +.rampAlphaRow { + display: flex; + align-items: center; + gap: 8px; + font-family: var(--font-inter), system-ui, sans-serif; + font-size: 11px; + text-transform: none; + color: rgba(23, 21, 18, 0.65); +} + +.rampAlphaRow input[type="range"] { + flex: 1 1 auto; + min-width: 0; + accent-color: #171512; +} + +.rampAlphaValue { + width: 36px; + text-align: right; +} + .rampHint { margin-left: auto; font-family: var(--font-inter), system-ui, sans-serif; diff --git a/web/src/app/pattern-lab/panels/RampPanel.tsx b/web/src/app/pattern-lab/panels/RampPanel.tsx index 329bd7bff..b7baa4a71 100644 --- a/web/src/app/pattern-lab/panels/RampPanel.tsx +++ b/web/src/app/pattern-lab/panels/RampPanel.tsx @@ -275,7 +275,7 @@ function RampEditor({ layer }: { layer: RampLayer }) { )}
-
+
-
+
Alpha - {Math.round(activeStop.alpha * 100)}% + {Math.round(activeStop.alpha * 100)}%
)} diff --git a/web/src/app/pattern-lab/panels/pixel/PixelToolbars.tsx b/web/src/app/pattern-lab/panels/pixel/PixelToolbars.tsx index a211bf87e..e4a65da91 100644 --- a/web/src/app/pattern-lab/panels/pixel/PixelToolbars.tsx +++ b/web/src/app/pattern-lab/panels/pixel/PixelToolbars.tsx @@ -11,6 +11,7 @@ import type { PixelSelection } from "./usePixelSelection"; import type { PixelToolState } from "./usePixelToolState"; import type { PixelViewport } from "./usePixelViewport"; import dock from "../../LabPanels.module.css"; +import local from "../PixelPanel.module.css"; export function PixelToolBar({ tools, switchTool }: { tools: PixelToolState; switchTool: (tool: Tool) => void }) { const { tool, size, setSize, colorHex, setColorHex, alpha, setAlpha, swatches } = tools; @@ -27,7 +28,7 @@ export function PixelToolBar({ tools, switchTool }: { tools: PixelToolState; swi {entry.label} ))} - + - + {swatches.slice(0, 8).map((hex) => ( - + ) : ( @@ -172,7 +173,7 @@ export function PixelActionBar({ - +