From 6d8619b60c1de4b8997ecda050d2f4ef0918151f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 02:22:52 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20=EB=B0=94?= =?UTF-8?q?=ED=80=B4=2052?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UMTjuL6Y1wvEEdfktCv6MT From 88c455af6d463b57f63b690e4028584957bcbc4b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 02:26:27 +0000 Subject: [PATCH 2/4] web: say Copy failed when the browser refuses a copy in the Lab and the Live Editor The Lab's Copy prompt and To hardware buttons kept their label on a refused write, and the Live Editor said the prompt was copied before the write ended. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UMTjuL6Y1wvEEdfktCv6MT --- .../app/pattern-lab/HardwareModal.test.tsx | 23 ++++++ web/src/app/pattern-lab/HardwareModal.tsx | 17 ++-- .../app/pattern-lab/panels/CodePanel.test.tsx | 77 +++++++++++++++++++ web/src/app/pattern-lab/panels/CodePanel.tsx | 18 +++-- .../components/sections/PatternPanel.test.tsx | 47 ++++++++++- web/src/components/sections/PatternPanel.tsx | 25 ++++-- 6 files changed, 186 insertions(+), 21 deletions(-) create mode 100644 web/src/app/pattern-lab/panels/CodePanel.test.tsx diff --git a/web/src/app/pattern-lab/HardwareModal.test.tsx b/web/src/app/pattern-lab/HardwareModal.test.tsx index 6a86a56a0..eb4db6989 100644 --- a/web/src/app/pattern-lab/HardwareModal.test.tsx +++ b/web/src/app/pattern-lab/HardwareModal.test.tsx @@ -132,6 +132,29 @@ describe("HardwareModal", () => { expectHeaderKept(); }); + // The prompt goes to a model by paste. When the browser refuses the write, + // the label used to stay as it was, and the paste carried whatever the + // clipboard held before. + it("says Copy failed on the button whose copy the browser refused", async () => { + const writeText = vi.fn<(text: string) => Promise>(() => + Promise.reject(new DOMException("Write permission denied.", "NotAllowedError")), + ); + Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText } }); + try { + render(); + fireEvent.click(screen.getByText("Copy the conversion prompt")); + expect(await screen.findByText("Copy failed")).toBeTruthy(); + expect(screen.getByText("Copy .h")).toBeTruthy(); + + writeText.mockImplementation(() => Promise.resolve()); + fireEvent.click(screen.getByText("Copy failed")); + expect(await screen.findByText("Copied ✓")).toBeTruthy(); + expect(writeText).toHaveBeenCalledTimes(2); + } finally { + delete (navigator as { clipboard?: unknown }).clipboard; + } + }); + it("Esc over the Publish modal closes Publish alone and keeps the header", async () => { render(); pasteHeaderAndContinue(); diff --git a/web/src/app/pattern-lab/HardwareModal.tsx b/web/src/app/pattern-lab/HardwareModal.tsx index d0b9a1732..dda5ada6a 100644 --- a/web/src/app/pattern-lab/HardwareModal.tsx +++ b/web/src/app/pattern-lab/HardwareModal.tsx @@ -49,7 +49,7 @@ export default function HardwareModal({ const [name, setName] = useState(() => labPatternName(useLabStore.getState())); const [pastes, setPastes] = useState>({}); const [single, setSingle] = useState(""); - const [copiedKey, flashCopied] = useResettingState(null, 1200); + const [copied, flashCopied] = useResettingState<{ key: string; ok: boolean } | null>(null, 1200); const [stage, setStage] = useState("convert"); const [sendOpen, setSendOpen] = useState(false); @@ -110,12 +110,17 @@ export default function HardwareModal({ const copyText = async (key: string, text: string) => { try { await navigator.clipboard.writeText(text); - flashCopied(key); + flashCopied({ key, ok: true }); } catch { - /* clipboard may be blocked */ + // The browser refused the write. Saying nothing left the label as it + // was, and whatever the clipboard held before went to the model. + flashCopied({ key, ok: false }); } }; + const copyLabel = (key: string, idle: string) => + copied?.key === key ? (copied.ok ? "Copied ✓" : "Copy failed") : idle; + const downloadH = () => { const blob = new Blob([assembled], { type: "text/plain" }); const url = URL.createObjectURL(blob); @@ -217,7 +222,7 @@ export default function HardwareModal({ type="button" onClick={() => void copyText(`u${unit.index}`, unit.prompt)} > - {copiedKey === `u${unit.index}` ? "Copied ✓" : "Copy prompt"} + {copyLabel(`u${unit.index}`, "Copy prompt")}