diff --git a/web/src/app/pattern-lab/panels/CodePanel.test.tsx b/web/src/app/pattern-lab/panels/CodePanel.test.tsx index f31a0d203..2c8d8cac5 100644 --- a/web/src/app/pattern-lab/panels/CodePanel.test.tsx +++ b/web/src/app/pattern-lab/panels/CodePanel.test.tsx @@ -170,3 +170,62 @@ describe("CodePanel @matrix line", () => { expect(useLabStore.getState().matrix).toEqual({ width: 64, height: 64 }); }); }); + +// Paste falls back to a box when the browser won't hand over the clipboard, +// and the Make guide sends people to that box. Until 24 October 2026 it asked +// in Korean on an English page. +describe("CodePanel paste", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("asks in English when the clipboard can't be read, and pastes what goes in the box", async () => { + setClipboard({ + readText: () => Promise.reject(new DOMException("Read permission denied.", "NotAllowedError")), + }); + const prompt = vi.spyOn(window, "prompt").mockReturnValue("export function draw(display) { /* boxed */ }\n"); + render(); + + fireEvent.click(screen.getByText("Paste")); + await act(async () => {}); + + expect(prompt).toHaveBeenCalledWith("Paste the pattern code here and press OK."); + const state = useLabStore.getState(); + const active = state.layers.find((layer) => layer.id === state.activeLayerId); + expect(active?.type === "code" && active.code).toContain("/* boxed */"); + expect(screen.getByText("Pasted ✓")).toBeTruthy(); + }); + + it("leaves the layer alone when the box is cancelled", async () => { + setClipboard({ readText: () => Promise.reject(new Error("no clipboard")) }); + vi.spyOn(window, "prompt").mockReturnValue(null); + const before = useLabStore.getState().layers; + render(); + + fireEvent.click(screen.getByText("Paste")); + await act(async () => {}); + + expect(useLabStore.getState().layers).toBe(before); + expect(screen.getByText("Paste")).toBeTruthy(); + }); +}); + +// The hint named every other layer pixel art, written before text layers +// came in (#184). A selected text layer was told to use the Pixel panel. +describe("CodePanel on another layer", () => { + it("names a text layer and sends it to the Text panel", () => { + useLabStore.getState().addTextLayer(); + render(); + expect( + screen.getByText("The selected layer is text. Edit it in the Text panel, or select a code layer."), + ).toBeTruthy(); + }); + + it("names a pixel layer and sends it to the Pixel panel", () => { + useLabStore.getState().addPixelLayer(); + render(); + expect( + screen.getByText("The selected layer is pixel art. Edit it in the Pixel panel, or select a code layer."), + ).toBeTruthy(); + }); +}); diff --git a/web/src/app/pattern-lab/panels/CodePanel.tsx b/web/src/app/pattern-lab/panels/CodePanel.tsx index dea2441f9..8f5b915aa 100644 --- a/web/src/app/pattern-lab/panels/CodePanel.tsx +++ b/web/src/app/pattern-lab/panels/CodePanel.tsx @@ -20,7 +20,7 @@ import { buildVariantCopyPrompt } from "@/lib/ai/variantPrompt"; import { useResettingState } from "@/lib/hooks/useResettingState"; import { hasStackAnnotation, importCodeIntoLab } from "@/lib/lab/stackShare"; import { useActiveLayer, useLabStore } from "@/lib/lab/store"; -import { isCodeLayer } from "@/lib/lab/types"; +import { isCodeLayer, type Layer } from "@/lib/lab/types"; import styles from "../PatternLab.module.css"; import local from "./CodePanel.module.css"; import dock from "../LabPanels.module.css"; @@ -28,6 +28,13 @@ import CodeGuideModal from "./CodeGuideModal"; const labPresets = livePresets.filter((preset) => preset.labOnly); +// What the panel says when another kind of layer is selected. Keyed by type, +// so a new layer type has to say where it is edited. +const OTHER_LAYER_HINT: Record, string> = { + pixel: "The selected layer is pixel art. Edit it in the Pixel panel, or select a code layer.", + text: "The selected layer is text. Edit it in the Text panel, or select a code layer.", +}; + export default function CodePanel() { const layer = useActiveLayer(); const matrix = useLabStore((state) => state.matrix); @@ -67,9 +74,7 @@ export default function CodePanel() { return (
- {layer - ? "The selected layer is pixel art — use the Pixel panel to edit it, or select a code layer." - : "Select a layer to edit."} + {layer ? OTHER_LAYER_HINT[layer.type] : "Select a layer to edit."} @@ -133,7 +138,7 @@ export default function CodePanel() { } catch { // Permission blocked or unsupported in WebView — fall back to window.prompt } - const fallbackText = window.prompt("붙여넣을 패턴 코드를 아래 상자에 입력하거나 붙여넣어 주세요:"); + const fallbackText = window.prompt("Paste the pattern code here and press OK."); if (fallbackText !== null) { applyPastedCode(fallbackText); } diff --git a/web/src/app/pattern-lab/panels/PixelPanel.test.tsx b/web/src/app/pattern-lab/panels/PixelPanel.test.tsx index af3e06369..e6514806a 100644 --- a/web/src/app/pattern-lab/panels/PixelPanel.test.tsx +++ b/web/src/app/pattern-lab/panels/PixelPanel.test.tsx @@ -58,6 +58,24 @@ describe("PixelPanel", () => { expect(useLabStore.getState().layers.filter((entry) => entry.type === "pixel")).toHaveLength(2); }); + // The hint called every other layer a code pattern, written before text + // layers came in (#184). + it("names the selected layer's kind and the panel that edits it", () => { + const state = useLabStore.getState(); + state.selectLayer(state.layers.find((entry) => entry.type === "code")!.id); + const { unmount } = render(); + expect( + screen.getByText("The selected layer is a code pattern. Edit it in the Code panel, or select a pixel layer to draw."), + ).toBeTruthy(); + unmount(); + + useLabStore.getState().addTextLayer(); + render(); + expect( + screen.getByText("The selected layer is text. Edit it in the Text panel, or select a pixel layer to draw."), + ).toBeTruthy(); + }); + it("pen: a press paints a dot, a drag paints a line, release commits", () => { const { overlay } = mount(); const before = pixelLayer().rev; diff --git a/web/src/app/pattern-lab/panels/PixelPanel.tsx b/web/src/app/pattern-lab/panels/PixelPanel.tsx index 0d2636449..ec6f8708b 100644 --- a/web/src/app/pattern-lab/panels/PixelPanel.tsx +++ b/web/src/app/pattern-lab/panels/PixelPanel.tsx @@ -17,7 +17,7 @@ import { clearSurface, stampOver, type PixelSurface } from "@/lib/lab/pixelTools import { prunePixelHistories, redoPixels, snapshotPixels, undoPixels } from "@/lib/lab/pixelHistory"; import { buildPixelPatternCode } from "@/lib/lab/pixelToCode"; import { useActiveLayer, useLabStore } from "@/lib/lab/store"; -import { isPixelLayer } from "@/lib/lab/types"; +import { isPixelLayer, type Layer } from "@/lib/lab/types"; import ImportImageModal from "./ImportImageModal"; import { PixelActionBar, PixelToolBar, type CodeCopyState } from "./pixel/PixelToolbars"; import { idleStroke, type Tool } from "./pixel/tools"; @@ -29,6 +29,13 @@ import { usePixelToolState } from "./pixel/usePixelToolState"; import { usePixelViewport, ZOOM_MIN } from "./pixel/usePixelViewport"; import dock from "../LabPanels.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. +const OTHER_LAYER_HINT: Record, string> = { + code: "The selected layer is a code pattern. Edit it in the Code panel, or select a pixel layer to draw.", + text: "The selected layer is text. Edit it in the Text panel, or select a pixel layer to draw.", +}; + export default function PixelPanel() { const layer = useActiveLayer(); const bumpPixelLayer = useLabStore((state) => state.bumpPixelLayer); @@ -126,9 +133,7 @@ export default function PixelPanel() { return (
- {layer - ? "The selected layer is a code pattern — select a pixel layer to draw, or add one." - : "Select a pixel layer to draw."} + {layer && !isPixelLayer(layer) ? OTHER_LAYER_HINT[layer.type] : "Select a pixel layer to draw."}