diff --git a/apps/web/src/components/ClipboardCopyNotice.tsx b/apps/web/src/components/ClipboardCopyNotice.tsx new file mode 100644 index 00000000..ad1ed565 --- /dev/null +++ b/apps/web/src/components/ClipboardCopyNotice.tsx @@ -0,0 +1,26 @@ +import type { ReactNode } from "react"; +import { createPortal } from "react-dom"; +import { cn } from "@/lib/utils"; + +export const ClipboardCopyNotice = ({ + children, + status, +}: { + children: ReactNode; + status: "copied" | "error"; +}) => { + if (typeof document === "undefined") return null; + + return createPortal( +
+ {children} +
, + document.body, + ); +}; diff --git a/apps/web/src/components/EditorPane.tsx b/apps/web/src/components/EditorPane.tsx index a41ebed7..edac838f 100644 --- a/apps/web/src/components/EditorPane.tsx +++ b/apps/web/src/components/EditorPane.tsx @@ -42,6 +42,7 @@ import { } from "lucide-react"; import { Button } from "@/components/ui/button"; import { GitHubRepositoryLink } from "@/components/GitHubRepositoryLink"; +import { ClipboardCopyNotice } from "@/components/ClipboardCopyNotice"; import { Input } from "@/components/ui/input"; import { Command, @@ -4273,15 +4274,9 @@ const RichEditorPane = ({ )} {memoIdCopyNotice && ( -
+ {t(memoIdCopyNotice.status === "copied" ? "editor.noteIdCopied" : "editor.noteIdCopyFailed", { id: memoIdCopyNotice.id })} -
+ )} {false && useMobilePlainTextEditor && ( diff --git a/apps/web/src/components/MemoListPane.tsx b/apps/web/src/components/MemoListPane.tsx index 787c4620..bd3d8557 100644 --- a/apps/web/src/components/MemoListPane.tsx +++ b/apps/web/src/components/MemoListPane.tsx @@ -58,6 +58,7 @@ import { } from "@/components/ui/dropdown-menu"; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; import { MemoCard } from "./MemoCard"; +import { ClipboardCopyNotice } from "./ClipboardCopyNotice"; import { cn } from "@/lib/utils"; import { WORKSPACE_PAGE_TITLE_CLASSNAME } from "@/lib/workspace-ui"; import type { Notebook, MemoSummary } from "@edgeever/shared"; @@ -1608,15 +1609,9 @@ export const MemoListPane = ({ ) : null} {memoIdCopyNotice && ( -
+ {t(memoIdCopyNotice.status === "copied" ? "editor.noteIdCopied" : "editor.noteIdCopyFailed", { id: memoIdCopyNotice.id })} -
+ )} {selectionMode && ( diff --git a/apps/web/src/components/TemplatesPane.tsx b/apps/web/src/components/TemplatesPane.tsx index 2c7e02b4..adfa15a3 100644 --- a/apps/web/src/components/TemplatesPane.tsx +++ b/apps/web/src/components/TemplatesPane.tsx @@ -22,6 +22,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { WORKSPACE_PAGE_TITLE_CLASSNAME } from "@/lib/workspace-ui"; import { copyTextToClipboard } from "@/lib/clipboard"; +import { ClipboardCopyNotice } from "@/components/ClipboardCopyNotice"; import type { MemoTemplate as SavedMemoTemplate } from "@edgeever/shared"; const TemplateIconAction = ({ @@ -543,17 +544,12 @@ export const TemplatesPane = ({ )} {templateIdCopyNotice && ( -
+ {t( templateIdCopyNotice.status === "copied" ? "templates.idCopied" : "templates.idCopyFailed", { id: templateIdCopyNotice.id }, )} -
+ )} diff --git a/tests/note-id-copy.test.ts b/tests/note-id-copy.test.ts index f6a4cd86..fc520497 100644 --- a/tests/note-id-copy.test.ts +++ b/tests/note-id-copy.test.ts @@ -4,6 +4,8 @@ import { readFileSync } from "node:fs"; const readSource = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8"); const webEditorSource = readSource("../apps/web/src/components/EditorPane.tsx"); +const webMemoListSource = readSource("../apps/web/src/components/MemoListPane.tsx"); +const webCopyNoticeSource = readSource("../apps/web/src/components/ClipboardCopyNotice.tsx"); const androidDetailSource = readSource("../apps/mobile/src/screens/WorkspaceMemoDetail.tsx"); const iosDetailSource = readSource("../apps/ios/EdgeEver/Features/Workspace/MemoDetailView.swift"); @@ -13,6 +15,13 @@ describe("copy current note ID", () => { expect(webEditorSource).toContain('"editor.copyNoteId"'); }); + test("renders copy feedback at the page root so panes cannot clip it", () => { + expect(webMemoListSource).toContain(" { expect(androidDetailSource).toContain("Clipboard.setStringAsync(memo.id)"); expect(androidDetailSource).toContain('label={canCopyMemoId ? "复制笔记 ID"'); diff --git a/tests/template-id-copy.test.ts b/tests/template-id-copy.test.ts index eaf1e758..5f3794a6 100644 --- a/tests/template-id-copy.test.ts +++ b/tests/template-id-copy.test.ts @@ -11,5 +11,6 @@ describe("copy note template ID", () => { expect(templatesPaneSource).toContain("copyTextToClipboard(template.id)"); expect(templatesPaneSource).toContain('label={t("templates.copyId")}'); expect(templatesPaneSource).toContain('"templates.idCopied"'); + expect(templatesPaneSource).toContain("