@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +53 -14
  3. package/components/Actions.tsx +2 -0
  4. package/components/COMMON_COMPONENTS.md +5 -1
  5. package/components/FormWorkflow.tsx +25 -0
  6. package/components/MOTION.md +6 -2
  7. package/components/Navigation.tsx +42 -0
  8. package/components/Overlays.tsx +9 -3
  9. package/components/README.md +13 -5
  10. package/components/SPACING.md +82 -0
  11. package/components/StatusTimeline.tsx +29 -0
  12. package/components/TYPOGRAPHY.md +34 -2
  13. package/components/common.css +12 -3
  14. package/components/index.ts +7 -1
  15. package/components/patterns.css +39 -0
  16. package/components/styles.css +12 -8
  17. package/components/theme-arknights.css +301 -0
  18. package/components/theme-neo-brutalism.css +84 -0
  19. package/components/theme.tsx +58 -0
  20. package/components/timeline.css +23 -0
  21. package/components/tokens.css +6 -1
  22. package/docs/DESIGN_SYSTEM.md +12 -2
  23. package/docs/DESIGN_SYSTEM_AUDIT.md +4 -4
  24. package/docs/INTERACTION_PATTERNS.md +129 -0
  25. package/docs/REFERENCE_RESEARCH.md +36 -0
  26. package/docs/THEME_NEO_BRUTALISM.md +37 -0
  27. package/docs/THEMING.md +104 -0
  28. package/package.json +5 -1
  29. package/preview/CommonGallery.tsx +8 -8
  30. package/preview/GalleryShell.tsx +106 -0
  31. package/preview/LineHeightGallery.tsx +27 -0
  32. package/preview/MotionGallery.tsx +6 -2
  33. package/preview/PatternGallery.tsx +52 -0
  34. package/preview/SpacingGallery.tsx +32 -0
  35. package/preview/SubscriptionGallery.tsx +84 -0
  36. package/preview/line-height.css +2 -0
  37. package/preview/main.tsx +39 -25
  38. package/preview/patterns.css +7 -0
  39. package/preview/preview.css +2 -8
  40. package/preview/shell.css +35 -0
  41. package/preview/spacing.css +2 -0
  42. package/preview/subscriptions.css +27 -0
  43. package/preview/theme-arknights.css +234 -0
  44. package/preview/theme-neo-brutalism.css +11 -0
  45. package/skills/rhine-design/SKILL.md +5 -3
  46. package/skills/rhine-design/references/API_MAP.md +7 -3
  47. package/skills/rhine-design/references/TASK_MAP.md +4 -1
  48. package/skills/rhine-design/references/integration.md +4 -2
  49. package/tests/GalleryShell.test.tsx +98 -0
  50. package/tests/Patterns.test.tsx +75 -0
  51. package/tests/StatusTimeline.test.tsx +15 -0
  52. package/tests/Theme.test.tsx +112 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # 版本记录
2
2
 
3
+ ## 0.4.0
4
+
5
+ - 新增 `@linhey/rhine-design/theme`:`ThemeProvider`、`useTheme`、`themes`、`ThemeName` 与 `ThemeProviderProps`,自动加载三套内置主题,支持受控/非受控切换及并列 Provider;嵌套覆盖暂不支持。切换保留组件、表单与浮层状态,偏好存储由宿主管理。展台复用公共 API;主题 CSS 中的展台布局和 HUD/扫描/开机装饰移至 `preview/theme-*.css`。补齐接入文档及 API/展台回归测试。
6
+
7
+ - 新增「橙趣 · 粗描边」主题 `theme-neo-brutalism.css`:浅灰蓝点阵、米白大圆角、黑色粗描边、无模糊硬阴影、橙色主操作与低饱和类型标签。展台主题切换器支持选择与记忆,`?theme=neo-brutalism#subscriptions` 可直达单页订阅示例;复用 Dialog/Menu/DataTable 展示卡片详情、管理菜单和可更新的演示数量与金额。可单独导入。
8
+
9
+ - 新增明日方舟·战术终端主题 `theme-arknights.css`:暗色舱底、平行四边形按钮、信号黄高亮、坐标网格与警示斜纹,附 HUD 取景框、终端读数与扫描/开机装饰动效;仅覆盖变量与装饰层,不改组件 API 与交互契约,减少动态下装饰动效全部关闭。展台右上角可切换主题并记忆选择。
10
+
11
+ - 将密集任务浮层经验整理为通用规范:自然高度/单层滚动、导航行、渐进展示、视觉与命中区分离,以及选择即保存的适用边界;补充临界溢出和末条详情验收要求。本项仅整理文档,不代表发布新API。
12
+
13
+ - compact健康条将20px视觉高度与44px命中区分开,避免列表重复叠加空白;间距规范补充标题8px、分隔线两侧16px及操作区避让规则。
14
+
15
+ - Dialog的sidePanel支持渲染函数,将传入的唯一关闭按钮与侧栏标题、说明组合成顶部导航;取消额外关闭行,左右标题对齐,展台同步示例。
16
+
17
+ - 健康时间轴使用独立明亮health色;新增compact模式,重复列表隐藏常驻日期/详情,悬停与键盘聚焦时查看,保留44px操作区和aria-valuetext。规范补充“选择即保存”的忙碌/确认/失败契约,默认与紧凑展台同步更新。
18
+
19
+ - 新增 StatusTimeline / TimelineLegend,源码组件增至48个。五类历史证据、44px键盘/指针操作区、时间文字与图例;不计算健康评分,不对未知补绿色,不播放装饰动画。监控场景展台可交互查看。
20
+ - Dialog新增可选sidePanel:左侧主任务、右侧关联预览,最大960px、24px分栏间距,720px及以下上下排列;保留同一个模态焦点范围、关闭与减少动态规则。展台提供左右分栏预览。
21
+
22
+ - SideNav支持可选20px线性图标(Icon新增connection/chart),移除当前项的外链箭头;采用44px最小操作行、14/22文字、4px行间距与3px圆角,选中状态保留苔绿底/左侧细边界和aria-current。导航展台同步示例;已有不带图标用法继续可用。
23
+
24
+ - SelectionCard统一24px内边距、16/24标题、600字重与8px标题/内容间隔;DataTable标题显式16/24。SideNav分组、Breadcrumbs、Progress标签及KeyValue标签明确12/18,侧栏分组标题后8px。既有真实展台同步展示,不再继承正文行高。
25
+
26
+ - 补充文字行高、段落与控件规则及可切换行框辅助线的多行展台。沿用现有字号/行高token,按钮、Tab和单行输入同步消费body行高,默认尺寸不变。
27
+
28
+ - 新增间距规范与`#spacing`真实组合展台:24/8/16卡片配方,默认/加载/错误/长文案;复用Stack/Inline/Grid,公共API和组件数量不变。Surface内边距改为消费既有space-6 token,默认外观不变。
29
+
30
+ - 新增 SideNav、Breadcrumbs、Steps、Drawer、Fieldset、ErrorSummary,main 公共组件增至46个;展台自身复用导航组件。
31
+ - 新增导航与步骤、表单流程、14类场景与交互选型章节,动效实验室增加抽屉与步骤播放/重播,总计12组。
32
+ - 参考 GitHub 高星项目与 skills,沉淀组件选择、错误恢复、监控告警、动效与无障碍契约;不增加运行时依赖。
33
+
34
+ - 在线展台改为侧栏目录与单章节工作区,支持章节深链接、历史导航、当前项高亮、独立滚动与窄屏折叠目录;全局动效偏好跨章节保留,离开的演示卸载。
35
+
3
36
  ## 0.3.0
