Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions web/src/app/pattern-lab/panels/CodePanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<CodePanel />);

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(<CodePanel />);

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(<CodePanel />);
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(<CodePanel />);
expect(
screen.getByText("The selected layer is pixel art. Edit it in the Pixel panel, or select a code layer."),
).toBeTruthy();
});
});
15 changes: 10 additions & 5 deletions web/src/app/pattern-lab/panels/CodePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,21 @@ 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";
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<Exclude<Layer["type"], "code">, 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);
Expand Down Expand Up @@ -67,9 +74,7 @@ export default function CodePanel() {
return (
<div className={dock.panel}>
<div className={dock.panelHint}>
{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."}
<button type="button" onClick={addCodeLayer}>
+ Add code layer
</button>
Expand Down Expand Up @@ -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);
}
Expand Down
18 changes: 18 additions & 0 deletions web/src/app/pattern-lab/panels/PixelPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<PixelPanel />);
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(<PixelPanel />);
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;
Expand Down
13 changes: 9 additions & 4 deletions web/src/app/pattern-lab/panels/PixelPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<Exclude<Layer["type"], "pixel">, 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);
Expand Down Expand Up @@ -126,9 +133,7 @@ export default function PixelPanel() {
return (
<div className={dock.panel}>
<div className={dock.panelHint}>
{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."}
<button type="button" onClick={addPixelLayer}>
+ Add pixel layer
</button>
Expand Down
Loading