diff --git a/web/src/app/pattern-lab/LabPanels.module.css b/web/src/app/pattern-lab/LabPanels.module.css index 21d6bccb1..a3fffdeee 100644 --- a/web/src/app/pattern-lab/LabPanels.module.css +++ b/web/src/app/pattern-lab/LabPanels.module.css @@ -1,6 +1,8 @@ /* ── Layered lab: dock header + panel chrome ───────────────────────────────── - New-in-v2 styles only; knob/ramp/gallery/modal styles are shared from - PatternLab.module.css. */ + The dock with its header 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. + Modal styles are in PatternLab.module.css. */ .dockShell { height: 100vh; @@ -311,195 +313,7 @@ image-rendering: auto; } -/* ── Layers panel ── */ - -.layerList { - list-style: none; - margin: 0; - padding: 4px; - display: flex; - flex-direction: column; - gap: 2px; -} - -.layerRow { - min-height: 34px; - border-radius: 4px; - display: flex; - align-items: center; - gap: 6px; - padding: 5px 6px; - border: 1px solid transparent; - background: #fbf7ee; - cursor: pointer; - user-select: none; -} - -.layerRow:hover { - border-color: rgba(23, 21, 18, 0.25); -} - -.layerRowActive { - background: #171512; - color: #f7f3ea; - border-color: #171512; -} - -.layerRow[data-dragover="true"] { - border-top: 2px solid #ff4d00; -} - -.layerEye { - appearance: none; - border: 0; - background: none; - color: inherit; - cursor: pointer; - font-size: 12px; - width: 20px; - padding: 0; - opacity: 0.85; -} - -.layerEyeOff { - opacity: 0.3; -} - -.layerName { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 12px; - font-weight: 600; -} - -.layerRenameInput { - flex: 1 1 auto; - min-width: 0; - border: 1px solid rgba(23, 21, 18, 0.4); - background: #fffdf8; - color: #171512; - font: inherit; - font-size: 12px; - padding: 1px 4px; -} - -.layerType { - font-family: var(--font-inter), system-ui, sans-serif; - font-size: 11px; - letter-spacing: 0.06em; - padding: 1px 5px; - border: 1px solid currentColor; - opacity: 0.65; - flex: 0 0 auto; -} - -.layerErrDot { - width: 8px; - height: 8px; - border-radius: 50%; - background: #d63e2e; - flex: 0 0 auto; -} - -.layerMeta { - font-family: var(--font-inter), system-ui, sans-serif; - font-size: 11px; - opacity: 0.55; - flex: 0 0 auto; -} - -.layerControls { - display: flex; - flex-direction: column; - gap: 8px; - padding: 10px; - border-top: 1px solid rgba(23, 21, 18, 0.12); - background: #fbf7ee; - flex: 0 0 auto; - font-family: var(--font-inter), system-ui, sans-serif; - font-size: 12px; -} - -.layerControlRow { - display: flex; - align-items: center; - gap: 8px; -} - -.layerControlRow > span { - width: 58px; - flex: 0 0 auto; - text-transform: none; - color: rgba(23, 21, 18, 0.6); - font-size: 11px; -} - -.layerControlRow input[type="range"] { - flex: 1 1 auto; - min-width: 0; - accent-color: #171512; -} - -.layerControlRow select { - min-height: 28px; - border-radius: 4px; - min-width: 0; - flex: 1 1 auto; - border: 1px solid rgba(23, 21, 18, 0.3); - background: #fffdf8; - font-family: inherit; - font-size: 12px; - padding: 2px 4px; -} - -.layerControlValue { - width: 40px; - text-align: right; - flex: 0 0 auto; - font-size: 11px; -} - -.layerMaskLabel { - display: inline-flex; - align-items: center; - gap: 4px; - font-size: 11px; - text-transform: none; - cursor: pointer; -} - -/* Touch screens get fingertip-sized targets, as in the Text panel. On a - 390px phone the bar buttons were 28px tall, Del 37px wide, the eye 20×17px - and the mask box 13×13px. The row grows to 46px so the eye can take its - whole height. */ @media (hover: none) { - .layerBar button { - min-width: 44px; - min-height: 44px; - } - - .layerRow { - min-height: 46px; - padding-block: 0; - } - - .layerEye { - align-self: stretch; - width: 44px; - } - - .layerMaskLabel { - min-height: 44px; - } - - .layerControlRow input[type="range"], - .layerControlRow select { - min-height: 44px; - } - /* 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 @@ -640,8 +454,7 @@ /* Keyboard focus stays visible throughout the panel controls. */ .panelBar :is(button, select, input):focus-visible, -.menuPop button:focus-visible, -.layerControlRow select:focus-visible { +.menuPop button:focus-visible { outline: 2px solid #9b4827; outline-offset: 2px; } diff --git a/web/src/app/pattern-lab/labCss.test.ts b/web/src/app/pattern-lab/labCss.test.ts index 3549ab2e4..05c499343 100644 --- a/web/src/app/pattern-lab/labCss.test.ts +++ b/web/src/app/pattern-lab/labCss.test.ts @@ -118,32 +118,33 @@ describe("Lab dock on a touch screen", () => { describe("Layers and Knobs on a touch screen", () => { const dock = css("LabPanels.module.css"); + const own = css("panels/LayersPanel.module.css"); const knobs = css("panels/KnobsPanel.module.css"); const layers = css("panels/LayersPanel.tsx"); const touch = (source: string, selector: string) => mediaRule(source, "(hover: none)", selector); it("give the Layers bar buttons a 44px box", () => { - const button = touch(dock, ".layerBar button"); + const button = touch(own, ".layerBar button"); expect(button).toMatch(/min-width: 44px;/); expect(button).toMatch(/min-height: 44px;/); // Only the Layers bar: the Pixel toolbars carry a dozen buttons each. - expect(layers).toMatch(/className=\{`\$\{dock\.panelBar\} \$\{dock\.layerBar\}`\}/); + expect(layers).toMatch(/className=\{`\$\{dock\.panelBar\} \$\{local\.layerBar\}`\}/); expect(touch(dock, ".panelBar button")).toBe(""); }); it("let the eye fill a 44px row", () => { - const row = touch(dock, ".layerRow"); + const row = touch(own, ".layerRow"); expect(row).toMatch(/min-height: 46px;/); expect(row).toMatch(/padding-block: 0;/); - const eye = touch(dock, ".layerEye"); + const eye = touch(own, ".layerEye"); expect(eye).toMatch(/align-self: stretch;/); expect(eye).toMatch(/width: 44px;/); }); it("give the mask, opacity and blend rows a 44px height", () => { - expect(touch(dock, ".layerMaskLabel")).toMatch(/min-height: 44px;/); - expect(touch(dock, ".layerControlRow select")).toMatch(/min-height: 44px;/); - expect(mediaBlocks(dock, "(hover: none)").join("")).toContain('.layerControlRow input[type="range"],\n .layerControlRow select {'); + expect(touch(own, ".layerMaskLabel")).toMatch(/min-height: 44px;/); + expect(touch(own, ".layerControlRow select")).toMatch(/min-height: 44px;/); + expect(mediaBlocks(own, "(hover: none)").join("")).toContain('.layerControlRow input[type="range"],\n .layerControlRow select {'); }); it("give Push a 44px box", () => { @@ -154,17 +155,17 @@ describe("Layers and Knobs on a touch screen", () => { it("come after the rules they override", () => { // Same specificity again, so each touch block has to sit below them. - const dockTouch = dock.lastIndexOf("@media (hover: none) {"); - for (const base of [ - "\n.panelBar button {", - "\n.layerRow {", - "\n.layerEye {", - "\n.layerControlRow select {", - "\n.layerMaskLabel {", - ]) { - expect(dock.indexOf(base)).toBeGreaterThan(-1); - expect(dockTouch).toBeGreaterThan(dock.indexOf(base)); + const ownTouch = own.lastIndexOf("@media (hover: none) {"); + for (const base of ["\n.layerRow {", "\n.layerEye {", "\n.layerControlRow select {", "\n.layerMaskLabel {"]) { + expect(own.indexOf(base)).toBeGreaterThan(-1); + expect(ownTouch).toBeGreaterThan(own.indexOf(base)); } + // `.layerBar button` answers `.panelBar button` from the other file. The + // bundle keeps the stylesheets in the order the panel imports them. + expect(dock.indexOf("\n.panelBar button {")).toBeGreaterThan(-1); + const dockImport = layers.indexOf('import dock from "../LabPanels.module.css";'); + expect(dockImport).toBeGreaterThan(-1); + expect(layers.indexOf('import local from "./LayersPanel.module.css";')).toBeGreaterThan(dockImport); // The 980px block sets Push back to 22px. expect(knobs.lastIndexOf("@media (hover: none) {")).toBeGreaterThan(knobs.indexOf("@media (max-width: 980px) {")); }); diff --git a/web/src/app/pattern-lab/panels/LayersPanel.module.css b/web/src/app/pattern-lab/panels/LayersPanel.module.css new file mode 100644 index 000000000..4b21cb0e9 --- /dev/null +++ b/web/src/app/pattern-lab/panels/LayersPanel.module.css @@ -0,0 +1,198 @@ +/* ── LayersPanel: its own classes ────────────────────────────────────────── + Moved out of LabPanels.module.css on 2026-10-25; only this file uses them. */ + +.layerList { + list-style: none; + margin: 0; + padding: 4px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.layerRow { + min-height: 34px; + border-radius: 4px; + display: flex; + align-items: center; + gap: 6px; + padding: 5px 6px; + border: 1px solid transparent; + background: #fbf7ee; + cursor: pointer; + user-select: none; +} + +.layerRow:hover { + border-color: rgba(23, 21, 18, 0.25); +} + +.layerRowActive { + background: #171512; + color: #f7f3ea; + border-color: #171512; +} + +.layerRow[data-dragover="true"] { + border-top: 2px solid #ff4d00; +} + +.layerEye { + appearance: none; + border: 0; + background: none; + color: inherit; + cursor: pointer; + font-size: 12px; + width: 20px; + padding: 0; + opacity: 0.85; +} + +.layerEyeOff { + opacity: 0.3; +} + +.layerName { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 12px; + font-weight: 600; +} + +.layerRenameInput { + flex: 1 1 auto; + min-width: 0; + border: 1px solid rgba(23, 21, 18, 0.4); + background: #fffdf8; + color: #171512; + font: inherit; + font-size: 12px; + padding: 1px 4px; +} + +.layerType { + font-family: var(--font-inter), system-ui, sans-serif; + font-size: 11px; + letter-spacing: 0.06em; + padding: 1px 5px; + border: 1px solid currentColor; + opacity: 0.65; + flex: 0 0 auto; +} + +.layerErrDot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #d63e2e; + flex: 0 0 auto; +} + +.layerMeta { + font-family: var(--font-inter), system-ui, sans-serif; + font-size: 11px; + opacity: 0.55; + flex: 0 0 auto; +} + +.layerControls { + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px; + border-top: 1px solid rgba(23, 21, 18, 0.12); + background: #fbf7ee; + flex: 0 0 auto; + font-family: var(--font-inter), system-ui, sans-serif; + font-size: 12px; +} + +.layerControlRow { + display: flex; + align-items: center; + gap: 8px; +} + +.layerControlRow > span { + width: 58px; + flex: 0 0 auto; + text-transform: none; + color: rgba(23, 21, 18, 0.6); + font-size: 11px; +} + +.layerControlRow input[type="range"] { + flex: 1 1 auto; + min-width: 0; + accent-color: #171512; +} + +.layerControlRow select { + min-height: 28px; + border-radius: 4px; + min-width: 0; + flex: 1 1 auto; + border: 1px solid rgba(23, 21, 18, 0.3); + background: #fffdf8; + font-family: inherit; + font-size: 12px; + padding: 2px 4px; +} + +.layerControlValue { + width: 40px; + text-align: right; + flex: 0 0 auto; + font-size: 11px; +} + +.layerMaskLabel { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + text-transform: none; + cursor: pointer; +} + +/* Touch screens get fingertip-sized targets, as in the Text panel. On a + 390px phone the bar buttons were 28px tall, Del 37px wide, the eye 20×17px + and the mask box 13×13px. The row grows to 46px so the eye can take its + whole height. `.layerBar button` beats `.panelBar button` in + LabPanels.module.css only by coming after it, which is why LayersPanel.tsx + imports that file before this one. */ +@media (hover: none) { + .layerBar button { + min-width: 44px; + min-height: 44px; + } + + .layerRow { + min-height: 46px; + padding-block: 0; + } + + .layerEye { + align-self: stretch; + width: 44px; + } + + .layerMaskLabel { + min-height: 44px; + } + + .layerControlRow input[type="range"], + .layerControlRow select { + min-height: 44px; + } +} + +/* Keyboard focus stays visible on the blend picker, as on the panel bars. */ +.layerControlRow select:focus-visible { + outline: 2px solid #9b4827; + outline-offset: 2px; +} diff --git a/web/src/app/pattern-lab/panels/LayersPanel.tsx b/web/src/app/pattern-lab/panels/LayersPanel.tsx index d1770ad35..f931fb763 100644 --- a/web/src/app/pattern-lab/panels/LayersPanel.tsx +++ b/web/src/app/pattern-lab/panels/LayersPanel.tsx @@ -10,6 +10,8 @@ import { revealEditor } from "@/lib/lab/editorReveal"; import { useLabStore } from "@/lib/lab/store"; import { STARTER_CODE } from "@/lib/lab/store/shared"; import dock from "../LabPanels.module.css"; +// Second, so its touch rule for the bar buttons lands after `.panelBar button`. +import local from "./LayersPanel.module.css"; // A pixel layer nobody has drawn on, a code layer still holding the starter // pattern, or a text layer that still says nothing, has nothing to lose, so @@ -57,7 +59,7 @@ export default function LayersPanel() { return (
-
+
-
    +
      {layers.map((layer, index) => { const isActive = layer.id === activeLayerId; const error = layerErrors[layer.id]; return (
    1. { @@ -171,7 +173,7 @@ export default function LayersPanel() { >
    2. ); @@ -227,11 +229,11 @@ export default function LayersPanel() {
{active && ( -
-
+
+
Mask