Add application feedback and refine headless canvas design boundaries

This commit is contained in:
2026-11-07 00:31:16 +08:00
parent 20d877a346
commit d0a9627cb6
12 changed files with 352 additions and 12 deletions
+18
View File
@@ -0,0 +1,18 @@
"use client";
import { createContext, type ReactNode } from "react";
import * as Tooltip from "@radix-ui/react-tooltip";
import { Toaster } from "sonner";
export const FeedbackContext = createContext(false);
export function AppFeedback({ children }: { children: ReactNode }) {
return <FeedbackContext.Provider value={true}>
<Tooltip.Provider delayDuration={400} skipDelayDuration={200}>
{children}
<Toaster position="bottom-right" closeButton visibleToasts={3} duration={4500}
offset={{ bottom: 24, right: 20 }} containerAriaLabel="操作通知"
toastOptions={{ closeButtonAriaLabel: "关闭通知", style: { background: "var(--popover, #fffcf9)", color: "var(--popover-foreground, #38281f)", border: "1px solid var(--border, #ded5cc)" } }} />
</Tooltip.Provider>
</FeedbackContext.Provider>;
}
+3
View File
@@ -17,3 +17,6 @@
.button:hover { background: #e9e7e3; }
.button:focus-visible { outline: 2px solid #9d7d66; outline-offset: 2px; }
.button:disabled { opacity: 0.45; cursor: not-allowed; }
.tooltip { z-index: 1000; max-width: 240px; padding: 6px 9px; border-radius: 6px; background: #49372e; color: #fffdf9; font-size: 12px; line-height: 1.45; box-shadow: 0 3px 12px #372f2420; user-select: none; }
.arrow { fill: #49372e; }
+13 -4
View File
@@ -1,6 +1,8 @@
"use client";
import { forwardRef, type ButtonHTMLAttributes } from "react";
import { FeedbackContext } from "./app-feedback";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useContext, forwardRef, type ButtonHTMLAttributes } from "react";
import type { LucideIcon } from "lucide-react";
import styles from "./icon-button.module.css";
@@ -10,18 +12,25 @@ export type IconButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "chi
};
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
function IconButton({ icon: Icon, label, className = "", type = "button", ...props }, ref) {
return (
function IconButton({ icon: Icon, label, title = label, className = "", type = "button", ...props }, ref) {
const feedback = useContext(FeedbackContext);
const button = (
<button
ref={ref}
type={type}
aria-label={label}
title={label}
title={feedback ? undefined : title}
className={`${styles.button} ${className}`}
{...props}
>
<Icon size={16} strokeWidth={1.6} aria-hidden="true" />
</button>
);
if (!feedback || !title) return button;
return <Tooltip.Root><Tooltip.Trigger asChild>{button}</Tooltip.Trigger><Tooltip.Portal>
<Tooltip.Content className={styles.tooltip} sideOffset={7} collisionPadding={8}>
{title}<Tooltip.Arrow className={styles.arrow} />
</Tooltip.Content>
</Tooltip.Portal></Tooltip.Root>;
},
);