@robert-stackflow/oneui
React 组件、应用外壳和两套可互换的主题。无 Electron、React Router、账户 API 或持久化存储依赖。
紧凑操作栏与切换按钮(0.6.26)
Popover width="content" arrow scrollable={false} 按内容确定宽度,并限制在视口边距内,适合文本和表格的上下文操作栏。关闭时保持组件挂载并传入 open={false},由 OneUI 完成箭头、进退场和减少动态效果处理。
IconButton variant="ghost" aria-pressed={active} 使用主题选中底色与前景色,保持无描边;不要用 secondary 变体模拟切换选中。
带指向箭头的浮层(0.6.22)
Popover arrow offset={14} 在浮层边缘显示指向 anchor 的小三角,并以该点为原点轻微缩放、位移和淡入淡出。offset 控制浮层边缘与触发区域的距离;默认值保持原有布局。箭头在窗口边缘钳制位置后仍对准触发区域,内容在内部滚动。动效继承主题时长和减少动态效果设置。
IconButton disclosure aria-expanded={open} 自动添加有间距的展开箭头、旋转动效和展开状态;调用方只传主要图标。组合按钮的尺寸与焦点继续由 OneUI 负责。
内容链接与阅读字号(0.6.21)
Link 从根入口或 @robert-stackflow/oneui/Link 导入。默认使用主题链接色;variant="subtle" 继承周围标题或正文的颜色与字体,统一 hover、键盘焦点和 disabled 行为,保留原生链接与新标签打开语义。应用无需单独定义标题链接样式。
Markdown 支持 fontSize={18},正文与相对字号标题共同缩放;不传时保持默认 14 px。有限数值限制在 10–48 px,非有限数值沿用默认值。
可读前景色(0.6.16 候选)
--accent-ink 用于文字、小图标、细标记和焦点;按钮实心填充仍使用 --accent-fill / --accent。极简主题的 ink 随前景色变化,蓝白主题使用相应模式的强调色;自定义强调色混入当前前景色后用于 ink,避免深色模式中的深灰标记或浅色模式中的白色标记不可见。可单独覆盖此变量。徽标、链接、步骤、菜单/单选标记、进度、图表、时间线及信息反馈复用这套颜色。
导航指示线(0.6.15 候选)
indicator 风格的 SideBar 使用当前主题的导航前景色绘制细线,极简深色不再复用深灰强调色。自定义强调色会混入当前前景色,保持浅、深模式中的可辨识度;可通过 --navigation-indicator-color 覆盖。此颜色独立于按钮填充色,几何和滑动动画保持原有行为。
设置项(0.6.14 候选)
基础 SettingRow 保留自由组合的右侧内容,支持 titleBold(默认 false)、align 和 controlWidth。派生组件复用同一行布局:TextSettingRow、TextAreaSettingRow、SwitchSettingRow、SelectSettingRow,均支持 title、description、divider、titleBold;文本控件支持原生输入属性、ref、hint 和 error,onChange 直接返回文本值,开关返回 boolean。下拉选择继承 Select 的 options/value/onChange 等属性。
<TextSettingRow title="工作区名称" titleBold value={name} onChange={setName} />
<TextAreaSettingRow title="个人简介" value={bio} onChange={setBio} rows={3} />
<SwitchSettingRow title="接收通知" checked={enabled} onChange={setEnabled} />
<SelectSettingRow title="默认语言" value={language} onChange={setLanguage} options={languages} />
派生项将标题关联到控件,说明文字并入可访问描述;有宽度设置的输入/选择项在窄屏自动上下排列,多行文本的标题默认顶部对齐。所有组件和 Props 类型可从根入口导入,也提供各自同名子路径。示例入口为 /forms?view=settings。Form / FormField / FormGroup 继续负责可提交表单的布局、分组和校验,不要求用 SettingRow 替代。
提示与对话框(0.6.14 候选)
Dialog、MessageDialog、ConfirmDialog、FormDialog 统一使用浅底 footer、顶部细分割线和靠右的操作按钮。标题与正文左对齐,状态提示用小尺寸语义图标区分,不使用居中大图标布局。Dialog 可传 headerIcon 与 tone="neutral" | "info" | "success" | "warning" | "danger"。MessageDialog 可通过 onAction 为主按钮提供异步操作,closeLabel 设置其文字;等待期间禁止重复提交和关闭,失败时保留内容并显示 Banner,返回 false 可保留弹窗。
Banner 的 soft 使用低对比底色,outline 使用中性细边框,filled 提高底色强度;标题、正文和语义图标分层显示。操作和详情开关位于正文下方的同一行,关闭独立放在右上角。详情开关复用文字按钮,hover 无按钮底色;窄屏操作自然换行,减少动效时仍可完整展开。
阅读与反馈(0.6.13 候选)
TableOfContents 使用 contentRef 发现标题,或接收 {id,label,level?}[]。variant="tree" 使用层级路径、高亮可见标题范围和沿路径移动的定位点;几何只在尺寸变化时测量,滚动由 IntersectionObserver 更新状态。设计参考 Fumadocs TOC。variant="rail" 是独立的沉浸式样式,只标记当前阅读位置,短线 hover 有局部伸展,旁边的卡片使用无层级、无编号的平面目录。短线本身是可点击的原生锚点链接。hoverMode="single" | "nearby" | "all" 控制悬浮卡片展示当前标题与摘要、附近目录或全部目录,previewCount(默认 5)控制附近目录的总条数,depthBasedLength 可按层级改变短线基准长度。显式 items 可提供 description,自动发现时提取标题后的段落摘要。浮层跟随当前 hover 的短线定位,支持自动定位、键盘焦点、Escape、视口翻转及全局滚动条;不会接管正文之外的滚动。
Table、DataTable、TreeTable、TableMarkup 共用 TableAppearance:striped、density="comfortable" | "compact"、hoverable、bordered、stickyHeader。选中行优先于交替行和 hover,层级表格只保留一层外框。
提示横幅主入口改为 Banner,两端应用与示例均使用新名称。支持五种 tone、soft/outline/filled 变体、compact、图标、标题、操作、关闭,以及受控或内部状态的可展开 details。旧的 Alert 仅保留兼容别名,不维护另一套实现。
Toast 沿用现有深色/柔和外观。通知服务保存独立队列,位移使用 transform 过渡;新消息入栈、移除后补位,hover/聚焦暂停当前位置组的计时。ToastProvider maxVisible 默认 3;notify(message,{id:"task"}) 用同一个 id 原位更新异步任务。createToaster().getSnapshot() 仍返回最新一条,getQueueSnapshot() 返回队列;带 id 的 dismiss/clear 只影响对应消息,无参时作用于全部消息。旧关闭函数不会影响后来的其他消息。
Markdown
从 @robert-stackflow/oneui/Markdown 导入 <Markdown>{source}</Markdown>。统一渲染 CommonMark/GFM 标题、列表、引用、表格、任务列表、链接、图片、删除线与脚注;代码块带语言标记和复制操作,长代码和表格在各自区域滚动,排版跟随 OneUI 主题。默认省略原始 HTML,保留解析器的 URL 过滤;外部 HTTP 链接打开新标签页,相对链接和文档内锚点保留当前页面跳转。
可通过 className 设置外层布局,components 覆盖单个元素的渲染。每个实例使用独立脚注 ID,允许在同一页面并排展示。基于 [email protected] 和 [email protected],无需应用再配置解析器。示例位于「数据展示 → Markdown」。
高亮主题通过 codeTheme="auto" | "github-light" | "github-dark" | "nord" | "dracula" 选择。语法解析由 lowlight 生成虚拟节点,不注入 HTML;未知语言保留原文。表格直接使用 TableMarkup,可用 tableProps 传入相同的 TableAppearance 选项。标题和脚注 ID 按实例隔离,可直接与 TableOfContents 联动。
导航组件(0.6.11 候选)
SideBar 统一纵向导航,通过 variant="sidebar" | "card" | "indicator" 切换普通侧栏、卡片背景、透明背景加左侧指示线。三种样式共用结构和状态,选中底板保持固定宽度并沿纵向移动,指示线与底板同步。groupHeaderBold(默认 false)控制分组标题整体加粗,单个分组的 headerBold 可覆盖;AppShell 和 SettingsLayout 同样支持整体设置。侧栏和卡片保留组间分隔线;指示线样式默认静态分组。
使用 items/groups、active、onNavigate 和可选 basePath;默认保留真实链接及修饰键打开行为,mode="buttons" 适用于设置等页内切换。项目使用稳定 id 与 path,通过 children 嵌套。expandedIds / defaultExpandedIds / onExpandedChange 控制嵌套展开,collapsedGroupIds / defaultCollapsedGroupIds / onCollapsedGroupsChange 控制分组折叠。组的 collapsible 可覆盖默认行为。
item 的 trailing 可放数字、圆点、Spinner 或自定义组件,布局与导航按钮、展开按钮分开,支持独立交互。分组的 trailing 也接受 ({collapsed, active}) => ReactNode;例如 trailing: ({collapsed}) => collapsed ? <Badge>4</Badge> : null 仅在折叠时显示数量,不自动插入圆点。
分组和嵌套通过同一套高度/透明度过渡展开,允许中途反向收起,关闭内容立即进入 inert 状态且保留内部状态。方向键上下、Home/End 移动焦点,左右键展开/收起嵌套;减少动态效果时直接切换。AppShell 共享桌面/移动导航状态,SettingsLayout 使用 SideBar variant="indicator" 并保留原有 value/groups API,窄屏使用分类选择器。旧的 SidebarNavigation、NavigationButton、Indicator 导出继续兼容。
NavBar 独立提供顶部品牌、链接、下拉导航和 actions,使用 items、active 及可选 onNavigate;窄屏切换到抽屉,未提供回调时保留浏览器跳转。示例位于「导航组件 → 侧栏与导航条」。
浮层滚动(0.6.10 候选)
Popover 默认由外框滚动,最大高度为 360px。带独立列表的组合组件使用 scrollable={false},通过弹性布局让列表在可用空间内收缩,搜索和底部操作保持固定。日历与图标、字符选择器使用 maxHeight={Infinity} 按内容展开,仍受视口边距限制;空间不足时日历保留外框滚动,图标和字符只滚动内部列表。
设置项与表单
统一设置行使用 SettingRow,提供 title、description、右侧 children 和 divider;设置页面容器为 SettingsLayout / SettingsCard。表单以 Form + FormField + useFormState 组合,FormGroup 负责相关字段分组,FormErrorSummary 统一展示错误,FormDialog 提供表单弹窗。Form 支持 stacked / inline 布局及 labelWidth,提交时自动维护 busy/disabled 状态。
基础输入包括 Input、Textarea、PasswordField、NumberInput、SearchField、PinInput、TagInput、ShortcutInput;选择包括 Checkbox、RadioGroup、Switch、Select、Combobox、MultiSelect、TreeSelect、Cascader、Segmented。日期、颜色、图标、文件与滑动输入继续使用对应的 DatePicker / DateRangePicker、ColorField / ColorPicker、IconPicker / EmojiPicker / SymbolPicker、FilePicker / Upload、Slider / RangeSlider。
标签创建与导航分组(0.5.1)
MultiSelect display="tags" 将可移除标签放在同一个触发框内;可选 onCreate(label) 返回新选项值(支持 Promise)。搜索结果中显示创建入口,成功后连续多选;错误可重试,关闭或卸载后不再应用异步结果。原有摘要模式和静态选项用法保持兼容。
<MultiSelect label="标签" display="tags" value={tags} options={options}
onChange={setTags} onCreate={name => name} />
AppShell.navigationGroups 支持 {id, label?, items} 分组,主导航、标签或其他入口共享分隔与标题样式,桌面与移动抽屉一致;不传时沿用 navigation。Page.title 可省略或传 ReactNode,可在无标题内容区复用共享入场动画。
操作面板(0.5.0)
ActionSheet 使用带无障碍名称的更多图标按钮打开底部操作面板;items 支持 label、icon、disabled、danger 和 action。选择后先完成关闭和焦点恢复,再执行操作,可安全打开另一个弹窗。桌面为紧凑面板,手机贴合底部安全区域。
<ActionSheet
label="图片操作 1"
title="图片操作"
items={[
{ label: "编辑图片", action: editPhoto },
{ label: "移除图片", danger: true, action: removePhoto },
]}
/>
横向 FormField 内包含 Textarea 时,标签自动靠上排列;手机仍使用纵向布局。Drawer.onAfterClose 可在关闭动画与焦点恢复完成后执行后续操作。
嵌套侧栏(0.6.0)
AppShell.navigation 支持多层 children。异步节点可传 expandable: true,通过 content 提供加载、空状态或重试操作。链接与展开按钮分离,保留新标签打开行为,展开按钮支持键盘和 aria-expanded / aria-controls。
expandedIds + onExpandedChange 提供受控展开;defaultExpandedIds 提供非受控初值。展开以稳定节点 ID 保存,刷新数据或整体折叠不丢失状态。宿主根据路由主动展开祖先,并自行决定何时请求子节点。桌面和手机抽屉共用状态;桌面整体折叠不会隐藏手机抽屉的节点文字。
可单独使用 SidebarNavigation,支持 items、active、onNavigate、basePath、compact 和相同的展开参数。原有扁平导航和导入路径保持兼容。
使用
npm install @robert-stackflow/oneui
import {
ThemeProvider,
UIProvider,
ToastProvider,
Button,
} from "@robert-stackflow/oneui";
import "@robert-stackflow/oneui/styles/themes.css";
import "@robert-stackflow/oneui/styles/base.css";
export function App() {
return (
<ThemeProvider theme="minimal" mode="system">
<UIProvider>
<ToastProvider>
<Button variant="primary">继续</Button>
</ToastProvider>
</UIProvider>
</ThemeProvider>
);
}
theme 支持 minimal(极简)、blue(蓝白);mode 支持 light、dark、system。也可以给 ThemeProvider 传入由 appearanceDefaults()、normalizeAppearance() 产生的完整 appearance,与 AppearanceSettings 共同使用。外观配置的保存与恢复由应用负责。
ThemeProvider 管理当前文档的主题,应用根部放置一个即可。颜色模式、动效、字体、密度、圆角和自定义配色均由共享层应用。
蓝白默认色板取自 Moment 的 admin/src/styles.css 最终变量及 admin/src/main.tsx 的 victoria 色阶:浅色背景 #F5F7FB、面板 #FFFFFF、正文 #25324A、次级文字 #8592A8、边框 #E3E8F1。导航/焦点蓝为 #5067B8,主按钮为 #4059AA,开关为 #526EDB;深色同样使用 Moment 的对应值。各颜色用途分开配置,不再共用一种近似蓝。
normalizeAppearance 使用 paletteVersion: 2 兼容旧蓝白默认值;非默认自定义配色保留,已带版本的显式颜色也不会被迁移。Desktop 与 Web 共用该规范化函数。参考值和源码摘要保存在 tests/fixtures/moment-palette.json,验证不依赖 Moment 或 Mantine 安装。
蓝白输入与选择控件的 hover 边框复用 Moment 的混色规则:强调色 35% 与基础边框色混合。聚焦或展开时使用完整强调色,错误状态保持错误色;禁用、只读控件不响应 hover。图标、Emoji 和符号选择器的分类栏使用透明背景与主题选中块,不继承普通块状标签页的底板和投影。
边界
AppShell通过导航数据和onNavigate接入任何路由或状态机制。headerActions、sidebarFooter、windowControls为应用能力插槽;draggable仅标记 header 的拖动区域。UIProvider配置 Toast 位置、Tooltip 位置和必填标记;组件不读取宿主设置。- 普通组件也支持子路径导入,例如
@robert-stackflow/oneui/Input。 - 页面直接复用 Form / FormField / useFormState 的校验与提交状态、Alert 的错误反馈和 ConfirmDialog 的确认流程;避免另写原生表单、按钮样式或错误提示。
- Button 支持
href,渲染标准链接并沿用按钮样式;禁用或加载时阻止导航。Card 传入onClick后渲染可通过键盘激活的按钮卡片。导航与业务回调由消费应用提供。 - CSS Modules 随组件分发,由消费项目的构建工具处理。图标/字符目录通过标准
new URL(..., import.meta.url)加载随包资源。 - React 和 React DOM 为 peer dependencies(^19.2.0),不打入组件包。独立消费验证覆盖 React 19.3.1,以及 React 19.3.0 + TypeScript 5.9.3;浏览器验证使用 Chromium。
弹窗与加载状态
Dialog 用于自定义内容,支持 small / medium / large。MessageDialog 提供语义图标、消息与主操作按钮,ConfirmDialog 提供普通、危险操作确认,FormDialog 提供有统一字段间距的表单及保存操作。三者均可从根入口或对应组件子路径导入。
确认与表单支持异步回调,等待期间禁止重复提交及关闭,失败时保留内容并显示错误;回调返回 false 可保留弹窗。FormDialog 接收共享 useFormState 的 controller,字段使用 FormField,onSubmit 接收校验后的 values,错误样式与普通 Form 一致。
LoadingState 复用 Spinner;showLabel={false} 仅显示动画,fullPage 用于页面内容区域。应用自行放置等待边界,保留路由外壳。
以上 API 从 0.2.0 起提供。
概览卡片与内置目录(0.4.2)
Card 可通过 title、description、action 组合概览与操作;省略 children 时不会渲染空白正文。图标选择器不传 options 即使用库内置的完整目录;valuePrefix="lucide:" 可保留宿主保存值的前缀。图标、Emoji 和符号资源均由组件库按需加载,支持 Vite 的开发预打包与生产构建,宿主无需维护另一套选项或资源加载器。
弹窗焦点与分页(0.4.1)
Dialog 和 Drawer 在普通关闭、条件卸载时都会恢复触发控件的焦点,支持 StrictMode 和嵌套弹窗。异步打开导致触发按钮在加载期间失焦时,可传入 returnFocus={triggerRef} 明确返回位置;已卸载、禁用或位于其他模态层之外的目标不会被聚焦。
Pagination 的 total 是总条数,pageSize 是每页条数。compact 移除外层留白并在窄屏重排页码、统计和跳转,size="small" 使用当前主题的小尺寸控件;正常和紧凑模式均支持长页码列表换行。宿主可通过 className 调整外层位置。
<Pagination
compact
size="small"
page={page}
pageSize={10}
total={records.total}
onPageChange={setPage}
/>
层级选择、补全与标签
TreeSelect variant="category"在原有树选择交互上增加图标底块、子项数量与层级连线;默认variant="tree"保留简洁样式。两种样式均支持搜索、父子联动、多选、禁用、异步加载及键盘操作。HierarchyOption.count可单独提供数量。TreeTable复用Table的列定义,接收rows、rowKey、getChildren、rowLabel;通过expanded/onExpandedChange控制展开,或使用defaultExpanded/defaultExpandAll。treeColumn默认为第一列,支持多级连线、方向键导航及独立行操作。Column.align、Column.width可用于对齐和列宽。Combobox mode="input"直接在触发框输入并在下方显示候选,保留loadOptions、onCreate和键盘选择。输入中的文本只用于搜索;选择候选或创建成功才提交选项值,关闭时恢复已选标签。默认mode="panel"继续使用弹层内搜索。- 需要自由文本补全时加
allowCustomValue:每次输入直接更新值,失焦或回车不会强制替换成候选;方向键后回车或点击仍可选择候选。onChange(value, option?)在主动选择时提供对应选项,自由输入时不提供选项,可用于区分手写地点与带坐标的地点候选。 MultiSelect display="tags"在触发框内展示可移除标签,复用TagInput的标签样式;默认display="summary"保留文字摘要。两种模式共享连续选择、搜索、数量限制和表单值。只读标签不可移除,禁用选项保持不可操作。
菜单项间距统一为 --menu-item-spacing: 1px,菜单快捷键为靠右的纯文字提示,不预留空的指示器位置。Desktop 与 Web 共用示例:/pickers?view=hierarchy、/forms?view=advanced、/data?view=hierarchy;层级表格中的增删改仅更新示例内存数据。以上扩展从 0.2.0 起提供。
接入现有应用(0.3.0)
AppShell 默认仍显示 OneUI 品牌和顶部栏。可通过 brandName、brandIcon 设置品牌,或用 brand 替换完整品牌内容;homePath 指定宿主内部首页,basePath="/admin" 为实际链接添加基础路径。onNavigate 收到未加基础路径的内部路径,适合 React Router basename;Ctrl/Cmd 点击和新标签页使用完整 href。showHeader={false} 保留移动端浮动导航按钮。settingsItem 和 onCollapsedChange 可省略。自定义完整品牌内容由宿主负责紧凑侧栏适配。
<AppShell
brandName="Moment"
brandIcon={<Brand />}
homePath="/workbench"
basePath="/admin"
showHeader={false}
navigation={navigation}
active={active}
title={title}
onNavigate={navigate}
>
{children}
</AppShell>
组件外通知使用独立服务,并将同一实例交给 Provider;不需要在 API 模块调用 Hook。未挂载时保留最新消息;notify 返回该消息的关闭函数,旧消息的关闭函数不会关闭新消息。dismiss 触发关闭动画,clear 立即清除。默认仍只显示最新一条通知,多个控制器相互隔离;服务端渲染时应为每个应用/请求创建实例。
// notifications.ts
export const notifications = createToaster();
export const notifyError = (error: Error) =>
notifications.notify(error.message, { tone: "error" });
// application root
<ToastProvider controller={notifications}>{children}</ToastProvider>;
IconPicker valuePrefix="lucide:" 将内置图标名转换为带前缀的存储值;options 可提供 { value, label, icon, keywords?, category? } 自定义目录,选择后原样返回 value。自定义目录不加载内置图标资源,设置 options 时不应用 valuePrefix。IconPickerProps、IconPickerOption 可从根入口或组件子路径导入。
DatePicker mode="datetime" timeZone="Asia/Shanghai" seconds clearable 使用指定 IANA 时区的今天/此刻,并支持清空及 placeholder。value 始终是 YYYY-MM-DDTHH:mm[:ss] 日历字段字符串;时区参数不把输入值变成时间戳,也不校验夏令时缺口/重复时间。定时发布、UTC 换算和时区歧义规则由宿主业务负责。无 timeZone 时继续使用设备本地时间。
分类迁移时,如原应用父子分类独立选择,使用 TreeSelect multiple checkStrictly;图片上传的 EXIF/GPS、接口结果转换、串行策略(concurrency=1)与未保存状态属于宿主逻辑。
构建与打包
在仓库根目录运行 npm run pack:ui。源码工作区供两端即时开发使用;发布内容单独生成到本目录 dist,含 ESM、TypeScript 声明、CSS 和资源。构建使用干净的临时目录并检查所有包内引用,成功后替换 dist,避免旧文件混入。dist/package.json 只导出生成内容。
发布内容已通过独立 Vite 消费项目验证。包的许可为 UNLICENSED;资源目录内保留图标许可文件。
设置与账户项目
Form / FormGroup / FormField 的 layout 支持 stacked(默认)与 inline,子字段继承并可覆盖;labelWidth 可设数字像素或 CSS 宽度。inline 在 700px 以下自动改为上下排列。标签、帮助、错误与控件共享同一字段状态。
NumberInput 使用 nullable value={numberOrNull} onChange={setValue} 允许留空,清空并提交时返回 null;默认数字 API 保持不变。
AccountItem 接受标题、图标、描述、元数据及操作。PasskeyItem 和 SessionItem 封装通行密钥及登录会话布局,回调只通知宿主操作;不依赖账户接口或日期格式。
0.6.0 应用组合
新增嵌套 SidebarNavigation、ResourceCard、AuthLayout、PageHeading、TableMarkup、布局与文本基础组件、CopyButton 和 BarChart。Surface 复用 Card 的样式,TableMarkup 复用 Table 的样式,页面标题复用 Page。普通下拉继续使用 Select;带搜索的选择直接使用既有 Combobox,搜索栏使用 SearchField;数据概览使用 Statistic,键值详情使用 Descriptions,不增加另一套选择或统计样式。
扩展 Menu 的自定义触发内容、NumberInput 单位、Pagination 汇总显隐、Badge 原生属性、Tooltip 禁用选项及 Button 文字链接。修复日历月份标签碰撞和宽度适配,表格容器继承外框圆角。既有导出与默认 API 保持兼容。
验证:npm run test:sidebar-navigation、npm run test:visual-foundations、npm run test:account-forms、npm run test:modal-pagination、npm run test:package。新增回归覆盖 React 间距覆盖、正文颜色、表格圆角与文字按钮、现有 Combobox 键盘筛选、日期标签和窄屏溢出。
Menu 支持 iconOnly:<Menu iconOnly label="更多操作" items={items} /> 复用 IconButton,默认显示更多图标;trigger 可替换图标,variant 可设置按钮类型。