# CakeUI 技术文档 · 面向 AI 与开发者 本文描述当前仓库已经实现的行为,用于让 AI 在不了解项目历史的情况下完成集成、生成界面、修改组件和验证改动。公开版本信息、完整 Props 类型与主题变量附录由源码生成,位于在线全文末尾。本文正文的唯一维护源是 `docs/ai.md`。 在线阅读:https://gallery.vanillacake.cn/?page=docs 完整纯文本:https://gallery.vanillacake.cn/llms-full.txt 简短索引:https://gallery.vanillacake.cn/llms.txt 源码:https://github.com/hatsune-miku/cakeui 维护署名:miku。 ## 1. 先建立正确的项目模型 CakeUI 是 React + TypeScript + SCSS 的组件库,面向日常长期使用的软件界面。它提供外观一致、名称熟悉的小零件,保留原生 HTML 结构和事件。数据获取、业务状态、排序、筛选、分页切片、存储、权限判断和异步任务由应用负责。 库的公开入口是 `src/index.ts`,发布产物入口是 `dist/index.js`。所有组件使用具名导出,没有默认导出。对应的 `*Props`、`Size`、`Tone`、`CakeTheme`、`CakeMode` 同时导出。内部 context、hooks、工具函数和 demo 组件不属于公开 API,不从内部路径导入。 优先遵守这些约定: - 输入控件使用原生 `onChange(event)`,读取 `event.currentTarget.value` 或 `checked`。不要凭其他 UI 库经验发明 `onValueChange`、`onCheckedChange`、`modelValue` 等接口。`Tabs` 是使用 `onValueChange(value)` 的例外。 - `Button` 默认 `type="button"`,需要提交表单时明确使用 `type="submit"`。 - 表格、列表、菜单、日志、标签页通过 JSX children 组合。没有 `columns`、`dataSource`、`items`、`renderItem` 或通用 `asChild` / `as` 属性。 - 不导入 Drawer、SidePanel、Select、Input、Tooltip、Modal、Checkbox 等未导出的名称。实际名称分别是 `ComboBox`、`TextBox`、`HoverTips`、`Dialog`、`CheckBox`;本项目没有 Drawer 或侧边面板组件。 - 默认配色为蓝;`theme` 使用英文值 `blue` / `pink` / `gold`,面向用户的名称为“蓝 / 粉 / 金”。 - 必须显式引入 `@a1knla/cakeui/style.css`。无需 Tailwind、CSS-in-JS、全局通知服务、图标包或额外 Provider 服务。 - 功能是否存在以当前源码和类型为准。不要将工作台的搜索、任务管理、主题持久化、图标或 Toast 调用函数当成库导出。 ## 2. 安装、入口与执行环境 正式包名是 `@a1knla/cakeui`,npm 页面为 https://www.npmjs.com/package/@a1knla/cakeui 。品牌名与 GitHub 仓库仍为 CakeUI / cakeui;安装和导入时必须包含 `@a1knla/` scope。开发环境使用 Node.js 22.12+ 和 npm,运行项目时 React 与 React DOM 都应为 19 或更高版本。 在已有 React 19 项目中安装: ```sh npm install @a1knla/cakeui ``` 如果使用的 npm 镜像还未同步新版本,可追加 `--registry=https://registry.npmjs.org/` 从官方注册表安装。维护者发布步骤、验证方式与署名约定见 `docs/publishing.md`。 需要验证未发布的本地改动时,仍可从仓库打包安装: ```sh git clone https://github.com/hatsune-miku/cakeui.git cd cakeui npm ci npm pack # npm pack 的 prepack 会构建库;将输出的 a1knla-cakeui-<版本>.tgz 交给消费项目 # 在已有 React 19 项目内,用实际文件名替换占位符 npm install /absolute/path/to/a1knla-cakeui-<版本>.tgz ``` 使用方式: ```tsx example=hello import { Button, CakeProvider, TextBox } from '@a1knla/cakeui' import '@a1knla/cakeui/style.css' export function HelloCakeUI() { return ( ) } ``` 构建提供 ESM、TypeScript 声明与一份完整 CSS。JS 可以 tree-shake;CSS 不按组件拆包,没有 CommonJS 入口,也没有公开 SCSS 子路径。消费项目不需要安装 Sass。React / React DOM 是 peer dependencies;库不捆绑 React,也没有其他运行时依赖。 库的 JavaScript 构建带有 `'use client'` 指令。支持服务端渲染首屏 HTML,DOM 操作在 effect 或事件内执行;Dialog 和 Popover 等交互需要客户端挂载后才能打开。在使用 Server Components 的框架中,管理事件和状态的消费组件应处于客户端边界,CSS 按框架的全局样式规则引入。SSR 支持不代表服务端输出会自动打开模态框或提示层。 浏览器需要支持原生 dialog、Popover API、`:has()`、`color-mix()` 等当前实现使用的能力。库不捆绑 polyfill。真实浏览器自动测试使用 Chrome;Firefox / Safari 没有完成全面实机验证。ComboBox 的可定制选择器有独立的能力降级,见对应章节,不将“可运行”与“所有浏览器外观完全相同”混为一谈。 ## 3. 通用 API 与样式规则 ### 原生属性与 ref 多数组件以 `ComponentPropsWithRef<'标签'>` 为基础。可以使用该 HTML 元素支持的 `id`、`className`、`style`、`aria-*`、`data-*`、事件和 React 19 `ref`。请根据具体组件的类型检查属性,不假设每个组件都能传 `disabled`、`size`、`href` 或 `ref`。 `CheckBox` / `RadioButton` / `Switch` 是例外结构:外层为 label,`className` 加到 label;其余原生 input 属性、事件、style、id、name 和 ref 指向内部 input。不要将它们再包在另一个 label 内。 `ContextMenu`、`Dialog`、`LogView`、`Toast` 使用不带 ref 的原生属性类型。`HoverTips` 和 `WhatsThis` 只接受明确列出的 Props,不透传任意 DOM 属性。`Dialog.initialFocus` 用于指定初始焦点,不能当作 Dialog 容器 ref 使用。 组件会设置所需的 role、type、id、hidden、aria 状态或 data 属性;类型允许传入某个原生属性,不意味着可以覆盖组件自身管理的同名语义。例如 Tab 的 id、role、type、tabIndex,MenuItem 的 role、type、tabIndex,以及 Dialog 的 aria-labelledby 都由组件决定。不要靠覆盖这些属性改变交互模型。 ### 受控、非受控与表单 原生输入沿用 React 规则:受控使用 `value` / `checked` 加 `onChange`,非受控使用 `defaultValue` / `defaultChecked`;不要在同一生命周期里切换模式。数字输入的 `value` 仍以字符串形式读取,可使用 `valueAsNumber`,为空时结果可能是 NaN。 `FormData` 按原生成功控件规则工作:需要 name,disabled 不提交,未勾选的 checkbox 不提交;checked checkbox 无显式 value 时通常提交 `on`。浏览器验证来自 required、type、min、max、step 等原生约束。CakeUI 不提供校验 schema,也不会将 Field.error 自动写入控件有效性。 原生 reset 对非受控输入恢复默认值;受控输入的状态需要应用配合 reset 更新。MenuItem、Tab 不用作表单提交按钮。Button.loading 会禁用按钮,但不替代应用自己的并发请求处理。 ### 样式作用域与全局影响 不包含全局 reset,不修改页面的 margin 或原生 h1 / button / table 的常规样式。导入 CSS 后,会在 `:root` 提供默认主题变量并设置 `user-select: none` 与 `-webkit-user-select: none`,因此页面默认不可选择文字。 TextBox / TextArea / NumberBox 和 LogView 允许选择文字;ComboBox 明确保持不可选择。普通业务正文、原生 pre / code 或 contentEditable 若需要复制,应由应用显式开启选择。Gallery 的代码和 AI 文档阅读区已开启选择。 ```scss .app-copyable { user-select: text; -webkit-user-select: text; } ``` 样式通过完整 `cake-*` className 与 `--cake-*` 变量组成。优先用自己的 className 和主题变量调整外观,避免依赖内部 DOM 深度;内部子类名与布局不作为稳定的跨版本 API。库不替应用安排表单网格或页面布局,使用自己的 flex / grid 容器。 公共类型:`Size = 'small' | 'medium' | 'large'`;`Tone = 'neutral' | 'accent' | 'success' | 'warning' | 'danger'`。Tone 的错误值叫 `danger`,只有 LogEntry 的错误级别叫 `error`。 ## 4. 完整组件参考 以下每个三级标题对应一个公开组件。原生类型及扩展属性的精确声明见全文附录;这里说明默认值、DOM 结构、语义和交互边界。 ### CakeProvider 原生 div,ref 指向该 div。可选 `theme='blue'`、`mode='system'`、`density='comfortable'`。theme 为 blue / pink / gold;mode 为 light / dark / system;density 为 comfortable / compact。 Provider 是可嵌套的 CSS 容器,产生实际 DOM,设置主题背景、文字色、字体、字号与行高,不创建 React 全局状态或 body portal。嵌套 Provider 按自己的 Props 和默认值重新确定主题,不自动继承外层 Props。system 用 CSS 媒体查询响应系统明暗。不使用 Provider 时默认变量为蓝色浅色,根变量本身不会跟随系统变暗。 默认舒适密度字号 14px、控件高度变量 38px;紧凑密度字号 13px、高度变量 32px。个别组件有固定尺寸、padding、最小高度或触屏规则,不保证所有控件最终测量高度都等于该变量。Provider 不保存用户设置,主题持久化属于应用。 ### Button 原生 button,ref 指向按钮。`variant='default'` 可取 default / primary / ghost / danger;`size='medium'` 使用 Size;`loading=false`;默认 `type='button'`。 默认按钮常态为中性背景,按下时染上 accent-soft / accent-ink 并下移 1px,使用 AnyDrop 风格反馈。primary 常态已有主题色;ghost 透明;danger 用危险色。其他 variant 的按压为 scale(0.98)。动画使用 fast / ease 变量,默认 150ms 与 cubic-bezier(0.29, 0, 0, 1)。loading 显示小 Spinner、保留 children、设置 aria-busy 并禁用按钮。图标由应用传 children;纯图标按钮必须有可访问名称。 ### Card 原生 div。`padding='medium'`,可取 none / small / medium / large。只提供表面、圆角、间距,不自带 header / footer / title / selectable / onSelect 等接口。按业务需要放普通元素,不将有点击事件的 div 当作天然可键盘操作的按钮。 ### Separator 原生 hr,无扩展属性,默认水平分隔。没有 orientation 属性;垂直分隔需要应用自己的样式与语义。 ### TextBox 原生 input,`type='text'`。支持 type、name、value、defaultValue、placeholder、required、disabled、readOnly、autoComplete、min/max 等对应原生属性,ref 为 HTMLInputElement。可组合 email / password / search / date / file 等原生类型;日期和文件选择器本身由浏览器负责,不是额外的 CakeUI 日期或上传组件。 普通输入 focus 使用一圈 1.5px 内描边,不叠加第二圈 outline。`aria-invalid='true'` 使用危险色内描边。placeholder 不代替 label。 ### TextArea 原生 textarea,`rows=4`,ref 为 HTMLTextAreaElement。支持原生 value / defaultValue、name、maxLength、required 等。默认允许垂直 resize。没有 autosize 属性,自动高度由应用实现。 ### NumberBox 固定 `input type='number'`,公开类型去掉 type。支持 min / max / step、原生事件、ref。没有返回 number 的自定义回调,也不内置千分位、货币格式、精度计算或业务范围修正。 ### ComboBox 原生 select,ref 为 HTMLSelectElement,无额外 Props。children 使用原生 option / optgroup,支持 Fragment 嵌套;组件递归给它们合并 `cake-combobox-option` / `cake-combobox-group` 样式类,不丢弃已有 className。自定义 React 组件若在内部返回 option,不会被这一步递归展开;需要相同外观时直接传 option 元素。 单选使用 value / defaultValue 和 `onChange(event)`。可以传原生 multiple 或 size,相关选择与提交仍由 select 负责;已验证的定制弹出菜单主要是单选下拉路径,不保证 multiple / size 列表框具有相同弹出外观。disabled、禁用 option / optgroup、required、form、重置和键盘操作保留原生规则。选项值使用字符串。 下拉表面与 ContextMenu 共用圆角、padding、颜色、阴影;实现使用 `appearance: base-select`、`::picker(select)`、`::picker-icon`、`::checkmark` 和 anchor-size。支持时显示自定义菜单,picker 最大高度为 min(320px, 视口高度减 16px),选中项有标记。不支持时降级为浏览器原生选择器,不是运行时切换到另一个 JS 组件。 不接受 options 数组、placeholder、filterOption、searchable、onValueChange 等虚构属性。占位项可以用 `` 配合 required;搜索、异步数据加载、虚拟滚动和可编辑输入由应用另外实现。不要给 option 放交互按钮并期待库管理它们。 ### CheckBox 外层 label,内部固定 checkbox input,children 是标签文字。输入类型排除 type / size,增加 `indeterminate=false`。className 属于外层,ref 及其他原生 input 属性属于内部输入。 indeterminate 设置 DOM input.indeterminate,并输出 aria-checked='mixed';它是混合外观与语义,不等于 checked,也不自动控制全选逻辑。浏览器在用户操作时可能清除 DOM 的混合状态,应用应同步自己的 indeterminate 与 checked 状态。无文字 children 时提供 aria-label。 ### RadioButton 外层 label,内部固定 radio input,children 是标签。无额外状态管理,类型排除 type / size。用相同 name 建立原生组;每项设置不同 value。原生浏览器负责组内互斥和键盘方向切换。没有单独的 RadioGroup 导出,语义分组可使用 GroupBox。 ### Switch 外层 label,内部固定 checkbox input 并带 role='switch'。children 是标签,类型排除 type / size。沿用 checked / defaultChecked / onChange,参与 FormData,没有独立的 onCheckedChange。使用原生 disabled,不仅仅改透明度。 ### Slider 固定 `input type='range'`,类型去掉 type。支持原生 min、max、step、value、defaultValue、onChange、ref。默认数值范围由浏览器决定,建议业务中明确指定。没有 marks、双滑块、值提示或自动格式化;用旁边的 output / 文本显示值,并提供可访问名称。 ### Field 原生 div 容器,必填 `label: ReactNode`、`htmlFor: string`,可选 description / error。渲染 label、children、说明节点。非空 error 优先于 description,说明节点 id 为 `${htmlFor}-help`,错误说明带 role='alert'。 不会克隆 children,不生成输入 id,不自动添加 aria-describedby / aria-invalid,也不自动验证。调用方必须将 htmlFor 与输入 id 对齐,并显式关联说明。输入需要必填时,required 放在输入上;Field 没有 required 扩展属性。 ### GroupBox 原生 fieldset,必填 `label: ReactNode` 用作 legend,children 为分组内容。disabled 使用原生 fieldset 的禁用规则,ref 指向 fieldset。布局和每个控件的标签仍由调用方负责。 ### Tabs 原生 div,结合内部 context 管理选择。`defaultValue=''`,可选受控 value 与 `onValueChange(value: string)`,`orientation='horizontal'`,也支持 vertical。没有自动选择第一个标签的逻辑,必须提供一个实际存在且可用的初始 value,否则所有面板可能隐藏、所有标签不在普通 Tab 顺序中。 受控模式必须在回调中更新 value;非受控模式由内部状态维护。只有值变化才调用 onValueChange。value 应在同组内唯一且稳定,数据移除或禁用当前标签时由应用选取新的合法值。用 React useId 隔离不同组的 DOM id,支持嵌套。 ### TabList 原生 div,必须放在 Tabs 中,输出 role='tablist' 与 aria-orientation。提供 aria-label 或 aria-labelledby。横向 Left / Right,纵向 Up / Down,Home / End 选首尾;跳过原生 disabled 项并循环,横向考虑 RTL。键盘移动焦点时会同时激活新标签。自定义 onKeyDown 先执行,preventDefault 可阻止内置导航。 ### Tab 原生 button,必填 `value: string`。必须放在 Tabs 作用域内,通常作为 TabList 的子项。组件固定 type='button'、role='tab'、关联 id 和 ARIA,只有活动且未禁用标签的 tabIndex 为 0。onClick 先执行;未 preventDefault 时请求更新 Tabs 的值。 ### TabPanel 原生 div,必填 `value: string` 与对应 Tab 匹配,必须放在 Tabs 中。组件固定 role='tabpanel'、tabIndex=0 和关联 ARIA。非活动面板通过 hidden 隐藏但保留挂载,内部状态、effect 和数据订阅仍然存在。若业务需要卸载,应由应用自行条件渲染。不要用 CSS display 强行覆盖 hidden。 ### Breadcrumb 原生 nav,内部生成 ol,children 应为 BreadcrumbItem。默认 aria-label='Breadcrumb',可替换为本地化名称。没有路由状态和链接生成逻辑,ref 指向 nav。 ### BreadcrumbItem 原生 li,无扩展属性,放在 Breadcrumb 中。链接由 children 中的原生 a 或路由 Link 提供,当前项自行设置 aria-current='page'。href 属于链接,不属于 BreadcrumbItem。 ### Pagination 原生 nav,必填 `page: number`、`count: number`、`onPageChange(page: number)`。count 是总页数,不是条目总数;页码从 1 开始。可选 previousLabel='Previous page'、nextLabel='Next page'、pageLabel 默认生成 'Page N';nav 默认 aria-label='Pagination',均可本地化。 显示首尾页、当前页及前后相邻页,在间隔处显示省略号。count 向下取整并至少为 1,非有限数按 1;page 向下取整后约束到有效范围,非有限数按 1。这仅修正显示与点击计算,不会主动回调修正应用传入的状态。当前页按钮仍可点击;边界处上一页 / 下一页禁用。没有 pageSize、total、默认页或内置数据切片。空数据时是否隐藏分页由应用决定。 ### ListView 原生 ul,只负责列表排版。children 使用 ListItem,ref 指向 ul。不是 listbox,没有选择、拖动、虚拟化或键盘选中管理。数据渲染使用应用自己的 map。 ### ListItem 原生 li,只负责一项的布局。把动作放在内部 Button 或 a 上;不要仅添加 onClick 就假设 li 有按钮语义。没有 selected 属性和 onSelect 选择模型。 ### Table 真正的 table,没有额外包裹层,ref 指向 HTMLTableElement。接受原生 caption、colgroup 及下列结构组件。需要水平滚动时由应用在外层添加 overflow 容器。没有 columns、dataSource、rowKey、排序或选择配置。 ### TableHead 原生 thead,通常包含 TableRow,无额外属性。它是表头分组,不是单个表头单元格。 ### TableBody 原生 tbody,通常通过数组 map 渲染 TableRow。空态由应用用 TableRow / TableCell + colSpan 表达;没有 loading 或 emptyText 属性。 ### TableRow 原生 tr,children 为 TableHeader 或 TableCell。React key 由调用方设置。行交互、选中态和数据对象由应用负责。 ### TableHeader 原生 th,`scope='col'`。行标题可传 scope='row',支持 colSpan / rowSpan / aria-sort。需要排序时在其中放 Button,应用排序数据并维护 aria-sort;本组件不会自行排序。 ### TableCell 原生 td,支持原生跨行跨列属性,children 可自由组合。不要把 div 直接放到 tr 代替 td。 ### Avatar 原生 span,必填 name,可选 src、`size='medium'`、children。外层默认 role='img'、aria-label=name。图片 alt 为空,由外层提供名称。图片失败后显示 children,否则显示 trim 后名称前两个字符转大写。失败源按 src 记忆,更换 src 可以加载新图;没有上传、裁剪或在线状态逻辑。ref 指向 span,不能当作内部 img ref。 ### Dot 原生 span,`tone='neutral'`。没有 aria-label 时默认 aria-hidden,适合作为装饰;有 aria-label 时默认 role='img' 并暴露名称。表达重要状态时同时提供文字,避免仅靠颜色区分。 ### Tag 原生 span,`tone='neutral'`。可选 onRemove,存在时显示内部移除按钮;removeLabel 默认 'Remove',建议提供如“移除文档标签”的具体名称。onRemove 仅通知应用,组件不会自行删除或隐藏。外层 ref 指向 span。没有 checked / selected 状态模型。 ### Badge 原生 span,`tone='accent'`。children 自行传计数或文字,不自动处理 max、overflowCount、角标定位或隐藏 0。 ### Alert 原生 div,`tone='accent'`。默认 danger 使用 role='alert',其他 tone 使用 role='status';可用原生 role 覆写。children 自由组织提示正文、图标或操作,不内置 title / description / closable 属性。静态说明若不应播报,可显式选择适当 role。 ### ProgressBar 原生 progress,`max=100`。传 value 表示确定进度,省略 value 为不确定进度。应用负责数值和任务状态。提供 aria-label 或 label;不要只依靠视觉进度传达名称。没有 status / percentage / showText 属性。 ### Spinner 原生 span,`size='medium'`。默认 role='status'、aria-label='Loading',可用原生属性替换或本地化。只提供旋转指示,不负责遮罩、loading 状态切换或内容禁用。纯装饰时可 aria-hidden,附近另有状态文字时避免重复播报。 ### Skeleton 原生 span,默认 aria-hidden=true。用 style / className 指定所需宽高、圆角;没有 count / rows / loading Props。加载结束后由应用切换为真实内容。减少动态效果时关闭持续脉冲。 ### HoverTips 必填 content: ReactNode 和单个 `ReactElement<{ 'aria-describedby'?: string }>` children;可选 `delay=180` 毫秒、`placement='top'`(top / bottom)和 className。className 作用在外层 span,没有通用原生属性或 ref 透传。 渲染外层 span 与原生手动 Popover,提示 role='tooltip'。克隆唯一子元素,将生成的说明 id 合并到其 aria-describedby,不增加额外的 Tab 停靠点。子组件必须将该属性传到实际可聚焦元素;不要传纯文本、数组或不能接收此属性的 Fragment。禁用按钮无法正常接收键盘焦点,需要额外的可聚焦帮助入口。 悬停按 delay 打开,焦点或点击立即请求打开,离开使用 100ms 宽限,光标可进入提示层阅读,Escape 关闭。顶部空间不足时显示在下方;水平与垂直位置限制在视口 8px 边距内。滚动和 resize 重新定位。内容应为简短说明,不放表单、链接或其他必须操作的控件;它不是通用交互 Popover。 ### WhatsThis 必填 children 为说明,可选 label='More information',无通用 DOM Props。生成一个问号按钮并用 HoverTips 包裹。label 是按钮的无障碍名称,不是提示内容,可本地化为“关于同步方式”。帮助文字放 children。 ### Dialog 原生 dialog,必填 `open: boolean`、`onOpenChange(open: boolean)`、`title: ReactNode`;可选 description、footer、`closeLabel='Close'`、`closeOnBackdrop=true`、`initialFocus: RefObject`。其余 Props 来自去掉 open / title / ref 的原生 dialog 类型。 open 是受控状态,应用必须接受关闭请求并更新状态。组件使用 showModal / close,保留原生顶层、背景 inert 与焦点恢复。标题始终渲染为 h2 并与 dialog 关联;description 存在时由组件关联说明 id。组件内部有标题栏、关闭按钮、正文与可选 footer;不提供 footer 默认保存操作。 初始焦点顺序:initialFocus.current;其次 autofocus 或 data-cake-autofocus='true';其次正文内第一个未禁用 input / select / textarea / button;没有指定目标时保留浏览器初始焦点行为。目标必须位于 dialog 内。初始目标不可聚焦或被隐藏时应由应用修正,不把任意 div ref 当成可聚焦目标。 Tab / Shift+Tab 在可用停靠点间约束;Escape 触发原生 cancel,调用方 onCancel.preventDefault 可阻止关闭请求。右上角关闭按钮请求 onOpenChange(false)。遮罩点击默认关闭,并检查按下是否发生在遮罩,避免从正文拖拽选择到外面时误关。closeOnBackdrop=false 仅关闭遮罩行为,不关闭 Escape 或标题栏按钮。onClick.preventDefault 可阻止内置遮罩关闭。 正文不会在 open=false 时卸载,表单值与子组件状态可保留。需要每次重置时由应用改变 key、调用 reset 或更新受控状态。保存按钮在 footer 且 form 在正文时,使用原生 form='表单id' 与 type='submit' 关联。重要错误应在当前 Dialog 内呈现,不依赖被模态层遮挡的全局 Toast。 ### ContextMenu 原生 div 触发区域,加一个保留在本 DOM 子树内的顶层 Popover。必填 `menu: ReactNode`、`menuLabel: string`,children 为触发区域;`interactive=false`。其余 Props 来自不带 ref 的 div。没有 open / onOpenChange / placement / items 属性。 触发区域默认 tabIndex=0、aria-haspopup='menu';提供 aria-label 或其他清晰名称以说明用途。将希望一起下陷的完整区域放在 ContextMenu 内,而不是只包裹一小段文字。Popup 通过顶层显示,不会因普通 overflow 容器裁切,不 portal 到 body,因此继承当前主题。 普通模式:在 contextmenu 事件时打开,通常来自右键或系统等价操作;Shift+F10 和 Menu 键可在聚焦区域时打开。鼠标坐标或触发区左上角附近定位,并限制在视口至少 8px 边距内。打开时聚焦首个非 disabled MenuItem;没有项目时聚焦菜单容器。普通模式使用 popover='auto',具有原生轻关闭行为。 Interactive 模式的准确状态规则: | 输入事件 / 松开位置 | 菜单与动作 | 区域按压反馈 | | -------------------------------------------------- | -------------------------------------------------- | ------------------------ | | 区域内右键按下(mousedown,button=2) | 立即打开,焦点放在菜单容器,不预选第一项 | 整个区域开始下移 1px | | 仍按住右键,光标移动 | 根据实际悬停显示项目反馈,不执行业务回调 | 保持下陷 | | 右键松开在本菜单可用 MenuItem 或其内部元素 | 关闭菜单,然后通过该按钮的 click 调用 onClick 一次 | 恢复 | | 松开在空白、分隔线、禁用项、菜单外 | 不选择项目,菜单保持打开 | 恢复 | | Escape / Tab | 关闭并恢复之前焦点 | 恢复 | | 后续点击菜单外、触发区域祖先实际滚动、resize | 关闭 | 恢复 | | 窗口失焦或文档隐藏 | 关闭 | 恢复 | | pointercancel,或后续 mousemove 表明右键已不再按住 | 取消残留按压状态 | 恢复;不因此自动选择项目 | 下陷曲线与默认 Button 完全相同:transform translateY(1px),fast 默认 150ms,ease 默认 cubic-bezier(0.29, 0, 0, 1)。浮出的菜单保持定位,不随触发区域下移。减少动态效果时保留反馈结果,但过渡时间为 0。 松开命中判断基于 `document.elementFromPoint(clientX, clientY)`,不依赖原始事件 target、鼠标捕获对象或键盘焦点。只执行当前菜单内的 HTMLButtonElement role='menuitem',排除 :disabled 和 aria-disabled='true'。真正禁用菜单项请使用 disabled:键盘导航和普通点击依靠原生 disabled,单独 aria-disabled 不会自动阻止所有动作。 Interactive 使用 popover='manual' 来管理关闭时机,并吞掉同次手势后续的原生 contextmenu / auxclick,防止选择后再次弹出系统菜单。右键松开选择始终关闭,即使 MenuItem.onClick 里 preventDefault。普通左键点击或键盘选择仍允许 preventDefault 保持打开。右键松开调用的是按钮 click,业务处理请放 onClick,不要要求该 click 的 button=2 才处理。 两种模式都支持 Up / Down 循环、Home / End、600ms 内的键入前缀检索、原生 Enter / Space 激活、Escape / Tab 关闭。Tab 是关闭并恢复焦点,不是在菜单项之间逐个 Tab。普通选择后恢复打开前的焦点;祖先滚动、resize、外部点击等消失路径不应被应用当成统一的 focus 回调。 触发区的 onContextMenu / onKeyDown 在内部操作之前执行,可 preventDefault 阻止对应默认行为。onMouseDown.preventDefault 可以阻止 Interactive 的按下打开;若想完全禁止所有打开路径,还需处理后续 contextmenu 与键盘事件。组件仅提供单层菜单,不支持子菜单、受控打开、菜单复选项或鼠标手势拖放。触屏用户应有可见的等价操作入口,不仅依赖右键。 ### MenuItem 原生 button,可选 `shortcut: ReactNode`、`danger=false`,children 是项目内容。固定 type='button'、role='menuitem'、tabIndex=-1。用于 ContextMenu.menu 内,原生 disabled 会禁用交互。 onClick 先执行;未 preventDefault 时通过内部 context 请求关闭菜单。在 Interactive 的右键松开路径中,关闭已在 click 前发起,因此 preventDefault 无法保留菜单。shortcut 只是显示文字,不注册键盘快捷键;danger 只是危险操作样式,不执行确认、删除或权限校验。用同一个业务函数连接菜单项、快捷键和其他可见按钮。 ### MenuSeparator 原生 div,固定 role='separator',放在 ContextMenu.menu 的项目之间。不是可选中项,Interactive 松开在此处不会触发动作或关闭菜单。 ### Toast 原生 div 状态通知,必填 open / onOpenChange;`duration=4500` 毫秒、`tone='success'`、`closeLabel='Dismiss'`。类型不含 ref。默认 role='status'、aria-atomic=true,danger 使用 aria-live='assertive',其他为 polite。 单条受控通知,duration<=0 不自动关闭。悬停或焦点进入暂停,离开后按剩余时间继续。open 或 duration 改变会重置计时;仅更换 children 不会重置剩余时间,需要新一轮完整展示时可以改变 key 或重新开启。关闭按钮与计时器请求 onOpenChange(false),应用更新 open。 默认固定在视口右下 24px,宽度受视口限制,使用普通 z-index,不使用原生顶层或 portal。多个实例会重叠;队列、去重、位置编排和批量关闭由应用负责。open=false 保留空状态容器、卸载通知正文。不要将 Toast 当作必须被确认的 Dialog。 ### Accordion 原生 div 排版容器,无扩展属性。children 使用 AccordionItem。不管理受控展开值,不自动限制同时打开一项。 ### AccordionItem 原生 details,必填 `title: ReactNode` 替代原生字符串 title,用于内部 summary。支持原生 open、onToggle、name、ref。正文位于内部 div;summary 已带折叠箭头。浏览器支持时,用相同 name 的 details 实现互斥组,Accordion 自身不模拟该功能。没有 defaultOpen / onOpenChange 属性;需要受控同步时按原生 onToggle 读取 currentTarget.open。 ### LogView 原生 div,`follow=true`,类型不含 ref。默认 role='log'、aria-label='Logs'、aria-live='polite'、aria-relevant='additions text'、tabIndex=0。可用原生属性本地化或调整播报。默认高度 220px,最小高度 80px,overflow auto,日志文字可选择复制。 组件在 children 或 follow 变化时,仅当内部记录认为用户位于底部才滚到末尾。scroll 事件用底部剩余距离 <24px 判断是否在底部;用户向上翻阅后暂停跟随,滚回底部恢复。onScroll 在内部记录之后调用。follow=false 禁用自动跟随;改回 true 并不会无条件强行回到底部。 没有虚拟滚动、最大条数、自动清空或过滤 Props。大量日志由应用限制保留量或窗口化,避免无限堆积 DOM。实时区域高频播报策略也由应用决定。 ### LogEntry 原生 div,可选 time、dateTime、`level='info'`,level 为 info / success / warning / error / debug。渲染 time、级别、正文三个部分。time 是显示字符串;dateTime 写入 time.dateTime 和 title;children 为内容。不会解析日期、补充当前时间、国际化级别或省略相邻重复时间。省略 time 会保留时间列布局,适合应用自己合并连续相同时间的显示。 ## 5. 可直接集成的组合示例 标有 `example=` 的 TSX 代码块都是独立模块,包验证会将它们放进真实安装 CakeUI tgz 的消费项目进行严格类型检查。为了展示 API,示例中的保存函数只回显数据;接入实际服务时替换相应业务函数。 ### 表单、错误关联与原生选择器 ```tsx example=settings-form import { type FormEvent, useId, useState } from 'react' import { Button, CheckBox, ComboBox, Field, GroupBox, NumberBox, TextArea, TextBox } from '@a1knla/cakeui' export function SettingsForm() { const id = useId() const [error, setError] = useState('') const [result, setResult] = useState('') function save(event: FormEvent) { event.preventDefault() const data = new FormData(event.currentTarget) const name = String(data.get('name') ?? '').trim() if (!name) { setError('名称不能只包含空格。') return } setError('') setResult(`已保存 ${name},同步方式 ${data.get('sync')}`) } return (
{ setError('') setResult('') }} >