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."}