refactor(extension): adopt shadcn Sonner component structure
This commit is contained in:
1 parent
143873b18a
commit
ab6353f44d
8 files changed
+117
-15
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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(<Toaster theme="light" position="bottom-right" offset={16} mobileOffset={16} visibleToasts={1} closeButton hotkey={[]} containerAriaLabel={payload.regionLabel} toastOptions={{ closeButtonAriaLabel: payload.closeLabel, style: { background: "#fff", color: "#334155", border: "1px solid #e2e8f0", pointerEvents: "auto" } }} />));
|
||||
flushSync(() => root.render(<Toaster containerAriaLabel={payload.regionLabel} toastOptions={{ closeButtonAriaLabel: payload.closeLabel }} />));
|
||||
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);
|
||||
|
||||
@@ -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.
|
||||
@@ -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 <Sonner
|
||||
theme={theme}
|
||||
position={position}
|
||||
offset={offset}
|
||||
mobileOffset={mobileOffset}
|
||||
visibleToasts={visibleToasts}
|
||||
closeButton={closeButton}
|
||||
hotkey={hotkey}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: <CircleCheckIcon size={16} className="size-4" aria-hidden="true" />,
|
||||
info: <InfoIcon size={16} className="size-4" aria-hidden="true" />,
|
||||
warning: <TriangleAlertIcon size={16} className="size-4" aria-hidden="true" />,
|
||||
error: <OctagonXIcon size={16} className="size-4" aria-hidden="true" />,
|
||||
loading: <Loader2Icon size={16} className="size-4 animate-spin motion-reduce:animate-none" aria-hidden="true" />,
|
||||
...icons,
|
||||
}}
|
||||
style={{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--border-radius": "var(--radius)",
|
||||
...style,
|
||||
} as CSSProperties}
|
||||
toastOptions={{ duration: notificationDuration.normal, ...toastOptions }}
|
||||
{...props}
|
||||
/>;
|
||||
}
|
||||
|
||||
export { toast } from "sonner";
|
||||
@@ -0,0 +1,10 @@
|
||||
/* Sonner provides layout, motion and interaction. Only theme tokens vary here. */
|
||||
.toaster [data-sonner-toast] {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.toaster [data-sonner-toast][data-type="error"] {
|
||||
--normal-bg: var(--notification-error-background);
|
||||
--normal-text: var(--notification-error-foreground);
|
||||
--normal-border: var(--notification-error-border);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/* One palette for extension pages and the Shadow DOM notification component. */
|
||||
:root,
|
||||
:host {
|
||||
--background: #f5f5f5;
|
||||
--foreground: #212121;
|
||||
--card: #fff;
|
||||
--primary: #171717;
|
||||
--primary-foreground: #fff;
|
||||
--muted: #f5f5f5;
|
||||
--muted-foreground: #666666;
|
||||
--popover: #fff;
|
||||
--popover-foreground: #334155;
|
||||
--border: #e0e0e0;
|
||||
--ring: #262626;
|
||||
--radius: 0.5rem;
|
||||
--notification-error-background: #fff1f2;
|
||||
--notification-error-foreground: #be123c;
|
||||
--notification-error-border: #ffe4e6;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
@import "tailwindcss" source(none);
|
||||
@import "./theme-tokens.css";
|
||||
@import "./notification.css";
|
||||
@source "./";
|
||||
@source "../../../web/src/components/ui/button.tsx";
|
||||
@source "../../../web/src/components/ui/input.tsx";
|
||||
@@ -6,15 +8,15 @@
|
||||
@source "../../../web/src/components/ui/tooltip.tsx";
|
||||
|
||||
@theme inline {
|
||||
--color-background: #f5f5f5;
|
||||
--color-foreground: #212121;
|
||||
--color-card: #fff;
|
||||
--color-primary: #171717;
|
||||
--color-primary-foreground: #fff;
|
||||
--color-muted: #f5f5f5;
|
||||
--color-muted-foreground: #666666;
|
||||
--color-border: #e0e0e0;
|
||||
--color-ring: #262626;
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-ring: var(--ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
@@ -21,7 +21,7 @@ export default defineConfig({
|
||||
tailwindcss(),
|
||||
{
|
||||
name: "edgeever-extension-manifest",
|
||||
async generateBundle() {
|
||||
async generateBundle(_options, bundle) {
|
||||
this.emitFile({
|
||||
type: "asset",
|
||||
fileName: "manifest.json",
|
||||
@@ -47,13 +47,20 @@ export default defineConfig({
|
||||
source: capture.outputFiles[0].contents,
|
||||
});
|
||||
}
|
||||
// Reuse the generated Tailwind stylesheet so the shadcn component and
|
||||
// its theme tokens behave identically in pages and in the shadow root.
|
||||
const themeStyles = Object.values(bundle)
|
||||
.filter((item) => item.type === "asset" && item.fileName.endsWith(".css"))
|
||||
.map((item) => item.type === "asset" ? typeof item.source === "string" ? item.source : new TextDecoder().decode(item.source) : "")
|
||||
.join("\n");
|
||||
if (!themeStyles) throw new Error("Missing compiled extension theme for shadow notifications");
|
||||
const feedback = await buildStandaloneScript({
|
||||
entryPoints: [fileURLToPath(new URL("./src/ui/clip-feedback.tsx", import.meta.url))],
|
||||
bundle: true, format: "iife", platform: "browser", target: "es2022", minify: true, write: false,
|
||||
jsx: "automatic",
|
||||
define: {
|
||||
"process.env.NODE_ENV": '"production"',
|
||||
__CLIP_FEEDBACK_CSS__: JSON.stringify(await readFile(require.resolve("sonner/dist/styles.css"), "utf8")),
|
||||
__CLIP_FEEDBACK_CSS__: JSON.stringify(`${await readFile(require.resolve("sonner/dist/styles.css"), "utf8")}\n${themeStyles}`),
|
||||
},
|
||||
plugins: [{
|
||||
name: "shadow-only-sonner-styles",
|
||||
|
||||
Reference in new issue
Block a user