From ab6353f44d2e0113036f7911e540aaa4d3c63c6e Mon Sep 17 00:00:00 2001 From: tianma-if Date: Mon, 12 Oct 2026 23:29:47 +0800 Subject: [PATCH] refactor(extension): adopt shadcn Sonner component structure --- apps/extension/README.md | 2 +- apps/extension/src/ui/clip-feedback.tsx | 6 +-- .../src/ui/components/sonner.LICENSE | 21 +++++++++ apps/extension/src/ui/components/sonner.tsx | 43 +++++++++++++++++++ apps/extension/src/ui/notification.css | 10 +++++ apps/extension/src/ui/theme-tokens.css | 19 ++++++++ apps/extension/src/ui/theme.css | 20 +++++---- apps/extension/vite.config.ts | 11 ++++- 8 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 apps/extension/src/ui/components/sonner.LICENSE create mode 100644 apps/extension/src/ui/components/sonner.tsx create mode 100644 apps/extension/src/ui/notification.css create mode 100644 apps/extension/src/ui/theme-tokens.css diff --git a/apps/extension/README.md b/apps/extension/README.md index b5ac15df..4420751d 100644 --- a/apps/extension/README.md +++ b/apps/extension/README.md @@ -29,7 +29,7 @@ HN usage and verification limits are documented in [English](../../docs/browser- ## UI architecture -Extension pages use React, shared shadcn/ui-style Button and Input components, and Tailwind CSS 4 with a neutral theme. The webpage feedback UI uses Sonner inside a Shadow DOM, bundled as a standalone classic script. Its stylesheet stays inside the shadow root; image readers and content extractors do not load React. Permission requests remain in click handlers. +Extension pages use React, shared shadcn/ui-style Button and Input components, and Tailwind CSS 4 with a neutral theme. The webpage feedback UI uses the official shadcn/ui Sonner component structure, adapted for an explicit extension theme, inside a Shadow DOM. Extension pages and notifications share theme tokens. The feedback entry only mounts the component and maps clipping states, and is bundled as a standalone classic script. Its stylesheet stays inside the shadow root; image readers and content extractors do not load React. Permission requests remain in click handlers. ## Localization diff --git a/apps/extension/src/ui/clip-feedback.tsx b/apps/extension/src/ui/clip-feedback.tsx index 656150d5..8e9810bd 100644 --- a/apps/extension/src/ui/clip-feedback.tsx +++ b/apps/extension/src/ui/clip-feedback.tsx @@ -1,6 +1,6 @@ import { createRoot } from "react-dom/client"; import { flushSync } from "react-dom"; -import { Toaster, toast } from "sonner"; +import { Toaster, toast, notificationDuration } from "./components/sonner"; // Re-injection reuses the mounted component and its Sonner store. Keep both the // stylesheet and React tree inside the shadow root, away from the site's CSS. @@ -26,11 +26,11 @@ if (payload?.action === "toast") { shadow.append(style, mount); (document.documentElement ?? document.body).appendChild(host); const root = createRoot(mount); - flushSync(() => root.render()); + flushSync(() => root.render()); service = { host, show(message, kind) { - const options = { id: "edgeever-clip", duration: kind === "error" ? 8000 : 4000, style: { background: kind === "error" ? "#fff1f2" : "#fff", color: kind === "error" ? "#be123c" : "#334155", border: kind === "error" ? "1px solid #ffe4e6" : "1px solid #e2e8f0", pointerEvents: "auto" as const } }; + const options = { id: "edgeever-clip", duration: notificationDuration[kind] }; if (kind === "error") toast.error(message, options); else if (kind === "success") toast.success(message, options); else toast(message, options); diff --git a/apps/extension/src/ui/components/sonner.LICENSE b/apps/extension/src/ui/components/sonner.LICENSE new file mode 100644 index 00000000..fad4d887 --- /dev/null +++ b/apps/extension/src/ui/components/sonner.LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 shadcn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/extension/src/ui/components/sonner.tsx b/apps/extension/src/ui/components/sonner.tsx new file mode 100644 index 00000000..2d262440 --- /dev/null +++ b/apps/extension/src/ui/components/sonner.tsx @@ -0,0 +1,43 @@ +// Adapted from shadcn/ui's official new-york-v4 Sonner component (MIT): +// https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/sonner.tsx +// Extension pages provide their theme explicitly rather than using next-themes. +import type { CSSProperties } from "react"; +import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon } from "lucide-react"; +import { Toaster as Sonner, type ToasterProps } from "sonner"; + +export const notificationDuration = { normal: 4000, success: 4000, error: 8000 } as const; + +export function Toaster({ + theme = "light", position = "bottom-right", offset = 16, mobileOffset = 16, + visibleToasts = 1, closeButton = true, hotkey = [], icons, style, toastOptions, ...props +}: ToasterProps) { + return