@linhey/rhine-design 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +53 -14
- package/components/Actions.tsx +2 -0
- package/components/COMMON_COMPONENTS.md +5 -1
- package/components/FormWorkflow.tsx +25 -0
- package/components/MOTION.md +6 -2
- package/components/Navigation.tsx +42 -0
- package/components/Overlays.tsx +9 -3
- package/components/README.md +13 -5
- package/components/SPACING.md +82 -0
- package/components/StatusTimeline.tsx +29 -0
- package/components/TYPOGRAPHY.md +34 -2
- package/components/common.css +12 -3
- package/components/index.ts +7 -1
- package/components/patterns.css +39 -0
- package/components/styles.css +12 -8
- package/components/theme-arknights.css +301 -0
- package/components/theme-neo-brutalism.css +84 -0
- package/components/theme.tsx +58 -0
- package/components/timeline.css +23 -0
- package/components/tokens.css +6 -1
- package/docs/DESIGN_SYSTEM.md +12 -2
- package/docs/DESIGN_SYSTEM_AUDIT.md +4 -4
- package/docs/INTERACTION_PATTERNS.md +129 -0
- package/docs/REFERENCE_RESEARCH.md +36 -0
- package/docs/THEME_NEO_BRUTALISM.md +37 -0
- package/docs/THEMING.md +104 -0
- package/package.json +5 -1
- package/preview/CommonGallery.tsx +8 -8
- package/preview/GalleryShell.tsx +106 -0
- package/preview/LineHeightGallery.tsx +27 -0
- package/preview/MotionGallery.tsx +6 -2
- package/preview/PatternGallery.tsx +52 -0
- package/preview/SpacingGallery.tsx +32 -0
- package/preview/SubscriptionGallery.tsx +84 -0
- package/preview/line-height.css +2 -0
- package/preview/main.tsx +39 -25
- package/preview/patterns.css +7 -0
- package/preview/preview.css +2 -8
- package/preview/shell.css +35 -0
- package/preview/spacing.css +2 -0
- package/preview/subscriptions.css +27 -0
- package/preview/theme-arknights.css +234 -0
- package/preview/theme-neo-brutalism.css +11 -0
- package/skills/rhine-design/SKILL.md +5 -3
- package/skills/rhine-design/references/API_MAP.md +7 -3
- package/skills/rhine-design/references/TASK_MAP.md +4 -1
- package/skills/rhine-design/references/integration.md +4 -2
- package/tests/GalleryShell.test.tsx +98 -0
- package/tests/Patterns.test.tsx +75 -0
- package/tests/StatusTimeline.test.tsx +15 -0
- package/tests/Theme.test.tsx +112 -0
package/docs/DESIGN_SYSTEM.md
CHANGED
|
@@ -14,12 +14,18 @@
|
|
|
14
14
|
- 组件优先:先更新规范、组件与真实展台,再由消费项目选择版本;业务流程不进入通用组件。
|
|
15
15
|
- 动效克制:反馈服务于状态理解,就绪不等待装饰,减少动态不削弱功能。
|
|
16
16
|
|
|
17
|
+
密集任务浮层的落地规则:先按用户决策划分常显/按需信息;区域标题、数量和操作集中到导航行;短内容自然撑高、默认单层滚动;可见间距与44px操作范围分别验证。详见[场景与交互规范](INTERACTION_PATTERNS.md#弹窗高度与滚动归属)和[间距组合](../components/SPACING.md#视觉间距与操作范围),选择即保存仅按其适用边界使用。
|
|
18
|
+
|
|
17
19
|
## 3. 视觉变量
|
|
18
20
|
|
|
19
21
|
公开样式接口统一使用 `rhine-`:主题根 `.rhine-theme`、变量 `--rhine-*`、组件类与关键帧 `rhine-*`、减少动态属性 `data-rhine-motion="reduced"`。消费项目的品牌名称只通过内容与组件参数传入,不进入共享命名空间。
|
|
20
22
|
|
|
21
23
|
### 3.1 色彩
|
|
22
24
|
|
|
25
|
+
健康历史使用独立的明亮图形色:`--rhine-health-healthy: #19B88A`、`--rhine-health-recovered: #42B8C8`、`--rhine-health-degraded: #EFA629`、`--rhine-health-failed: #EF6464`、`--rhine-health-unknown: #C5CCD3`。这些颜色只用于健康条和图例,不替换苔绿品牌色或正文/错误文字色;恢复态叠加斜纹,选中时段提供文字与可访问名称。不能把图形色用于小号文字或只凭颜色区分状态。
|
|
26
|
+
|
|
27
|
+
密集账号列表只常显身份、额度和健康条,日期、粒度、优先级机制等共享说明移至按需入口;未知/不完整/错误仍常显。`StatusTimeline compact`将重复时段说明改为悬停/聚焦浮层,保持44px操作区与键盘浏览,不以健康百分比替代历史证据。
|
|
28
|
+
|
|
23
29
|
| Token | 值 | 用途 |
|
|
24
30
|
| --- | --- | --- |
|
|
25
31
|
| `--rhine-bg` | `#F2F1E9` | 窗口底色 |
|
|
@@ -53,9 +59,11 @@
|
|
|
53
59
|
|
|
54
60
|
正常视图不出现小于 12px 的业务信息。英文短标签可使用 0.08em 字距;中文不刻意拉大字距。大写英文只作为少量层级辅助。
|
|
55
61
|
|
|
62
|
+
行高与字号成对消费token;正文14/22、辅助说明12/18、卡片标题16/24、分区标题18/26。多行自然增高,独立正文段落默认12px间距,不用增大行高代替段间距。按钮14/22,点击高度由padding/min-height负责。完整规则及组件例外见[行高规范](../components/TYPOGRAPHY.md#行高段落与控件)。
|
|
63
|
+
|
|
56
64
|
### 3.3 几何与布局
|
|
57
65
|
|
|
58
|
-
- 间距序列 4/8/12/16/24/32/48px
|
|
66
|
+
- 间距序列 4/8/12/16/24/32/48px。默认卡片采用24px内边距、8px标题/说明、16px内容/操作;具体用途、组合与响应式规则见[间距规范](../components/SPACING.md)与展台`#spacing`。窗口内容边距24px;既有窄窗20px边距为布局例外,不新增gap token。
|
|
59
67
|
- 展台内容按容器重排;卡片最小宽度按信息密度设定,不能由某个产品窗口宽度推导所有宿主布局。状态区预留文字换行空间。
|
|
60
68
|
- 卡片圆角 4px,按钮 3px;保留原生窗口圆角,不在 WebView 内再画一层假窗口。
|
|
61
69
|
- 控件最小高度 40px,主要操作 44px;移动预览/触屏按至少 44px 目标校验。
|
|
@@ -67,7 +75,7 @@
|
|
|
67
75
|
|
|
68
76
|
## 4. 组件与内容契约
|
|
69
77
|
|
|
70
|
-
使用[
|
|
78
|
+
使用[组件契约](../components/README.md)、[常用组合](../components/COMMON_COMPONENTS.md)及[场景与交互选型](./INTERACTION_PATTERNS.md)。0.4.0现有48个组件,npm 0.3.0为40个;接入时核对安装版本的实际导出。表单标签持续可见,提示与错误关联;交互元素使用原生语义或可访问基础。选择、筛选、排序和分页向宿主回传意图,宿主决定数据来源及操作结果。
|
|
71
79
|
|
|
72
80
|
错误说明发生了什么及可执行的下一步,避免内部堆栈和秘密;成功、警告、未知与空态有文字区分。通知不得依赖颜色或短暂动画才能理解。品牌、实体名、时区和业务单位由宿主定义,具体文案规则见[字体与文字](../components/TYPOGRAPHY.md)。
|
|
73
81
|
|
|
@@ -79,6 +87,8 @@
|
|
|
79
87
|
|
|
80
88
|
## 6. 独立迭代与验收
|
|
81
89
|
|
|
90
|
+
展台浏览采用左右分栏:桌面目录固定在左侧并可独立滚动,右侧一次展示一个章节;窄屏收为顶部目录按钮。目录使用普通链接与`aria-current="page"`,保留深链接、前进/后退,切换后将焦点移到内容区。展开目录按Escape收起并归还焦点。全局动效偏好保持,章节内演示离开后卸载,避免隐藏动画或浮层继续运行。主题根不加transform、contain或裁剪,浮层继续挂载到主题根。此为`preview/GalleryShell.tsx`展台导航模式,未新增公共组件API。
|
|
91
|
+
|
|
82
92
|
在本仓运行npm run check,新增组件同步契约和默认/禁用/加载/错误/长文本/键盘/动效示例。展台浏览器验收400/520/880宽度、200%缩放、焦点、快速中断、重播和减少动态;独立消费构建验证CSS及SVG资源。
|
|
83
93
|
|
|
84
94
|
变化记录在CHANGELOG并按语义版本提供迁移说明。消费项目显式选择版本,不随主线自动升级。库验收不依赖某个App迁移完成;真实业务、原生平台与辅助技术由宿主按适用范围补验。剩余能力见[系统对照](DESIGN_SYSTEM_AUDIT.md)。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 莱茵设计 · 主流系统对照与补齐顺序
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
初次调研:2026-10-22;2026-10-23追加[GitHub项目与skills调研](./REFERENCE_RESEARCH.md),main新增六个组件、14类场景指南和两组动效。对照[莱茵设计](../README.md)的`components/`与通用表单、数据展示、反馈和导航场景。以下优先级是本项目判断;引用用于说明参考系统的原则,不代表它们要求采用我们的尺寸、配色或组件清单。独立展台使用18309的`/#…`入口。
|
|
4
4
|
|
|
5
5
|
## 参考与取舍
|
|
6
6
|
|
|
@@ -29,7 +29,7 @@ V1.3新增Text/Select/Toolbar、9档文字样式和[字体/文案规范](../comp
|
|
|
29
29
|
| P0 | 统一状态矩阵 | 默认/悬停/按下/焦点/禁用/选中/只读/加载/错误,按适用范围验收;信息不可只靠颜色 | 规则已补;完整逐组件辅助技术审计待做 |
|
|
30
30
|
| P1 | 数据展示组合 | Metric、KeyValue、DataTable、Pagination;未知/零、刷新加载/失败、窄窗局部滚动;请求明细业务组合由页面负责 | V1.4已实现,待业务接入 |
|
|
31
31
|
| P1 | 链接、功能图标、图标按钮 | 16/20px统一图标网格、明确名称、44px操作目标、禁用与focus;链接跳转和按钮动作区分 | V1.4已实现Link/Icon/IconButton/CopyButton |
|
|
32
|
-
| P1 | 表单Field公共结构 | 输入/Select统一label/hint/error,必填和只读;多字段失败时焦点与错误汇总 |
|
|
32
|
+
| P1 | 表单Field公共结构 | 输入/Select统一label/hint/error,必填和只读;多字段失败时焦点与错误汇总 | 0.4.0已提供Fieldset与ErrorSummary及失败提交焦点示例 |
|
|
33
33
|
| P1 | 完整主题语义 | 表面、文字、边界、选中、焦点、错误与图表在深色下成对校验 | 未实现完整深色模式;不能只反转颜色 |
|
|
34
34
|
| P1 | 焦点与原生平台验收 | WebView、VoiceOver/Narrator、纯键盘、纯文本缩放、系统高对比与字体差异 | 浏览器证据不能替代原生验证 |
|
|
35
35
|
| P2 | Dialog/ConfirmDialog/Popover/Tooltip/Menu | Escape、焦点进入/归还、遮罩、滚动、层级和减少动态一起处理 | V1.4采用Radix无样式基础并提供真实展台 |
|
|
@@ -37,7 +37,7 @@ V1.3新增Text/Select/Toolbar、9档文字样式和[字体/文案规范](../comp
|
|
|
37
37
|
| P2 | 搜索、Checkbox/Radio/Switch、Textarea、Combobox | 原生表单习惯、受控值、IME保护;Switch不混同选择账号 | V1.4已实现;查询与过期响应仍由业务层处理 |
|
|
38
38
|
| P2 | Divider/Stack/Inline/Grid | 同源间距、自适应网格、换行与缩放 | V1.4已用于展台布局 |
|
|
39
39
|
|
|
40
|
-
常用组件补齐后,下一步按用户授权先改页面设计稿,再将同一组件接入正式App
|
|
40
|
+
常用组件补齐后,下一步按用户授权先改页面设计稿,再将同一组件接入正式App。剩余明确缺口是完整深色主题及原生辅助技术验收;不因Logo支持深色而标记整套主题完成。40个组件存在不等于正式业务页已迁移。
|
|
41
41
|
|
|
42
42
|
## 状态与交互补充规范
|
|
43
43
|
|
|
@@ -58,6 +58,6 @@ V1.3新增Text/Select/Toolbar、9档文字样式和[字体/文案规范](../comp
|
|
|
58
58
|
|
|
59
59
|
## 消融与范围
|
|
60
60
|
|
|
61
|
-
-
|
|
61
|
+
- 独立通用库现已提供SideNav、Breadcrumbs、Steps、Drawer、Fieldset、ErrorSummary,不再沿用原产品仅有两个页面的限制。命令面板、日期选择器、拖拽、富文本与图表引擎仍需具体交互需求后再补。
|
|
62
62
|
- 不引入动画框架、主题生成器或整体UI库。V1.4用户已授权完整浮层能力,采用Radix的5个无样式primitive包处理焦点/键盘/边缘避让;视觉和动效仍使用Rhine Design CSS。
|
|
63
63
|
- 不把每种视觉模式封装成组件:先出现稳定复用,再抽取;业务数据契约与服务端状态继续保留在调用方。
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# 场景、组件与交互选型
|
|
2
|
+
|
|
3
|
+
本规范面向通用工具、管理界面和数据应用。先确定用户任务、信息是否需要持续可见、是否允许中断,再选组件。视觉沿用莱茵设计;行为参考成熟模式。`preview/PatternGallery.tsx` 提供可操作的选型示例。
|
|
4
|
+
|
|
5
|
+
**版本边界:**本页新增的 SideNav、Breadcrumbs、Steps、Drawer、Fieldset、ErrorSummary、StatusTimeline、TimelineLegend 及 Dialog.sidePanel 自 0.4.0 提供。npm 0.3.0 不包含它们;接入前核对安装版本及 `components/index.ts`。
|
|
6
|
+
|
|
7
|
+
## 选择与关联证据并排
|
|
8
|
+
|
|
9
|
+
在提交前需要比较资源、额度或健康记录时,使用 `Dialog sidePanel={预览}`:children放左侧选择与提交,sidePanel放右侧只读证据;description保留任务影响说明。最大宽960px,左/右约40/60,24px内边距与分栏间距;720px及以下上下排列,模态整体可滚动。避免将选择、长列表、图例与提交挤在同一窄列。右侧有导航区时,使用`sidePanel={closeButton => ...}`将唯一关闭按钮与右侧标题、说明入口放在同一行;导航与左侧标题顶端对齐,下面直接展示内容,不再预留关闭按钮空行。渲染函数必须展示传入的closeButton,保留Escape与焦点归还;纯内容sidePanel保持原布局。左右仍是一个模态、同一个焦点范围;默认选择不提交,即时保存模式须按下方规范明确说明。主任务先于预览的DOM顺序保留键盘阅读关系,使用默认160ms浮层动画,减少动态立即呈现。
|
|
10
|
+
|
|
11
|
+
## 弹窗高度与滚动归属
|
|
12
|
+
|
|
13
|
+
短任务和有界分页预览默认随内容增高,不为了固定外观给内部列表设置独立高度上限。少量溢出先让弹窗长高;现有Dialog仅在超过`100dvh - 32px`时整体滚动。保留服务端分页,不以取消内层滚动为由一次加载全部记录。
|
|
14
|
+
|
|
15
|
+
- 默认只有弹窗这一层滚动。短列表不声明“可滚动”,不为不可滚动容器增加Tab停靠点。
|
|
16
|
+
- 只有需要持续比较两个独立长内容区域、固定编辑器工作区等明确场景,才采用分区滚动;需有可识别的滚动边界、可达的关闭/操作入口,并单独验证键盘和窄屏。
|
|
17
|
+
- 最后一项的提示、焦点环与分页也计入所需空间。不能只让静态内容刚好装下,却裁切悬停或键盘详情。
|
|
18
|
+
- 高度由CSS与内容决定,不为内容变化添加整页高度动画。具体产品的每页条数、列表高度不固化为通用token。
|
|
19
|
+
|
|
20
|
+
## 信息密度与渐进展示
|
|
21
|
+
|
|
22
|
+
先确定用户在这里做什么决定,再决定常显内容;消融的是重复解释,不是决策依据或异常状态。
|
|
23
|
+
|
|
24
|
+
| 信息 | 默认位置 | 保留边界 |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| 对象身份、核心指标、决定是否可操作的状态 | 每条记录常显 | 单位与统计周期明确;未知不能变成0 |
|
|
27
|
+
| 标题/数量、共享说明入口、关闭 | 区域顶部同一导航行 | 单一关闭入口;加载、空、错误状态仍可退出 |
|
|
28
|
+
| 重复的时间范围、粒度、排序机制、图例 | 共用说明Popover | 不藏起改变用户决策的影响或风险 |
|
|
29
|
+
| 某时段的时间与状态 | 悬停、键盘聚焦时显示 | 同时提供可访问名称;触屏也能聚焦/操作 |
|
|
30
|
+
| 失败、未知、数据不完整、关键陈旧状态 | 相关区域常显 | 不用减少文字来掩盖数据质量问题 |
|
|
31
|
+
|
|
32
|
+
## 历史健康记录
|
|
33
|
+
|
|
34
|
+
`StatusTimeline({label,segments,startLabel,endLabel})` 接收按时间排序的 `{id,label,status}`;status仅限 healthy/recovered/degraded/failed/unknown。宿主负责证据分类、时区、缺失时段补unknown和陈旧说明;组件不计算评分、不查询后端。`TimelineLegend()`提供五种状态的文字和色块,恢复态另加纹理。两者从公共barrel导入并使用同一主题。
|
|
35
|
+
|
|
36
|
+
时间轴44px操作区,鼠标定位时段,键盘方向键及Home/End浏览;当前时间与状态同时提供文字和aria-valuetext,不能只用红绿表达。默认模式显示时间范围及详情;`compact`用于重复列表行,仅在悬停/聚焦时显示详情,日期范围仍供辅助技术读取。明亮健康色采用独立health tokens,恢复态斜纹,不扩散到正文颜色。空数组禁用滑块;宿主必须常显无数据原因。没有数据、采集不全或只有未确认输出时不得标为健康;历史有输出也不代表当前可用保证。时段颜色与数值原位更新,不做进度扫光、闪烁或数字滚动。展台“场景与交互 → 监控数据与告警”提供默认及紧凑双栏示例。
|
|
37
|
+
|
|
38
|
+
## 选择即保存的适用边界
|
|
39
|
+
|
|
40
|
+
适用于独立单项设置,且用户在选择前能够理解改变的对象与影响;不默认推广到多字段编辑、批量修改或不可逆操作。需要比较未提交方案时仍采用显式提交。
|
|
41
|
+
|
|
42
|
+
仅当宿主明确采用“选择即保存”,在选择前说明语义;初始回显/打开不提交,用户选择后锁定控件并显示保存中,服务端确认后显示已保存;有版本校验的接口使用返回的新revision继续操作。失败/未知结果不可显示成功或自动重试。此模式去掉重复提交按钮;说明信息按需查看,但影响范围不能消融。保存成功后可保持上下文继续查看关联证据;关闭只退出视图,不暗示撤销已保存内容。
|
|
43
|
+
|
|
44
|
+
## 侧栏导航
|
|
45
|
+
|
|
46
|
+
SideNav采用44px最小操作行、14/22文字、4px行间距,组间16px;图标可选,使用20px线性Icon且不替代文字。当前页用苔绿底、左侧2px细边界和aria-current表达,不使用容易误解为外链的箭头。少量并列页面优先一个分组;长名称自然换行、不截断、保持图标宽度。悬停仅120ms背景过渡,键盘焦点即时,减少动态沿用全局规则。
|
|
47
|
+
|
|
48
|
+
## 快速决策
|
|
49
|
+
|
|
50
|
+
| 场景 | 组件组合 | 交互与状态 | 不宜使用 |
|
|
51
|
+
| --- | --- | --- | --- |
|
|
52
|
+
| 跨工作区、跨模块 | SideNav + Breadcrumbs | 当前页有文字、边界与 aria-current;路由变化后由宿主聚焦主内容 | Tabs 不代替全局导航;面包屑不显示浏览历史 |
|
|
53
|
+
| 同一对象的内容页签 | Tabs | 方向键选项、隐藏面板退出可访问树;焦点留在选项 | 不用一串按钮伪装页签 |
|
|
54
|
+
| 单视图指标或显示模式 | SegmentedControl | 原生单选,状态即时更新 | 不用于独立页面或执行命令 |
|
|
55
|
+
| 多字段配置 | Fieldset + TextField/Select + ErrorSummary | 提交后校验,摘要聚焦,链接回字段,保留已填值 | 不只弹 Toast;不在每次输入时抢焦点 |
|
|
56
|
+
| 线性配置流程 | Steps + Fieldset | 业务验证后推进,已完成步骤可返回;保留跨步数据 | 不自动走步骤,不拿步骤数估计后台进度 |
|
|
57
|
+
| 列表记录临时详情 | Drawer + KeyValue | 模态详情保留底层列表;进入限制焦点,Escape 关闭并归还 | 长编辑、需要分享链接的详情用独立页 |
|
|
58
|
+
| 短时独立任务 | Dialog | 一个明确任务,标题、说明、关闭路径齐全 | 不叠加多层任务弹窗 |
|
|
59
|
+
| 不可逆操作 | ConfirmDialog | 具体对象、影响、取消与明确动作;等真实结果,失败保留上下文 | 常规保存无需二次确认;可撤销优先撤销 |
|
|
60
|
+
| 简短成功反馈 | Toast | 不抢焦点;自动消失前允许 hover/focus 暂停 | 不能作为关键错误唯一载体 |
|
|
61
|
+
| 持续错误、离线或权限状态 | Notice | 就地说明原因、影响、下一步;需行动的信息持续显示 | 不用自动消失通知掩盖失败 |
|
|
62
|
+
| 首次加载 | Skeleton / Progress | 已知结构用骨架,未知进度不写百分比 | 不伪造进度,不循环品牌标志 |
|
|
63
|
+
| 保存、刷新 | Button busy + 保留旧数据 | 防重复提交;刷新明确旧值和更新时间;真实响应决定成败 | 不清空阅读上下文,不提前显示成功 |
|
|
64
|
+
| 大列表查询 | SearchField / Combobox + DataTable + Pagination | 宿主处理防抖、IME、取消与过期响应;服务端返回筛选排序分页 | 不为 UI 便利拉取全量本地筛选 |
|
|
65
|
+
| 少量/较多互斥选项 | RadioGroup / Select | Radio 展开选项便于比较,Select 压缩较长清单 | Menu 是命令,不能代替选择器 |
|
|
66
|
+
| 多选 / 即时二态设置 | Checkbox / Switch | Checkbox 选择项;Switch 即时偏好,若需保存明确说明 | 不混同“选中”与“已生效” |
|
|
67
|
+
| 补充说明 / 局部交互 | Tooltip / Popover | Tooltip 不放操作;Popover 可交互;鼠标、键盘均可触发 | 必要说明必须常驻,不能只在 hover 下出现 |
|
|
68
|
+
| 次级命令 | Menu | 动词命名、危险动作区分;键盘导航,Escape 返回入口 | 不把长表单放入菜单 |
|
|
69
|
+
| 尚无数据 / 筛选无结果 | EmptyState | 分别给创建或清除筛选;读失败用 Notice | 不能把错误、无权限当作空列表 |
|
|
70
|
+
| 监控与告警 | Metric + Badge + DataTable + Notice | 数值、单位、时间范围、最后更新时间齐全;陈旧数据标记;告警有文字和处理路径 | 未知不能填 0;红绿不能是唯一编码;告警不闪烁 |
|
|
71
|
+
| 启动与品牌识别 | Logo / 启动编排 | 三角、细线、分块归位;仅启动或主动重播;就绪立即进入 | 日常数据变化不反复播放品牌动画 |
|
|
72
|
+
|
|
73
|
+
## 新组件契约
|
|
74
|
+
|
|
75
|
+
| 导出 | 输入与责任 | 键盘、焦点、边界 |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| SideNav | `groups[{label,items[{id,label,href,icon?}]}]`、`currentId`、`onNavigate(item,event)`;icon为IconName,支持 nav 原生属性 | 原生链接可 Tab、打开新页;宿主接管路由时自行决定是否 preventDefault,保留修饰键语义;不自动滚动、鉴权或请求 |
|
|
78
|
+
| Breadcrumbs | `items[{label,href?}]`、`label`;最后一项是当前位置 | 带名称的 nav/ol;最后项 aria-current=page;只传真实父子层级,无 href 项为文本 |
|
|
79
|
+
| Steps | `items[{id,label,description?}]`、`current`、`error?`、`onStepChange?` | current 为从 0 开始的位置,等于项数代表完成;只允许点击已完成步骤;当前项 aria-current=step;错误有文字,不自动推进 |
|
|
80
|
+
| Drawer | 与 Dialog 相同:`trigger,title,description,children,open?,onOpenChange?` | 右侧模态面板;Radix 管理焦点限制、Escape 和归还;最近主题根继承动效偏好;可编辑内容需宿主保留草稿并在受控 onOpenChange 中处理未保存规则 |
|
|
81
|
+
| Fieldset | `legend,hint?,children` + 原生 fieldset 属性 | 原生 legend 分组、提示关联;disabled 禁用组内表单控件;不替代单字段标签 |
|
|
82
|
+
| ErrorSummary | `errors[{fieldId,message}]`、`title?`、`focusKey?` | 非空且 focusKey 变化时聚焦摘要;每次失败提交递增;链接聚焦已有字段并滚动到可见位置,不修改 hash 路由;fieldId 必须指向唯一、可聚焦字段;无错误不渲染 |
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// 提交时验证;不要在每次键入时递增 attempt。
|
|
86
|
+
const [errors, setErrors] = useState<FieldError[]>([]);
|
|
87
|
+
const [attempt, setAttempt] = useState(0);
|
|
88
|
+
function submit(event: FormEvent) {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
const next = name.trim() ? [] : [{ fieldId: 'name', message: '请输入名称' }];
|
|
91
|
+
setErrors(next);
|
|
92
|
+
setAttempt(value => value + 1);
|
|
93
|
+
if (!next.length) { /* 宿主执行真实提交并处理 pending / success / failure */ }
|
|
94
|
+
}
|
|
95
|
+
// 在 form 内:
|
|
96
|
+
<ErrorSummary errors={errors} focusKey={attempt} />
|
|
97
|
+
<Fieldset legend="基本信息">
|
|
98
|
+
<TextField id="name" label="名称" required value={name}
|
|
99
|
+
onChange={event => setName(event.target.value)} error={errors[0]?.message} />
|
|
100
|
+
</Fieldset>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
示例是组合片段;React hooks/types、组件导入、state 及 form 由宿主提供。异步校验必须忽略过期结果;摘要不负责业务验证。
|
|
104
|
+
|
|
105
|
+
## 动效按意图选择
|
|
106
|
+
|
|
107
|
+
| 意图 | 参数与效果 | 触发/结束 | 减少动态 |
|
|
108
|
+
| --- | --- | --- | --- |
|
|
109
|
+
| 指明可操作、选中 | 120ms 颜色/边界,按下 80ms | hover/press/选择;语义立即更新 | 即时颜色变化 |
|
|
110
|
+
| 呈现新内容、步骤 | 180ms opacity + 最多 3px | 新面板或当前步骤出现;单次 | 直接显示最终态 |
|
|
111
|
+
| 临时浮层、抽屉 | 160ms opacity,抽屉从右侧 3px | 打开;关闭立即解除遮挡并归还焦点 | 保留同样焦点规则,无位移 |
|
|
112
|
+
| 表单错误 | 即时文字、边界、摘要焦点 | 失败提交;纠正后重新验证 | 行为一致,无抖动 |
|
|
113
|
+
| 刷新数值、告警 | 原位替换,文字说明时间/严重性 | 真实数据更新 | 行为一致,无数字翻转 |
|
|
114
|
+
| 加载占位 | 已有 Skeleton/Progress 配方 | 请求结束或卸载终止 | 静态占位仍显示加载文字 |
|
|
115
|
+
|
|
116
|
+
共享 CSS token 是参数来源。仅声明必要属性,不使用 transition:all;优先 transform/opacity,不做整页高度动画、循环滤镜或滚动监听驱动的装饰。DOM 测量先读后写;能使用 CSS 的交互不另引动画运行时。
|
|
117
|
+
|
|
118
|
+
`/#motion` 有 12 组播放/重播/复位、4 倍慢放、逐例/全局减少动态示例。新增 Drawer 的弹层内重播只重启动画,不重新挂载、不清空输入或移动焦点;Steps 只模拟一次本步完成。错误反馈不加入“错误动画”,直接在 `/#form-workflow` 验证聚焦与纠错。
|
|
119
|
+
|
|
120
|
+
## 验收清单
|
|
121
|
+
|
|
122
|
+
1. 明确默认、加载、空、错误、成功、禁用、长文本各状态;不能仅凭颜色理解。
|
|
123
|
+
2. 键盘顺序符合阅读顺序;可见焦点;浮层 Escape 与焦点归还;错误定位不改变页面路由。
|
|
124
|
+
3. 400/520/880 宽度与 200% 等效重排无关键内容丢失;窄屏模态可滚动且关闭入口可达。
|
|
125
|
+
4. 系统/手动减少动态都保留信息;重复播放不会重执行业务动作或留下计时器。
|
|
126
|
+
5. 当前浏览器验证不代表原生 WebView、VoiceOver/Narrator 或完整 WCAG 审计通过。
|
|
127
|
+
6. 分别检查内容刚好装下、仅溢出少量、明显超出窗口三种高度;确认滚动归属,末条详情不裁切、不遮挡分页。间距同时测可见边界和命中区,不只读gap配置。
|
|
128
|
+
|
|
129
|
+
参考来源与取舍见 [GitHub 调研](./REFERENCE_RESEARCH.md)。
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# GitHub 与主流设计系统调研
|
|
2
|
+
|
|
3
|
+
查询日期:2026-10-23(本地日期)。星标来自当次 GitHub API,是关注度快照,不是质量或安全认证。许可证状态也需在实际复制代码时重新核对。
|
|
4
|
+
|
|
5
|
+
| 项目 / skill | 星标 | 许可 | 本轮采用的原则 |
|
|
6
|
+
| --- | ---: | --- | --- |
|
|
7
|
+
| [shadcn/ui](https://github.com/shadcn-ui/ui) | 126,213 | MIT | 按任务组织导航、表单、Sheet 等常用能力;用自己的组件 API 与莱茵样式实现,不整套安装皮肤 |
|
|
8
|
+
| [Impeccable](https://github.com/pbakaus/impeccable) | 84,840 | Apache-2.0 | 区分操作界面与阅读界面,保留现有设计语言;在真实尺寸检查结果 |
|
|
9
|
+
| [Vercel agent-skills](https://github.com/vercel-labs/agent-skills) | 32,820 | API 未返回许可 | web-design-guidelines 的面向实际源码审阅方法;未复制源码或安装其执行入口 |
|
|
10
|
+
| [Radix Primitives](https://github.com/radix-ui/primitives) | 19,415 | MIT | Drawer 复用库内已经依赖的 Dialog primitive,统一焦点、Escape、Portal |
|
|
11
|
+
| [React Spectrum / React Aria](https://github.com/adobe/react-spectrum) | 15,975 | Apache-2.0 | 语义、标签、输入方式与焦点作为组件契约;没有引入第二套交互基础库 |
|
|
12
|
+
| [UI Skills](https://github.com/ibelick/ui-skills) | 10,020 | MIT | fixing-accessibility 与 fixing-motion-performance:原生语义、可访问名称、焦点归还、合成属性优先、停止无期限动画 |
|
|
13
|
+
| [Carbon](https://github.com/carbon-design-system/carbon) | 9,572 | Apache-2.0 | 区分组件和任务模式;步骤指示、通知与复杂表单的使用场景 |
|
|
14
|
+
|
|
15
|
+
已读 skill 文件:[Vercel web-design-guidelines](https://github.com/vercel-labs/agent-skills/blob/main/skills/web-design-guidelines/SKILL.md)、[Impeccable](https://github.com/pbakaus/impeccable/blob/main/.agents/skills/impeccable/SKILL.md)、[UI Skills motion](https://github.com/ibelick/ui-skills/blob/main/skills/fixing-motion-performance/SKILL.md)、[UI Skills accessibility](https://github.com/ibelick/ui-skills/blob/main/skills/fixing-accessibility/SKILL.md)。
|
|
16
|
+
|
|
17
|
+
本轮把外部 skill 作为调研材料,未运行其中的安装脚本、下载二进制或改变 agent 权限。只将适用规则写入本项目的场景规范与现有 rhine-design skill;没有复制第三方实现代码,也未增加运行时依赖。
|
|
18
|
+
|
|
19
|
+
## 组件行为的官方依据
|
|
20
|
+
|
|
21
|
+
- [WAI-ARIA Breadcrumb Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/):有名称的导航区与当前页语义,非历史记录。
|
|
22
|
+
- [GOV.UK Error summary](https://design-system.service.gov.uk/components/error-summary/):字段错误之外提供集中定位入口;本库采用失败提交聚焦与原位修正。
|
|
23
|
+
- [Carbon Progress indicator](https://www.carbondesignsystem.com/building-blocks/core/components/progress-indicator/guidelines):区分当前、已完成和未来步骤;不是进度百分比。
|
|
24
|
+
- [Carbon Notifications](https://www.carbondesignsystem.com/building-blocks/core/patterns/notifications):依任务上下文选择通知位置和持续性。
|
|
25
|
+
- [Carbon Forms](https://www.carbondesignsystem.com/building-blocks/core/patterns/forms):按任务组织表单层次、标签与反馈。
|
|
26
|
+
- [Radix Dialog](https://www.radix-ui.com/primitives/docs/components/dialog):模态焦点与关闭交互由成熟 primitive 处理。
|
|
27
|
+
- [shadcn Sheet](https://ui.shadcn.com/docs/components/base/sheet):临时侧面板的组件分类参考;当前文档有 Base/Radix 路线,本库继续使用现有 Radix。
|
|
28
|
+
- [React Aria Quality](https://react-aria.adobe.com/quality):键盘、辅助技术、移动与不同输入方式是独立验证项,不能因依赖库具备这些能力就宣称本库合规。
|
|
29
|
+
|
|
30
|
+
## 莱茵设计的取舍
|
|
31
|
+
|
|
32
|
+
新增 SideNav、Breadcrumbs、Steps、Drawer、Fieldset、ErrorSummary;增加 14 类场景指南和两组动效示例,六个组件均进入公共导出。侧栏与面包屑已被展台自身复用,不再依赖“原产品只有两个页面”的历史范围判断。
|
|
33
|
+
|
|
34
|
+
继续保留暖白、苔绿、三角构架、细线和 120–240ms 日常动效。表单错误不抖动,数字不翻转,告警不闪烁,启动不阻塞真实就绪。移除这些效果不会损害任务理解,反而保持文字和操作目标稳定。
|
|
35
|
+
|
|
36
|
+
暂不加入日历/日期范围、树、拖拽、富文本、图表引擎、全局命令面板:需要先确定数据规模、选择规则和真实使用入口。完整深色语义主题、原生辅助技术和强制颜色模式仍是独立缺口;这次新增组件不意味着覆盖所有设计系统能力。
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 橙趣 · 粗描边
|
|
2
|
+
|
|
3
|
+
可选主题,标识 `neo-brutalism`。从包含本文件的源码版本导入 `@linhey/rhine-design/theme-neo-brutalism.css`,置于组件样式之后,再给 `.rhine-theme` 添加 `data-rhine-theme="neo-brutalism"`。自 0.4.0 提供,npm 0.3.0 不包含此主题。
|
|
4
|
+
|
|
5
|
+
React 接入推荐使用 `@linhey/rhine-design/theme` 的 `<ThemeProvider defaultTheme="neo-brutalism">`,它自动加载样式并管理主题根;子组件可通过 `useTheme().setTheme()` 切换。详见[主题 API](THEMING.md)。展台专属布局装饰位于 `preview/theme-neo-brutalism.css`,不包含在公共主题入口中。
|
|
6
|
+
|
|
7
|
+
## 视觉规则
|
|
8
|
+
|
|
9
|
+
| 元素 | 规范 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| 背景 | `#EAF0F4` 浅灰蓝,14px 间距细点阵;不穿入卡片 |
|
|
12
|
+
| 卡片 | `#FFFDF5` 米白,2px `#202323` 描边,22px 圆角,`5px 5px 0` 硬阴影 |
|
|
13
|
+
| 弹窗 | 同色同描边,24px 圆角,`8px 8px 0` 硬阴影;遮罩无模糊 |
|
|
14
|
+
| 按钮 | 主操作 `#FF8B3D` 橙底黑字,次操作白底黑框,12px 圆角,`3px 3px 0` 硬阴影 |
|
|
15
|
+
| 文字 | 深色系统无衬线,辅助 `#59616A`,标题加粗,指标 36/44px;金额等宽数字 |
|
|
16
|
+
| 类型标签 | 绿色 `#DCEBD7`、橙色 `#FCE0C4`、蓝紫色 `#E3E0F7`,均配黑字与类型名称 |
|
|
17
|
+
| 状态 | 成功仍为绿色,警告与失败保留独立语义色和文字,不以橙色主操作代替成功状态 |
|
|
18
|
+
|
|
19
|
+
主题变量均限定在主题根。既有 `--rhine-*` 尺寸、字体、语义色覆盖见 CSS;新增 `--rhine-hard-shadow`、`--rhine-tag-green`、`--rhine-tag-orange`、`--rhine-tag-violet` 可由宿主调整。
|
|
20
|
+
|
|
21
|
+
类型标签使用通用样式,不给 Badge 增加产品特定枚举:
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<span class="rhine-type-tag" data-color="green">GPT100</span>
|
|
25
|
+
<span class="rhine-type-tag" data-color="orange">GPT200</span>
|
|
26
|
+
<span class="rhine-type-tag" data-color="violet">Grok</span>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 布局与交互
|
|
30
|
+
|
|
31
|
+
单页内容先展示数量、金额与状态,再以卡片和表格展开。主题 CSS 不强制业务布局;展台 `#subscriptions` 使用静态演示数据呈现此组合,类型、价格和文案由宿主定义,不代表实际产品报价。
|
|
32
|
+
|
|
33
|
+
详情卡片使用原生 button 作为 Dialog trigger;不将静态 Surface 或带 checkbox 的 SelectionCard 改成伪按钮。卡片内不再嵌套操作,次要管理命令放右上角 Menu。Dialog 保留关闭按钮、Escape、焦点限制与关闭后归还焦点;Portal 继承主题根。主题根不能使用 transform、contain 或裁剪。
|
|
34
|
+
|
|
35
|
+
按钮按下向右下移动 3px 并收起阴影,松开恢复;禁用与忙碌状态不响应按压。系统减少动态与 `data-rhine-motion="reduced"` 均禁用位移和过渡,反馈保留颜色与阴影变化。键盘焦点为明显黑色轮廓。
|
|
36
|
+
|
|
37
|
+
卡片按可用空间自动换列,说明自然换行。表格仅在自身区域横向滚动,不让整页超宽;弹窗内容随视口约束纵向滚动。桌面展台目录用于浏览组件,订阅示例自身保持单页,不增加业务路由。
|
package/docs/THEMING.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# 主题接入
|
|
2
|
+
|
|
3
|
+
`ThemeProvider`、`useTheme`、`themes` 和相关类型从 `@linhey/rhine-design/theme` 导出。此能力自 0.4.0 提供,npm 0.3.0 不包含;建议固定版本并提交 lockfile。组件仍从 `@linhey/rhine-design` 导入。
|
|
4
|
+
|
|
5
|
+
## 接入一次,随处切换
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Button } from '@linhey/rhine-design';
|
|
9
|
+
import { ThemeProvider, useTheme } from '@linhey/rhine-design/theme';
|
|
10
|
+
|
|
11
|
+
function Appearance() {
|
|
12
|
+
const { theme, setTheme } = useTheme();
|
|
13
|
+
return <Button onClick={() => setTheme('arknights')}>
|
|
14
|
+
当前:{theme},切换战术主题
|
|
15
|
+
</Button>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function App() {
|
|
19
|
+
return <ThemeProvider defaultTheme="rhine"><Appearance /></ThemeProvider>;
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
主题入口自动加载基础样式与三套内置主题,不需要另行导入主题 CSS、添加 `.rhine-theme` 或修改 DOM。Provider 渲染一个真实的 `div.rhine-theme`,通过 `data-rhine-theme` 应用主题,不重建子组件。普通组件入口保持独立,不额外加载可选主题。
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
type ThemeName = 'rhine' | 'arknights' | 'neo-brutalism';
|
|
29
|
+
|
|
30
|
+
interface ThemeProviderProps extends HTMLAttributes<HTMLDivElement> {
|
|
31
|
+
defaultTheme?: ThemeName;
|
|
32
|
+
theme?: ThemeName;
|
|
33
|
+
onThemeChange?: (theme: ThemeName) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function useTheme(): {
|
|
37
|
+
theme: ThemeName;
|
|
38
|
+
setTheme: (theme: ThemeName) => void;
|
|
39
|
+
};
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| 属性 / 行为 | 约定 |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `defaultTheme` | 非受控初始值,默认 `rhine`;后续修改该属性不会重置当前选择 |
|
|
45
|
+
| `theme` | 受控值;提供后以宿主传入值为准,`defaultTheme` 不生效 |
|
|
46
|
+
| `onThemeChange` | 请求切换到不同主题时同步调用;初次挂载、宿主更新 props、重复选择当前主题不会调用 |
|
|
47
|
+
| `setTheme(next)` | 非受控时更新内部值并通知;受控时仅通知宿主,宿主回传 `theme` 后生效 |
|
|
48
|
+
| 根节点属性 | 支持 `children`、`className`、`style`、`id`、ARIA、事件及 `data-rhine-motion` 等 div 属性;库保留 `.rhine-theme` 和 `data-rhine-theme` 的控制权 |
|
|
49
|
+
| 无 Provider | `useTheme()` 抛出明确错误 |
|
|
50
|
+
| 无效主题名 | TypeScript 限制为 `ThemeName`;运行时传入非法名字也会抛错,不静默应用不存在的主题 |
|
|
51
|
+
|
|
52
|
+
受控模式中需由 `onThemeChange` 更新宿主状态,否则选择仅产生请求、界面保持原值。不要在同一个 Provider 生命周期中切换受控和非受控模式。
|
|
53
|
+
|
|
54
|
+
## 使用主题列表生成选择器
|
|
55
|
+
|
|
56
|
+
`themes` 是只读元数据列表,每项包含稳定标识 `name` 和展示名称 `label`;`ThemeName` 从列表推导。不要根据中文名称保存偏好。
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { Select } from '@linhey/rhine-design';
|
|
60
|
+
import { themes, useTheme } from '@linhey/rhine-design/theme';
|
|
61
|
+
|
|
62
|
+
export function ThemeSelect() {
|
|
63
|
+
const { theme, setTheme } = useTheme();
|
|
64
|
+
return <Select label="界面主题" value={theme} onChange={event => {
|
|
65
|
+
const next = themes.find(item => item.name === event.target.value);
|
|
66
|
+
if (next) setTheme(next.name);
|
|
67
|
+
}}>
|
|
68
|
+
{themes.map(item => <option key={item.name} value={item.name}>{item.label}</option>)}
|
|
69
|
+
</Select>;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 宿主管理偏好
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { useState } from 'react';
|
|
77
|
+
import { ThemeProvider, type ThemeName } from '@linhey/rhine-design/theme';
|
|
78
|
+
|
|
79
|
+
export function App() {
|
|
80
|
+
const [theme, setTheme] = useState<ThemeName>('rhine');
|
|
81
|
+
return <ThemeProvider theme={theme} onThemeChange={setTheme}>
|
|
82
|
+
<ThemeSelect />
|
|
83
|
+
</ThemeProvider>;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
库不读写 `localStorage`、Cookie、URL 或账号设置。宿主将这些偏好校验为 `ThemeName` 后传入;读取失败或未知名称时应自行选择默认值。服务端渲染与其他构建器尚未验收;若宿主使用 SSR,需确保服务端和客户端初始值一致。
|
|
88
|
+
|
|
89
|
+
展台 `preview/GalleryShell.tsx` 是受控接入示例:有效 URL `?theme=` 优先,其次有效本地偏好,最后 `rhine`;主动切换后移除主题查询参数,保留其他参数和章节 hash;存储不可用时仍可切换。上述持久化行为属于展台,不是库默认行为。
|
|
90
|
+
|
|
91
|
+
## 范围、浮层和动态
|
|
92
|
+
|
|
93
|
+
- 第一版支持多个并列 Provider,各自维护主题。**不支持嵌套 Provider**,会抛错;不要把 Provider 嵌在另一个带主题的 `.rhine-theme` 内,也不要用嵌套手写主题根尝试覆盖风格。现有后代 CSS 选择器尚不具备嵌套隔离能力。
|
|
94
|
+
- Provider 放在页面根部,不添加 `transform`、裁剪或 `contain`。Dialog、Drawer、Menu、Popover、Tooltip、Combobox 的浮层沿用最近 `.rhine-theme` 挂载机制,主题切换时同步继承。
|
|
95
|
+
- 切换只改变视觉属性,不重挂载子树,不清除表单、关闭弹窗或重置滚动。宿主不要给 Provider 或应用子树添加 `key={theme}`。
|
|
96
|
+
- 主题 API 不注入切换动画。系统减少动态和 `data-rhine-motion="reduced"` 沿用组件规则。
|
|
97
|
+
- 内置主题属于视觉风格;不提供 `system` / `dark` / `light` 模式枚举。Logo 的 `theme` 仍是独立配色属性。
|
|
98
|
+
- 公共主题 CSS 只包含组件视觉。展台布局、HUD、终端读数、扫描与开机装饰位于 `preview/theme-*.css`,不会随公共主题入口进入应用。
|
|
99
|
+
|
|
100
|
+
## 从手写主题根迁移
|
|
101
|
+
|
|
102
|
+
将 `<div className="rhine-theme" data-rhine-theme="arknights">` 替换为 `<ThemeProvider defaultTheme="arknights">`,删除额外主题 CSS 导入;原根节点的布局类、`id`、动效属性直接传给 Provider。需要动态切换时调用 `useTheme().setTheme()`。
|
|
103
|
+
|
|
104
|
+
旧的 CSS-only 用法继续支持:先导入基础组件样式,再导入 `theme-arknights.css` 或 `theme-neo-brutalism.css`,在根节点设置属性。两种用法选择一种即可,不要为迁移再包一层主题根。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linhey/rhine-design",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -15,12 +15,16 @@
|
|
|
15
15
|
},
|
|
16
16
|
"exports": {
|
|
17
17
|
".": "./components/index.ts",
|
|
18
|
+
"./theme": "./components/theme.tsx",
|
|
18
19
|
"./tokens.css": "./components/tokens.css",
|
|
19
20
|
"./styles.css": "./components/styles.css",
|
|
21
|
+
"./theme-arknights.css": "./components/theme-arknights.css",
|
|
22
|
+
"./theme-neo-brutalism.css": "./components/theme-neo-brutalism.css",
|
|
20
23
|
"./logo.svg": "./components/assets/logo.svg"
|
|
21
24
|
},
|
|
22
25
|
"sideEffects": [
|
|
23
26
|
"./components/index.ts",
|
|
27
|
+
"./components/theme.tsx",
|
|
24
28
|
"**/*.css"
|
|
25
29
|
],
|
|
26
30
|
"files": [
|
|
@@ -3,33 +3,33 @@ import { Badge, Button, Checkbox, Combobox, ConfirmDialog, CopyButton, DataTable
|
|
|
3
3
|
|
|
4
4
|
const demoRows = [{ id:'request-a', time:'14:59:18', model:'gpt-6.1-sol', tokens:9600 }, { id:'request-b', time:'14:58:18', model:'gpt-6-sol', tokens:0 }];
|
|
5
5
|
const demoOptions = [{value:'a',label:'研究工作区'}, {value:'disabled',label:'归档工作区',disabled:true}, {value:'b',label:'共享工作区'}];
|
|
6
|
-
export function CommonGallery() {
|
|
6
|
+
export function CommonGallery({ section }: { section?: string }) {
|
|
7
7
|
const [tableState,setTableState]=useState('normal'), [page,setPage]=useState(1), [sort,setSort]=useState<TableSort>();
|
|
8
8
|
const [checked,setChecked]=useState(false), [mixed,setMixed]=useState(true), [radio,setRadio]=useState('daily'), [switchOn,setSwitchOn]=useState(false);
|
|
9
9
|
const [query,setQuery]=useState(''), [queryEvent,setQueryEvent]=useState('尚未提交'), [candidateQuery,setCandidateQuery]=useState(''), [candidate,setCandidate]=useState<string|null>(null), [candidateState,setCandidateState]=useState('normal');
|
|
10
10
|
const [toast,setToast]=useState(false), [toastTone,setToastTone]=useState<'success'|'error'>('success'), [confirmFail,setConfirmFail]=useState(false), [action,setAction]=useState('尚无操作');
|
|
11
11
|
return <>
|
|
12
|
-
<section id="data"><div className="ds-section-title"><span>06</span><h2>数据与分页 · 完整表达状态</h2></div>
|
|
12
|
+
{(!section || section === 'data') && <section id="data"><div className="ds-section-title"><span>06</span><h2>数据与分页 · 完整表达状态</h2></div>
|
|
13
13
|
<Surface><Stack><Grid minWidth={150}><Metric label="请求次数" value={128} description="失败 3 次"/><Metric label="预估费用" value="$5.44" description="3 次未计价"/><Metric label="平均耗时" value={null} unit="s" description="暂不可用"/><Metric label="总 Token" value={null} loading/></Grid><Divider/><KeyValue items={[{key:'mode',label:'本地配置',value:'OAuth'},{key:'model',label:'推荐模型',value:'gpt-6.1-sol'},{key:'state',label:'同步状态',value:<Badge tone="success">运行中</Badge>}]}/></Stack></Surface>
|
|
14
14
|
<Surface className="ds-gap-top"><Stack><Select label="表格状态" value={tableState} onChange={event=>setTableState(event.target.value)}><option value="normal">正常</option><option value="loading">刷新中(保留数据)</option><option value="empty">无记录</option><option value="error">读取失败(保留数据)</option></Select>
|
|
15
15
|
<DataTable caption="请求明细 · 合成数据" rows={tableState==='empty'?[]:demoRows} rowKey={row=>row.id} loading={tableState==='loading'} error={tableState==='error'?'连接暂时不可用,请重试。':undefined} onRetry={()=>setTableState('normal')} sort={sort} onSortChange={value=>{setSort(value);setAction(`排序事件:${value.key} / ${value.direction}`);}} columns={[{key:'time',header:'时间 · 上海',render:row=>row.time,sortable:true},{key:'model',header:'模型',render:row=>row.model},{key:'tokens',header:'Token',render:row=>row.tokens.toLocaleString('en-US'),align:'right',sortable:true}]}/>
|
|
16
16
|
<Pagination page={page} pageSize={10} total={23} loading={tableState==='loading'} onPageChange={setPage}/><Text variant="caption" tone="muted">分页和排序只演示事件;此处保留同一组合成记录,组件不本地切片或排序。{action}</Text>
|
|
17
17
|
</Stack></Surface>
|
|
18
|
-
</section>
|
|
19
|
-
<section id="forms"><div className="ds-section-title"><span>07</span><h2>表单与选择 · 原生操作习惯</h2></div><Grid>
|
|
18
|
+
</section>}
|
|
19
|
+
{(!section || section === 'forms') && <section id="forms"><div className="ds-section-title"><span>07</span><h2>表单与选择 · 原生操作习惯</h2></div><Grid>
|
|
20
20
|
<Surface><Stack><Checkbox label="包含失败请求" checked={checked} onChange={event=>setChecked(event.target.checked)} hint="复选项可以独立启停。"/><Checkbox label="混合状态示例" indeterminate={mixed} onChange={()=>setMixed(false)}/><Checkbox label="暂不可选择" disabled/>
|
|
21
21
|
<Divider/><RadioGroup label="报表周期" items={[{value:'daily',label:'每日'},{value:'weekly',label:'每周'},{value:'monthly',label:'每月',disabled:true}]} value={radio} onValueChange={setRadio}/><Switch label="显示辅助说明" checked={switchOn} onCheckedChange={setSwitchOn} hint="偏好示例,不控制实际同步。"/></Stack></Surface>
|
|
22
22
|
<Surface><Stack><Textarea label="备注" placeholder="输入工作区说明" hint="允许换行与垂直调整;此处不保存。"/><Textarea label="只读说明" readOnly value="由组织统一设置,可选择和复制。" rows={2}/><Textarea label="校验失败示例" error="请填写说明后继续。" rows={2}/></Stack></Surface>
|
|
23
23
|
</Grid>
|
|
24
24
|
<Grid className="ds-gap-top"><Surface><Stack><SearchField label="搜索请求" value={query} onValueChange={setQuery} onSearch={value=>setQueryEvent(value?`已提交:${value}`:'已清空查询')}/><Text variant="caption" tone="muted">{queryEvent}。只回传查询,不访问接口。</Text></Stack></Surface>
|
|
25
|
-
<Surface><Stack><Select label="候选状态" value={candidateState} onChange={event=>setCandidateState(event.target.value)}><option value="normal">正常</option><option value="loading">读取中</option><option value="empty">无匹配结果</option><option value="error">读取失败</option></Select><Combobox label="选择工作区" query={candidateQuery} onQueryChange={value=>{setCandidateQuery(value);setCandidate(null);}} value={candidate} onValueChange={option=>{setCandidate(option.value);setCandidateQuery(option.label);}} options={candidateState==='empty'?[]:demoOptions} loading={candidateState==='loading'} error={candidateState==='error'?'候选读取失败,请重新查询。':undefined}/><Text variant="caption" tone="muted">候选列表为固定合成返回结果,不随输入执行本地筛选。</Text></Stack></Surface></Grid></section>
|
|
26
|
-
<section id="utilities"><div className="ds-section-title"><span>08</span><h2>操作与布局 · 按同一规则组合</h2></div><Surface><Stack><Inline><Link href="#typography">查看文字规范</Link><IconButton icon="refresh" label="刷新示例" onClick={()=>setAction('已点击刷新')}/><IconButton icon="close" label="不可关闭" disabled/><Icon name="info"/><Text variant="caption">图标提供辅助识别;按钮始终有名称。</Text></Inline><Divider/><Inline><CopyButton value="Rhine Design · 组件复制示例" label="复制演示文字"/><CopyButton value="demo" label="演示复制失败" copy={()=>Promise.reject(new Error('demo'))}/></Inline><Text variant="caption" tone="muted">Stack / Inline / Grid / Divider 在本页直接参与布局,无额外业务状态。</Text></Stack></Surface></section>
|
|
27
|
-
<section id="overlays"><div className="ds-section-title"><span>09</span><h2>浮层与反馈 · 焦点有来有回</h2></div><Surface><Stack><Inline>
|
|
25
|
+
<Surface><Stack><Select label="候选状态" value={candidateState} onChange={event=>setCandidateState(event.target.value)}><option value="normal">正常</option><option value="loading">读取中</option><option value="empty">无匹配结果</option><option value="error">读取失败</option></Select><Combobox label="选择工作区" query={candidateQuery} onQueryChange={value=>{setCandidateQuery(value);setCandidate(null);}} value={candidate} onValueChange={option=>{setCandidate(option.value);setCandidateQuery(option.label);}} options={candidateState==='empty'?[]:demoOptions} loading={candidateState==='loading'} error={candidateState==='error'?'候选读取失败,请重新查询。':undefined}/><Text variant="caption" tone="muted">候选列表为固定合成返回结果,不随输入执行本地筛选。</Text></Stack></Surface></Grid></section>}
|
|
26
|
+
{(!section || section === 'utilities') && <section id="utilities"><div className="ds-section-title"><span>08</span><h2>操作与布局 · 按同一规则组合</h2></div><Surface><Stack><Inline><Link href="#typography">查看文字规范</Link><IconButton icon="refresh" label="刷新示例" onClick={()=>setAction('已点击刷新')}/><IconButton icon="close" label="不可关闭" disabled/><Icon name="info"/><Text variant="caption">图标提供辅助识别;按钮始终有名称。</Text></Inline><Divider/><Inline><CopyButton value="Rhine Design · 组件复制示例" label="复制演示文字"/><CopyButton value="demo" label="演示复制失败" copy={()=>Promise.reject(new Error('demo'))}/></Inline><Text variant="caption" tone="muted">Stack / Inline / Grid / Divider 在本页直接参与布局,无额外业务状态。</Text></Stack></Surface></section>}
|
|
27
|
+
{(!section || section === 'overlays') && <section id="overlays"><div className="ds-section-title"><span>09</span><h2>浮层与反馈 · 焦点有来有回</h2></div><Surface><Stack><Inline>
|
|
28
28
|
<Tooltip content="可用额度可能延迟更新,以最新采集结果为准。"><IconButton label="额度说明" icon="info"/></Tooltip>
|
|
29
29
|
<Popover trigger={<Button variant="secondary">查看详情</Button>} title="工作区详情"><Stack><Text>气泡用于补充内容,不阻断页面。</Text><Link href="#data">查看数据组件</Link></Stack></Popover>
|
|
30
30
|
<Menu label="更多操作" trigger={<Button variant="secondary">更多操作</Button>} items={[{id:'refresh',label:'刷新',onSelect:()=>setAction('菜单:刷新')},{id:'disabled',label:'导出(暂不可用)',disabled:true,onSelect:()=>{}},{id:'archive',label:'归档示例',danger:true,onSelect:()=>setAction('菜单:归档示例')}]}/>
|
|
31
31
|
<Dialog trigger={<Button variant="secondary">打开对话框</Button>} title="编辑工作区" description="此对话框仅演示输入、焦点与关闭行为。"><Stack><TextField label="工作区名称" defaultValue="研究工作区"/><Textarea label="工作区说明"/><Popover trigger={<Button variant="secondary">对话框内详情</Button>} title="嵌套详情"><Text>Escape先关闭当前气泡,再返回对话框。</Text></Popover></Stack></Dialog>
|
|
32
32
|
<ConfirmDialog trigger={<Button variant="danger">打开确认框</Button>} title="停止演示任务?" description="这是合成操作,不会修改实际同步。默认焦点在取消按钮。" confirmLabel="停止演示任务" danger onConfirm={async()=>{if(confirmFail)throw new Error('demo');setAction('确认已完成(演示)');}}/>
|
|
33
|
-
</Inline><Checkbox label="模拟确认操作失败" checked={confirmFail} onChange={event=>setConfirmFail(event.target.checked)}/><Text variant="caption" tone="muted">{action}。Tab/方向键/Escape可操作;减少动态保留相同功能。</Text><Divider/><Inline><Button variant="secondary" onClick={()=>{setToastTone('success');setToast(true);}}>显示成功通知</Button><Button variant="secondary" onClick={()=>{setToastTone('error');setToast(true);}}>显示错误通知</Button></Inline><Toast open={toast} onOpenChange={setToast} tone={toastTone} title={toastTone==='success'?'演示操作已完成':'演示操作失败'} description={toastTone==='success'?'5秒后关闭;悬停、聚焦或页面隐藏时暂停。':'错误通知保持显示,请重试或手动关闭。'}/></Stack></Surface></section>
|
|
33
|
+
</Inline><Checkbox label="模拟确认操作失败" checked={confirmFail} onChange={event=>setConfirmFail(event.target.checked)}/><Text variant="caption" tone="muted">{action}。Tab/方向键/Escape可操作;减少动态保留相同功能。</Text><Divider/><Inline><Button variant="secondary" onClick={()=>{setToastTone('success');setToast(true);}}>显示成功通知</Button><Button variant="secondary" onClick={()=>{setToastTone('error');setToast(true);}}>显示错误通知</Button></Inline><Toast open={toast} onOpenChange={setToast} tone={toastTone} title={toastTone==='success'?'演示操作已完成':'演示操作失败'} description={toastTone==='success'?'5秒后关闭;悬停、聚焦或页面隐藏时暂停。':'错误通知保持显示,请重试或手动关闭。'}/></Stack></Surface></section>}
|
|
34
34
|
</>;
|
|
35
35
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
2
|
+
import { Breadcrumbs, Button, Logo, SegmentedControl, Select, SideNav } from '../components';
|
|
3
|
+
import { ThemeProvider, themes, useTheme, type ThemeName } from '../components/theme';
|
|
4
|
+
import './shell.css';
|
|
5
|
+
import './theme-arknights.css';
|
|
6
|
+
import './theme-neo-brutalism.css';
|
|
7
|
+
|
|
8
|
+
export const galleryGroups = [
|
|
9
|
+
{ label: '开始', items: [['top', '设计总览'], ['subscriptions', '订阅管理示例']] },
|
|
10
|
+
{ label: '设计语言', items: [['foundation', '视觉基础'], ['spacing', '间距与组合'], ['typography', '字体与文字'], ['brand', '品牌标志']] },
|
|
11
|
+
{ label: '通用组件', items: [['actions', '按钮与输入'], ['selection', '选择与切换'], ['feedback', '状态与加载'], ['disclosure', '展开与组合'], ['data', '数据与分页'], ['forms', '表单与选择'], ['utilities', '操作与布局'], ['overlays', '浮层与反馈'], ['navigation', '导航与步骤'], ['form-workflow', '表单流程']] },
|
|
12
|
+
{ label: '交互与动效', items: [['patterns', '场景与交互指南'], ['motion', '动效实验室'], ['startup', '启动动画']] },
|
|
13
|
+
] as const;
|
|
14
|
+
export type GallerySection = typeof galleryGroups[number]['items'][number][0];
|
|
15
|
+
const entries = galleryGroups.flatMap(group => [...group.items]);
|
|
16
|
+
function currentSection(): GallerySection {
|
|
17
|
+
const hash = window.location.hash.slice(1);
|
|
18
|
+
return entries.find(([id]) => id === hash)?.[0] ?? 'top';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface GalleryShellProps {
|
|
22
|
+
motion: string; onMotionChange: (value: string) => void;
|
|
23
|
+
children: (section: GallerySection) => ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function initialTheme(): ThemeName {
|
|
27
|
+
let stored: string | null = null;
|
|
28
|
+
try { stored = window.localStorage.getItem('rhine-preview-theme'); } catch { /* Storage is optional in the host. */ }
|
|
29
|
+
const requested = new URLSearchParams(window.location.search).get('theme');
|
|
30
|
+
return themes.find(item => item.name === requested)?.name
|
|
31
|
+
?? themes.find(item => item.name === stored)?.name ?? 'rhine';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function GalleryShell(props: GalleryShellProps) {
|
|
35
|
+
const [theme, setTheme] = useState(initialTheme);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
try { window.localStorage.setItem('rhine-preview-theme', theme); } catch { /* Keep switching available without storage. */ }
|
|
38
|
+
}, [theme]);
|
|
39
|
+
return <ThemeProvider theme={theme} onThemeChange={next => {
|
|
40
|
+
setTheme(next);
|
|
41
|
+
const url = new URL(window.location.href);
|
|
42
|
+
url.searchParams.delete('theme');
|
|
43
|
+
window.history.replaceState(null, '', url);
|
|
44
|
+
}} className="ds-shell" data-rhine-motion={props.motion}>
|
|
45
|
+
<GalleryContent {...props} />
|
|
46
|
+
</ThemeProvider>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function GalleryContent({ motion, onMotionChange, children }: GalleryShellProps) {
|
|
50
|
+
const { theme, setTheme } = useTheme();
|
|
51
|
+
const [section, setSection] = useState(currentSection);
|
|
52
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
53
|
+
const main = useRef<HTMLElement>(null);
|
|
54
|
+
const first = useRef(true);
|
|
55
|
+
const label = entries.find(([id]) => id === section)![1];
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const navigate = () => { setSection(currentSection()); setMenuOpen(false); };
|
|
58
|
+
window.addEventListener('hashchange', navigate);
|
|
59
|
+
return () => window.removeEventListener('hashchange', navigate);
|
|
60
|
+
}, []);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
document.title = `${label} · 莱茵设计`;
|
|
63
|
+
if (main.current) main.current.scrollTop = 0;
|
|
64
|
+
if (!first.current) main.current?.focus({ preventScroll: true });
|
|
65
|
+
first.current = false;
|
|
66
|
+
}, [section, label]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const nav = document.getElementById('gallery-navigation');
|
|
69
|
+
const current = nav?.querySelector<HTMLElement>('[aria-current=page]');
|
|
70
|
+
if (!nav || !current || !nav.clientHeight) return;
|
|
71
|
+
const bounds = nav.getBoundingClientRect(), item = current.getBoundingClientRect();
|
|
72
|
+
// Reveal deep-linked chapters inside the directory without scrolling the workspace.
|
|
73
|
+
if (item.top < bounds.top) nav.scrollTop += item.top - bounds.top;
|
|
74
|
+
else if (item.bottom > bounds.bottom) nav.scrollTop += item.bottom - bounds.bottom;
|
|
75
|
+
}, [section, menuOpen]);
|
|
76
|
+
const [now, setNow] = useState(() => new Date());
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (theme !== 'arknights') return;
|
|
79
|
+
const timer = setInterval(() => setNow(new Date()), 1000);
|
|
80
|
+
return () => clearInterval(timer);
|
|
81
|
+
}, [theme]);
|
|
82
|
+
const utcOffset = -now.getTimezoneOffset() / 60;
|
|
83
|
+
const readout = `LOCAL ${now.toLocaleTimeString('zh-CN', { hour12: false })} // UTC${utcOffset >= 0 ? '+' : ''}${utcOffset}`;
|
|
84
|
+
return <>
|
|
85
|
+
<a className="ds-skip" href="#gallery-content" onClick={event => { event.preventDefault(); main.current?.focus(); }}>跳到内容</a>
|
|
86
|
+
<div className="ak-hud" aria-hidden="true"><i /><i /><i /><i /></div>
|
|
87
|
+
<aside className="ds-sidebar" data-open={menuOpen} onKeyDown={event => {
|
|
88
|
+
if (event.key === 'Escape' && menuOpen) { setMenuOpen(false); event.currentTarget.querySelector<HTMLButtonElement>('.ds-menu-toggle')?.focus(); }
|
|
89
|
+
}}>
|
|
90
|
+
<div className="ds-sidebar-brand"><a href="#top" aria-label="Rhine Design · 设计总览"><Logo size={32} /><span>莱茵设计<small>RHINE DESIGN</small></span></a>
|
|
91
|
+
<Button className="ds-menu-toggle" variant="secondary" aria-expanded={menuOpen} aria-controls="gallery-navigation" onClick={() => setMenuOpen(value => !value)}>目录</Button>
|
|
92
|
+
</div>
|
|
93
|
+
<SideNav id="gallery-navigation" className="ds-sidebar-nav" aria-label="组件目录" currentId={section} groups={galleryGroups.map(group=>({label:group.label,items:group.items.map(([id,label])=>({id,label,href:`#${id}`}))}))} onNavigate={item=>{
|
|
94
|
+
setMenuOpen(false); if(section===item.id) main.current?.focus({preventScroll:true});
|
|
95
|
+
}}/>
|
|
96
|
+
<div className="ds-sidebar-foot"><span>COMPONENT LIBRARY</span><span>48 个组件 · 持续演进</span><span className="ak-readout">{readout}</span></div>
|
|
97
|
+
</aside>
|
|
98
|
+
<main className="ds-workspace" id="gallery-content" ref={main} tabIndex={-1} aria-label={label}>
|
|
99
|
+
<header className="ds-workspace-head"><Breadcrumbs label="展台位置" items={[{label:"组件展台",href:"#top"},{label}]} /><div className="ds-head-controls"><SegmentedControl label="动效偏好" items={[{value:'system',label:'跟随系统'},{value:'reduced',label:'减少动态'}]} value={motion} onValueChange={onMotionChange} /><Select label="界面主题" value={theme} onChange={event => {
|
|
100
|
+
const next = themes.find(item => item.name === event.target.value);
|
|
101
|
+
if (next) setTheme(next.name);
|
|
102
|
+
}}>{themes.map(item => <option key={item.name} value={item.name}>{item.label}</option>)}</Select></div></header>
|
|
103
|
+
<div className="ds-gallery ds-page" key={section}>{children(section)}<footer className="ds-footer"><span>Rhine Design · 通用设计组件</span><span>演示数据,不执行业务操作。</span></footer></div>
|
|
104
|
+
</main>
|
|
105
|
+
</>;
|
|
106
|
+
}
|