docs(ui): explain shared controls and canvas display adapters
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
/**
|
||||
* 公用品牌首页链接。组合Logo、中文名与英文名,供登录页、落地页等位置复用;不读取用户或工作区状态。
|
||||
*/
|
||||
import Link from "next/link";
|
||||
/** light选择brand-light展示样式以及配套Logo资源:true用logo-dark.svg,false用logo-light.svg。
|
||||
* 链接的aria-label已经说明品牌与目的地,图片使用空alt避免重复朗读;固定尺寸减少加载时布局跳动。 */
|
||||
export function Brand({ light = false }: { light?: boolean }) {
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
/**
|
||||
* 按名称异步加载画布SVG图标。先读取共享缓存,再订阅图标加载器,展示加载中/失败/已就绪三种图形。
|
||||
* 组件负责显示和重试入口;网络并发、缓存以及订阅取消的实现位于canvas-icon-loader。
|
||||
*/
|
||||
"use client";
|
||||
import { createElement, useEffect, useState } from "react";
|
||||
import type { LucideProps } from "lucide-react";
|
||||
import { loadIcon, peekIcon, type IconPaths } from "@/lib/canvas-icon-loader";
|
||||
|
||||
/** 兼容Lucide尺寸与描边属性,name默认Star;width/height未提供时跟随size。
|
||||
* 成功后按加载器返回的SVG标签与属性创建子元素,避免把整段SVG作为HTML插入。 */
|
||||
export function CanvasIcon({ name = "Star", size = 24, width = size, height = size, strokeWidth = 2, absoluteStrokeWidth, ...props }: LucideProps & { name?: string }) {
|
||||
// 结果携带图标名,渲染时只采用当前name对应的内容,换名时不会短暂展示上一个图标。
|
||||
const [result, setResult] = useState<{ name: string; paths?: IconPaths; error?: boolean }>(() => ({ name, paths: peekIcon(name) }));
|
||||
// retry仅用于触发同名图标重新订阅;它不是全局缓存版本或网络请求ID。
|
||||
const [retry, setRetry] = useState(0);
|
||||
// 换名或点击重试后,先采用可同步取得的缓存,再等待loadIcon。清理时取消本组件订阅;
|
||||
// AbortError不显示失败,其它错误显示重试占位。底层决定共享请求是否仍有其它使用者。
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setResult({ name, paths: peekIcon(name) });
|
||||
@@ -14,8 +24,12 @@ export function CanvasIcon({ name = "Star", size = 24, width = size, height = si
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [name, retry]);
|
||||
// effect可能还没处理新name,故读取结果前再次比对名称;失败标记也必须属于当前图标。
|
||||
const paths = result.name === name ? result.paths : undefined;
|
||||
const failed = result.name === name && result.error;
|
||||
// 以24×24为标准坐标;absoluteStrokeWidth在size为数值时按尺寸反向补偿描边宽度。
|
||||
// 成功时保留调用方onClick;失败时点击改为停止冒泡并重试,避免同时触发外层节点点击。
|
||||
// 加载中用虚线方框,失败用叉号与title,data-icon-state供样式或检查观察显示状态。
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={width} height={height} fill="none" stroke="currentColor" strokeWidth={absoluteStrokeWidth && typeof size === "number" ? Number(strokeWidth) * 24 / size : strokeWidth} strokeLinecap="round" strokeLinejoin="round"
|
||||
{...props} data-icon-id={name} data-icon-state={paths ? "loaded" : failed ? "error" : "loading"}
|
||||
onClick={failed ? (event) => { event.stopPropagation(); setRetry((value) => value + 1); } : props.onClick}>
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
/**
|
||||
* 画布图片显示层:远端图片异步加载时,允许用Provider提供的本地预览覆盖,减轻上传或资源切换时的空白。
|
||||
* 组件不上传文件、不管理Blob URL寿命;这些职责留给提供sources与loaded回调的上层。
|
||||
*/
|
||||
"use client";
|
||||
import { createContext, useContext } from "react";
|
||||
/** 按元素id查找预览URL,并在底层图片加载成功时通知上层;默认空Map与空回调支持独立渲染。 */
|
||||
export const ImagePreviews = createContext<{ sources: Map<string,string>; loaded: (id: string) => void }>({ sources: new Map(), loaded: () => {} });
|
||||
/** src用于底层正式图片,title作为其替代文本;图片禁止原生拖拽,避免干扰画布手势。
|
||||
* 底图使用懒加载、异步解码并禁止发送Referer;onLoad只通知上层,不直接删除预览。 */
|
||||
export function CanvasImage({ id, src, title, className }: { id: string; src?: string; title: string; className: string }) {
|
||||
// 本地源按元素id而非远端URL索引,同一资源在不同节点中可拥有各自的显示交接状态。
|
||||
const previews = useContext(ImagePreviews), local = previews.sources.get(id);
|
||||
const remote = <img loading="lazy" decoding="async" className={className} src={src} alt={title} draggable={false} referrerPolicy="no-referrer" onLoad={() => previews.loaded(id)} />;
|
||||
// 没有预览或预览已等于正式源时,只绘制一张图片,避免冗余叠层。
|
||||
if (!local || local === src) return remote;
|
||||
// 正式图片继续在底层加载,本地预览绝对定位覆盖在上方;Provider清掉该id后自然移除覆盖。
|
||||
// 覆盖图是装饰性的、忽略指针事件,读屏与节点交互都交给底层正式图片和外层节点。
|
||||
return <div style={{position:"relative",width:"100%",height:"100%",minHeight:0,flex:1,borderRadius:"inherit"}}>{remote}<img className={className} src={local} alt="" aria-hidden draggable={false} style={{position:"absolute",inset:0,pointerEvents:"none"}} /></div>;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* 内置画布类型到React组件的装配表。把bindings层的renderer描述绑定到实际节点/连线组件,生成默认React渲染注册表。
|
||||
* 这张表保存可复用的显示定义,不保存每个元素的数据、选择或编辑状态。
|
||||
*/
|
||||
"use client";
|
||||
import type { NodeProps } from '@xyflow/react';
|
||||
import type { CanvasNode } from '@/lib/canvas-elements';
|
||||
@@ -9,6 +13,12 @@ import { PlainImageNode } from './plain-image-node';
|
||||
import { FrameNode } from './frame-node';
|
||||
import { IconNode } from './icon-node';
|
||||
import { CanvasEdge } from './canvas-edge';
|
||||
/** 新版分区复用FrameNode,但开启directAnchors,使它使用该模式的直接连接点。
|
||||
* 其它NodeProps透传,包装层不再创建一份分区模型。 */
|
||||
function HeadlessFrameNode(props:NodeProps<CanvasNode>){return <FrameNode {...props} directAnchors />;}
|
||||
/** rendererId到节点组件的固定映射;这里的键与builtinRendererDefinitions中节点的rendererId配套。 */
|
||||
const nodes={material:MaterialNode,plainText:PlainTextNode,plainImage:PlainImageNode,frame:HeadlessFrameNode,icon:IconNode};
|
||||
/** 每项保留原始类型/结构描述,node按rendererId查表,connection绑定CanvasEdge。
|
||||
* render与preview目前共用同一个组件;createReactRenderers负责生成供ReactFlow使用的注册结果。
|
||||
* 本表没有自定义类型的通配组件,兼容占位由投影/渲染选择链路处理。 */
|
||||
export const defaultCanvasRenderers=createReactRenderers(builtinRendererDefinitions.map(definition=>{const render=definition.structure==='node'?nodes[definition.rendererId as keyof typeof nodes]:CanvasEdge;return{...definition,render,preview:render} as ReactRendererDefinition;}));
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
/**
|
||||
* 兼容占位组件:显示可理解的公共几何与说明文字,不解析未知类型的业务payload。
|
||||
* 原始数据由模型保留;这些组件只提供占位绘制,不执行数据迁移,也不自行决定外层是否允许移动或选择。
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { BaseEdge, Handle, Position, getBezierPath, type EdgeProps, type NodeProps } from "@xyflow/react";
|
||||
import type {ReactFlowProjectionNode as ProjectionNode} from "@/lib/canvas-bindings/react-flow";
|
||||
import type { CanvasEdgeModel } from "@/lib/canvas-v2";
|
||||
import styles from "./canvas.module.css";
|
||||
/** 将投影提供的四边方位转换成ReactFlow Handle要求的枚举。 */
|
||||
const positions={top:Position.Top,right:Position.Right,bottom:Position.Bottom,left:Position.Left};
|
||||
|
||||
/** Render only the known envelope and derived endpoints, never interpret opaque payload fields. */
|
||||
/** 使用投影已提供的标题、旋转和fallback元数据绘制占位节点,尺寸填满外层节点容器。
|
||||
* renderer-unavailable明确表示当前缺少显示实现,其它情形提示原内容保留。
|
||||
* 仍绘制已知连接点供现有连线定位,但禁止从这些Handle发起或接收新连接;不推断未知内容字段。 */
|
||||
export function UnsupportedNode({data,selected}:NodeProps<ProjectionNode>){
|
||||
// fallback来自bindings投影;缺失时仍显示通用提示,不尝试从data猜测组件结构。
|
||||
const fallback=data.__headlessFallback;
|
||||
return <div className={`${styles.node} ${selected?styles.selectedNode:""}`} style={{width:"100%",height:"100%",padding:14,transform:`rotate(${data.rotation||0}deg)`}} role="note" aria-label="暂不支持编辑的元素">
|
||||
<span>{data.title}</span><small style={{display:"block"}}>此元素需要兼容的组件版本才能编辑</small>
|
||||
@@ -15,6 +24,8 @@ export function UnsupportedNode({data,selected}:NodeProps<ProjectionNode>){
|
||||
{fallback?.handles.map(handle=><Handle key={handle.id} id={handle.id} type="source" position={positions[handle.side]} className={styles.centerHandle} isConnectable={false} isConnectableStart={false} isConnectableEnd={false} aria-hidden />)}
|
||||
</div>;
|
||||
}
|
||||
/** 直接使用ReactFlow推导的端点生成贝塞尔路径,附可访问标题;不调用未知类型的编辑/样式逻辑。
|
||||
* BaseEdge绘制统一颜色的可见线条,数据保留与版本兼容仍归模型层负责。 */
|
||||
export function UnsupportedEdge(props:EdgeProps<CanvasEdgeModel>){
|
||||
const [path]=getBezierPath(props);
|
||||
return <g role="img" aria-label="暂不支持编辑的连线"><title>原始连线内容已保留</title><BaseEdge id={props.id} path={path} style={{stroke:'#aa968b',strokeWidth:1.5}} /></g>;
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* 按既有静态资源命名约定展示仪表盘图标的小封装,不负责画布图标库的分片加载或搜索。
|
||||
*/
|
||||
/** name是调用方提供的资源后缀,拼成/assets/dashboardcontext-imgIcon{name}.svg。
|
||||
* size同时设置宽高,默认18px;空alt表示装饰用途,承载动作的外层控件须另有可访问名称。
|
||||
* 此处没有资源白名单校验或加载失败占位,调用方应使用项目已有的图标名。 */
|
||||
export function Icon({ name, size = 18 }: { name: string; size?: number }) {
|
||||
return (
|
||||
<img
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* 公开落地页的客户端展示组件。包含品牌导航、可点击的创作示意图、功能说明、工作方式、FAQ与登录/演示入口。
|
||||
* 示意卡片只改变本页高亮,不创建真实画布元素;本模块不读取账号、不调用编辑器,也不持久化状态。
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
@@ -6,7 +10,9 @@ import { ArrowUpRight, ArrowRight, ChevronDown, FileText, Image as ImageIcon, La
|
||||
import { Brand } from "@/components/brand";
|
||||
import styles from "./landing-page.module.css";
|
||||
|
||||
/** 三个示意卡片对应的页脚说明;数组索引与active及预览按钮一一对应。 */
|
||||
const cards = ["先留住一个念头", "让不同的线索相遇", "慢慢成为你的作品"];
|
||||
/** FAQ静态问答数据;这里只呈现文案,访问权限与演示存储的实际执行在对应业务模块中。 */
|
||||
const questions = [
|
||||
["还没想好要做什么,也可以开始吗?", "当然。一个词、一张参考图、一段还不成形的想法,都可以成为起点。先放下来,再慢慢找到它们之间的联系。"],
|
||||
["我可以放些什么在画布里?", "文字、笔记、图片、脚本和图标。你可以自由移动、调整样式、用线连接,也可以用分区把相关内容放在一起。"],
|
||||
@@ -14,9 +20,17 @@ const questions = [
|
||||
["可以先体验,再注册吗?", "可以。点击「先逛逛工作台」进入本地演示。演示内容保留在当前浏览器,登录后可以建立自己的工作空间。"],
|
||||
];
|
||||
|
||||
/** 本页只有两个交互状态:预览卡片单选,以及最多展开一个问题的FAQ。
|
||||
* 导航锚点定位本页三个section;登录入口进入/login,本地演示入口携带demo=1进入工作台。
|
||||
* 主体的SVG连线、花朵、光标和功能插图是展示图形,不接入画布命中、拖动或同步链路。 */
|
||||
export default function LandingPage() {
|
||||
// 初始选中第一张预览卡;三张卡片与底部三个圆点均直接设置同一个索引,aria-pressed同步表达选择。
|
||||
const [active, setActive] = useState(0);
|
||||
// null表示全部收起;点击当前问题置null,点击其它问题改索引。hidden与aria-expanded/controls对应同一状态。
|
||||
const [faq, setFaq] = useState<number | null>(null);
|
||||
// 页面顺序:导航→首屏交互预览→理念横条→三个功能说明→工作步骤→FAQ→行动入口→页脚。
|
||||
// 功能卡与步骤通过静态内容展示;FAQ以问题文本作key,步骤以序号作key,预览以说明作key。
|
||||
// 状态仅在本次组件挂载期间保留;切换预览不自动轮播,FAQ展开不发网络请求。
|
||||
return <div className="landing"><div className={styles.page}>
|
||||
<header className={styles.nav}>
|
||||
<Brand />
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
/**
|
||||
* 应用级反馈设施:提供共享Tooltip时间参数与通知挂载点。
|
||||
* 这个模块只负责提示的呈现环境,任务进度、错误事实和通知触发仍由业务调用方管理。
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { createContext, type ReactNode } from "react";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
/** 默认false使独立挂载的IconButton退回原生title;只有AppFeedback子树启用统一浮层提示。 */
|
||||
export const FeedbackContext = createContext(false);
|
||||
|
||||
/** 包住页面一次即可共享提示延迟;400ms首次出现,200ms内切换提示可跳过等待。
|
||||
* Toaster放在右下角,最多展示三条通知,默认4500ms消失并允许关闭;颜色读取主题变量。
|
||||
* children正常透传,本组件不订阅文档、队列或服务端状态。 */
|
||||
export function AppFeedback({ children }: { children: ReactNode }) {
|
||||
return <FeedbackContext.Provider value={true}>
|
||||
<Tooltip.Provider delayDuration={400} skipDelayDuration={200}>
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
/**
|
||||
* 通用按钮的样式与属性适配层。variant表达视觉语义,size控制尺寸;点击、禁用和表单行为由调用方传入。
|
||||
* 可通过asChild复用子元素,避免链接外再套按钮;这里不保存业务状态,也不默认指定button的type。
|
||||
*/
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** 将基础交互样式、语义变体和尺寸变体组合成className;未指定时使用default/default。
|
||||
* focus-visible提供键盘焦点反馈,disabled类限制指针交互并淡化外观,内嵌SVG统一尺寸。 */
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
@@ -34,6 +40,7 @@ const buttonVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
/** 在原生button属性上增加样式变体和asChild;ref仍按按钮声明,实际宿主由asChild决定。 */
|
||||
export interface ButtonProps
|
||||
extends
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
@@ -41,8 +48,11 @@ export interface ButtonProps
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
/** 转发ref和剩余原生属性,供调用方设置事件、可访问名称、disabled和表单属性。
|
||||
* Slot把属性合并到唯一子元素;使用链接等宿主时,其语义及行为由该子元素承担。 */
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
// 只选择宿主,不另包一层DOM;cn合并变体和调用方样式,业务逻辑不进入这个封装。
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
@@ -53,6 +63,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
);
|
||||
},
|
||||
);
|
||||
// forwardRef生成包装组件后补显示名,便于React开发工具定位。
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* 基于Radix的对话框展示封装:保留其开关、焦点管理和可访问语义,补项目统一的遮罩、布局与动画。
|
||||
* 调用方通过Root的open/onOpenChange控制业务流程;保存中是否允许关闭等规则不由本模块决定。
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
@@ -6,14 +10,19 @@ import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** Root提供受控或非受控开关上下文,本层不再复制一份open状态。 */
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
/** Trigger接入同一Root,负责触发打开;可沿用Radix的asChild组合调用方控件。 */
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
/** Portal把浮层移出页面普通布局,避免受父容器裁切;挂载方式沿用Radix。 */
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
/** 暴露关闭原语,供业务在底部动作区等位置放额外关闭入口。 */
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
/** 转发遮罩ref与属性,铺满视口;开关动画由Radix的data-state驱动,className允许调用方补样式。 */
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
@@ -29,6 +38,9 @@ const DialogOverlay = React.forwardRef<
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
/** 组合Portal、遮罩与居中内容面板,并在children之后固定附带右上角关闭按钮。
|
||||
* 面板ref和剩余属性交给Radix Content,保留焦点及外部交互事件;关闭按钮有读屏文字。
|
||||
* 本封装没有单独隐藏默认关闭按钮的参数,业务若要阻止关闭应在受控开关/事件层处理。 */
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
@@ -53,6 +65,7 @@ const DialogContent = React.forwardRef<
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
/** 标题区只组织纵向间距与响应式文字对齐;原生div属性和children原样透传。 */
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
@@ -67,6 +80,7 @@ const DialogHeader = ({
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
/** 动作区在窄屏使用反向纵排,在sm及以上改为靠右横排;不绑定提交或取消动作。 */
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
@@ -81,6 +95,7 @@ const DialogFooter = ({
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
/** 转发Radix标题的ref/属性并统一字号;标题仍参与对话框的可访问命名关联。 */
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
@@ -96,6 +111,7 @@ const DialogTitle = React.forwardRef<
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
/** 转发Radix说明文字的ref/属性并使用弱化文字样式;说明关联由Radix上下文维护。 */
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* 画布等工具栏使用的图标按钮。统一按钮尺寸、图标与可访问名称,并按反馈Provider决定使用浮层提示还是原生title。
|
||||
* 按钮的业务动作由onClick等原生属性传入;这里不维护工具选择状态。
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { FeedbackContext } from "./app-feedback";
|
||||
@@ -6,13 +10,17 @@ import { useContext, forwardRef, type ButtonHTMLAttributes } from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import styles from "./icon-button.module.css";
|
||||
|
||||
/** icon提供Lucide图标实现,label提供默认可访问名称与提示;不接受任意children替换内部图标。 */
|
||||
export type IconButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
};
|
||||
|
||||
/** 将ref交给真实button;type默认为button,避免放进表单时意外提交。
|
||||
* 调用方仍可传aria-pressed、disabled等状态,组件不会自行判断命令是否可用。 */
|
||||
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
function IconButton({ icon: Icon, label, title = label, className = "", type = "button", ...props }, ref) {
|
||||
// Provider存在时由Radix显示提示,移除原生title以免出现两层提示;图标自身对读屏隐藏。
|
||||
const feedback = useContext(FeedbackContext);
|
||||
const button = (
|
||||
<button
|
||||
@@ -26,6 +34,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
<Icon size={16} strokeWidth={1.6} aria-hidden="true" />
|
||||
</button>
|
||||
);
|
||||
// 没有全局提示环境就返回原生按钮;空title也不创建Tooltip。提示通过Portal避免被工具栏裁切。
|
||||
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}>
|
||||
|
||||
Reference in New Issue
Block a user