diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1e5a8d2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,50 @@ +# AGENTS.md + +## Cursor Cloud specific instructions + +### Overview + +This is a **Nova UI** monorepo — an enterprise React component library with two workspaces: + +- `packages/ui` — the publishable `@wuyangfan/nova-ui` NPM package (~80 source files, 6 component categories) +- `docs` — a VitePress documentation site with live React playground (via `react-live`) + +No databases, Docker, or backend services are required. Only Node.js 20+ and npm. + +### Key commands + +All commands run from the workspace root. See `package.json` for the full list. +This is an npm workspaces monorepo with two packages: + +- `packages/ui` — publishable React component library (`@wuyangfan/nova-ui`) +- `docs` — VitePress documentation site with interactive playground + +### Key commands (run from workspace root) + +| Task | Command | +|---|---| +| Install deps | `npm install` | +| Build UI lib | `npm run build:ui` | +| Lint | `npm run lint:ui` | +| Typecheck | `npm run typecheck:ui` | +| Docs dev server | `npm run dev:docs` (port 5173) | +| UI dev server | `cd packages/ui && npm run dev` (port 5174 if docs running) | +| Full build | `npm run build` | + +### Important caveats + +- **Build order matters**: the docs workspace has a `file:` dependency on `../packages/ui`. You must run `npm run build:ui` before `npm run dev:docs` or `npm run build:docs` — otherwise the docs site will fail to resolve the component library. +- **No test framework**: there are currently no automated tests (no Jest, Vitest, or testing-library). Lint (`npm run lint:ui`) and typecheck (`npm run typecheck:ui`) are the primary code-quality checks. +- The docs dev server runs at `http://localhost:5173/react-ui-library/` (note the base path). +- The standalone UI dev server at `packages/ui` uses Vite and serves at `http://localhost:5174/` (or the next available port). +| Lint UI | `npm run lint:ui` | +| Typecheck UI | `npm run typecheck:ui` | +| Dev docs server | `npm run dev:docs` | +| Build docs | `npm run build:docs` | +| Build all | `npm run build` | + +### Caveats + +- You must run `npm run build:ui` before `npm run dev:docs` so the docs package can resolve `@wuyangfan/nova-ui` (it uses a `file:` reference to the local build output). +- The docs dev server runs at `http://localhost:5173/react-ui-library/` (note the base path). +- No databases, Docker, or external services are required. diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 4da9da4..d0fc7c0 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -6,7 +6,11 @@ export default defineConfig({ title: 'Nova UI', description: 'Enterprise React component library with TypeScript and Tailwind CSS', lang: 'zh-CN', + appearance: true, themeConfig: { + darkModeSwitchLabel: '主题', + lightModeSwitchTitle: '切换到浅色模式', + darkModeSwitchTitle: '切换到深色模式', logo: '/favicon.svg', nav: [ { text: '指南', link: '/guide/introduction' }, @@ -20,20 +24,120 @@ export default defineConfig({ items: [ { text: '项目介绍', link: '/guide/introduction' }, { text: '快速开始', link: '/guide/getting-started' }, + { text: '可访问性', link: '/guide/accessibility' }, + { text: '文档优化 TODO', link: '/guide/docs-optimization-todo' }, ], }, ], '/components/': [ + { text: '概览', link: '/components/overview' }, { - text: '组件文档', + text: '布局', + collapsed: false, items: [ - { text: '概览', link: '/components/overview' }, - { text: '布局组件', link: '/components/layout' }, - { text: '基础组件', link: '/components/basic' }, - { text: '表单组件', link: '/components/form' }, - { text: '反馈组件', link: '/components/feedback' }, - { text: '数据组件', link: '/components/data' }, - { text: '导航组件', link: '/components/navigation' }, + { text: 'Container 布局容器', link: '/components/container' }, + { text: 'Row 行', link: '/components/row' }, + { text: 'Col 列', link: '/components/col' }, + { text: 'Grid 网格', link: '/components/grid' }, + { text: 'Flex 弹性布局', link: '/components/flex' }, + { text: 'Space 间距', link: '/components/space' }, + { text: 'Divider 分割线', link: '/components/divider' }, + { text: 'SplitPane 分栏', link: '/components/split-pane' }, + ], + }, + { + text: '基础', + collapsed: false, + items: [ + { text: 'Button 按钮', link: '/components/button' }, + { text: 'Icon 图标', link: '/components/icon' }, + { text: 'Typography 排版', link: '/components/typography' }, + ], + }, + { + text: '表单', + collapsed: false, + items: [ + { text: 'Input 输入框', link: '/components/input' }, + { text: 'InputNumber 数字输入框', link: '/components/input-number' }, + { text: 'AutoComplete 自动完成', link: '/components/auto-complete' }, + { text: 'Select 选择器', link: '/components/select' }, + { text: 'Checkbox 多选框', link: '/components/checkbox' }, + { text: 'Radio 单选框', link: '/components/radio' }, + { text: 'Switch 开关', link: '/components/switch' }, + { text: 'DatePicker 日期选择', link: '/components/date-picker' }, + { text: 'TimePicker 时间选择', link: '/components/time-picker' }, + { text: 'Slider 滑动条', link: '/components/slider' }, + { text: 'Rate 评分', link: '/components/rate' }, + { text: 'Upload 上传', link: '/components/upload' }, + { text: 'Form 表单', link: '/components/form' }, + { text: 'Calendar 日历', link: '/components/calendar' }, + { text: 'Transfer 穿梭框', link: '/components/transfer' }, + { text: 'Cascader 级联选择', link: '/components/cascader' }, + { text: 'TreeSelect 树选择', link: '/components/tree-select' }, + { text: 'ColorPicker 颜色选择', link: '/components/color-picker' }, + { text: 'Segmented 分段控制器', link: '/components/segmented' }, + { text: 'Mentions 提及', link: '/components/mentions' }, + ], + }, + { + text: '反馈', + collapsed: false, + items: [ + { text: 'Alert 警告提示', link: '/components/alert' }, + { text: 'Modal 对话框', link: '/components/modal' }, + { text: 'Drawer 抽屉', link: '/components/drawer' }, + { text: 'Toast 轻提示', link: '/components/toast' }, + { text: 'Tooltip 文字提示', link: '/components/tooltip' }, + { text: 'Popover 气泡卡片', link: '/components/popover' }, + { text: 'Popconfirm 气泡确认', link: '/components/popconfirm' }, + { text: 'Loading 加载中', link: '/components/loading' }, + { text: 'Spin 加载动画', link: '/components/spin' }, + { text: 'Skeleton 骨架屏', link: '/components/skeleton' }, + { text: 'Notification 通知', link: '/components/notification' }, + { text: 'Tour 漫游式引导', link: '/components/tour' }, + { text: 'Watermark 水印', link: '/components/watermark' }, + ], + }, + { + text: '数据展示', + collapsed: false, + items: [ + { text: 'Table 表格', link: '/components/table' }, + { text: 'List 列表', link: '/components/list' }, + { text: 'Card 卡片', link: '/components/card' }, + { text: 'Carousel 走马灯', link: '/components/carousel' }, + { text: 'Tag 标签', link: '/components/tag' }, + { text: 'Badge 徽标', link: '/components/badge' }, + { text: 'Avatar 头像', link: '/components/avatar' }, + { text: 'Image 图片', link: '/components/image' }, + { text: 'Pagination 分页', link: '/components/pagination' }, + { text: 'Progress 进度条', link: '/components/progress' }, + { text: 'Statistic 统计数值', link: '/components/statistic' }, + { text: 'Descriptions 描述列表', link: '/components/descriptions' }, + { text: 'Empty 空状态', link: '/components/empty' }, + { text: 'Result 结果', link: '/components/result' }, + { text: 'Timeline 时间轴', link: '/components/timeline' }, + { text: 'QRCode 二维码', link: '/components/qrcode' }, + { text: 'ImagePreview 图片预览', link: '/components/image-preview' }, + { text: 'VirtualList 虚拟列表', link: '/components/virtual-list' }, + ], + }, + { + text: '导航', + collapsed: false, + items: [ + { text: 'Tabs 标签页', link: '/components/tabs' }, + { text: 'Menu 菜单', link: '/components/menu' }, + { text: 'Breadcrumb 面包屑', link: '/components/breadcrumb' }, + { text: 'Dropdown 下拉菜单', link: '/components/dropdown' }, + { text: 'Steps 步骤条', link: '/components/steps' }, + { text: 'Collapse 折叠面板', link: '/components/collapse' }, + { text: 'Tree 树形控件', link: '/components/tree' }, + { text: 'Anchor 锚点', link: '/components/anchor' }, + { text: 'Affix 固钉', link: '/components/affix' }, + { text: 'BackTop 回到顶部', link: '/components/back-top' }, + { text: 'FloatButton 悬浮按钮', link: '/components/float-button' }, ], }, ], diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index 399575e..dca50b4 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -43,3 +43,43 @@ background: #7f1d1d; color: #fff; } + +.comp-overview-grid { + display: grid; + gap: 12px; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + margin: 18px 0; +} + +.comp-overview-card { + display: block; + border: 1px solid var(--vp-c-divider); + border-radius: 12px; + padding: 14px; + background: var(--vp-c-bg-soft); + text-decoration: none; + transition: border-color 0.2s ease, transform 0.2s ease; +} + +.comp-overview-card:hover { + border-color: var(--vp-c-brand-1); + transform: translateY(-1px); +} + +.comp-overview-card h3 { + margin: 0 0 6px; + color: var(--vp-c-text-1); +} + +.comp-overview-card p { + margin: 0 0 8px; + color: var(--vp-c-brand-1); + font-size: 13px; +} + +.comp-overview-card code { + display: block; + white-space: normal; + line-height: 1.6; + color: var(--vp-c-text-2); +} diff --git a/docs/components/affix.md b/docs/components/affix.md new file mode 100644 index 0000000..98fce7a --- /dev/null +++ b/docs/components/affix.md @@ -0,0 +1,22 @@ +# Affix 固钉 + +将元素钉在可视范围内。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| offsetTop | 距顶偏移 | `number` | `0` | +| children | 内容 | `ReactNode` | - | diff --git a/docs/components/alert.md b/docs/components/alert.md new file mode 100644 index 0000000..28a828d --- /dev/null +++ b/docs/components/alert.md @@ -0,0 +1,32 @@ +# Alert 警告提示 + +警告提示组件,对齐 Ant Design Alert。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| type | 类型 | `'info' \| 'success' \| 'warning' \| 'error'` | `'info'` | +| message | 提示内容 | `ReactNode` | - | +| description | 描述文案 | `ReactNode` | - | +| closable | 可关闭 | `boolean` | `false` | +| showIcon | 显示图标 | `boolean` | `true` | +| icon | 自定义图标 | `ReactNode` | - | +| banner | 横幅模式 | `boolean` | `false` | +| action | 操作区域 | `ReactNode` | - | +| onClose | 关闭回调 | `() => void` | - | diff --git a/docs/components/anchor.md b/docs/components/anchor.md new file mode 100644 index 0000000..81d799b --- /dev/null +++ b/docs/components/anchor.md @@ -0,0 +1,22 @@ +# Anchor 锚点 + +锚点导航组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| items | 锚点项 | `AnchorItem[]` | - | diff --git a/docs/components/auto-complete.md b/docs/components/auto-complete.md new file mode 100644 index 0000000..40c912d --- /dev/null +++ b/docs/components/auto-complete.md @@ -0,0 +1,37 @@ +# AutoComplete 自动完成 + +输入框自动完成功能,对齐 Ant Design AutoComplete。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| options | 选项数据 | `{ value: string; label?: string }[]` | `[]` | +| value | 当前值 | `string` | - | +| defaultValue | 默认值 | `string` | `''` | +| onChange | 值变化回调 | `(value: string) => void` | - | +| onSelect | 选中回调 | `(value: string) => void` | - | +| filterOption | 过滤方式 | `boolean \| ((input, option) => boolean)` | `true` | +| allowClear | 允许清除 | `boolean` | `false` | +| placeholder | 占位文本 | `string` | - | diff --git a/docs/components/avatar.md b/docs/components/avatar.md new file mode 100644 index 0000000..2bc2917 --- /dev/null +++ b/docs/components/avatar.md @@ -0,0 +1,25 @@ +# Avatar 头像 + +用户头像组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| name | 名称(显示首字母) | `string` | - | +| src | 图片地址 | `string` | - | +| size | 尺寸 | `'sm' \| 'md' \| 'lg'` | `'md'` | diff --git a/docs/components/back-top.md b/docs/components/back-top.md new file mode 100644 index 0000000..54cc372 --- /dev/null +++ b/docs/components/back-top.md @@ -0,0 +1,19 @@ +# BackTop 回到顶部 + +返回页面顶部的按钮。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| visibilityHeight | 滚动高度达到此参数值才出现 | `number` | `400` | diff --git a/docs/components/badge.md b/docs/components/badge.md new file mode 100644 index 0000000..f8317b7 --- /dev/null +++ b/docs/components/badge.md @@ -0,0 +1,24 @@ +# Badge 徽标 + +徽标数组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| count | 展示数字 | `number` | - | +| dot | 显示小红点 | `boolean` | `false` | diff --git a/docs/components/basic.md b/docs/components/basic.md deleted file mode 100644 index 149cee5..0000000 --- a/docs/components/basic.md +++ /dev/null @@ -1,23 +0,0 @@ -# 基础组件 - - - -## API - -| 组件 | 关键属性 | -| --- | --- | -| Button | `variant`, `size`, `loading`, `color`, `disabled` | -| Icon | `name`, `size` | -| Title/Text/Paragraph | `className`, `children` | diff --git a/docs/components/breadcrumb.md b/docs/components/breadcrumb.md new file mode 100644 index 0000000..f447213 --- /dev/null +++ b/docs/components/breadcrumb.md @@ -0,0 +1,24 @@ +# Breadcrumb 面包屑 + +面包屑导航。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| items | 面包屑项 | `BreadcrumbItem[]` | - | +| separator | 分隔符 | `string` | `'/'` | diff --git a/docs/components/button.md b/docs/components/button.md new file mode 100644 index 0000000..6230380 --- /dev/null +++ b/docs/components/button.md @@ -0,0 +1,41 @@ +# Button 按钮 + +按钮用于开始一个即时操作。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| variant | 按钮样式 | `'solid' \| 'outline' \| 'ghost'` | `'solid'` | +| size | 尺寸 | `'sm' \| 'md' \| 'lg'` | `'md'` | +| color | 颜色 | `'primary' \| 'neutral' \| 'danger'` | `'primary'` | +| loading | 加载状态 | `boolean` | `false` | +| disabled | 禁用状态 | `boolean` | `false` | +| icon | 图标 | `ReactNode` | - | diff --git a/docs/components/calendar.md b/docs/components/calendar.md new file mode 100644 index 0000000..ec19f72 --- /dev/null +++ b/docs/components/calendar.md @@ -0,0 +1,22 @@ +# Calendar 日历 + +日历组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| year | 年份 | `number` | - | +| month | 月份 | `number` | - | +| value | 选中日期 | `Date` | - | +| onChange | 日期变化回调 | `(date) => void` | - | diff --git a/docs/components/card.md b/docs/components/card.md new file mode 100644 index 0000000..f136bcd --- /dev/null +++ b/docs/components/card.md @@ -0,0 +1,23 @@ +# Card 卡片 + +通用卡片容器。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| title | 标题 | `ReactNode` | - | +| extra | 右上角操作区 | `ReactNode` | - | diff --git a/docs/components/carousel.md b/docs/components/carousel.md new file mode 100644 index 0000000..69aa931 --- /dev/null +++ b/docs/components/carousel.md @@ -0,0 +1,29 @@ +# Carousel 走马灯 + +轮播组件,对齐 Ant Design Carousel。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| items | 轮播内容 | `ReactNode[]` | - | +| autoplay | 自动播放 | `boolean` | `false` | +| autoplaySpeed | 自动播放速度(ms) | `number` | `3000` | +| dots | 显示指示点 | `boolean` | `true` | +| arrows | 显示箭头 | `boolean` | `true` | diff --git a/docs/components/cascader.md b/docs/components/cascader.md new file mode 100644 index 0000000..7472c2d --- /dev/null +++ b/docs/components/cascader.md @@ -0,0 +1,27 @@ +# Cascader 级联选择 + +级联选择框。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| options | 级联数据 | `CascaderOption[]` | - | +| value | 当前值 | `string[]` | - | +| defaultValue | 默认值 | `string[]` | - | +| onChange | 变化回调 | `(value) => void` | - | diff --git a/docs/components/checkbox.md b/docs/components/checkbox.md new file mode 100644 index 0000000..6287e9b --- /dev/null +++ b/docs/components/checkbox.md @@ -0,0 +1,27 @@ +# Checkbox 多选框 + +多选框组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| checked | 是否选中 | `boolean` | - | +| defaultChecked | 默认选中 | `boolean` | `false` | +| onChange | 变化回调 | `(e) => void` | - | +| label | 标签文本 | `string` | - | +| disabled | 禁用 | `boolean` | `false` | diff --git a/docs/components/col.md b/docs/components/col.md new file mode 100644 index 0000000..aa0b4b2 --- /dev/null +++ b/docs/components/col.md @@ -0,0 +1,24 @@ +# Col 列 + +配合 Row 使用的栅格列组件,12列栅格。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| span | 占据列数 | `number` | - | +| offset | 偏移列数 | `number` | - | diff --git a/docs/components/collapse.md b/docs/components/collapse.md new file mode 100644 index 0000000..5ea90c6 --- /dev/null +++ b/docs/components/collapse.md @@ -0,0 +1,26 @@ +# Collapse 折叠面板 + +可以折叠/展开的内容区域。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| items | 面板项 | `CollapseItem[]` | - | +| activeKey | 展开的 key | `string \| string[]` | - | +| defaultActiveKey | 默认展开 | `string \| string[]` | - | +| accordion | 手风琴模式 | `boolean` | `false` | +| onChange | 变化回调 | `(key) => void` | - | diff --git a/docs/components/color-picker.md b/docs/components/color-picker.md new file mode 100644 index 0000000..c632af5 --- /dev/null +++ b/docs/components/color-picker.md @@ -0,0 +1,22 @@ +# ColorPicker 颜色选择 + +颜色选择器组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| value | 颜色值 | `string` | - | +| defaultValue | 默认颜色 | `string` | - | +| onChange | 颜色变化回调 | `(color: string) => void` | - | +| showValue | 显示颜色值 | `boolean` | `true` | diff --git a/docs/components/container.md b/docs/components/container.md new file mode 100644 index 0000000..21d2951 --- /dev/null +++ b/docs/components/container.md @@ -0,0 +1,25 @@ +# Container 布局容器 + +用于包裹页面内容,提供固定宽度或全宽布局。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| fluid | 是否撑满整个宽度 | `boolean` | `false` | +| className | 自定义类名 | `string` | - | +| style | 自定义样式 | `CSSProperties` | - | diff --git a/docs/components/data.md b/docs/components/data.md deleted file mode 100644 index ecfb611..0000000 --- a/docs/components/data.md +++ /dev/null @@ -1,72 +0,0 @@ -# 数据组件 - - - -## API - -| 组件 | 关键属性 | -| --- | --- | -| Table | `columns(sorter/filters/width/render)`, `dataSource`, `rowKey`, `emptyText`, `title`, `searchable`, `columnConfigurable` | -| Pagination | `current/defaultCurrent`, `total`, `pageSize`, `onChange` | -| Card | `title`, `extra` | -| Tag | `color` | -| Badge | `count`, `dot` | -| Avatar | `name`, `src`, `size` | -| Progress | `percent`, `status`, `showInfo` | -| Statistic | `title`, `value`, `prefix`, `suffix` | -| Descriptions | `items`, `columns` | -| Timeline | `items` | -| Result | `status`, `title`, `subTitle`, `extra` | -| Empty | `description`, `image` | -| QRCode | `value`, `size` | -| ImagePreview | `src`, `alt`, `width`, `height` | -| VirtualList | `items`, `itemHeight`, `height`, `renderItem` | diff --git a/docs/components/date-picker.md b/docs/components/date-picker.md new file mode 100644 index 0000000..adb4bb0 --- /dev/null +++ b/docs/components/date-picker.md @@ -0,0 +1,22 @@ +# DatePicker 日期选择 + +日期选择器组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| value | 当前日期 | `Date \| string` | - | +| defaultValue | 默认日期 | `Date \| string` | - | +| onChange | 日期变化回调 | `(date) => void` | - | +| placeholder | 占位文本 | `string` | - | diff --git a/docs/components/descriptions.md b/docs/components/descriptions.md new file mode 100644 index 0000000..dc28e16 --- /dev/null +++ b/docs/components/descriptions.md @@ -0,0 +1,27 @@ +# Descriptions 描述列表 + +成组展示只读信息。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| items | 描述项 | `DescriptionItem[]` | - | +| columns | 列数 | `number` | `3` | diff --git a/docs/components/divider.md b/docs/components/divider.md new file mode 100644 index 0000000..c024022 --- /dev/null +++ b/docs/components/divider.md @@ -0,0 +1,23 @@ +# Divider 分割线 + +区隔内容的分割线。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| orientation | 方向 | `'horizontal' \| 'vertical'` | `'horizontal'` | diff --git a/docs/components/drawer.md b/docs/components/drawer.md new file mode 100644 index 0000000..3f10bfb --- /dev/null +++ b/docs/components/drawer.md @@ -0,0 +1,28 @@ +# Drawer 抽屉 + +屏幕边缘滑出的浮层面板。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| open | 是否可见 | `boolean` | - | +| onClose | 关闭回调 | `() => void` | - | +| placement | 抽屉方向 | `'left' \| 'right' \| 'top' \| 'bottom'` | `'right'` | +| title | 标题 | `ReactNode` | - | diff --git a/docs/components/dropdown.md b/docs/components/dropdown.md new file mode 100644 index 0000000..25e1e51 --- /dev/null +++ b/docs/components/dropdown.md @@ -0,0 +1,32 @@ +# Dropdown 下拉菜单 + +下拉菜单组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| trigger | 触发元素 | `ReactNode` | - | +| options | 选项列表 | `DropdownOption[]` | - | +| open | 是否显示 | `boolean` | - | +| defaultOpen | 默认显示 | `boolean` | - | +| onOpen | 打开回调 | `() => void` | - | +| onClose | 关闭回调 | `() => void` | - | +| onChange | 选中回调 | `(key: string) => void` | - | diff --git a/docs/components/empty.md b/docs/components/empty.md new file mode 100644 index 0000000..23235f1 --- /dev/null +++ b/docs/components/empty.md @@ -0,0 +1,20 @@ +# Empty 空状态 + +空状态占位组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| description | 描述文字 | `ReactNode` | `'No Data'` | +| image | 自定义图标 | `ReactNode` | - | diff --git a/docs/components/feedback.md b/docs/components/feedback.md deleted file mode 100644 index 98fff6c..0000000 --- a/docs/components/feedback.md +++ /dev/null @@ -1,47 +0,0 @@ -# 反馈组件 - - - - - -## API - -| 组件 | 关键属性 | -| --- | --- | -| Modal | `open`, `onClose`, `title` | -| Drawer | `open`, `onClose`, `placement`, `title` | -| Toast/Message | `open`, `duration`, `status`, `onClose` | -| Tooltip | `content`, `disabled` | -| Popover | `trigger`, `content`, `open/defaultOpen`, `onOpen/onClose` | -| Loading | `text`, `size` | -| Notification | `type`, `title`, `description`, `open`, `onClose` | -| Tour | `steps`, `open/defaultOpen`, `current`, `onChange`, `onClose` | -| Watermark | `content`, `children` | diff --git a/docs/components/flex.md b/docs/components/flex.md new file mode 100644 index 0000000..adc9ab2 --- /dev/null +++ b/docs/components/flex.md @@ -0,0 +1,38 @@ +# Flex 弹性布局 + +基于 CSS Flexbox 的弹性布局容器组件,类似 Ant Design Flex。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| direction | 主轴方向 | `'row' \| 'column' \| 'row-reverse' \| 'column-reverse'` | `'row'` | +| vertical | 是否垂直方向(等价 direction='column') | `boolean` | `false` | +| align | 交叉轴对齐 | `'start' \| 'center' \| 'end' \| 'stretch' \| 'baseline'` | - | +| justify | 主轴对齐 | `'start' \| 'center' \| 'end' \| 'between' \| 'around' \| 'evenly'` | - | +| wrap | 是否换行 | `boolean` | `false` | +| gap | 间距 | `number \| string` | - | diff --git a/docs/components/float-button.md b/docs/components/float-button.md new file mode 100644 index 0000000..cb4c6e6 --- /dev/null +++ b/docs/components/float-button.md @@ -0,0 +1,27 @@ +# FloatButton 悬浮按钮 + +固定在页面角落的悬浮按钮,对齐 Ant Design FloatButton。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| icon | 图标 | `ReactNode` | - | +| tooltip | 提示文字 | `string` | - | +| shape | 形状 | `'circle' \| 'square'` | `'circle'` | +| position | 位置 | `{ right?: number; bottom?: number }` | `{ right: 24, bottom: 24 }` | diff --git a/docs/components/form.md b/docs/components/form.md index 2631c4a..28e31c6 100644 --- a/docs/components/form.md +++ b/docs/components/form.md @@ -1,93 +1,49 @@ -# 表单组件 +# Form 表单 + +表单组件,配合 FormItem 使用,提供验证、布局与提交能力。 + +## 示例 ## API -| 组件 | 关键属性 | -| --- | --- | -| Input | `value/defaultValue`, `onValueChange`, `prefix/suffix`, `size` | -| Select | `options`, `value/defaultValue`, `onChange` | -| Checkbox | `checked/defaultChecked`, `onChange` | -| Radio | `value`, `name`, `onChange` | -| Switch | `checked/defaultChecked`, `onChange` | -| DatePicker | `value/defaultValue`, `onChange` | -| Form / FormItem | `initialValues`, `rules(minLength/maxLength/pattern/when/validator)`, `dependencies`, `onSubmit` | -| Upload | `accept`, `multiple`, `fileList`, `onChange` | -| Slider | `min/max/step`, `value/defaultValue`, `onChange` | -| Rate | `count`, `value/defaultValue`, `allowClear`, `onChange` | -| Calendar | `year/month`, `value`, `onChange` | -| Transfer | `dataSource`, `targetKeys/defaultTargetKeys`, `onChange` | -| Cascader | `options`, `value/defaultValue`, `onChange` | -| TreeSelect | `data`, `value/defaultValue`, `onChange` | -| ColorPicker | `value/defaultValue`, `onChange`, `showValue` | -| Segmented | `options`, `value/defaultValue`, `onChange` | -| Mentions | `options`, `value/defaultValue`, `onChange` | +### Form + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| initialValues | 初始值 | `Record` | - | +| onSubmit | 提交回调 | `(values) => void` | - | + +### FormItem + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| name | 字段名 | `string` | - | +| label | 标签 | `string` | - | +| rules | 验证规则 | `FormRule[]` | - | +| dependencies | 依赖字段 | `string[]` | - | diff --git a/docs/components/grid.md b/docs/components/grid.md new file mode 100644 index 0000000..22ecb3e --- /dev/null +++ b/docs/components/grid.md @@ -0,0 +1,24 @@ +# Grid 网格 + +CSS Grid 网格布局组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| cols | 列数 | `number` | - | +| gap | 间距 | `number` | - | diff --git a/docs/components/icon.md b/docs/components/icon.md new file mode 100644 index 0000000..98e0fbe --- /dev/null +++ b/docs/components/icon.md @@ -0,0 +1,24 @@ +# Icon 图标 + +语义化的矢量图标。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| name | 图标名称 | `string` | - | +| size | 图标大小 | `number` | `16` | diff --git a/docs/components/image-preview.md b/docs/components/image-preview.md new file mode 100644 index 0000000..c5c410b --- /dev/null +++ b/docs/components/image-preview.md @@ -0,0 +1,22 @@ +# ImagePreview 图片预览 + +图片预览组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| src | 图片地址 | `string` | - | +| alt | 替代文本 | `string` | - | +| width | 宽度 | `number` | - | +| height | 高度 | `number` | - | diff --git a/docs/components/image.md b/docs/components/image.md new file mode 100644 index 0000000..3463f47 --- /dev/null +++ b/docs/components/image.md @@ -0,0 +1,25 @@ +# Image 图片 + +图片组件,对齐 Ant Design Image,支持预览和失败占位。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| src | 图片地址 | `string` | - | +| alt | 替代文本 | `string` | `''` | +| fallback | 失败占位图 | `string` | `''` | +| placeholder | 加载占位 | `ReactNode` | - | +| preview | 是否支持预览 | `boolean` | `true` | diff --git a/docs/components/input-number.md b/docs/components/input-number.md new file mode 100644 index 0000000..8091515 --- /dev/null +++ b/docs/components/input-number.md @@ -0,0 +1,33 @@ +# InputNumber 数字输入框 + +通过鼠标或键盘输入数值,对齐 Ant Design InputNumber。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| value | 当前值 | `number` | - | +| defaultValue | 默认值 | `number` | - | +| min | 最小值 | `number` | `-Infinity` | +| max | 最大值 | `number` | `Infinity` | +| step | 步长 | `number` | `1` | +| precision | 精度 | `number` | - | +| controls | 显示增减按钮 | `boolean` | `true` | +| disabled | 禁用 | `boolean` | `false` | +| size | 尺寸 | `'sm' \| 'md' \| 'lg'` | `'md'` | +| onChange | 值变化回调 | `(value: number \| null) => void` | - | diff --git a/docs/components/input.md b/docs/components/input.md new file mode 100644 index 0000000..0d252a2 --- /dev/null +++ b/docs/components/input.md @@ -0,0 +1,32 @@ +# Input 输入框 + +基础表单输入组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| value | 输入值 | `string` | - | +| defaultValue | 默认值 | `string` | - | +| onValueChange | 值变化回调 | `(value: string) => void` | - | +| label | 标签 | `string` | - | +| placeholder | 占位文本 | `string` | - | +| prefix | 前缀 | `ReactNode` | - | +| suffix | 后缀 | `ReactNode` | - | +| size | 尺寸 | `'sm' \| 'md' \| 'lg'` | `'md'` | +| disabled | 禁用 | `boolean` | `false` | diff --git a/docs/components/layout.md b/docs/components/layout.md deleted file mode 100644 index dce1f74..0000000 --- a/docs/components/layout.md +++ /dev/null @@ -1,40 +0,0 @@ -# 布局组件 - -## 示例 - - - -## API - -| 组件 | 关键属性 | -| --- | --- | -| Container | `fluid` | -| Row | `gap`, `justify`, `align`, `wrap` | -| Col | `span`, `offset` | -| Grid | `cols`, `gap` | -| Space | `direction`, `size`, `wrap` | -| Divider | `orientation` | -| SplitPane | `left`, `right`, `ratio` | diff --git a/docs/components/list.md b/docs/components/list.md new file mode 100644 index 0000000..fa0972f --- /dev/null +++ b/docs/components/list.md @@ -0,0 +1,34 @@ +# List 列表 + +通用列表组件,对齐 Ant Design List。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| dataSource | 数据源 | `ListItem[]` | `[]` | +| header | 头部 | `ReactNode` | - | +| footer | 底部 | `ReactNode` | - | +| bordered | 是否有边框 | `boolean` | `true` | +| size | 尺寸 | `'sm' \| 'md' \| 'lg'` | `'md'` | +| loading | 加载中 | `boolean` | `false` | +| renderItem | 自定义渲染 | `(item, index) => ReactNode` | - | +| grid | 网格配置 | `{ cols?: number; gap?: number }` | - | diff --git a/docs/components/loading.md b/docs/components/loading.md new file mode 100644 index 0000000..aa25b65 --- /dev/null +++ b/docs/components/loading.md @@ -0,0 +1,24 @@ +# Loading 加载中 + +加载状态指示器。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| text | 加载文本 | `string` | `'Loading...'` | +| size | 尺寸 | `'sm' \| 'md' \| 'lg'` | `'md'` | diff --git a/docs/components/mentions.md b/docs/components/mentions.md new file mode 100644 index 0000000..0f678c2 --- /dev/null +++ b/docs/components/mentions.md @@ -0,0 +1,22 @@ +# Mentions 提及 + +提及组件,输入 @ 触发。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| options | 提及选项 | `MentionsOption[]` | - | +| value | 当前值 | `string` | - | +| defaultValue | 默认值 | `string` | - | +| onChange | 变化回调 | `(value: string) => void` | - | diff --git a/docs/components/menu.md b/docs/components/menu.md new file mode 100644 index 0000000..2cb0fa4 --- /dev/null +++ b/docs/components/menu.md @@ -0,0 +1,26 @@ +# Menu 菜单 + +菜单导航组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| items | 菜单项 | `MenuItem[]` | - | +| selectedKey | 选中 key | `string` | - | +| mode | 模式 | `'horizontal' \| 'vertical'` | - | +| onChange | 选中回调 | `(key: string) => void` | - | diff --git a/docs/components/modal.md b/docs/components/modal.md new file mode 100644 index 0000000..08c06bf --- /dev/null +++ b/docs/components/modal.md @@ -0,0 +1,27 @@ +# Modal 对话框 + +模态对话框。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| open | 是否可见 | `boolean` | - | +| onClose | 关闭回调 | `() => void` | - | +| title | 标题 | `ReactNode` | - | diff --git a/docs/components/navigation.md b/docs/components/navigation.md deleted file mode 100644 index a3e6759..0000000 --- a/docs/components/navigation.md +++ /dev/null @@ -1,55 +0,0 @@ -# 导航组件 - - - -## API - -| 组件 | 关键属性 | -| --- | --- | -| Tabs | `items`, `activeKey/defaultActiveKey`, `onChange` | -| Menu | `items`, `selectedKey`, `mode`, `onChange` | -| Breadcrumb | `items`, `separator` | -| Dropdown | `trigger`, `options`, `open/defaultOpen`, `onOpen/onClose`, `onChange` | -| Steps | `items`, `current` | -| Collapse | `items`, `activeKey/defaultActiveKey`, `accordion`, `onChange` | -| Tree | `data`, `expandedKeys/defaultExpandedKeys`, `onExpand` | -| Anchor | `items` | -| Affix | `offsetTop`, `children` | -| BackTop | `visibilityHeight` | diff --git a/docs/components/notification.md b/docs/components/notification.md new file mode 100644 index 0000000..0c1efec --- /dev/null +++ b/docs/components/notification.md @@ -0,0 +1,26 @@ +# Notification 通知 + +通知提醒框。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| type | 类型 | `'info' \| 'success' \| 'warning' \| 'error'` | `'info'` | +| title | 标题 | `string` | - | +| description | 描述内容 | `string` | - | +| open | 是否显示 | `boolean` | `true` | +| onClose | 关闭回调 | `() => void` | - | diff --git a/docs/components/overview.md b/docs/components/overview.md index 5b222f3..75c09ae 100644 --- a/docs/components/overview.md +++ b/docs/components/overview.md @@ -1,12 +1,66 @@ # 组件概览 -Nova UI 当前包含以下组件: +Nova UI 提供覆盖中后台常见场景的 React 组件,按能力分为 **6 大类,80+ 组件**。你可以从下方分类卡片快速进入。 -1. 布局:Container、Row、Col、Grid、Space、Divider、SplitPane -2. 基础:Button、Icon、Typography(Title/Text/Paragraph) -3. 表单:Input、Select、Checkbox、Radio、Switch、DatePicker、Form、Upload、Slider、Rate、Calendar、Transfer、Cascader、TreeSelect、ColorPicker、Segmented、Mentions -4. 反馈:Modal、Drawer、Toast/Message、Tooltip、Popover、Loading、Notification、Tour、Watermark -5. 数据:Table、Pagination、Card、Tag、Badge、Avatar、Progress、Statistic、Descriptions、Empty、Result、Timeline、QRCode、ImagePreview、VirtualList -6. 导航:Tabs、Menu、Breadcrumb、Dropdown、Steps、Collapse、Tree、Anchor、Affix、BackTop + -所有组件都支持 `className` 与 `style` 覆盖,并提供类型定义与受控交互。组件体系已逐步对齐 AntD / ElementUI 的中后台常用能力。 +## 全量组件清单 + +### 布局 Layout + +Container、Row、Col、Grid、Flex、Space、Divider、SplitPane + +### 基础 Basic + +Button、Icon、Typography(Title / Text / Paragraph) + +### 表单 Form + +Input、InputNumber、AutoComplete、Select、Checkbox、Radio、Switch、DatePicker、TimePicker、Slider、Rate、Upload、Form / FormItem、Calendar、Transfer、Cascader、TreeSelect、ColorPicker、Segmented、Mentions + +### 反馈 Feedback + +Alert、Modal、Drawer、Toast / Message、Tooltip、Popover、Popconfirm、Loading、Spin、Skeleton、Notification、Tour、Watermark + +### 数据展示 Data Display + +Table、List、Card、Carousel、Tag、Badge、Avatar、Image、Pagination、Progress、Statistic、Descriptions、Empty、Result、Timeline、QRCode、ImagePreview、VirtualList + +### 导航 Navigation + +Tabs、Menu、Breadcrumb、Dropdown、Steps、Collapse、Tree、Anchor、Affix、BackTop、FloatButton + +--- + +所有组件都支持 `className` 与 `style` 覆盖,并提供完整 TypeScript 类型与受控交互能力。 diff --git a/docs/components/pagination.md b/docs/components/pagination.md new file mode 100644 index 0000000..cdc8db4 --- /dev/null +++ b/docs/components/pagination.md @@ -0,0 +1,23 @@ +# Pagination 分页 + +分页组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| current | 当前页 | `number` | - | +| defaultCurrent | 默认页 | `number` | `1` | +| total | 总数 | `number` | - | +| pageSize | 每页条数 | `number` | `10` | +| onChange | 页码变化回调 | `(page: number) => void` | - | diff --git a/docs/components/popconfirm.md b/docs/components/popconfirm.md new file mode 100644 index 0000000..a64ac7a --- /dev/null +++ b/docs/components/popconfirm.md @@ -0,0 +1,33 @@ +# Popconfirm 气泡确认 + +点击元素弹出确认气泡,对齐 Ant Design Popconfirm。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| title | 确认标题 | `ReactNode` | - | +| description | 描述 | `ReactNode` | - | +| onConfirm | 确认回调 | `() => void` | - | +| onCancel | 取消回调 | `() => void` | - | +| okText | 确认文本 | `string` | `'确定'` | +| cancelText | 取消文本 | `string` | `'取消'` | +| open | 是否显示 | `boolean` | - | +| defaultOpen | 默认显示 | `boolean` | `false` | diff --git a/docs/components/popover.md b/docs/components/popover.md new file mode 100644 index 0000000..85c841f --- /dev/null +++ b/docs/components/popover.md @@ -0,0 +1,24 @@ +# Popover 气泡卡片 + +点击/悬停弹出气泡卡片。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| trigger | 触发元素 | `ReactNode` | - | +| content | 卡片内容 | `ReactNode` | - | +| open | 是否显示 | `boolean` | - | +| defaultOpen | 默认显示 | `boolean` | - | +| onOpen | 打开回调 | `() => void` | - | +| onClose | 关闭回调 | `() => void` | - | diff --git a/docs/components/progress.md b/docs/components/progress.md new file mode 100644 index 0000000..d904127 --- /dev/null +++ b/docs/components/progress.md @@ -0,0 +1,25 @@ +# Progress 进度条 + +进度展示组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| percent | 百分比 | `number` | - | +| status | 状态 | `'normal' \| 'success' \| 'exception'` | `'normal'` | +| showInfo | 显示百分比文字 | `boolean` | `true` | diff --git a/docs/components/qrcode.md b/docs/components/qrcode.md new file mode 100644 index 0000000..b869e40 --- /dev/null +++ b/docs/components/qrcode.md @@ -0,0 +1,20 @@ +# QRCode 二维码 + +二维码组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| value | 编码内容 | `string` | - | +| size | 尺寸 | `number` | `128` | diff --git a/docs/components/radio.md b/docs/components/radio.md new file mode 100644 index 0000000..0c46cc6 --- /dev/null +++ b/docs/components/radio.md @@ -0,0 +1,26 @@ +# Radio 单选框 + +单选框组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| value | 值 | `string` | - | +| name | 组名 | `string` | - | +| onChange | 变化回调 | `(e) => void` | - | +| label | 标签文本 | `string` | - | +| disabled | 禁用 | `boolean` | `false` | diff --git a/docs/components/rate.md b/docs/components/rate.md new file mode 100644 index 0000000..da12554 --- /dev/null +++ b/docs/components/rate.md @@ -0,0 +1,26 @@ +# Rate 评分 + +评分组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| count | 星数 | `number` | `5` | +| value | 当前值 | `number` | - | +| defaultValue | 默认值 | `number` | - | +| allowClear | 允许清除 | `boolean` | `true` | +| onChange | 值变化回调 | `(value: number) => void` | - | diff --git a/docs/components/result.md b/docs/components/result.md new file mode 100644 index 0000000..23a6b8e --- /dev/null +++ b/docs/components/result.md @@ -0,0 +1,22 @@ +# Result 结果 + +结果状态页组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| status | 状态 | `'success' \| 'error' \| 'info' \| 'warning'` | - | +| title | 标题 | `string` | - | +| subTitle | 副标题 | `string` | - | +| extra | 操作区 | `ReactNode` | - | diff --git a/docs/components/row.md b/docs/components/row.md new file mode 100644 index 0000000..af15eda --- /dev/null +++ b/docs/components/row.md @@ -0,0 +1,25 @@ +# Row 行 + +配合 Col 使用的栅格行组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| gap | 列间距 | `number` | - | +| justify | 水平对齐方式 | `string` | - | +| align | 垂直对齐方式 | `string` | - | +| wrap | 是否换行 | `boolean` | `true` | diff --git a/docs/components/segmented.md b/docs/components/segmented.md new file mode 100644 index 0000000..0f412fa --- /dev/null +++ b/docs/components/segmented.md @@ -0,0 +1,29 @@ +# Segmented 分段控制器 + +分段控制器组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| options | 选项列表 | `SegmentedOption[]` | - | +| value | 当前值 | `string` | - | +| defaultValue | 默认值 | `string` | - | +| onChange | 变化回调 | `(value: string) => void` | - | diff --git a/docs/components/select.md b/docs/components/select.md new file mode 100644 index 0000000..1f32cde --- /dev/null +++ b/docs/components/select.md @@ -0,0 +1,33 @@ +# Select 选择器 + +下拉选择组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| options | 选项列表 | `{ label: string; value: string }[]` | - | +| value | 当前值 | `string` | - | +| defaultValue | 默认值 | `string` | - | +| onChange | 值变化回调 | `(value: string) => void` | - | +| label | 标签 | `string` | - | +| placeholder | 占位文本 | `string` | - | diff --git a/docs/components/skeleton.md b/docs/components/skeleton.md new file mode 100644 index 0000000..c0e461b --- /dev/null +++ b/docs/components/skeleton.md @@ -0,0 +1,27 @@ +# Skeleton 骨架屏 + +加载占位组件,对齐 Ant Design Skeleton。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| active | 是否展示动画 | `boolean` | `true` | +| avatar | 显示头像占位 | `boolean` | `false` | +| title | 显示标题占位 | `boolean` | `true` | +| paragraph | 显示段落占位 | `boolean \| { rows?: number }` | `true` | +| loading | 为 true 时显示骨架屏,否则显示 children | `boolean` | `true` | diff --git a/docs/components/slider.md b/docs/components/slider.md new file mode 100644 index 0000000..9150637 --- /dev/null +++ b/docs/components/slider.md @@ -0,0 +1,24 @@ +# Slider 滑动条 + +滑动输入条。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| min | 最小值 | `number` | `0` | +| max | 最大值 | `number` | `100` | +| step | 步长 | `number` | `1` | +| value | 当前值 | `number` | - | +| defaultValue | 默认值 | `number` | - | +| onChange | 值变化回调 | `(value: number) => void` | - | diff --git a/docs/components/space.md b/docs/components/space.md new file mode 100644 index 0000000..d66824b --- /dev/null +++ b/docs/components/space.md @@ -0,0 +1,31 @@ +# Space 间距 + +设置组件之间的间距。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| direction | 排列方向 | `'horizontal' \| 'vertical'` | `'horizontal'` | +| size | 间距大小 | `number` | `8` | +| wrap | 是否换行 | `boolean` | `false` | diff --git a/docs/components/spin.md b/docs/components/spin.md new file mode 100644 index 0000000..ac7d7fb --- /dev/null +++ b/docs/components/spin.md @@ -0,0 +1,26 @@ +# Spin 加载动画 + +全局加载动画组件,对齐 Ant Design Spin,可包裹子元素。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| spinning | 是否加载中 | `boolean` | `true` | +| size | 尺寸 | `'sm' \| 'md' \| 'lg'` | `'md'` | +| tip | 提示文字 | `ReactNode` | - | diff --git a/docs/components/split-pane.md b/docs/components/split-pane.md new file mode 100644 index 0000000..d82db51 --- /dev/null +++ b/docs/components/split-pane.md @@ -0,0 +1,25 @@ +# SplitPane 分栏 + +左右分栏布局组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| left | 左侧内容 | `ReactNode` | - | +| right | 右侧内容 | `ReactNode` | - | +| ratio | 比例(CSS grid-template-columns 值) | `string` | `'1fr 1fr'` | diff --git a/docs/components/statistic.md b/docs/components/statistic.md new file mode 100644 index 0000000..73a7788 --- /dev/null +++ b/docs/components/statistic.md @@ -0,0 +1,25 @@ +# Statistic 统计数值 + +展示统计数值。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| title | 标题 | `string` | - | +| value | 数值 | `number \| string` | - | +| prefix | 前缀 | `ReactNode` | - | +| suffix | 后缀 | `ReactNode` | - | diff --git a/docs/components/steps.md b/docs/components/steps.md new file mode 100644 index 0000000..b4e6be1 --- /dev/null +++ b/docs/components/steps.md @@ -0,0 +1,24 @@ +# Steps 步骤条 + +引导用户按照流程完成任务的步骤条。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| items | 步骤项 | `StepItem[]` | - | +| current | 当前步骤 | `number` | `0` | diff --git a/docs/components/switch.md b/docs/components/switch.md new file mode 100644 index 0000000..db9c245 --- /dev/null +++ b/docs/components/switch.md @@ -0,0 +1,26 @@ +# Switch 开关 + +开关选择器。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| checked | 是否开启 | `boolean` | - | +| defaultChecked | 默认开启 | `boolean` | `false` | +| onChange | 变化回调 | `(checked: boolean) => void` | - | +| disabled | 禁用 | `boolean` | `false` | diff --git a/docs/components/table.md b/docs/components/table.md new file mode 100644 index 0000000..94207f4 --- /dev/null +++ b/docs/components/table.md @@ -0,0 +1,36 @@ +# Table 表格 + +展示行列数据。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| columns | 列配置 | `TableColumn[]` | - | +| dataSource | 数据源 | `object[]` | - | +| rowKey | 行 key | `string` | - | +| title | 标题 | `string` | - | +| searchable | 可搜索 | `boolean` | `false` | +| columnConfigurable | 可配置列 | `boolean` | `false` | +| emptyText | 空提示 | `string` | - | diff --git a/docs/components/tabs.md b/docs/components/tabs.md new file mode 100644 index 0000000..39039f9 --- /dev/null +++ b/docs/components/tabs.md @@ -0,0 +1,26 @@ +# Tabs 标签页 + +选项卡切换组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| items | 标签项 | `TabItem[]` | - | +| activeKey | 当前激活 | `string` | - | +| defaultActiveKey | 默认激活 | `string` | - | +| onChange | 切换回调 | `(key: string) => void` | - | diff --git a/docs/components/tag.md b/docs/components/tag.md new file mode 100644 index 0000000..5a2e2f9 --- /dev/null +++ b/docs/components/tag.md @@ -0,0 +1,24 @@ +# Tag 标签 + +标签组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| color | 颜色 | `string` | - | diff --git a/docs/components/time-picker.md b/docs/components/time-picker.md new file mode 100644 index 0000000..b4efecc --- /dev/null +++ b/docs/components/time-picker.md @@ -0,0 +1,29 @@ +# TimePicker 时间选择 + +时间选择器组件,对齐 Ant Design TimePicker。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| value | 当前时间 | `string` (HH:mm) | - | +| defaultValue | 默认时间 | `string` | `''` | +| format | 时间格式 | `'12' \| '24'` | `'24'` | +| disabled | 禁用 | `boolean` | `false` | +| placeholder | 占位文本 | `string` | `'选择时间'` | +| size | 尺寸 | `'sm' \| 'md' \| 'lg'` | `'md'` | +| onChange | 时间变化回调 | `(value: string) => void` | - | diff --git a/docs/components/timeline.md b/docs/components/timeline.md new file mode 100644 index 0000000..d65b6dd --- /dev/null +++ b/docs/components/timeline.md @@ -0,0 +1,23 @@ +# Timeline 时间轴 + +垂直展示的时间流信息。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| items | 节点列表 | `TimelineItem[]` | - | diff --git a/docs/components/toast.md b/docs/components/toast.md new file mode 100644 index 0000000..40ba2a1 --- /dev/null +++ b/docs/components/toast.md @@ -0,0 +1,25 @@ +# Toast 轻提示 + +全局轻提示消息。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| open | 是否显示 | `boolean` | - | +| duration | 显示时长(ms),0 表示不自动关闭 | `number` | `3000` | +| status | 状态 | `string` | - | +| onClose | 关闭回调 | `() => void` | - | diff --git a/docs/components/tooltip.md b/docs/components/tooltip.md new file mode 100644 index 0000000..1a7e24a --- /dev/null +++ b/docs/components/tooltip.md @@ -0,0 +1,23 @@ +# Tooltip 文字提示 + +简单的文字提示气泡框。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| content | 提示内容 | `ReactNode` | - | +| disabled | 禁用 | `boolean` | `false` | diff --git a/docs/components/tour.md b/docs/components/tour.md new file mode 100644 index 0000000..5e8ec23 --- /dev/null +++ b/docs/components/tour.md @@ -0,0 +1,30 @@ +# Tour 漫游式引导 + +漫游式引导组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| steps | 引导步骤 | `TourStep[]` | - | +| open | 是否显示 | `boolean` | - | +| defaultOpen | 默认显示 | `boolean` | - | +| current | 当前步骤 | `number` | - | +| onChange | 步骤变化回调 | `(current: number) => void` | - | +| onClose | 关闭回调 | `() => void` | - | diff --git a/docs/components/transfer.md b/docs/components/transfer.md new file mode 100644 index 0000000..c0469b3 --- /dev/null +++ b/docs/components/transfer.md @@ -0,0 +1,28 @@ +# Transfer 穿梭框 + +双栏穿梭选择框。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| dataSource | 数据源 | `TransferItem[]` | - | +| targetKeys | 右侧已选 key 列表 | `string[]` | - | +| defaultTargetKeys | 默认已选 key 列表 | `string[]` | - | +| onChange | 变化回调 | `(targetKeys) => void` | - | diff --git a/docs/components/tree-select.md b/docs/components/tree-select.md new file mode 100644 index 0000000..0f93e55 --- /dev/null +++ b/docs/components/tree-select.md @@ -0,0 +1,29 @@ +# TreeSelect 树选择 + +树形选择器。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| data | 树结构数据 | `TreeNode[]` | - | +| value | 当前值 | `string` | - | +| defaultValue | 默认值 | `string` | - | +| onChange | 变化回调 | `(value) => void` | - | diff --git a/docs/components/tree.md b/docs/components/tree.md new file mode 100644 index 0000000..47d276f --- /dev/null +++ b/docs/components/tree.md @@ -0,0 +1,30 @@ +# Tree 树形控件 + +树形数据展示组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| data | 树数据 | `TreeNode[]` | - | +| expandedKeys | 展开的节点 | `string[]` | - | +| defaultExpandedKeys | 默认展开 | `string[]` | - | +| onExpand | 展开回调 | `(keys) => void` | - | diff --git a/docs/components/typography.md b/docs/components/typography.md new file mode 100644 index 0000000..8fb3873 --- /dev/null +++ b/docs/components/typography.md @@ -0,0 +1,42 @@ +# Typography 排版 + +文本的基本格式,包含 Title、Text、Paragraph 三个子组件。 + +## 示例 + + + +## API + +### Title + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| level | 标题级别 | `1 \| 2 \| 3 \| 4 \| 5` | `1` | +| className | 自定义类名 | `string` | - | + +### Text + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| className | 自定义类名 | `string` | - | +| children | 内容 | `ReactNode` | - | + +### Paragraph + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| className | 自定义类名 | `string` | - | +| children | 内容 | `ReactNode` | - | diff --git a/docs/components/upload.md b/docs/components/upload.md new file mode 100644 index 0000000..9bef5ad --- /dev/null +++ b/docs/components/upload.md @@ -0,0 +1,22 @@ +# Upload 上传 + +文件上传组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| accept | 接受的文件类型 | `string` | - | +| multiple | 是否多选 | `boolean` | `false` | +| fileList | 文件列表 | `UploadFileItem[]` | - | +| onChange | 文件变化回调 | `(files) => void` | - | diff --git a/docs/components/virtual-list.md b/docs/components/virtual-list.md new file mode 100644 index 0000000..9d29c2a --- /dev/null +++ b/docs/components/virtual-list.md @@ -0,0 +1,25 @@ +# VirtualList 虚拟列表 + +高性能虚拟滚动列表。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| items | 数据列表 | `any[]` | - | +| itemHeight | 每项高度 | `number` | `36` | +| height | 容器高度 | `number` | `300` | +| renderItem | 渲染函数 | `(item) => ReactNode` | - | diff --git a/docs/components/watermark.md b/docs/components/watermark.md new file mode 100644 index 0000000..b72d163 --- /dev/null +++ b/docs/components/watermark.md @@ -0,0 +1,22 @@ +# Watermark 水印 + +页面水印组件。 + +## 示例 + + + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| content | 水印文字 | `string` | - | +| children | 被包裹内容 | `ReactNode` | - | diff --git a/docs/guide/accessibility.md b/docs/guide/accessibility.md new file mode 100644 index 0000000..d7b8d4f --- /dev/null +++ b/docs/guide/accessibility.md @@ -0,0 +1,78 @@ +# 可访问性(a11y)检查清单与审计 + +## 检查清单 + +### 1) 键盘导航 +- 组件可通过 `Tab` 到达关键交互点。 +- 弹出层支持 `Esc` 关闭(Modal / Dropdown)。 +- 菜单类支持方向键浏览(Menu / Dropdown 列表)。 +- 当前激活项可通过 `tabIndex` 或等价机制感知。 + +### 2) 焦点管理 +- 打开浮层后焦点进入浮层内部(优先落到首个可操作元素)。 +- 关闭浮层后焦点返回触发源。 +- 不出现“焦点丢失”或跳到页面顶部的问题。 + +### 3) 语义标签与 ARIA +- Modal 使用 `role="dialog"` + `aria-modal="true"`。 +- 触发器使用 `aria-expanded`、`aria-haspopup`、`aria-controls` 关联弹层。 +- 表单错误态暴露 `aria-invalid`,错误文案通过 `aria-describedby` 绑定。 +- 文本标签通过 `label[for]` 绑定表单控件。 + +## 组件级审计结果(本轮) + +| 组件 | 结果 | 说明 | +|---|---|---| +| Modal | 通过 | 已补齐标题关联与焦点进出管理 | +| Dropdown | 通过 | 已补齐键盘展开/关闭、方向键遍历、触发器 ARIA | +| Menu | 通过 | 已补齐方向键遍历与活动项焦点语义 | +| Select | 通过 | 原生 `select` + `label` 绑定已满足基础可访问性 | +| Form(含表单控件接入) | 通过 | 已补齐 `label-for`、`aria-invalid`、`aria-describedby` | + +## 修复明细(问题 - 改法 - 验证方式) + +### Modal +- **问题**:打开后焦点未自动进入弹窗;关闭后未恢复到原焦点;标题未通过 `aria-labelledby` 绑定。 +- **改法**: + - 打开时记录 `document.activeElement`,并将焦点移动到关闭按钮。 + - 关闭(卸载)时恢复到打开前焦点。 + - 为标题生成稳定 `id` 并通过 `aria-labelledby` 关联 `role="dialog"`。 +- **验证方式**: + - 键盘触发打开 Modal 后,焦点落在关闭按钮。 + - 按 `Esc` 或点击关闭后,焦点回到触发按钮。 + - 用浏览器无障碍树检查 `dialog` 的可访问名称来源于标题。 + +### Dropdown +- **问题**:仅鼠标可用;触发器缺少展开状态语义;缺少 `Esc` 与方向键导航;点击外部不收起会影响键盘流。 +- **改法**: + - 触发按钮添加 `aria-expanded`、`aria-haspopup="menu"`、`aria-controls`。 + - 支持触发器 `Enter / Space / ArrowDown` 打开。 + - 菜单支持 `ArrowUp/ArrowDown` 切换活动项,`Esc` 关闭并将焦点还给触发器。 + - 增加 click outside 关闭行为。 +- **验证方式**: + - 仅键盘完成“打开 -> 切换项 -> 关闭”。 + - 用读屏检查触发器可读到展开状态。 + +### Menu +- **问题**:缺少方向键导航,活动项缺少 roving 焦点语义。 +- **改法**: + - 在 `menu` 层监听方向键(垂直:上下;水平:左右)。 + - 维护活动项 key,并为活动项设置 `tabIndex=0`,其他为 `-1`。 +- **验证方式**: + - 通过方向键在菜单项间循环切换。 + - Tab 进入菜单后,焦点落在当前活动项。 + +### Form / 表单控件接入 +- **问题**:`FormItem` 标签与输入控件缺少强绑定;错误信息未与控件语义关联。 +- **改法**: + - `FormItem` 生成控件 id,`label` 使用 `htmlFor` 关联。 + - 有错误时向控件注入 `aria-invalid` 与 `aria-describedby`,并给错误文本分配 id。 +- **验证方式**: + - 点击 `label` 可聚焦对应控件。 + - 触发校验错误后,读屏可读到错误状态及错误文案。 + +## 行为变更影响范围 + +- Dropdown / Menu 新增键盘方向键行为,可能影响依赖自定义 `onKeyDown` 冒泡逻辑的页面。 +- Modal 打开后会自动聚焦关闭按钮,关闭后恢复先前焦点;如果业务依赖“打开不夺焦”,需评估。 +- 以上变更均未修改公开 API,属于行为增强。 diff --git a/docs/guide/docs-optimization-todo.md b/docs/guide/docs-optimization-todo.md new file mode 100644 index 0000000..ca4550b --- /dev/null +++ b/docs/guide/docs-optimization-todo.md @@ -0,0 +1,103 @@ +# 文档优化 TODO 清单 + +> 目标:把 Nova UI 文档从“可用”升级到“高效查阅 + 易维护 + 可度量改进”。 + +## P0(优先处理) + +### 1) 统一页面信息架构(IA) +- [ ] 为所有组件页统一结构:`介绍 -> 何时使用 -> 示例 -> API -> 可访问性 -> 常见问题`。 +- [ ] 每个组件页新增 `何时使用 / 何时不建议使用` 小节,减少误用。 +- [ ] API 表字段统一(`属性 / 说明 / 类型 / 默认值 / 版本`)。 + +**验收标准** +- 任意抽查 10 个组件页面,目录结构一致。 +- 新同学 5 分钟内能定位到组件 API 与示例。 + +### 2) 提升组件示例质量 +- [ ] 每个组件至少提供 3 类示例:基础、进阶、边界场景(空值/禁用/错误态)。 +- [ ] 补齐“真实业务组合示例”(如 Form + Select + Upload 联动)。 +- [ ] 对复杂示例增加“关键点说明”,避免仅展示代码。 + +**验收标准** +- 组件示例覆盖率 >= 90%(有基础 + 进阶示例)。 +- 用户无需查看源码即可理解关键交互。 + +### 3) API 文档自动化(减少手工维护) +- [ ] 评估从 TypeScript 类型自动生成 API 表(props / events / methods)。 +- [ ] 约定注释规范(如 `@default`, `@since`, `@deprecated`)。 +- [ ] 建立“变更即更新 API 文档”的 CI 检查。 + +**验收标准** +- 新增/修改 props 后,文档可自动同步或触发 CI 提示。 +- API 表手工更新工作量明显下降。 + +### 4) 导航与检索优化 +- [ ] 在组件总览页增加“按场景筛选”(表单录入/反馈提示/数据展示等)。 +- [ ] 为高频组件增加“快速入口”区块(Button / Form / Table / Modal)。 +- [ ] 优化页面标题与关键词,提升站内搜索命中率。 + +**验收标准** +- 站内搜索前 3 条结果命中用户意图。 +- 常用组件平均点击深度下降(更快抵达)。 + +--- + +## P1(次优先级) + +### 5) 可访问性(A11y)专题补齐 +- [ ] 每类组件补充键盘操作说明(Tab/Enter/Esc 等)。 +- [ ] 标注 ARIA 使用建议与常见误区。 +- [ ] 增加“屏幕阅读器体验”注意事项。 + +### 6) 国际化与术语规范 +- [ ] 建立中英文术语表(如 Drawer/抽屉、Popover/气泡卡片)。 +- [ ] 统一按钮文案与状态文案风格(确定/取消/保存中)。 +- [ ] 预留英文文档结构(先不全量翻译也可)。 + +### 7) 版本与迁移文档 +- [ ] 建立 `Changelog` 阅读入口与“升级指引”页面。 +- [ ] 对破坏性变更提供“迁移前后代码对比”。 +- [ ] 标注组件/属性的引入版本与废弃计划。 + +### 8) FAQ 与故障排查 +- [ ] 汇总高频问题:样式覆盖、暗黑模式、SSR、按需引入。 +- [ ] 每个 FAQ 给出“最小复现 + 解决方案 + 原因”。 +- [ ] 在组件页底部增加“相关 FAQ”跳转。 + +--- + +## P2(持续优化) + +### 9) 文档体验增强 +- [ ] 提供“复制代码成功提示”和“展开/折叠代码”。 +- [ ] 示例支持切换主题(亮色/暗色)并保持状态。 +- [ ] 优化移动端阅读样式(表格横向滚动、代码折行策略)。 + +### 10) 文档质量度量看板 +- [ ] 定义指标:页面访问、停留时长、跳出率、搜索无结果词。 +- [ ] 每月输出一次“文档优化报告”。 +- [ ] 将无结果高频词反向驱动文档补齐。 + +### 11) 贡献流程与规范 +- [ ] 新增“文档贡献指南”(写作模板、提交流程、Review 清单)。 +- [ ] 增加 PR 模板:示例截图、变更影响、验证步骤。 +- [ ] 约定“组件发布必须附文档更新”的门禁。 + +--- + +## 可立即执行的两周计划(建议) + +### Week 1 +- [ ] 统一 10 个高频组件页面结构(Button / Input / Select / Form / Table...)。 +- [ ] 为上述页面补齐“何时使用 + 边界示例 + FAQ”。 +- [ ] 确认 API 表字段规范并完成首批试点。 + +### Week 2 +- [ ] 落地总览页筛选与快速入口。 +- [ ] 完成 A11y 说明模板并接入 10 个页面。 +- [ ] 建立基础度量(搜索无结果词 + 高访问低停留页面)。 + +## 负责人建议(可选) +- 文档 owner:负责信息架构与验收。 +- 组件 owner:负责示例与 API 正确性。 +- 设计/体验 owner:负责术语、视觉一致性与可读性。 diff --git a/packages/ui/src/components/data/Carousel.tsx b/packages/ui/src/components/data/Carousel.tsx new file mode 100644 index 0000000..031bbf0 --- /dev/null +++ b/packages/ui/src/components/data/Carousel.tsx @@ -0,0 +1,82 @@ +import { forwardRef, useState, useEffect, useCallback, type HTMLAttributes, type ReactNode } from 'react' +import { cn } from '../../utils/cn' + +export interface CarouselProps extends HTMLAttributes { + items: ReactNode[] + autoplay?: boolean + autoplaySpeed?: number + dots?: boolean + arrows?: boolean +} + +export const Carousel = forwardRef(function Carousel( + { + className, + items, + autoplay = false, + autoplaySpeed = 3000, + dots = true, + arrows = true, + ...props + }, + ref, +) { + const [current, setCurrent] = useState(0) + const count = items.length + + const goTo = useCallback( + (index: number) => setCurrent(((index % count) + count) % count), + [count], + ) + + useEffect(() => { + if (!autoplay || count <= 1) return + const timer = setInterval(() => goTo(current + 1), autoplaySpeed) + return () => clearInterval(timer) + }, [autoplay, autoplaySpeed, current, count, goTo]) + + return ( +
+
+ {items.map((item, i) => ( +
+ {item} +
+ ))} +
+ {arrows && count > 1 ? ( + <> + + + + ) : null} + {dots && count > 1 ? ( +
+ {items.map((_, i) => ( +
+ ) : null} +
+ ) +}) diff --git a/packages/ui/src/components/data/Image.tsx b/packages/ui/src/components/data/Image.tsx new file mode 100644 index 0000000..affb5b5 --- /dev/null +++ b/packages/ui/src/components/data/Image.tsx @@ -0,0 +1,57 @@ +import { forwardRef, useState, type ImgHTMLAttributes, type ReactNode } from 'react' +import { cn } from '../../utils/cn' + +export interface ImageProps extends ImgHTMLAttributes { + fallback?: string + placeholder?: ReactNode + preview?: boolean +} + +export const Image = forwardRef(function Image( + { + className, + fallback = '', + placeholder, + preview = true, + src, + alt = '', + onError, + ...props + }, + ref, +) { + const [failed, setFailed] = useState(false) + const [loaded, setLoaded] = useState(false) + const [previewOpen, setPreviewOpen] = useState(false) + + const imgSrc = failed ? fallback : src + + return ( + <> + + {!loaded && placeholder ? {placeholder} : null} + {alt} setLoaded(true)} + onError={(e) => { + setFailed(true) + onError?.(e) + }} + onClick={preview ? () => setPreviewOpen(true) : undefined} + className={cn('block max-w-full', preview && 'cursor-pointer', !loaded && 'opacity-0')} + {...props} + /> + + {previewOpen ? ( +
setPreviewOpen(false)} + > + {alt} +
+ ) : null} + + ) +}) diff --git a/packages/ui/src/components/data/List.tsx b/packages/ui/src/components/data/List.tsx new file mode 100644 index 0000000..1c1245c --- /dev/null +++ b/packages/ui/src/components/data/List.tsx @@ -0,0 +1,80 @@ +import { forwardRef, type HTMLAttributes, type ReactNode } from 'react' +import { cn } from '../../utils/cn' + +export interface ListItem { + key: string | number + content: ReactNode + avatar?: ReactNode + title?: ReactNode + description?: ReactNode + extra?: ReactNode +} + +export interface ListProps extends HTMLAttributes { + dataSource?: ListItem[] + header?: ReactNode + footer?: ReactNode + bordered?: boolean + size?: 'sm' | 'md' | 'lg' + loading?: boolean + renderItem?: (item: ListItem, index: number) => ReactNode + grid?: { cols?: number; gap?: number } +} + +export const List = forwardRef(function List( + { + className, + dataSource = [], + header, + footer, + bordered = true, + size = 'md', + loading = false, + renderItem, + grid, + ...props + }, + ref, +) { + const paddingCls = { 'px-3 py-2': size === 'sm', 'px-4 py-3': size === 'md', 'px-6 py-4': size === 'lg' } + + const defaultRender = (item: ListItem) => ( +
+ {item.avatar ?
{item.avatar}
: null} +
+ {item.title ?
{item.title}
: null} + {item.description ?
{item.description}
: null} + {item.content} +
+ {item.extra ?
{item.extra}
: null} +
+ ) + + return ( +
+ {header ?
{header}
: null} + {loading ? ( +
Loading...
+ ) : grid ? ( +
+ {dataSource.map((item, i) => ( +
{renderItem ? renderItem(item, i) : defaultRender(item)}
+ ))} +
+ ) : ( +
    + {dataSource.map((item, i) => ( +
  • + {renderItem ? renderItem(item, i) : defaultRender(item)} +
  • + ))} +
+ )} + {footer ?
{footer}
: null} +
+ ) +}) diff --git a/packages/ui/src/components/data/Table.tsx b/packages/ui/src/components/data/Table.tsx index 0deebb0..7c7da09 100644 --- a/packages/ui/src/components/data/Table.tsx +++ b/packages/ui/src/components/data/Table.tsx @@ -19,6 +19,12 @@ export interface TableProps> columnConfigurable?: boolean rowKey?: keyof T | ((record: T, index: number) => string) emptyText?: ReactNode + pagination?: { + current: number + pageSize: number + total: number + onChange: (page: number, pageSize: number) => void + } } export const Table = forwardRef>>( @@ -32,6 +38,7 @@ export const Table = forwardRef { + if (!pagination) { + return processedRows + } + const start = (pagination.current - 1) * pagination.pageSize + const end = start + pagination.pageSize + return processedRows.slice(start, end) + }, [pagination, processedRows]) + + const totalPages = pagination ? Math.max(1, Math.ceil(pagination.total / pagination.pageSize)) : 1 + const canGoPrev = Boolean(pagination && pagination.current > 1) + const canGoNext = Boolean(pagination && pagination.current < totalPages) + return (
{(title || searchable || columnConfigurable) && ( @@ -172,20 +192,21 @@ export const Table = forwardRef - {processedRows.length === 0 ? ( + {paginatedRows.length === 0 ? ( {emptyText} ) : ( - processedRows.map((record, index) => { + paginatedRows.map((record, index) => { + const rowIndex = pagination ? (pagination.current - 1) * pagination.pageSize + index : index const key = typeof rowKey === 'function' - ? rowKey(record, index) + ? rowKey(record, rowIndex) : rowKey ? String(record[rowKey]) - : String(index) + : String(rowIndex) return ( @@ -193,7 +214,7 @@ export const Table = forwardRef - {column.render ? column.render(value, record, index) : (value as ReactNode)} + {column.render ? column.render(value, record, rowIndex) : (value as ReactNode)} ) })} @@ -204,6 +225,29 @@ export const Table = forwardRef
+ {pagination ? ( +
+ + Page {pagination.current} / {totalPages} + + + +
+ ) : null} ) }, diff --git a/packages/ui/src/components/data/index.ts b/packages/ui/src/components/data/index.ts index 8d7253b..ae15f9d 100644 --- a/packages/ui/src/components/data/index.ts +++ b/packages/ui/src/components/data/index.ts @@ -42,3 +42,12 @@ export type { ImagePreviewProps } from './ImagePreview' export { VirtualList } from './VirtualList' export type { VirtualListProps } from './VirtualList' + +export { List } from './List' +export type { ListProps, ListItem } from './List' + +export { Carousel } from './Carousel' +export type { CarouselProps } from './Carousel' + +export { Image } from './Image' +export type { ImageProps } from './Image' diff --git a/packages/ui/src/components/feedback/Alert.tsx b/packages/ui/src/components/feedback/Alert.tsx new file mode 100644 index 0000000..a1e5c3d --- /dev/null +++ b/packages/ui/src/components/feedback/Alert.tsx @@ -0,0 +1,81 @@ +import { forwardRef, useState, type HTMLAttributes, type ReactNode } from 'react' +import { cn } from '../../utils/cn' + +export interface AlertProps extends HTMLAttributes { + type?: 'info' | 'success' | 'warning' | 'error' + message: ReactNode + description?: ReactNode + closable?: boolean + showIcon?: boolean + icon?: ReactNode + onClose?: () => void + banner?: boolean + action?: ReactNode +} + +const typeIconMap: Record = { + info: 'ℹ️', + success: '✅', + warning: '⚠️', + error: '❌', +} + +export const Alert = forwardRef(function Alert( + { + className, + type = 'info', + message, + description, + closable = false, + showIcon = true, + icon, + onClose, + banner = false, + action, + ...props + }, + ref, +) { + const [visible, setVisible] = useState(true) + + if (!visible) return null + + return ( +
+ {showIcon ? {icon ?? typeIconMap[type]} : null} +
+
{message}
+ {description ?
{description}
: null} +
+ {action ?
{action}
: null} + {closable ? ( + + ) : null} +
+ ) +}) diff --git a/packages/ui/src/components/feedback/Modal.tsx b/packages/ui/src/components/feedback/Modal.tsx index 64b8796..20f69b2 100644 --- a/packages/ui/src/components/feedback/Modal.tsx +++ b/packages/ui/src/components/feedback/Modal.tsx @@ -1,4 +1,4 @@ -import { forwardRef, type HTMLAttributes, useRef } from 'react' +import { forwardRef, type HTMLAttributes, useEffect, useId, useRef } from 'react' import { Portal } from '../../utils/portal' import { cn } from '../../utils/cn' import { useEscapeKey } from '../../hooks/useEscapeKey' @@ -15,9 +15,23 @@ export const Modal = forwardRef(function Modal( _ref, ) { const panelRef = useRef(null) + const closeButtonRef = useRef(null) + const titleId = useId() + const lastActiveElementRef = useRef(null) useEscapeKey(() => onClose?.(), open) useClickOutside(panelRef, () => onClose?.(), open) + useEffect(() => { + if (!open) return + + lastActiveElementRef.current = document.activeElement as HTMLElement | null + closeButtonRef.current?.focus() + + return () => { + lastActiveElementRef.current?.focus() + } + }, [open]) + if (!open) { return null } @@ -29,13 +43,13 @@ export const Modal = forwardRef(function Modal( ref={panelRef} role="dialog" aria-modal="true" - aria-label={title} + aria-labelledby={title ? titleId : undefined} className={cn('w-full max-w-lg rounded-xl bg-white p-6 shadow-2xl dark:bg-slate-900', className)} {...props} >
-

{title}

-
diff --git a/packages/ui/src/components/feedback/Popconfirm.tsx b/packages/ui/src/components/feedback/Popconfirm.tsx new file mode 100644 index 0000000..92e7cf5 --- /dev/null +++ b/packages/ui/src/components/feedback/Popconfirm.tsx @@ -0,0 +1,89 @@ +import { forwardRef, useState, useRef, useEffect, type HTMLAttributes, type ReactNode, type ReactElement } from 'react' +import { cn } from '../../utils/cn' + +export interface PopconfirmProps extends Omit, 'title'> { + title: ReactNode + description?: ReactNode + open?: boolean + defaultOpen?: boolean + onConfirm?: () => void + onCancel?: () => void + onOpenChange?: (open: boolean) => void + okText?: string + cancelText?: string + children: ReactElement +} + +export const Popconfirm = forwardRef(function Popconfirm( + { + className, + title, + description, + open: controlledOpen, + defaultOpen = false, + onConfirm, + onCancel, + onOpenChange, + okText = '确定', + cancelText = '取消', + children, + ...props + }, + ref, +) { + const [internalOpen, setInternalOpen] = useState(defaultOpen) + const isOpen = controlledOpen ?? internalOpen + const wrapperRef = useRef(null) + + const setOpen = (val: boolean) => { + setInternalOpen(val) + onOpenChange?.(val) + } + + useEffect(() => { + if (!isOpen) return + const handler = (e: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) { + setOpen(false) + } + } + document.addEventListener('mousedown', handler) + return () => document.removeEventListener('mousedown', handler) + }) + + return ( +
+
setOpen(!isOpen)}>{children}
+ {isOpen ? ( +
+
+ ⚠️ +
+
{title}
+ {description ?
{description}
: null} +
+
+
+ + +
+
+ ) : null} +
+ ) +}) diff --git a/packages/ui/src/components/feedback/Skeleton.tsx b/packages/ui/src/components/feedback/Skeleton.tsx new file mode 100644 index 0000000..13a9a88 --- /dev/null +++ b/packages/ui/src/components/feedback/Skeleton.tsx @@ -0,0 +1,49 @@ +import { forwardRef, type HTMLAttributes } from 'react' +import { cn } from '../../utils/cn' + +export interface SkeletonProps extends Omit, 'title'> { + active?: boolean + avatar?: boolean + title?: boolean + paragraph?: boolean | { rows?: number } + loading?: boolean +} + +export const Skeleton = forwardRef(function Skeleton( + { + className, + active = true, + avatar = false, + title = true, + paragraph = true, + loading = true, + children, + ...props + }, + ref, +) { + if (!loading) return <>{children} + + const rows = typeof paragraph === 'object' ? (paragraph.rows ?? 3) : paragraph ? 3 : 0 + const animCls = active ? 'animate-pulse' : '' + + return ( +
+ {avatar ?
: null} +
+ {title ?
: null} + {rows > 0 ? ( +
+ {Array.from({ length: rows }).map((_, i) => ( +
+ ))} +
+ ) : null} +
+
+ ) +}) diff --git a/packages/ui/src/components/feedback/Spin.tsx b/packages/ui/src/components/feedback/Spin.tsx new file mode 100644 index 0000000..5666377 --- /dev/null +++ b/packages/ui/src/components/feedback/Spin.tsx @@ -0,0 +1,45 @@ +import { forwardRef, type HTMLAttributes, type ReactNode } from 'react' +import { cn } from '../../utils/cn' + +export interface SpinProps extends HTMLAttributes { + spinning?: boolean + size?: 'sm' | 'md' | 'lg' + tip?: ReactNode +} + +export const Spin = forwardRef(function Spin( + { className, spinning = true, size = 'md', tip, children, ...props }, + ref, +) { + const spinner = ( +
+ + {tip ? {tip} : null} +
+ ) + + if (!children) { + return spinning ? ( +
+ {spinner} +
+ ) : null + } + + return ( +
+ {children} + {spinning ? ( +
+ {spinner} +
+ ) : null} +
+ ) +}) diff --git a/packages/ui/src/components/feedback/index.ts b/packages/ui/src/components/feedback/index.ts index 5ac0f1c..9fe4e8b 100644 --- a/packages/ui/src/components/feedback/index.ts +++ b/packages/ui/src/components/feedback/index.ts @@ -24,3 +24,15 @@ export type { TourProps, TourStep } from './Tour' export { Watermark } from './Watermark' export type { WatermarkProps } from './Watermark' + +export { Alert } from './Alert' +export type { AlertProps } from './Alert' + +export { Skeleton } from './Skeleton' +export type { SkeletonProps } from './Skeleton' + +export { Popconfirm } from './Popconfirm' +export type { PopconfirmProps } from './Popconfirm' + +export { Spin } from './Spin' +export type { SpinProps } from './Spin' diff --git a/packages/ui/src/components/form/AutoComplete.tsx b/packages/ui/src/components/form/AutoComplete.tsx new file mode 100644 index 0000000..0b92ac1 --- /dev/null +++ b/packages/ui/src/components/form/AutoComplete.tsx @@ -0,0 +1,103 @@ +import { forwardRef, useState, useRef, useEffect, type InputHTMLAttributes } from 'react' +import { cn } from '../../utils/cn' + +export interface AutoCompleteOption { + value: string + label?: string +} + +export interface AutoCompleteProps extends Omit, 'onChange' | 'onSelect'> { + options?: AutoCompleteOption[] + value?: string + defaultValue?: string + onChange?: (value: string) => void + onSelect?: (value: string) => void + filterOption?: boolean | ((input: string, option: AutoCompleteOption) => boolean) + allowClear?: boolean +} + +export const AutoComplete = forwardRef(function AutoComplete( + { + className, + options = [], + value: controlledValue, + defaultValue = '', + onChange, + onSelect, + filterOption = true, + allowClear = false, + placeholder, + ...props + }, + ref, +) { + const [internal, setInternal] = useState(defaultValue) + const [open, setOpen] = useState(false) + const wrapperRef = useRef(null) + const val = controlledValue ?? internal + + const filtered = options.filter((opt) => { + if (!filterOption) return true + if (typeof filterOption === 'function') return filterOption(val, opt) + return (opt.label ?? opt.value).toLowerCase().includes(val.toLowerCase()) + }) + + const update = (v: string) => { + setInternal(v) + onChange?.(v) + } + + useEffect(() => { + if (!open) return + const handler = (e: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) setOpen(false) + } + document.addEventListener('mousedown', handler) + return () => document.removeEventListener('mousedown', handler) + }) + + return ( +
+
+ { + update(e.target.value) + setOpen(true) + }} + onFocus={() => setOpen(true)} + className="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100" + {...props} + /> + {allowClear && val ? ( + + ) : null} +
+ {open && filtered.length > 0 ? ( +
    + {filtered.map((opt) => ( +
  • { + update(opt.value) + onSelect?.(opt.value) + setOpen(false) + }} + > + {opt.label ?? opt.value} +
  • + ))} +
+ ) : null} +
+ ) +}) diff --git a/packages/ui/src/components/form/Form.tsx b/packages/ui/src/components/form/Form.tsx index 4f9e5b3..6d7076e 100644 --- a/packages/ui/src/components/form/Form.tsx +++ b/packages/ui/src/components/form/Form.tsx @@ -36,6 +36,24 @@ interface FormContextValue { const FormContext = createContext(null) +type MaybeNativeChangeEvent = + | ChangeEvent + | { target?: { value?: unknown; checked?: unknown; type?: string } } + +export function extractFieldValue(input: unknown) { + if (typeof input === 'object' && input !== null && 'target' in input) { + const event = input as MaybeNativeChangeEvent + if (event.target?.type === 'checkbox') { + return Boolean(event.target.checked) + } + if (event.target && 'value' in event.target) { + return event.target.value + } + } + + return input +} + export interface FormProps extends Omit, 'onSubmit'> { initialValues?: FormValues onSubmit?: (values: FormValues) => void @@ -162,40 +180,43 @@ export function FormItem({ const value = ctx.values[name] const error = ctx.errors[name] const required = rules.some((rule) => rule.required) + const fieldId = `field-${name}` + const errorId = `${fieldId}-error` const childNode = isValidElement(children) ? (children as ReactElement<{ + id?: string value?: unknown - onChange?: (event: ChangeEvent) => void + 'aria-invalid'?: boolean + 'aria-describedby'?: string + onChange?: (eventOrValue: unknown) => void }>) : null return (
{label ? ( -
) } diff --git a/packages/ui/src/components/form/InputNumber.tsx b/packages/ui/src/components/form/InputNumber.tsx new file mode 100644 index 0000000..91efe1e --- /dev/null +++ b/packages/ui/src/components/form/InputNumber.tsx @@ -0,0 +1,101 @@ +import { forwardRef, useState, type HTMLAttributes } from 'react' +import { cn } from '../../utils/cn' + +export interface InputNumberProps extends Omit, 'onChange'> { + value?: number + defaultValue?: number + min?: number + max?: number + step?: number + disabled?: boolean + size?: 'sm' | 'md' | 'lg' + controls?: boolean + precision?: number + placeholder?: string + onChange?: (value: number | null) => void +} + +export const InputNumber = forwardRef(function InputNumber( + { + className, + value: controlledValue, + defaultValue, + min = -Infinity, + max = Infinity, + step = 1, + disabled = false, + size = 'md', + controls = true, + precision, + placeholder, + onChange, + ...props + }, + ref, +) { + const [internal, setInternal] = useState(defaultValue ?? null) + const val = controlledValue ?? internal + + const clamp = (n: number) => { + let v = Math.max(min, Math.min(max, n)) + if (precision !== undefined) v = Number(v.toFixed(precision)) + return v + } + + const update = (n: number | null) => { + const clamped = n !== null ? clamp(n) : null + setInternal(clamped) + onChange?.(clamped) + } + + const heightCls = { 'h-8': size === 'sm', 'h-10': size === 'md', 'h-11': size === 'lg' } + + return ( +
+ {controls ? ( + + ) : null} + { + const raw = e.target.value + if (raw === '' || raw === '-') { update(null); return } + const n = Number(raw) + if (!isNaN(n)) update(n) + }} + onBlur={() => { if (val !== null) update(clamp(val)) }} + className="w-16 min-w-0 flex-1 bg-transparent px-2 text-center text-sm outline-none dark:text-slate-100" + /> + {controls ? ( + + ) : null} +
+ ) +}) diff --git a/packages/ui/src/components/form/TimePicker.tsx b/packages/ui/src/components/form/TimePicker.tsx new file mode 100644 index 0000000..25b7002 --- /dev/null +++ b/packages/ui/src/components/form/TimePicker.tsx @@ -0,0 +1,109 @@ +import { forwardRef, useState, useRef, useEffect, type HTMLAttributes } from 'react' +import { cn } from '../../utils/cn' + +export interface TimePickerProps extends Omit, 'onChange'> { + value?: string + defaultValue?: string + format?: '12' | '24' + disabled?: boolean + placeholder?: string + onChange?: (value: string) => void + size?: 'sm' | 'md' | 'lg' +} + +export const TimePicker = forwardRef(function TimePicker( + { + className, + value: controlledValue, + defaultValue = '', + format = '24', + disabled = false, + placeholder = '选择时间', + onChange, + size = 'md', + ...props + }, + ref, +) { + const [internal, setInternal] = useState(defaultValue) + const [open, setOpen] = useState(false) + const wrapperRef = useRef(null) + const val = controlledValue ?? internal + + const update = (v: string) => { + setInternal(v) + onChange?.(v) + } + + useEffect(() => { + if (!open) return + const handler = (e: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) setOpen(false) + } + document.addEventListener('mousedown', handler) + return () => document.removeEventListener('mousedown', handler) + }) + + const hours = Array.from({ length: format === '24' ? 24 : 12 }, (_, i) => format === '24' ? i : i + 1) + const minutes = Array.from({ length: 60 }, (_, i) => i) + + const [selH, selM] = val ? val.split(':').map(Number) : [null, null] + + const heightCls = { 'h-8': size === 'sm', 'h-10': size === 'md', 'h-11': size === 'lg' } + + return ( +
+
!disabled && setOpen(!open)} + > + {val || placeholder} + 🕐 +
+ {open ? ( +
+
+ {hours.map((h) => ( +
{ + const m = selM ?? 0 + update(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`) + }} + > + {String(h).padStart(2, '0')} +
+ ))} +
+
+ {minutes.map((m) => ( +
{ + const h = selH ?? 0 + update(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`) + setOpen(false) + }} + > + {String(m).padStart(2, '0')} +
+ ))} +
+
+ ) : null} +
+ ) +}) diff --git a/packages/ui/src/components/form/index.ts b/packages/ui/src/components/form/index.ts index 7d683c8..e964eae 100644 --- a/packages/ui/src/components/form/index.ts +++ b/packages/ui/src/components/form/index.ts @@ -48,3 +48,12 @@ export type { SegmentedProps, SegmentedOption } from './Segmented' export { Mentions } from './Mentions' export type { MentionsProps, MentionsOption } from './Mentions' + +export { AutoComplete } from './AutoComplete' +export type { AutoCompleteProps, AutoCompleteOption } from './AutoComplete' + +export { InputNumber } from './InputNumber' +export type { InputNumberProps } from './InputNumber' + +export { TimePicker } from './TimePicker' +export type { TimePickerProps } from './TimePicker' diff --git a/packages/ui/src/components/layout/Flex.tsx b/packages/ui/src/components/layout/Flex.tsx new file mode 100644 index 0000000..d8be23c --- /dev/null +++ b/packages/ui/src/components/layout/Flex.tsx @@ -0,0 +1,67 @@ +import { forwardRef, type HTMLAttributes } from 'react' +import { cn } from '../../utils/cn' + +export interface FlexProps extends HTMLAttributes { + direction?: 'row' | 'column' | 'row-reverse' | 'column-reverse' + align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' + justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly' + wrap?: boolean + gap?: number | string + vertical?: boolean +} + +const alignMap: Record = { + start: 'items-start', + center: 'items-center', + end: 'items-end', + stretch: 'items-stretch', + baseline: 'items-baseline', +} + +const justifyMap: Record = { + start: 'justify-start', + center: 'justify-center', + end: 'justify-end', + between: 'justify-between', + around: 'justify-around', + evenly: 'justify-evenly', +} + +export const Flex = forwardRef(function Flex( + { + className, + direction, + align, + justify, + wrap = false, + gap, + vertical = false, + style, + ...props + }, + ref, +) { + const dir = direction ?? (vertical ? 'column' : 'row') + const dirCls: Record = { + row: 'flex-row', + column: 'flex-col', + 'row-reverse': 'flex-row-reverse', + 'column-reverse': 'flex-col-reverse', + } + + return ( +
+ ) +}) diff --git a/packages/ui/src/components/layout/index.ts b/packages/ui/src/components/layout/index.ts index 0f9fa0b..0e566a2 100644 --- a/packages/ui/src/components/layout/index.ts +++ b/packages/ui/src/components/layout/index.ts @@ -18,3 +18,6 @@ export type { DividerProps } from './Divider' export { SplitPane } from './SplitPane' export type { SplitPaneProps } from './SplitPane' + +export { Flex } from './Flex' +export type { FlexProps } from './Flex' diff --git a/packages/ui/src/components/navigation/Dropdown.tsx b/packages/ui/src/components/navigation/Dropdown.tsx index 4bd1e6a..ac27d7e 100644 --- a/packages/ui/src/components/navigation/Dropdown.tsx +++ b/packages/ui/src/components/navigation/Dropdown.tsx @@ -1,5 +1,6 @@ -import { forwardRef, type HTMLAttributes, type ReactNode, useState } from 'react' +import { forwardRef, type HTMLAttributes, type KeyboardEvent, type ReactNode, useEffect, useId, useRef, useState } from 'react' import { cn } from '../../utils/cn' +import { useClickOutside } from '../../hooks/useClickOutside' export interface DropdownOption { key: string @@ -32,7 +33,14 @@ export const Dropdown = forwardRef(function Dropd ref, ) { const [innerOpen, setInnerOpen] = useState(defaultOpen) + const [activeIndex, setActiveIndex] = useState(-1) + const rootRef = useRef(null) + const triggerRef = useRef(null) + const menuId = useId() const open = controlledOpen ?? innerOpen + const enabledOptions = options.filter((item) => !item.disabled) + + useClickOutside(rootRef, () => setOpen(false), open) const setOpen = (next: boolean) => { if (controlledOpen === undefined) { @@ -45,18 +53,61 @@ export const Dropdown = forwardRef(function Dropd } } + const setCombinedRef = (node: HTMLDivElement | null) => { + rootRef.current = node + if (typeof ref === 'function') { + ref(node) + return + } + if (ref) { + ;(ref as { current: HTMLDivElement | null }).current = node + } + } + + useEffect(() => { + if (!open) { + setActiveIndex(-1) + return + } + setActiveIndex(0) + }, [open]) + + const handleTriggerKeyDown = (event: KeyboardEvent) => { + if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + setOpen(true) + } + } + + const handleMenuKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setOpen(false) + triggerRef.current?.focus() + return + } + if (event.key === 'ArrowDown') { + event.preventDefault() + setActiveIndex((prev) => (prev + 1) % enabledOptions.length) + } + if (event.key === 'ArrowUp') { + event.preventDefault() + setActiveIndex((prev) => (prev - 1 + enabledOptions.length) % enabledOptions.length) + } + } + return ( -
- {open ? ( -
    +