From b51d78b47d3051a123d98aad1d756d7d2c2dcedc Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 01:51:59 +0800 Subject: [PATCH] docs(ui): explain shared controls and canvas display adapters --- src/components/brand.tsx | 5 +++++ src/components/canvas/canvas-icon.tsx | 14 ++++++++++++++ src/components/canvas/canvas-image.tsx | 11 +++++++++++ src/components/canvas/canvas-renderers.tsx | 10 ++++++++++ src/components/canvas/unsupported-elements.tsx | 11 +++++++++++ src/components/icon.tsx | 6 ++++++ src/components/landing-page.tsx | 14 ++++++++++++++ src/components/ui/app-feedback.tsx | 8 ++++++++ src/components/ui/button.tsx | 11 +++++++++++ src/components/ui/dialog.tsx | 16 ++++++++++++++++ src/components/ui/icon-button.tsx | 9 +++++++++ 11 files changed, 115 insertions(+) diff --git a/src/components/brand.tsx b/src/components/brand.tsx index 9e8ed2d..919f6df 100644 --- a/src/components/brand.tsx +++ b/src/components/brand.tsx @@ -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 ( (() => ({ 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 { event.stopPropagation(); setRetry((value) => value + 1); } : props.onClick}> diff --git a/src/components/canvas/canvas-image.tsx b/src/components/canvas/canvas-image.tsx index 1bb4cb9..499a2b4 100644 --- a/src/components/canvas/canvas-image.tsx +++ b/src/components/canvas/canvas-image.tsx @@ -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; 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 = {title} previews.loaded(id)} />; + // 没有预览或预览已等于正式源时,只绘制一张图片,避免冗余叠层。 if (!local || local === src) return remote; + // 正式图片继续在底层加载,本地预览绝对定位覆盖在上方;Provider清掉该id后自然移除覆盖。 + // 覆盖图是装饰性的、忽略指针事件,读屏与节点交互都交给底层正式图片和外层节点。 return
{remote}
; } diff --git a/src/components/canvas/canvas-renderers.tsx b/src/components/canvas/canvas-renderers.tsx index fbb5bdb..e667edd 100644 --- a/src/components/canvas/canvas-renderers.tsx +++ b/src/components/canvas/canvas-renderers.tsx @@ -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){return ;} +/** 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;})); diff --git a/src/components/canvas/unsupported-elements.tsx b/src/components/canvas/unsupported-elements.tsx index b4dc361..c0eb586 100644 --- a/src/components/canvas/unsupported-elements.tsx +++ b/src/components/canvas/unsupported-elements.tsx @@ -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){ + // fallback来自bindings投影;缺失时仍显示通用提示,不尝试从data猜测组件结构。 const fallback=data.__headlessFallback; return
{data.title}此元素需要兼容的组件版本才能编辑 @@ -15,6 +24,8 @@ export function UnsupportedNode({data,selected}:NodeProps){ {fallback?.handles.map(handle=>)}
; } +/** 直接使用ReactFlow推导的端点生成贝塞尔路径,附可访问标题;不调用未知类型的编辑/样式逻辑。 + * BaseEdge绘制统一颜色的可见线条,数据保留与版本兼容仍归模型层负责。 */ export function UnsupportedEdge(props:EdgeProps){ const [path]=getBezierPath(props); return 原始连线内容已保留; diff --git a/src/components/icon.tsx b/src/components/icon.tsx index b282cd2..2574720 100644 --- a/src/components/icon.tsx +++ b/src/components/icon.tsx @@ -1,3 +1,9 @@ +/** + * 按既有静态资源命名约定展示仪表盘图标的小封装,不负责画布图标库的分片加载或搜索。 + */ +/** name是调用方提供的资源后缀,拼成/assets/dashboardcontext-imgIcon{name}.svg。 + * size同时设置宽高,默认18px;空alt表示装饰用途,承载动作的外层控件须另有可访问名称。 + * 此处没有资源白名单校验或加载失败占位,调用方应使用项目已有的图标名。 */ export function Icon({ name, size = 18 }: { name: string; size?: number }) { return ( (null); + // 页面顺序:导航→首屏交互预览→理念横条→三个功能说明→工作步骤→FAQ→行动入口→页脚。 + // 功能卡与步骤通过静态内容展示;FAQ以问题文本作key,步骤以序号作key,预览以说明作key。 + // 状态仅在本次组件挂载期间保留;切换预览不自动轮播,FAQ展开不发网络请求。 return
diff --git a/src/components/ui/app-feedback.tsx b/src/components/ui/app-feedback.tsx index 64c3661..5bc937d 100644 --- a/src/components/ui/app-feedback.tsx +++ b/src/components/ui/app-feedback.tsx @@ -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 diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 775d380..b287e0c 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -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, @@ -41,8 +48,11 @@ export interface ButtonProps asChild?: boolean; } +/** 转发ref和剩余原生属性,供调用方设置事件、可访问名称、disabled和表单属性。 + * Slot把属性合并到唯一子元素;使用链接等宿主时,其语义及行为由该子元素承担。 */ const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { + // 只选择宿主,不另包一层DOM;cn合并变体和调用方样式,业务逻辑不进入这个封装。 const Comp = asChild ? Slot : "button"; return ( ( ); }, ); +// forwardRef生成包装组件后补显示名,便于React开发工具定位。 Button.displayName = "Button"; export { Button, buttonVariants }; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index ac2b9bb..a0c2fec 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -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, React.ComponentPropsWithoutRef @@ -29,6 +38,9 @@ const DialogOverlay = React.forwardRef< )); DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; +/** 组合Portal、遮罩与居中内容面板,并在children之后固定附带右上角关闭按钮。 + * 面板ref和剩余属性交给Radix Content,保留焦点及外部交互事件;关闭按钮有读屏文字。 + * 本封装没有单独隐藏默认关闭按钮的参数,业务若要阻止关闭应在受控开关/事件层处理。 */ const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef @@ -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, React.ComponentPropsWithoutRef @@ -96,6 +111,7 @@ const DialogTitle = React.forwardRef< )); DialogTitle.displayName = DialogPrimitive.Title.displayName; +/** 转发Radix说明文字的ref/属性并使用弱化文字样式;说明关联由Radix上下文维护。 */ const DialogDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef diff --git a/src/components/ui/icon-button.tsx b/src/components/ui/icon-button.tsx index 5315ce8..1490afe 100644 --- a/src/components/ui/icon-button.tsx +++ b/src/components/ui/icon-button.tsx @@ -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, "children"> & { icon: LucideIcon; label: string; }; +/** 将ref交给真实button;type默认为button,避免放进表单时意外提交。 + * 调用方仍可传aria-pressed、disabled等状态,组件不会自行判断命令是否可用。 */ export const IconButton = forwardRef( function IconButton({ icon: Icon, label, title = label, className = "", type = "button", ...props }, ref) { + // Provider存在时由Radix显示提示,移除原生title以免出现两层提示;图标自身对读屏隐藏。 const feedback = useContext(FeedbackContext); const button = ( ); + // 没有全局提示环境就返回原生按钮;空title也不创建Tooltip。提示通过Portal避免被工具栏裁切。 if (!feedback || !title) return button; return {button}