4
37
 
5
38
  - **破坏性变更:**统一使用莱茵设计的 `rhine-` 命名空间,无旧前缀别名。
package/README.md CHANGED
@@ -16,13 +16,15 @@
16
16
  <a href="CHANGELOG.md">版本记录</a>
17
17
  </p>
18
18
 
19
- Rhine Design 是面向 React 应用的开源设计系统,适合桌面工具、管理界面和数据密集型应用。它将设计规范、40 个组件和动效示例放在一起,帮助团队建立一致、可复用的界面语言。
19
+ Rhine Design 是面向 React 应用的开源设计系统,适合桌面工具、管理界面和数据密集型应用。它将设计规范、48 个组件和动效示例放在一起,帮助团队建立一致、可复用的界面语言。
20
20
 
21
21
  - **一致的视觉语言**:色彩、字体、文字层级、间距、几何与焦点规则。
22
- - **40 个可组合组件**:覆盖布局、表单、数据、导航、浮层和反馈。
23
- - **可验证的动效**:10 组播放 / 重播 / 复位演示,以及三角构架启动编排。
22
+ - **48 个可组合组件**:覆盖布局、表单、数据、导航、浮层和反馈。
23
+ - **可验证的动效**:12 组播放 / 重播 / 复位演示,以及三角构架启动编排。
24
24
  - **面向开发协作**:组件契约、交互展台和 AI skill 帮助设计与实现保持一致。
25
25
 
26
+ **0.4.0 新增能力:**主题 API、三套内置主题,以及导航、表单流程与健康时间轴组件;在线展台跟随 main,安装前请核对[版本记录](CHANGELOG.md)。新增组件及选型见[交互规范](docs/INTERACTION_PATTERNS.md),参考项目见[GitHub 调研](docs/REFERENCE_RESEARCH.md)。
27
+
26
28
  基于 React 与 Radix UI,采用 [Apache-2.0](LICENSE) 许可证。
27
29
 
28
30
  <a id="quick-start"></a>
@@ -34,22 +36,26 @@ Rhine Design 是面向 React 应用的开源设计系统,适合桌面工具、
34
36
  在你的 React 项目中安装:
35
37
 
36
38
  ```sh
37
- npm install @linhey/rhine-design@0.3.0
39
+ npm install @linhey/rhine-design@0.4.0
38
40
  ```
39
41
 
40
42
  包与版本信息见 [npm](https://www.npmjs.com/package/@linhey/rhine-design)。如需使用未发版的源码,可安装 `git+https://github.com/linhay/rhine-design.git#<commit>`;建议固定版本并提交 lockfile。
41
43
 
42
44
  ```tsx
43
- import { Button, Stack, Surface, Text } from '@linhey/rhine-design';
45
+ import { Button, Inline, Stack, Surface, Text } from '@linhey/rhine-design';
44
46
 
45
47
  export function Welcome() {
46
48
  return (
47
49
  <div className="rhine-theme">
48
50
  <Surface>
49
51
  <Stack gap={4}>
50
- <Text as="h1" variant="title">开始新的工作</Text>
51
- <Text tone="muted">用统一的组件,构建属于你的工作空间。</Text>
52
- <Button onClick={() => { /* 在这里处理点击 */ }}>继续</Button>
52
+ <Stack gap={2}>
53
+ <Text as="h1" variant="title">开始新的工作</Text>
54
+ <Text tone="muted">用统一的组件,构建属于你的工作空间。</Text>
55
+ </Stack>
56
+ <Inline gap={2}>
57
+ <Button onClick={() => { /* 在这里处理点击 */ }}>继续</Button>
58
+ </Inline>
53
59
  </Stack>
54
60
  </Surface>
55
61
  </div>
@@ -59,6 +65,32 @@ export function Welcome() {
59
65
 
60
66
  入口自动加载样式,用 `.rhine-theme` 包裹页面即可使用默认主题。布局和颜色变量使用 `--rhine-*` 前缀;完整定义见 [tokens.css](components/tokens.css)。
61
67
 
68
+ ### 一键切换主题(0.4.0)
69
+
70
+ 展台右上角可切换并记忆主题。新增 **橙趣 · 粗描边**:浅灰蓝点阵背景、米白圆角卡片、黑色粗边、向右下偏移的硬阴影与亮橙主按钮。打开 [在线订阅管理示例](https://linhay.github.io/rhine-design/?theme=neo-brutalism#subscriptions),体验卡片详情、管理菜单与演示表单;本地开发访问 `http://127.0.0.1:18309/?theme=neo-brutalism#subscriptions`。
71
+
72
+ 从 0.4.0 接入,入口自动加载内置主题,页面只需包裹一次:
73
+
74
+ ```tsx
75
+ import { Button, Surface } from '@linhey/rhine-design';
76
+ import { ThemeProvider, useTheme } from '@linhey/rhine-design/theme';
77
+
78
+ function Appearance() {
79
+ const { setTheme } = useTheme();
80
+ return <Button onClick={() => setTheme('arknights')}>切换战术主题</Button>;
81
+ }
82
+
83
+ export function App() {
84
+ return <ThemeProvider defaultTheme="neo-brutalism">
85
+ <Surface><Appearance /></Surface>
86
+ </ThemeProvider>;
87
+ }
88
+ ```
89
+
90
+ 受控模式、主题列表、存储职责、浮层继承和迁移方式见 [主题 API 接入](docs/THEMING.md)。第一版支持并列 Provider,不支持嵌套覆盖;偏好由宿主管理,库不读写本地存储。此 API 自 0.4.0 提供。
91
+
92
+ 完整变量、类型标签、交互与减少动态规则见 [橙趣主题规范](docs/THEME_NEO_BRUTALISM.md)。现有 CSS-only 用法仍支持;展台装饰独立放在 `preview/theme-*.css`。
93
+
62
94
  类名、CSS 变量、动画名称与动态设置统一使用 `rhine-` 命名空间。升级到 0.3.0 时请同步迁移主题根、自定义样式与动效属性,具体见 [迁移说明](CHANGELOG.md#030)。
63
95
 
64
96
  ### 兼容范围
@@ -66,7 +98,7 @@ export function Welcome() {
66
98
  - 包导出 TS/TSX、CSS 和 SVG 源码,构建器需支持 CSS 导入与 Vite 的 SVG `?no-inline` 引用;目前未验证其他构建器和 SSR。
67
99
  - 浮层挂载到最近的 `.rhine-theme`。请将主题根放在页面根部,避免对它使用 `transform`、裁剪或 `contain`。
68
100
  - 组件提供呈现与交互,应用负责数据请求、搜索、分页和操作结果。
69
- - 当前提供默认暖白主题;Logo 支持独立的深浅配色,完整组件深色主题尚未提供。
101
+ - 默认使用暖白主题;0.4.0 还提供可选的战术终端与橙趣主题。Logo 的深浅配色独立于界面主题。
70
102
 
71
103
  <a id="components"></a>
72
104
 
@@ -77,12 +109,14 @@ export function Welcome() {
77
109
  | 品牌与文字 | Logo · Text |
78
110
  | 操作 | Button · Icon · IconButton · Link · CopyButton |
79
111
  | 布局 | Surface · Toolbar · Divider · Stack · Inline · Grid |
80
- | 表单与选择 | TextField · Textarea · Select · Checkbox · RadioGroup · Switch · SearchField · Combobox · SelectionCard |
81
- | 导航与展开 | Tabs · SegmentedControl · Disclosure |
82
- | 数据 | Metric · KeyValue · DataTable · Pagination |
83
- | 浮层 | Dialog · ConfirmDialog · Popover · Menu · Tooltip |
112
+ | 表单与选择 | TextField · Fieldset¹ · ErrorSummary¹ · Textarea · Select · Checkbox · RadioGroup · Switch · SearchField · Combobox · SelectionCard |
113
+ | 导航与展开 | SideNav¹ · Breadcrumbs¹ · Steps¹ · Tabs · SegmentedControl · Disclosure |
114
+ | 数据 | Metric · KeyValue · DataTable · Pagination · StatusTimeline¹ · TimelineLegend¹ |
115
+ | 浮层 | Drawer¹ · Dialog · ConfirmDialog · Popover · Menu · Tooltip |
84
116
  | 状态与反馈 | Badge · Notice · Toast · Progress · Skeleton · EmptyState |
85
117
 
118
+ ¹ 自 0.4.0 提供。
119
+
86
120
  参数、状态和组合示例见[组件 API](components/README.md)与[常用组件契约](components/COMMON_COMPONENTS.md)。
87
121
 
88
122
  <a id="documentation"></a>
@@ -94,13 +128,18 @@ export function Welcome() {
94
128
  | 主题 | 文档 | 在线预览 |
95
129
  | --- | --- | --- |
96
130
  | 视觉基础与组合原则 | [设计规范](docs/DESIGN_SYSTEM.md) | [视觉基础](https://linhay.github.io/rhine-design/#foundation) |
131
+ | 间距、分组与卡片组合 | [Spacing](components/SPACING.md) | [间距与组合](https://linhay.github.io/rhine-design/#spacing) |
97
132
  | 字体、文字层级与数值 | [Typography](components/TYPOGRAPHY.md) | [字体与文字](https://linhay.github.io/rhine-design/#typography) |
98
133
  | 品牌几何与 Logo | [Logo 契约](components/README.md#品牌标志) | [品牌标志](https://linhay.github.io/rhine-design/#brand) |
99
134
  | 动效、交互与减少动态 | [Motion](components/MOTION.md) | [动效实验室](https://linhay.github.io/rhine-design/#motion) |
100
135
  | 启动动画与就绪衔接 | [Startup](components/STARTUP.md) | [启动动画](https://linhay.github.io/rhine-design/#startup) |
136
+ | 场景与交互选型 | [使用规范](docs/INTERACTION_PATTERNS.md) | [场景指南](https://linhay.github.io/rhine-design/#patterns) |
137
+ | 导航与表单流程 | [组件契约](docs/INTERACTION_PATTERNS.md#新组件契约) | [导航](https://linhay.github.io/rhine-design/#navigation) · [表单](https://linhay.github.io/rhine-design/#form-workflow) |
101
138
  | 系统覆盖与演进方向 | [组件系统对照](docs/DESIGN_SYSTEM_AUDIT.md) | — |
102
139
 
103
- 展台包含组件状态、键盘操作、长文本、加载与错误示例。动效可播放、重播、复位和慢放,并支持减少动态模式。在线版本跟随 `main` 更新;如需本地运行:
140
+ 展台采用左侧分组目录、右侧单章节工作区;目录与内容独立滚动,当前章节高亮,原有锚点可直接分享并支持浏览器前进/后退。窄屏使用可展开目录,选择章节后自动收起。动效偏好跨章节保留。
141
+
142
+ 展台包含组件状态、键盘操作、长文本、加载与错误示例。动效可播放、重播、复位和慢放,并支持减少动态模式。离开章节会卸载其演示,返回后重新开始。在线版本跟随 `main` 更新;如需本地运行:
104
143
 
105
144
  ```sh
106
145
  git clone https://github.com/linhay/rhine-design.git
@@ -13,6 +13,8 @@ const paths = {
13
13
  external: 'M11 3h6v6M17 3 9 11M7 3H3v14h14v-4',
14
14
  check: 'm4 10 4 4 8-8',
15
15
  more: 'M4 10h.1M10 10h.1M16 10h.1',
16
+ connection: 'M7 2v4M13 2v4M5 6h10v3a5 5 0 0 1-10 0V6M10 14v4',
17
+ chart: 'M3 3v14h14M7 13V9M11 13V5M15 13V7',
16
18
  } as const;
17
19
  export type IconName = keyof typeof paths;
18
20
  export function Icon({ name, size = 20 }: { name: IconName; size?: 16 | 20 }) {
@@ -2,6 +2,10 @@
2
2
 
3
3
  本文件定义[莱茵设计](../README.md)的常用组件契约。先固定契约和组件展台,再验证;默认优先复用本目录,页面先改设计稿,再按授权接入App。具体业务由宿主定义,不进入通用契约。
4
4
 
5
+ ## 新增导航与流程能力(main,未发布)
6
+
7
+ SideNav、Breadcrumbs、Steps、Drawer、Fieldset、ErrorSummary、StatusTimeline、TimelineLegend 的 API、键盘与场景契约见[交互选型](../docs/INTERACTION_PATTERNS.md)。main 共48个导出组件;下表保留原24个扩展组件的契约。
8
+
5
9
  ## 范围与契约
6
10
 
7
11
  | 组件 | 最小职责与边界 |
@@ -40,7 +44,7 @@
40
44
 
41
45
  ## 接入契约
42
46
 
43
- 统一从 套件的`components/index.ts` 导入,并在页面根部使用 `.rhine-theme`。主题根不要放在带transform、overflow裁剪或contain的局部面板;浮层挂到最近主题根,保持token和减少动态偏好。Dialog、Popover、Menu的trigger与Tooltip的children必须是单个可聚焦、可接收ref和事件的元素;使用Button/IconButton即可。Dialog受控时必须同时传open与onOpenChange。
47
+ 统一从 套件的`components/index.ts` 导入,并在页面根部使用 `.rhine-theme`。主题根不要放在带transform、overflow裁剪或contain的局部面板;浮层挂到最近主题根,保持token和减少动态偏好。Dialog、Popover、Menu的trigger与Tooltip的children必须是单个可聚焦、可接收ref和事件的元素;使用Button/IconButton即可。Dialog受控时必须同时传open与onOpenChange。分栏的sidePanel可传ReactNode,或传`closeButton => ReactNode`将关闭按钮放进侧栏导航;函数模式必须展示传入按钮,不能复制第二个关闭入口。
44
48
 
45
49
  ```tsx
46
50
  <Stack gap={4}>
@@ -0,0 +1,25 @@
1
+ import { useEffect, useId, useRef, type FieldsetHTMLAttributes, type ReactNode } from 'react';
2
+
3
+ export interface FieldsetProps extends FieldsetHTMLAttributes<HTMLFieldSetElement> { legend: string; hint?: string; children: ReactNode }
4
+ export function Fieldset({ legend, hint, children, className = '', 'aria-describedby': describedBy, ...props }: FieldsetProps) {
5
+ const id = useId();
6
+ return <fieldset {...props} className={`rhine-fieldset ${className}`} aria-describedby={[describedBy, hint ? id : undefined].filter(Boolean).join(' ') || undefined}>
7
+ <legend>{legend}</legend>{hint && <p id={id}>{hint}</p>}<div className="rhine-fieldset__body">{children}</div>
8
+ </fieldset>;
9
+ }
10
+
11
+ export interface FieldError { fieldId: string; message: string }
12
+ export interface ErrorSummaryProps { errors: readonly FieldError[]; title?: string; focusKey?: number }
13
+ export function ErrorSummary({ errors, title = '请修正以下问题', focusKey }: ErrorSummaryProps) {
14
+ const ref = useRef<HTMLDivElement>(null), id = useId();
15
+ const hasErrors = errors.length > 0;
16
+ // Focus only on a failed submission, never on each keystroke. Increment focusKey for retries.
17
+ useEffect(() => { if (hasErrors && focusKey !== undefined) ref.current?.focus(); }, [focusKey, hasErrors]);
18
+ if (!hasErrors) return null;
19
+ return <div className="rhine-error-summary" ref={ref} tabIndex={-1} role="region" aria-labelledby={id}>
20
+ <h3 id={id}>{title}</h3><ul>{errors.map(error => <li key={error.fieldId}><a href={`#${error.fieldId}`} onClick={event => {
21
+ const field = document.getElementById(error.fieldId);
22
+ if (field) { event.preventDefault(); field.focus(); field.scrollIntoView?.({ block: 'center' }); }
23
+ }}>{error.message}</a></li>)}</ul>
24
+ </div>;
25
+ }
@@ -4,7 +4,7 @@
4
4
 
5
5
  2026-10-22,用户指定 [Transitions.dev](https://transitions.dev/) 作为UX与动画参考。本文件补充[组件规范](./README.md#动效规范),保留Rhine Design的苔绿、暖白、三角几何与清晰文字。以下参数是本项目设计决定,不是对参考站时长或曲线的测量结果。
6
6
 
7
- 用户已进一步授权实现展台:`/#motion`提供10组可播放、重播、复位的真实示例,支持原速/4倍慢放、逐例减少动态及全局偏好。组件总数保持40;新配方仍限定于展台,后续按“展台评审 → 通用组件采用 → App接入”推进。
7
+ 用户已进一步授权实现展台:`/#motion`目前提供12组可播放、重播、复位的真实示例,支持原速/4倍慢放、逐例减少动态及全局偏好。main组件共46个(npm0.3.0为40个);实验性配方仍限定于展台,后续按“展台评审 → 通用组件采用 → App接入”推进。
8
8
 
9
9
  ## 参考依据
10
10
 
@@ -20,7 +20,7 @@
20
20
  4. **动效可被主动比较。** 展台后续为每种动效提供静态起点、播放/切换、重播、参数说明及减少动态对照。重播只改变本地演示状态,不重新执行复制、请求、确认等真实动作。
21
21
  5. **解释适用条件。** 示例卡写清“触发 → 变化 → 结束状态”,并显示已实现/设计候选;筛选只改变展台展示范围,不改变组件业务值。宽屏并排对照,窄屏单列,播放按钮位置稳定。
22
22
 
23
- 动效实验室已为10组配方提供上述播放控制、状态说明、参数和逐例减少动态开关;并排卡片可分别设置偏好作比较。既有组件目录保留普通交互示例,不给每个无动画组件添加播放按钮。
23
+ 动效实验室已为12组配方提供上述播放控制、状态说明、参数和逐例减少动态开关;并排卡片可分别设置偏好作比较。既有组件目录保留普通交互示例,不给每个无动画组件添加播放按钮。
24
24
 
25
25
  ## Rhine Design 动效映射
26
26
 
@@ -70,3 +70,7 @@
70
70
  首次内容切换不闪空白、不让操作目标跳位;刷新不丢旧数据;退出后无透明遮挡层和遗留焦点;无隐藏控件可Tab到。文档参数、DOM存在与单张截图不能代替完整动效或宿主原生平台原生验收。
71
71
 
72
72
  2026-10-22展台验证:Chrome通过400/520/880/1200宽度、200%缩放、10组播放/复位、快速重播取消旧结果、四向位置/位移采样、菜单键盘、弹窗内重播保焦点与关闭归还、折叠中途反向、通知关闭、Tooltip Escape、逐例/全局/系统减少动态。采样确认4倍慢放的内容淡入720ms、反馈480ms、浮层640ms、折叠800ms及品牌960ms;未将合成浏览器结果扩展为原生或业务验收。
73
+
74
+ ## 导航与流程动效(main,未发布)
75
+
76
+ 新增抽屉与步骤两组示例,实验室现为12组。Drawer继承160ms浮层token,从右侧3px淡入;弹层内重播只重启CSS动画,焦点与内容不重建。Steps当前段使用180ms/3px进入,由业务校验推进。表单错误直接呈现并定位,无抖动效果。完整适用场景、减少动态与告警规则见[交互规范](../docs/INTERACTION_PATTERNS.md)。
@@ -0,0 +1,42 @@
1
+ import type { HTMLAttributes, MouseEvent } from 'react';
2
+ import { Icon, type IconName } from './Actions';
3
+
4
+ export interface NavItem { id: string; label: string; href: string; icon?: IconName }
5
+ export interface NavGroup { label: string; items: readonly NavItem[] }
6
+ export interface SideNavProps extends HTMLAttributes<HTMLElement> {
7
+ groups: readonly NavGroup[]; currentId: string;
8
+ onNavigate?: (item: NavItem, event: MouseEvent<HTMLAnchorElement>) => void;
9
+ }
10
+ export function SideNav({ groups, currentId, onNavigate, className = '', 'aria-label': label = '主导航', ...props }: SideNavProps) {
11
+ return <nav {...props} aria-label={label} className={`rhine-side-nav ${className}`}>
12
+ {groups.map(group => <div className="rhine-side-nav__group" key={group.label}><p>{group.label}</p><ul>
13
+ {group.items.map(item => <li key={item.id}><a href={item.href} aria-current={currentId === item.id ? 'page' : undefined} onClick={event => onNavigate?.(item, event)}>{item.icon && <Icon name={item.icon} size={20}/>}<span className="rhine-side-nav__label">{item.label}</span></a></li>)}
14
+ </ul></div>)}
15
+ </nav>;
16
+ }
17
+
18
+ export interface BreadcrumbItem { label: string; href?: string }
19
+ export interface BreadcrumbsProps { items: readonly BreadcrumbItem[]; label?: string }
20
+ export function Breadcrumbs({ items, label = '当前位置' }: BreadcrumbsProps) {
21
+ return <nav className="rhine-breadcrumbs" aria-label={label}><ol>{items.map((item, index) => <li key={index}>
22
+ {index > 0 && <span aria-hidden="true" className="rhine-breadcrumbs__separator">/</span>}
23
+ {item.href ? <a href={item.href} aria-current={index === items.length - 1 ? 'page' : undefined}>{item.label}</a> : <span aria-current={index === items.length - 1 ? 'page' : undefined}>{item.label}</span>}
24
+ </li>)}</ol></nav>;
25
+ }
26
+
27
+ export interface StepItem { id: string; label: string; description?: string }
28
+ export interface StepsProps {
29
+ items: readonly StepItem[]; current: number; label?: string; error?: string;
30
+ onStepChange?: (index: number) => void;
31
+ }
32
+ export function Steps({ items, current, label = '任务步骤', error, onStepChange }: StepsProps) {
33
+ const position = Math.max(0, Math.min(current, items.length));
34
+ return <ol className="rhine-steps" aria-label={label}>{items.map((item, index) => {
35
+ const state = index < position ? 'complete' : index === position ? (error ? 'error' : 'current') : 'pending';
36
+ const status = { complete: '已完成', current: '进行中', error: '需要修正', pending: '未开始' }[state];
37
+ const content = <><span className="rhine-steps__number" aria-hidden="true">{state === 'complete' ? '✓' : String(index + 1).padStart(2, '0')}</span><span><strong>{item.label}</strong><small>{status}{item.description ? ` · ${item.description}` : ''}{state === 'error' ? ` · ${error}` : ''}</small></span></>;
38
+ return <li key={item.id} data-state={state} aria-current={index === position ? 'step' : undefined}>
39
+ {onStepChange && index < position ? <button type="button" onClick={() => onStepChange(index)} aria-label={`返回${item.label}`}>{content}</button> : <div>{content}</div>}
40
+ </li>;
41
+ })}</ol>;
42
+ }
@@ -14,11 +14,17 @@ function useScope() {
14
14
  useLayoutEffect(() => { setContainer((ref.current?.closest('.rhine-theme') as HTMLElement) || document.body); }, []);
15
15
  return { ref, container };
16
16
  }
17
- export interface DialogProps { trigger: ReactElement; title: string; description: string; children: ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void }
18
- export function Dialog({ trigger, title, description, children, open, onOpenChange }: DialogProps) {
17
+ export interface DialogProps { trigger: ReactElement; title: string; description: string; children: ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void; sidePanel?: ReactNode | ((closeButton: ReactElement) => ReactNode) }
18
+ export function Dialog(props: DialogProps) {
19
+ return <DialogFrame {...props}/>;
20
+ }
21
+ export function Drawer(props: Omit<DialogProps, 'sidePanel'>) { return <DialogFrame {...props} drawer />; }
22
+ function DialogFrame({ trigger, title, description, children, open, onOpenChange, sidePanel, drawer = false }: DialogProps & { drawer?: boolean }) {
19
23
  const { ref, container } = useScope();
24
+ const sideNavigation = typeof sidePanel === 'function';
25
+ const closeButton = <DialogBase.Close asChild><IconButton label={drawer ? '关闭抽屉' : '关闭对话框'} icon="close" /></DialogBase.Close>;
20
26
  return <span className="rhine-overlay-anchor" ref={ref}><DialogBase.Root open={open} onOpenChange={onOpenChange}><DialogBase.Trigger asChild>{trigger}</DialogBase.Trigger><DialogBase.Portal container={container}>
21
- <DialogBase.Overlay className="rhine-overlay" /><DialogBase.Content className="rhine-dialog"><div className="rhine-dialog__head"><DialogBase.Title className="rhine-text rhine-text--section">{title}</DialogBase.Title><DialogBase.Close asChild><IconButton label="关闭对话框" icon="close" /></DialogBase.Close></div><DialogBase.Description className="rhine-text rhine-text--body rhine-text--muted">{description}</DialogBase.Description><div className="rhine-dialog__body">{children}</div></DialogBase.Content>
27
+ <DialogBase.Overlay className="rhine-overlay" /><DialogBase.Content className={`rhine-dialog${drawer ? ' rhine-drawer' : ''}${sidePanel ? ' rhine-dialog--split' : ''}${sideNavigation ? ' rhine-dialog--side-navigation' : ''}`}><div className="rhine-dialog__main"><div className="rhine-dialog__head"><DialogBase.Title className="rhine-text rhine-text--section">{title}</DialogBase.Title>{!sideNavigation && closeButton}</div><DialogBase.Description className="rhine-text rhine-text--body rhine-text--muted">{description}</DialogBase.Description><div className="rhine-dialog__body">{children}</div></div>{sidePanel && <div className="rhine-dialog__side">{typeof sidePanel === 'function' ? sidePanel(closeButton) : sidePanel}</div>}</DialogBase.Content>
22
28
  </DialogBase.Portal></DialogBase.Root></span>;
23
29
  }
24
30
  export function ConfirmDialog({ trigger, title, description, confirmLabel = '确认操作', danger = false, onConfirm }: { trigger: ReactElement; title: string; description: string; confirmLabel?: string; danger?: boolean; onConfirm: () => Promise<void> | void }) {
@@ -1,25 +1,31 @@
1
- # 莱茵设计 · 通用组件 V1.4
1
+ # 莱茵设计 · 通用组件
2
2
 
3
3
  本目录提供[莱茵设计 · Rhine Design](../README.md)的 React 组件、设计变量和 API 契约。组件接收应用传入的数据与动作,不绑定特定后端或原生平台。包名为`@linhey/rhine-design`;样式变量和类名使用`rhine-`命名空间。
4
4
 
5
- 共40个组件。已有16个基础组件,新增24个数据、表单、操作、浮层和布局组件,见[常用组件契约](./COMMON_COMPONENTS.md)。[字体与文字规范](./TYPOGRAPHY.md)与[主流系统对照与缺口](../docs/DESIGN_SYSTEM_AUDIT.md)共同约束复用。静态页面样稿也使用同源Toolbar/Select CSS,不运行React。
5
+ 0.4.0 共48个组件;0.3.0为40个。新增导航、表单流程及健康时间轴组件见[场景与交互规范](../docs/INTERACTION_PATTERNS.md),原24个扩展组件见[常用组件契约](./COMMON_COMPONENTS.md)。[字体与文字规范](./TYPOGRAPHY.md)与[主流系统对照与缺口](../docs/DESIGN_SYSTEM_AUDIT.md)共同约束复用。静态页面样稿也使用同源Toolbar/Select CSS,不运行React。
6
6
 
7
7
  页面改动遵循:优先复用组件 → 先在设计稿/展台修改并验证 → 再按任务授权实现到App。缺组件先补组件与展台,不在业务页另写同用途样式;同一任务已授权设计和实现时连续推进,无需重复确认。
8
8
 
9
+ ## 间距组合
10
+
11
+ [间距规范](SPACING.md)定义尺度、分组与卡片配方:Surface负责24px内边距,嵌套Stack提供8px标题/说明和16px内容/操作间隔,Inline提供8px按钮间隔;Surface不自动给子元素加间距。展台`#spacing`可检查默认、加载、错误和长文案。
12
+
9
13
  ## 使用
10
14
 
11
15
  先按[套件接入说明](../README.md#quick-start)安装组件包,再导入:
12
16
 
13
17
  ```tsx
14
- import { Button, Notice, Progress, Surface } from '@linhey/rhine-design';
18
+ import { Button, Inline, Notice, Progress, Stack, Surface } from '@linhey/rhine-design';
15
19
 
16
20
  export function Example() {
17
21
  return (
18
22
  <div className="rhine-theme">
19
23
  <Surface>
24
+ <Stack gap={4}>
20
25
  <Notice tone="warning" title="需要处理">请检查后重试。</Notice>
21
26
  <Progress label="任务进度" value={null} />
22
- <Button variant="secondary" onClick={() => { /* 业务动作 */ }}>重试</Button>
27
+ <Inline gap={2}><Button variant="secondary" onClick={() => { /* 业务动作 */ }}>重试</Button></Inline>
28
+ </Stack>
23
29
  </Surface>
24
30
  </div>
25
31
  );
@@ -28,6 +34,8 @@ export function Example() {
28
34
 
29
35
  入口 `index.ts` 自动导入组件 CSS。使用一个 `.rhine-theme` 包裹组件树,获得字体、盒模型、焦点和动效降级规则。主题根放在页面根部,不加transform、裁剪或contain;浮层Portal挂在最近的主题根以继承变量和减少动态偏好。独立运行与跨项目导入方式见[套件入口](../README.md);跨仓复用核对React19与套件package.json中的5个Radix依赖,不导入展台。
30
36
 
37
+ 0.4.0 新增主题入口 `@linhey/rhine-design/theme`,导出 `ThemeProvider`、`useTheme`、`themes`、`ThemeName` 和 `ThemeProviderProps`(自 0.4.0 提供)。Provider 自动生成主题根并加载内置主题;用 `setTheme('arknights')` 切换,受控偏好与存储由宿主管理。支持并列 Provider,暂不支持嵌套覆盖。完整契约及代码见[主题 API](../docs/THEMING.md)。
38
+
31
39
  - `tokens.css`:唯一色彩、尺寸与动画变量来源。
32
40
  - `styles.css`:`rhine-` 前缀组件样式,业务页面可组合布局但不覆盖组件状态。
33
41
  - `components.tsx`:原有基础组件;`Actions/Data/Forms/Layout/Overlays/Toast.tsx`承载新增组件。表单与业务数据由调用方控制,浮层开关、通知计时及复制结果只管理局部交互。
@@ -67,7 +75,7 @@ export function Example() {
67
75
 
68
76
  启动动画候选见[三角构架归位](./STARTUP.md)与`/#startup`。专属分镜不改变通用Logo入场参数,也不作为真实初始化等待门槛。
69
77
 
70
- 用户指定的Transitions.dev参考与莱茵设计取舍见[UX与动效补充](./MOTION.md)。`/#motion`已提供10组播放、重播、复位与4倍慢放示例,可逐例切换减少动态。方向展开、内容淡入与结果反馈仅在实验室作为候选实现;通用默认实现仍以下表和实际组件为准。
78
+ 用户指定的Transitions.dev参考与莱茵设计取舍见[UX与动效补充](./MOTION.md)。`/#motion`已提供12组播放、重播、复位与4倍慢放示例,可逐例切换减少动态。方向展开、内容淡入与结果反馈仅在实验室作为候选实现;通用默认实现仍以下表和实际组件为准。
71
79
 
72
80
  ### 品牌标志
73
81
 
@@ -0,0 +1,82 @@
1
+ # 间距与组合
2
+
3
+ 间距表达信息关系:同组紧、组间松、区域之间留白。颜色和边框不能代替分组。此规范适用于所有消费项目,真实示例见展台 `#spacing`。
4
+
5
+ ## 尺度与用途
6
+
7
+ 唯一尺寸来源是 [tokens.css](tokens.css)。`Stack / Inline / Grid` 的 `gap` 是 token 后缀,不是 px:`gap={4}` 表示16px。
8
+
9
+ | Token / gap | 尺寸 | 默认用途 |
10
+ | --- | --- | --- |
11
+ | `--rhine-space-1` / 1 | 4px | 标签与紧随其后的短提示、图标微间隔 |
12
+ | `--rhine-space-2` / 2 | 8px | 标题与说明、字段标签与控件、同组按钮 |
13
+ | `--rhine-space-3` / 3 | 12px | 同组信息行、密集工具栏项目 |
14
+ | `--rhine-space-4` / 4 | 16px | 内容与操作、表单字段、普通列表/网格项 |
15
+ | `--rhine-space-6` / 6 | 24px | Surface内边距、卡片之间、模块内部的独立信息组 |
16
+ | `--rhine-space-8` / 8 | 32px | 页面主要章节之间、宽屏页面边距 |
17
+ | `--rhine-space-12` / 12 | 48px | 独立大区域或品牌留白,不用于普通表单 |
18
+
19
+ 0只表示无间隔,不新增token。1px边线、控件高度、焦点环偏移不属于间距尺度。宿主既有窄窗20px边距可作为明确记录的布局例外,不能变成随处使用的新gap。
20
+
21
+ ## 视觉间距与操作范围
22
+
23
+ 分别定义可见图形、文字行盒、可点击范围与焦点环。视觉对齐按可见内容和行盒测量,操作范围不得为压缩列表而缩小;允许透明命中区向外扩展,但不能覆盖相邻按钮、链接或输入。不能同时叠加父gap、条目padding和控件内部透明留白来表达同一段距离。
24
+
25
+ 有分隔线的重复条目,分隔线前后使用同一组间距;首项不重复叠加区域顶部间距,末项为按需详情与焦点环留出空间。不要在分隔线后立刻贴标题、却把全部留白放到前一条末尾。
26
+
27
+ 密集健康列表:名称/额度行与20px健康条间隔8px,条目之间分隔线两侧各16px;首项不额外叠加顶部内距。compact时间轴的44px命中区在可见条上下各扩展12px,不占据文档流留白;调用方须保留这段安全空间,避免与相邻可点击控件重叠。不要再把44px命中框的内部空白叠加到视觉gap上。
28
+
29
+ 这是紧凑健康列表的配方,不要求所有列表都采用20px图形或16px内距。浮层的自然高度和单层滚动规则见[交互规范](../docs/INTERACTION_PATTERNS.md#弹窗高度与滚动归属)。
30
+
31
+ ## 默认卡片配方:24 / 8 / 16
32
+
33
+ - 外层 `Surface` 负责24px内边距与边界,不自动给任意子节点添加margin。`SelectionCard`同样使用24px内边距,组件自身提供标题与内容间的8px;内容内部仍由调用方分组。
34
+ - 标题与说明放进 `Stack gap={2}`,保持8px间隔。
35
+ - 内容组与操作组放进 `Stack gap={4}`,保持16px间隔。
36
+ - 操作组使用 `Inline gap={2}`,按钮间隔8px、允许换行、保持内容宽度。
37
+ - 卡片之间用 `Stack gap={6}` 或 `Grid gap={6}`,保持24px间隔。不要同时设置父级gap与子级margin来叠加同一段距离。
38
+
39
+ ```tsx
40
+ <Surface>
41
+ <Stack gap={4}>
42
+ <Stack gap={2}>
43
+ <Text as="h2" variant="heading">应用更新</Text>
44
+ <Text tone="muted">当前版本已是最新。</Text>
45
+ </Stack>
46
+ <Inline gap={2}>
47
+ <Button variant="secondary" onClick={checkUpdate}>检查更新</Button>
48
+ </Inline>
49
+ </Stack>
50
+ </Surface>
51
+ ```
52
+
53
+ 页面已有h1时卡片通常用h2;标题等级由结构决定,不能用字号决定。不要为布局引入空Text、空段落或连续br。
54
+
55
+ 行高与间距分别管理:正文14/22px、标题16/24px,独立正文段落默认12px段间距;不是在每一行之间再加gap。详见[行高规范](TYPOGRAPHY.md#行高段落与控件)。
56
+
57
+ ## 常见组合
58
+
59
+ | 场景 | 内部 | 组间 / 区域 | 组件选择 |
60
+ | --- | --- | --- | --- |
61
+ | 信息与设置卡片 | 标题/说明8px | 内容/动作16px;卡片24px | Surface +嵌套Stack +Inline |
62
+ | 表单 | 标签/控件由字段组件负责 | 字段16px;字段组24px;提交区24px | TextField/Fieldset +Stack |
63
+ | 工具栏 | 同组按钮8px | 独立组16px,窄屏换行 | Toolbar/Inline |
64
+ | 数据列表 | 同项说明4–8px | 列表项16px;筛选/表格/分页16–24px | Stack +DataTable/Pagination |
65
+ | 浮层 | 标题/说明由浮层组件负责 | 正文组16px;操作区24px | Dialog/Drawer +Stack/Inline |
66
+ | 反馈 | 提示内部由Notice负责 | 与前后内容16px | Notice放入Stack,不覆盖其内部样式 |
67
+
68
+ 这些是组合默认值,不重新覆盖现有组件内部的padding、gap或状态样式。要改变组件密度,先在库中定义并验证,不能在业务页用后代选择器临时压缩。
69
+
70
+ ## 长文本、窄窗与状态变化
71
+
72
+ 保持8/16/24的关系,不因窄屏把所有间距归零。长文本自然换行、卡片随内容增高;多列转单列,按钮换行,不缩小字号或点击区域。保持至少44px操作目标及焦点环空间。
73
+
74
+ 加载保留已有文案和按钮位置,只改变真实状态;额外进度或错误信息作为独立组加入16px间距。错误文案常驻直至处理,不用固定卡高或裁剪限制。状态元素移除后不保留空白占位组;不要用空的Inline或Stack充当spacer。
75
+
76
+ 间距本身不动画。状态变化复用现有组件动效,不为留白增加缩放、弹跳或循环;减少动态保留同样布局与操作。
77
+
78
+ ## 验收
79
+
80
+ 检查默认、加载、错误、长文案;400/520/880px及200%等效重排;文字、焦点与按钮不裁切;不存在父gap叠加子margin或按钮意外撑满。以真实边界间距为准,行高内部的字形留白不当成额外margin测量。
81
+
82
+ 本规范与展台随main更新;现有gap API在npm0.3.0可用,文档章节不代表已发布新版本。
@@ -0,0 +1,29 @@
1
+ import { useId, useState } from 'react';
2
+ import { Text } from './Typography';
3
+
4
+ export type TimelineStatus = 'healthy' | 'recovered' | 'degraded' | 'failed' | 'unknown';
5
+ export interface TimelineSegment { id: string; label: string; status: TimelineStatus }
6
+ export const timelineLabels: Record<TimelineStatus, string> = { healthy: '有输出·未见异常', recovered: '重试已恢复', degraded: '异常·结果待确认', failed: '存在最终失败', unknown: '未知 / 无数据' };
7
+
8
+ /** Host supplies ordered, validated evidence; this component never computes a health score. */
9
+ export function StatusTimeline({ label, segments, startLabel, endLabel, compact = false }: { label: string; segments: TimelineSegment[]; startLabel: string; endLabel: string; compact?: boolean }) {
10
+ const [index, setIndex] = useState<number>();
11
+ const detailId = useId();
12
+ const selected = Math.min(index ?? segments.length - 1, segments.length - 1);
13
+ const segment = segments[selected];
14
+ return <div className={`rhine-timeline${compact ? ' rhine-timeline--compact' : ''}`}>
15
+ <div className="rhine-timeline__track">
16
+ <div className="rhine-timeline__bars" aria-hidden="true">{segments.map((item, i) => <span key={item.id} data-status={item.status} data-selected={i === selected} />)}</div>
17
+ <input type="range" min={0} max={Math.max(0, segments.length - 1)} value={Math.max(0, selected)} disabled={!segments.length}
18
+ aria-label={label} aria-valuetext={segment ? `${segment.label} · ${timelineLabels[segment.status]}` : '暂无记录'} aria-describedby={detailId}
19
+ onChange={event => setIndex(Number(event.target.value))}
20
+ onPointerMove={event => { if (event.pointerType !== 'mouse' || !segments.length) return; const rect = event.currentTarget.getBoundingClientRect(); setIndex(Math.min(segments.length - 1, Math.max(0, Math.floor((event.clientX - rect.left) / rect.width * segments.length)))); }} />
21
+ </div>
22
+ <div className={compact ? 'rhine-sr-only' : 'rhine-timeline__range'}><Text variant="caption" tone="muted">{startLabel}</Text><Text variant="caption" tone="muted">{endLabel}</Text></div>
23
+ <Text id={detailId} className="rhine-timeline__detail" variant="caption">{segment ? `${segment.label} · ${timelineLabels[segment.status]}` : '暂无记录'}</Text>
24
+ </div>;
25
+ }
26
+
27
+ export function TimelineLegend() {
28
+ return <ul className="rhine-timeline__legend" aria-label="健康记录图例">{Object.entries(timelineLabels).map(([status, label]) => <li key={status}><span data-status={status} aria-hidden="true"/>{label}</li>)}</ul>;
29
+ }
@@ -1,6 +1,6 @@
1
- # 莱茵设计 · 字体与文字规范 V1.3
1
+ # 莱茵设计 · 字体与文字规范 V1.4
2
2
 
3
- 2026-10-22。适用于使用莱茵设计的中文界面与中英文混排。变量定义在 [tokens.css](./tokens.css),样式与 React 入口分别为 [typography.css](./typography.css)、[Typography.tsx](./Typography.tsx)。归属与接入见[套件入口](../README.md)。
3
+ 2026-10-23。适用于使用莱茵设计的中文界面与中英文混排。变量定义在 [tokens.css](./tokens.css),样式与 React 入口分别为 [typography.css](./typography.css)、[Typography.tsx](./Typography.tsx)。归属与接入见[套件入口](../README.md)。
4
4
 
5
5
  ## 字体与字重
6
6
 
@@ -34,6 +34,38 @@
34
34
  <Text as="code" variant="code">gpt-6.1-sol</Text>
35
35
  ```
36
36
 
37
+ ## 行高、段落与控件
38
+
39
+ 字号与行高成对使用,`Text`按variant同时设置两者;不在业务页单改字号而继承父级行高,不使用`normal`或固定容器高度猜测文本尺寸。上表是100%缩放的CSS尺寸,缩放后保持比例与自然换行,不以压低行高塞回原高度。
40
+
41
+ | 内容 | 字号 / 行高 | Token | 多行规则 |
42
+ | --- | --- | --- | --- |
43
+ | 大标题 | 30 / 38px | `--rhine-leading-title` | 换行仍为38px,不压成单行 |
44
+ | 分区标题 | 18 / 26px | `--rhine-leading-section` | 行距保持,标题之后的间隔另计 |
45
+ | 卡片标题 | 16 / 24px | `--rhine-leading-heading` | 中英文混排同一行高 |
46
+ | 正文、表单标签、按钮 | 14 / 22px | `--rhine-leading-body` | 两行正文占44px,三行占66px |
47
+ | 时间、来源、辅助说明 | 12 / 18px | `--rhine-leading-caption` | 两行36px;重要正文不要降为辅助字号 |
48
+ | 代码、路径 | 12 / 20px | `--rhine-leading-code` | 长标识可换行,行距不随字体回退变化 |
49
+ | 指标 | 26 / 32px;紧凑20 / 26px | `--rhine-leading-metric` / `--rhine-leading-metric-compact` | 字号与行高一起切换,单位不得把整行撑乱 |
50
+
51
+ - 行高是行框高度,包含字形上下留白,不等于两行字形之间的空白;不能把行高22px理解为额外22px的gap。
52
+ - 一个段落自然换行时不增加margin;两个独立段落默认用`Stack gap={3}`(12px)。标题与说明仍为8px,正文组与操作为16px,遵循[间距规范](SPACING.md)。标题/段落默认无浏览器自带margin。
53
+ - 按钮文字采用14/22,最小操作目标44px通过padding与min-height保证;不要把line-height设置为44px来垂直居中。长按钮文案可换行并增高,禁止固定height裁切。
54
+ - 现有紧凑组件的例外:Badge与Notice说明为12/20px,数据表普通文本为12/22px;这些是组件内部的既有密度,不用后代CSS临时覆盖。数据表中的技术标识仍使用Text code的12/20px。Logo字标是单行品牌几何,line-height=1仅限该品牌组件,不用于正文。
55
+ - 在行内混用不同字号时,行框可能由最大的子元素决定;需要固定对齐的指标、单位和标签使用组件既有组合,不承诺所有混排整行都等于单个Text的行高。
56
+ - 行高、字距与字号不做动画;状态文字立即更新,减少动态不改变排版。
57
+
58
+ 展台`#typography`的「多行行高与段落」使用真实Text和Stack,可显示行框辅助线;辅助线只标注每行区域,不模拟字体基线,也不进入产品。
59
+
60
+ ```tsx
61
+ <Stack gap={3}>
62
+ <Text>第一段说明自然换行,每行22px;段内不添加额外gap。</Text>
63
+ <Text>第二段由Stack与前一段保持12px距离。</Text>
64
+ </Stack>
65
+ ```
66
+
67
+ 验收:400/520/880px与200%等效重排下检查标题、长正文、路径、操作按钮;计算样式与token一致,文字不裁切,长文增高不挤压操作。系统纯文本放大和原生辅助技术另行验收。此补充随main更新,不代表新npm版本已发布。
68
+
37
69
  ## 混排、数字与单位
38
70
 
39
71
  - 中文正文不增加字距,不做两端强制分散;段落左对齐,长说明建议不超过60ch。居中仅用于简短空态与品牌展示。
@@ -14,11 +14,11 @@
14
14
  .rhine-metric__unit { font-size:var(--rhine-text-caption); font-weight:400; color:var(--rhine-muted); }
15
15
  .rhine-key-value { margin:0; display:grid; gap:12px; }
16
16
  .rhine-key-value>div { display:grid; grid-template-columns:minmax(80px,1fr) minmax(0,2fr); gap:16px; }
17
- .rhine-key-value dt { color:var(--rhine-muted); font-size:12px; }.rhine-key-value dd { margin:0; overflow-wrap:anywhere; }
17
+ .rhine-key-value dt { color:var(--rhine-muted); font-size:var(--rhine-text-caption); line-height:var(--rhine-leading-caption); }.rhine-key-value dd { margin:0; overflow-wrap:anywhere; }
18
18
  .rhine-data { display:grid; gap:12px; min-width:0; }
19
19
  .rhine-table-scroll { overflow:auto; max-width:100%; }
20
20
  .rhine-table { width:100%; min-width:500px; border-collapse:collapse; text-align:left; font-size:12px; }
21
- .rhine-table caption { text-align:left; font-size:16px; font-weight:600; margin-bottom:16px; }
21
+ .rhine-table caption { text-align:left; font-size:var(--rhine-text-heading); line-height:var(--rhine-leading-heading); font-weight:var(--rhine-weight-heading); margin-bottom:var(--rhine-space-4); }
22
22
  .rhine-table th,.rhine-table td { padding:12px; border-bottom:1px solid var(--rhine-line); max-width:240px; overflow-wrap:anywhere; }
23
23
  .rhine-table th { color:var(--rhine-muted); font-weight:500; }.rhine-table td { font-variant-numeric:tabular-nums; }
24
24
  .rhine-table [data-align=right] { text-align:right; }.rhine-table tbody tr:hover { background:var(--rhine-selected); }
@@ -50,7 +50,16 @@
50
50
  .rhine-overlay-anchor { display:inline-block; vertical-align:middle; max-width:100%; }
51
51
  .rhine-overlay { position:fixed; inset:0; background:#242b2870; z-index:40; }
52
52
  .rhine-dialog { position:fixed; z-index:41; top:50%; left:50%; transform:translate(-50%,-50%); width:min(480px,calc(100vw - 32px)); max-height:calc(100dvh - 32px); overflow:auto; padding:24px; border:1px solid var(--rhine-line); border-radius:4px; background:var(--rhine-surface); color:var(--rhine-ink); box-shadow:0 16px 48px #242b2826; }
53
- .rhine-dialog>p { margin-top:12px; }.rhine-dialog>.rhine-notice { margin-top:16px; }
53
+ .rhine-dialog>p,.rhine-dialog__main>p { margin-top:12px; }.rhine-dialog>.rhine-notice { margin-top:16px; }
54
+ .rhine-dialog__main { min-width:0; }
55
+ .rhine-dialog--split { width:min(960px,calc(100vw - 32px)); display:grid; grid-template-columns:minmax(240px,0.8fr) minmax(0,1.2fr); gap:24px; }
56
+ .rhine-dialog--split .rhine-dialog__head { padding-right:24px; }
57
+ .rhine-dialog--split .rhine-dialog__head>.rhine-icon-button { position:absolute; top:16px; right:16px; }
58
+ .rhine-dialog__side { min-width:0; border-left:1px solid var(--rhine-line); padding:40px 0 0 24px; }
59
+ .rhine-dialog--side-navigation .rhine-dialog__side { padding-top:0; }
60
+ .rhine-dialog--side-navigation .rhine-dialog__main>.rhine-dialog__head { min-height:44px; padding-right:0; }
61
+ @media(max-width:720px) { .rhine-dialog--split { grid-template-columns:minmax(0,1fr); }.rhine-dialog__side { border-left:0; border-top:1px solid var(--rhine-line); padding:24px 0 0; } }
62
+ @media(max-width:720px) { .rhine-dialog--side-navigation .rhine-dialog__side { padding-top:16px; } }
54
63
  .rhine-dialog__head { display:flex; align-items:center; justify-content:space-between; gap:12px; }
55
64
  .rhine-dialog__head>:first-child { min-width:0; overflow-wrap:anywhere; }
56
65
  .rhine-dialog__body { margin-top:24px; }.rhine-dialog__actions { display:flex; justify-content:flex-end; flex-wrap:wrap; gap:12px; margin-top:24px; }
@@ -1,4 +1,6 @@
1
1
  import './styles.css';
2
+ export { StatusTimeline, TimelineLegend } from './StatusTimeline';
3
+ export type { TimelineSegment, TimelineStatus } from './StatusTimeline';
2
4
 
3
5
  export { Logo } from './Logo';
4
6
  export type { LogoProps } from './Logo';
@@ -16,9 +18,13 @@ export { Metric, KeyValue, DataTable, Pagination } from './Data';
16
18
  export type { DataColumn, DataTableProps, TableSort } from './Data';
17
19
  export { Checkbox, RadioGroup, Switch, Textarea, SearchField, Combobox } from './Forms';
18
20
  export type { ComboboxProps } from './Forms';
19
- export { Dialog, ConfirmDialog, Popover, Menu, Tooltip } from './Overlays';
21
+ export { Dialog, Drawer, ConfirmDialog, Popover, Menu, Tooltip } from './Overlays';
20
22
  export type { DialogProps, MenuItem } from './Overlays';
21
23
  export { Toast } from './Toast';
24
+ export { SideNav, Breadcrumbs, Steps } from './Navigation';
25
+ export type { NavItem, NavGroup, SideNavProps, BreadcrumbItem, BreadcrumbsProps, StepItem, StepsProps } from './Navigation';
26
+ export { Fieldset, ErrorSummary } from './FormWorkflow';
27
+ export type { FieldsetProps, FieldError, ErrorSummaryProps } from './FormWorkflow';
22
28
 
23
29
  export { Button, Badge, Surface, SelectionCard, TextField, Tabs, SegmentedControl, Notice, Progress, Skeleton, Disclosure, EmptyState } from './components';
24
30
  export type { ButtonProps, Tone, Choice, TabItem } from './components';