@linhey/rhine-design 0.4.0 → 0.6.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 +20 -0
- package/README.md +24 -7
- package/components/AdvancedForms.tsx +184 -0
- package/components/AppShell.tsx +71 -0
- package/components/Background.tsx +29 -0
- package/components/COMMON_COMPONENTS.md +2 -2
- package/components/Charts.tsx +94 -0
- package/components/Data.tsx +98 -18
- package/components/Forms.tsx +14 -8
- package/components/MOTION.md +9 -1
- package/components/Media.tsx +82 -0
- package/components/OrbitBackdrop.tsx +33 -0
- package/components/Overlays.tsx +4 -3
- package/components/README.md +12 -1
- package/components/StatusTimeline.tsx +50 -12
- package/components/background.css +43 -0
- package/components/charts.css +27 -0
- package/components/common.css +5 -5
- package/components/data.css +6 -0
- package/components/drawer-placement.css +4 -0
- package/components/extensions.css +70 -0
- package/components/index.ts +14 -2
- package/components/media-parity.css +3 -0
- package/components/orbit.css +42 -0
- package/components/orbitRenderer.ts +230 -0
- package/components/patterns.css +1 -1
- package/components/styles.css +5 -1
- package/components/theme-arknights.css +10 -5
- package/components/theme-neo-brutalism.css +10 -4
- package/components/timeline.css +2 -2
- package/components/tokens.css +43 -0
- package/docs/ANTD_MUI_AUDIT.md +38 -0
- package/docs/APPLICATION_COMPONENTS.md +175 -0
- package/docs/DESIGN_SYSTEM_AUDIT.md +2 -0
- package/docs/ISSUE_4_VALIDATION.md +21 -0
- package/docs/ISSUE_VALIDATION.md +52 -0
- package/docs/ORBIT_BACKDROP.md +81 -0
- package/docs/REFERENCE_RESEARCH.md +2 -0
- package/docs/THEMING.md +41 -0
- package/docs/assets/application-shell.png +0 -0
- package/docs/assets/issue4-shell.png +0 -0
- package/docs/assets/media-viewer-mobile.png +0 -0
- package/docs/assets/monitoring-desktop.png +0 -0
- package/docs/assets/orbit-backdrop.png +0 -0
- package/docs/audits/DATA_ANTD_MUI.md +75 -0
- package/docs/audits/FORMS_ANTD_MUI.md +88 -0
- package/docs/audits/MONITORING_ANTD_MUI.md +44 -0
- package/docs/audits/SHELL_MEDIA_ANTD_MUI.md +91 -0
- package/package.json +1 -1
- package/preview/BackgroundGallery.tsx +46 -0
- package/preview/ExtensionsGallery.tsx +95 -0
- package/preview/GalleryShell.tsx +4 -3
- package/preview/MonitoringGallery.tsx +27 -0
- package/preview/OrbitGallery.tsx +47 -0
- package/preview/backgrounds.css +10 -0
- package/preview/extensions.css +6 -0
- package/preview/main.tsx +12 -1
- package/skills/rhine-design/SKILL.md +24 -3
- package/skills/rhine-design/references/API_MAP.md +6 -0
- package/skills/rhine-design/references/TASK_MAP.md +3 -1
- package/skills/rhine-design/references/integration.md +4 -2
- package/tests/Background.test.tsx +22 -0
- package/tests/DataParity.test.tsx +148 -0
- package/tests/Extensions.test.tsx +145 -0
- package/tests/FormParity.test.tsx +185 -0
- package/tests/Monitoring.test.tsx +38 -0
- package/tests/MonitoringParity.test.tsx +125 -0
- package/tests/OrbitBackdrop.test.tsx +183 -0
- package/tests/ShellMediaParity.test.tsx +180 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# 表单组件:Ant Design / MUI API 与交互核对
|
|
2
|
+
|
|
3
|
+
查阅日期:**2026-10-24**。范围为 issues #1–#6 本次新增的 DateField、DateRangePicker、NumberField、RemoteMultiSelect、TreeSelect,以及它们复用的 Combobox。实现位置为 `components/AdvancedForms.tsx` / `components/Forms.tsx`;这些 API 自 0.5.0 提供。
|
|
4
|
+
|
|
5
|
+
官方页面当次标注 Ant Design **6.6.7**、Material UI **9.5.0**、MUI X Date Pickers **9.15.0**、Tree View **9.16.0**。版本仅标识这次查阅内容,不表示安装或运行过这些框架。此核对是官方 API/交互文档对照与 Rhine 自身验证,没有声称完成上游源码或跨库像素比较。
|
|
6
|
+
|
|
7
|
+
## DateField
|
|
8
|
+
|
|
9
|
+
参考 [Ant Design DatePicker](https://ant.design/components/date-picker/#api) 的值、日期限制、只读输入等契约,以及 [MUI X DateField API](https://mui.com/x/api/date-pickers/date-field/) 的 `readOnly`、`required`、`name`、辅助文本、错误通知。Rhine 保留 date-only ISO 字符串;MUI 的日期适配对象不进入本库 API。
|
|
10
|
+
|
|
11
|
+
| 核对项 | 原实现 | 本次补齐 / 保留 |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| 受控值和空值 | `value: string` / 空字符串;宿主更新值 | 保留,不新增隐式日期、时区转换或自动钳制 |
|
|
14
|
+
| `min` / `max` 与外部无效值 | 仅传给原生 input;外部改约束后没有可见错误 | 校验 ISO 格式、真实日历日期、上下界及反向边界,显示 `aria-invalid` 和错误;不回写受控值 |
|
|
15
|
+
| 错误通知 | 只能传入 `error` | 新增 `onValidationChange(error: string \| null)`,初次渲染及校验结果变化通知;外部 `error` 优先 |
|
|
16
|
+
| 只读和表单关联 | 缺失 | 新增 `readOnly`、`required`、`id`、`name`、`hint`;只读仍可聚焦,不能改变值 |
|
|
17
|
+
| 键盘 / 移动日期面板 | 浏览器原生 `type=date` | 保留原生选择器;显示格式和日历面板由浏览器/系统决定 |
|
|
18
|
+
|
|
19
|
+
明确保留差异:没有自绘月历、任意日期禁选回调、月份/年份选择模式、适配器/时区、分段键盘编辑或统一跨浏览器日期显示格式。若产品需要这些能力,应单独定义 Calendar 的无障碍与移动端契约;不能把原生输入包装称为完整 AntD/MUI DatePicker 等价物。`required` 使用浏览器表单校验;`onValidationChange` 报告日期格式、范围及显式 `error`,不代替提交时的 required 校验。
|
|
20
|
+
|
|
21
|
+
## DateRangePicker
|
|
22
|
+
|
|
23
|
+
参考 [Ant Design RangePicker](https://ant.design/components/date-picker/#rangepicker) 的区间、预设、空值行为,以及 [MUI X DateRangePicker](https://mui.com/x/react-date-pickers/date-range-picker/) 和 [API](https://mui.com/x/api/date-pickers/date-range-picker/) 的组合、只读、表单和校验能力。
|
|
24
|
+
|
|
25
|
+
| 核对项 | 原实现 | 本次补齐 / 保留 |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| 区间与顺序 | `{ start, end }`;反向区间提示错误 | 保留两端受控、允许编辑中单端为空;不自动交换用户输入 |
|
|
28
|
+
| 区间边界 | 结束日期 min 跟随开始值 | 继续限制结束端;额外校验两端日期、全局 min/max 与非法边界 |
|
|
29
|
+
| 预设 | 仅检查开始低于 min / 结束高于 max | 反向日期、非法日期、任一端越界的预设都不能提交;`required` 时禁止空端预设 |
|
|
30
|
+
| 只读 / 清空 | 缺只读,清空按钮固定 | 新增 `readOnly`;同时锁住两端、预设和清空;`clearable` 默认 true |
|
|
31
|
+
| 标签和表单 | 开始/结束标签固定,不能命名字段 | 新增 `startLabel`、`endLabel`、`startName`、`endName`、`required`、`hint` |
|
|
32
|
+
| 可组合校验 | 仅页面错误文本 | 新增 `onValidationChange`,宿主可用于错误汇总/阻止提交 |
|
|
33
|
+
|
|
34
|
+
明确保留差异:Rhine 是两个原生日期字段及快捷预设的组合,没有双月联动月历、拖动范围、移动端确认面板、日期部分选择或适配器功能。预设由宿主提供明确日期,不内建“最近七天”等依赖时区/当前时间的计算。更新一个端点只发变更意图,不清空另一个端点。
|
|
35
|
+
|
|
36
|
+
## NumberField
|
|
37
|
+
|
|
38
|
+
参考 [Ant Design InputNumber](https://ant.design/components/input-number/#api) 的只读、步进、精度、控制按钮与动态边界。MUI 官方 [Number Field](https://mui.com/material-ui/react-number-field/) 页面提供的是 **Base UI 组合示例**,不是 `@mui/material` 开箱即用的 NumberField 导出;对应底层契约见 [Base UI NumberField](https://base-ui.com/react/components/number-field)。
|
|
39
|
+
|
|
40
|
+
| 核对项 | 原实现 | 本次补齐 / 保留 |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| 编辑草稿 / 数值提交 | `-` / `1.` 草稿保留;blur/Enter 提交合法数值 | 保留;新增 Escape 取消尚未提交的草稿并恢复宿主值,不发值变更 |
|
|
43
|
+
| 禁用 vs 只读 | 只有 disabled | 新增 `readOnly`,保留可聚焦输入,阻止输入、步进、提交,并禁用按钮 |
|
|
44
|
+
| 动态范围 | 改 min/max 不会立即标错 | 越界和反向配置立即显示错误;不钳制宿主值、不发送伪用户变更 |
|
|
45
|
+
| 空值步进 | `Number('')` 被视为 0,负值范围的加按钮可能被误禁用 | 空值不参与禁用边界比较;首次步进归入有效范围,非有限 step 回退 1 |
|
|
46
|
+
| 控制按钮 / 表单 | 固定按钮,缺字段身份 | 新增 `controls`(默认 true)、`name`、`id`、`required`、`hint` |
|
|
47
|
+
| 小数、IME、未知值 | 有精度/step/null/IME 契约 | 保留,输入法确认不会误提交或步进 |
|
|
48
|
+
|
|
49
|
+
明确保留差异:没有任意精度字符串、格式化/解析器、货币本地化、滚轮调值、拖动调值、长按加速或额外修饰键步长。数值仍为 JS `number \| null`,不能承诺财务高精度。越界手工输入保留草稿并拒绝提交;按钮/方向键步进会钳制到界限。`name` 提交当前显示的输入字符串;应用提交前仍应采用受控数值和现有表单校验。
|
|
50
|
+
|
|
51
|
+
## RemoteMultiSelect / Combobox
|
|
52
|
+
|
|
53
|
+
参考 [Ant Design Select](https://ant.design/components/select/#api) 的 multiple、选中标签、清空、远程搜索,以及 [MUI Autocomplete](https://mui.com/material-ui/react-autocomplete/#asynchronous-requests) / [API](https://mui.com/material-ui/api/autocomplete/) 的独立 input/value、持续选择、只读和选中语义。
|
|
54
|
+
|
|
55
|
+
| 核对项 | 原实现 | 本次补齐 / 保留 |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| 服务端结果 / 已选值 | query/options/value 独立受控;完整 Choice 保留跨查询标签 | 保留;组件不本地过滤、请求、防抖或处理响应次序 |
|
|
58
|
+
| 连续多选 | 每选一项就关闭;listbox 无多选语义 | 多选默认 `closeOnSelect=false`,持续选择;listbox 添加 `aria-multiselectable`,选中选项正确 `aria-selected` |
|
|
59
|
+
| 查询重置 | 保留 query | 继续默认保留;新增显式 `clearQueryOnSelect=true` 可在选择后发送空查询 |
|
|
60
|
+
| 删除 / 焦点 | 标签按钮删除后焦点可能落到 body | 删除/清空后回输入框;空查询 Backspace 删除最后一个可移除标签,尊重锁定项和 IME |
|
|
61
|
+
| 只读 / 清空 / 辅助 | 缺只读及配置 | 新增 `readOnly`、`clearable`、`placeholder`、`hint`;只读输入不打开候选,标签不能删除 |
|
|
62
|
+
| 原生 FormData | 缺失 | `name` 为每个受控选中 key 渲染同名 hidden input,包含锁定选中项,disabled 不提交 |
|
|
63
|
+
| 既有单选 Combobox 兼容 | 选完关闭 | 默认仍关闭;新增可选 `selectedValues`、`closeOnSelect`、`onEmptyBackspace` 及 readOnly/id/hint/placeholder 支持多选包装,不改变单选 value 类型 |
|
|
64
|
+
|
|
65
|
+
明确保留差异:没有 freeSolo、自动 tokenization、本地分组/筛选、虚拟列表或任意 option renderer。已经选中的候选会标记为选中并跳过重复添加;取消选择通过标签、清空或 Backspace,未把选项点击改成反选。loading/error 仍阻止选取过期候选;已选值保持可见并可按宿主规则删除。未提交的输入不成为选中值。
|
|
66
|
+
|
|
67
|
+
## TreeSelect
|
|
68
|
+
|
|
69
|
+
参考 [Ant Design TreeSelect API](https://ant.design/components/tree-select/#api) 的节点可选性、受控展开和严格独立选中;交互基线参考 [MUI X Tree View accessibility](https://mui.com/x/react-tree-view/accessibility/)。Rhine 当前是内联树选择视图,形态更接近 MUI Tree View,不是 AntD 的下拉 TreeSelect。
|
|
70
|
+
|
|
71
|
+
| 核对项 | 原实现 | 本次补齐 / 保留 |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| 选中/展开所有权 | 独立受控,父子不联动 | 保留;不自动传播选中,不隐式展开所有选中祖先 |
|
|
74
|
+
| 初次 Tab 进入 | 总在第一个可见节点 | 优先第一个可见已选项;之后保持用户浏览的节点 |
|
|
75
|
+
| Arrow / Home / End | 已支持层级导航 | 保留;DOM id 基于稳定 node id,避免展开改变数组下标导致标识漂移 |
|
|
76
|
+
| 字符导航 | 缺失 | 添加 500ms 连续前缀匹配;重复字符在同前缀节点间循环;不修改选择,不触发服务端查询 |
|
|
77
|
+
| 批量键盘操作 | 缺失 | `*` 展开同层分支;Ctrl/⌘+A 切换当前可见可选节点,保留不可见/禁选已选 key |
|
|
78
|
+
| 节点可选性 | 只有 disabled | 新增 `selectable=false` 作为结构节点,仍可展开/导航 |
|
|
79
|
+
| 外部收起 / 焦点 | 焦点节点移除后可能丢到 body | 宿主收起分支时恢复到可见祖先;没有祖先时恢复可见已选/首节点 |
|
|
80
|
+
| 只读 / 表单 | 缺失 | 新增 `readOnly`,可继续浏览展开但不能选中;`name` 同名 hidden inputs 提交受控 key |
|
|
81
|
+
|
|
82
|
+
明确保留差异:没有下拉搜索外壳、虚拟化、节点懒加载调度或级联半选;宿主更新 nodes/expandedKeys 即可显示加载后的节点。多选采用独立勾选模式,不内建 Shift 范围选择。Enter 与 Space 均选择节点,分支展开由方向键/展开按钮负责,保持原有契约;这和 MUI 的分支 Enter 展开行为不同。禁选节点仍可获焦浏览并展开,以免其可选子节点不可达。Ctrl/⌘+A 仅限可见节点,不推断未加载数据。
|
|
83
|
+
|
|
84
|
+
## 回归证据
|
|
85
|
+
|
|
86
|
+
`tests/FormParity.test.tsx` 新增 **14** 个行为测试,覆盖动态日期错误与通知、通知回调稳定性、反向/非法预设、只读保护、数值取消草稿和空值步进、多选连续选择与 aria、锁定标签/IME、删除焦点、单选兼容、树类型导航/批选/外部收起焦点和字符事件不冒泡。`tests/Extensions.test.tsx` 保留原有受控/IME/跨查询与禁选树行为证据。
|
|
87
|
+
|
|
88
|
+
本次负责范围已运行 FormParity + CommonComponents(27 个用例通过)及 TypeScript 检查。独立 Chromium 会话 `rhine-forms-parity` 在 `http://127.0.0.1:18309/#advanced-forms` 验证 16 项真实输入/键盘/焦点/只读断言;三主题 × 400/520/880 宽度及三次 CSS 200% 缩放均没有 document/main 横向溢出,控制台 0 错误。局部截图为忽略目录 `output/playwright/forms-parity-400.png`。完整生产构建和跨组件回归由本轮主报告汇总;这里不把 JSDOM 或 Chromium 日期字段测试称为所有系统原生日期面板或屏幕阅读器验证。
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 监控组件:Ant Design / MUI 对照
|
|
2
|
+
|
|
3
|
+
查阅:2026-10-24。对象为 issue #6 的 StatusTimeline 详情、TrendChart、StackedBar。Ant Design 的 Timeline 是事件序列,不能直接等同可拖动的健康历史;其图表由独立的 Ant Design Charts 提供。此处分别对照 Slider + Popover、Charts,避免把相近名称当作相同控件。
|
|
4
|
+
|
|
5
|
+
## 官方依据
|
|
6
|
+
|
|
7
|
+
- [Ant Design Slider](https://ant.design/components/slider/):value/defaultValue、onChange、disabled 与键盘。
|
|
8
|
+
- [Ant Design Popover](https://ant.design/components/popover/):open/defaultOpen/onOpenChange、触发方式与避让。
|
|
9
|
+
- [Ant Design Timeline](https://ant.design/components/timeline/):items、方向与事件内容;仅用于确认语义差异。
|
|
10
|
+
- [Ant Design Charts 官方 Line 示例](https://charts.ant.design/options/plots/style/)、[Axis](https://charts.ant.design/en/options/plots/axis/)、[Bar](https://ant-design-charts.antgroup.com/en/components/plots/bar):数据字段、轴、交互、堆叠和百分比。
|
|
11
|
+
- [MUI Slider API](https://mui.com/material-ui/api/slider/):受控状态、可访问值描述与禁用。
|
|
12
|
+
- [MUI X Charts 键盘](https://mui.com/x/react-charts/accessibility/):图形焦点、左右键、Home/End。
|
|
13
|
+
- [MUI X Lines](https://mui.com/x/react-charts/lines/):null、缺失断线及可选连接;[Tooltip](https://mui.com/x/react-charts/tooltip/):数值格式化。
|
|
14
|
+
|
|
15
|
+
## 逐项核对
|
|
16
|
+
|
|
17
|
+
| 组件 / 项目 | 原实现 | 本轮结果 / 验收 |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| StatusTimeline 选中状态 | 仅内部数组下标 | 新增 `activeId/defaultActiveId/onActiveChange`;插入、重排按稳定 ID 保留,移除时回到最新时段;不回写宿主 |
|
|
20
|
+
| 详情开关 | 仅内部 open | 新增 `open/defaultOpen/onOpenChange`;受控模式仅回传意图,父组件决定是否打开/关闭 |
|
|
21
|
+
| 禁用 | 仅空数组时禁用 | 新增 `disabled`,禁用时不显示详情,也不接受鼠标选择 |
|
|
22
|
+
| 键盘 | range 方向键;Escape | 保留原生方向键/Home/End;Enter/Space 进入详情关闭按钮;关闭焦点回健康条且不重新打开 |
|
|
23
|
+
| 浮层定位 / 叠层 | 已使用 Radix Portal、碰撞检测 | 保留同主题根、视口 12px 避让、内部滚动、非模态、不抢悬停焦点 |
|
|
24
|
+
| 可访问关联 | aria-valuetext/details | 增加展开状态、dialog 类型与 controls;同一份结构化详情供键盘/触屏/鼠标读取 |
|
|
25
|
+
| 业务证据 | 零/未知、原因、统计口径 | 保留;没有证据时不生成成功率或健康评分 |
|
|
26
|
+
| TrendChart 数值格式 | 固定 String | 新增 `valueFormatter(value)`,缺失与非有限值统一传 null;刻度和逐点详情采用格式化,原始数据表保留精确数值 |
|
|
27
|
+
| 逐点交互 | 仅 SVG title + 展开数据表 | 图形可 Tab 聚焦;左右/Home/End、悬停/轻触查看同一个 HTML 数值区;包括零与缺失桶 |
|
|
28
|
+
| 选中同步 | 无 | 新增 `activePointId/defaultActivePointId/onActivePointChange`,受控变化不越过宿主;按 ID 保留,不按数组位置漂移 |
|
|
29
|
+
| 断线 | null/nonfinite 不补零 | 保留缺失断线;没有增加 connectNulls,以免掩盖监控采集缺口 |
|
|
30
|
+
| 坐标 | 大幅正负数相减可能溢出 | 先按数值幅度归一化再映射,有限极值不产生 NaN/Infinity SVG 坐标 |
|
|
31
|
+
| StackedBar 零分段 | flex gap 为零值留下空隙 | 零不绘制色段,文字仍列 0 与 0.0% |
|
|
32
|
+
| 小数分段宽度 | flex-grow 原始计数总和小于 1 时色带无法填满 | 使用归一化权重分配全宽;0.1 / 0.2 与文字 33.3% / 66.7% 一致 |
|
|
33
|
+
| 分类数值格式 | 固定 String | 同样支持 valueFormatter;计数与比例计算仍使用原始值 |
|
|
34
|
+
| 未知/负数/总和溢出 | 不绘制百分比 | 保留;不复制图表引擎对缺失项的默认归一化行为 |
|
|
35
|
+
| 陈旧 / 错误 / 空 | 保留数据并说明 | 保留,loading 不清空已有数据;空数组和全未知不同文案 |
|
|
36
|
+
| 主题 / 动效 / 大量数据 | 同源 CSS tokens、无动画 | 保留;桶排序/聚合/限量在宿主,不新增图表引擎和网络层 |
|
|
37
|
+
|
|
38
|
+
## 保留差异
|
|
39
|
+
|
|
40
|
+
只提供单序列柱/折线及一组互斥分类的占比。多坐标轴、图例筛选、多序列、brush、缩放、导出和数据引擎属于另一个扩展范围;不把 MUI X / Ant Design Charts 的全部 API 套到运维轻图表。StatusTimeline 继续表达时段证据,不改成事件轴。受控/非受控模式应在组件生命周期内保持一致。
|
|
41
|
+
|
|
42
|
+
## 证据
|
|
43
|
+
|
|
44
|
+
`tests/MonitoringParity.test.tsx`:9 个新增行为回归,覆盖受控意图、稳定 ID、键盘、Enter 进入/关闭归还、格式化与原始值、有限极值、零/小数占比及禁用。与既有 Monitoring、StatusTimeline 测试共 14 项通过。真实浏览器矩阵及最终全量结果统一记录于 [ISSUE_VALIDATION](../ISSUE_VALIDATION.md),不以 JSDOM 代替定位或读屏验收。
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Background、AppShell 与媒体组件对标
|
|
2
|
+
|
|
3
|
+
查阅日期:**2026-10-24(Asia/Shanghai)**。对照对象为 Ant Design 与 MUI 官方在线文档;查阅时页面分别标示 Ant Design 6.6.7、Material UI 9.5.0。本记录核对 Rhine Design 0.5.0 的 API 与交互,不表示 npm 0.4.0 已有这些能力,也不宣称与两套组件库完全兼容。
|
|
4
|
+
|
|
5
|
+
## 官方依据与对应关系
|
|
6
|
+
|
|
7
|
+
| Rhine 组件 | Ant Design 对照 | MUI 对照 | 对应范围 |
|
|
8
|
+
| --- | --- | --- | --- |
|
|
9
|
+
| Background | [Layout](https://ant.design/components/layout/#api) 的区域与样式能力 | [Box](https://mui.com/material-ui/react-box/)、[Theming](https://mui.com/material-ui/customization/theming/) | 没有同名、同职责组件;只对照局部表面样式、主题继承和原生属性,不把 ThemeProvider 当作背景组件 |
|
|
10
|
+
| AppShell | [Layout.Sider](https://ant.design/components/layout/#layout.sider)、[Drawer](https://ant.design/components/drawer/#api) | [响应式 Drawer](https://mui.com/material-ui/react-drawer/#responsive-drawer)、[Drawer API](https://mui.com/material-ui/api/drawer/) | 主体布局、受控折叠、宽度/断点与临时侧栏;MUI 没有同名 AppShell |
|
|
11
|
+
| MediaCard | [Card](https://ant.design/components/card/#api)、[Image](https://ant.design/components/image/#api) 的加载/错误展示 | [Card / CardMedia / CardActionArea](https://mui.com/material-ui/react-card/#primary-action) | 媒体内容、主操作与次操作分区;图片请求不归 Card 容器 |
|
|
12
|
+
| MediaViewer | [Image.PreviewGroup](https://ant.design/components/image/#previewgrouptype) | [全屏 Dialog](https://mui.com/material-ui/react-dialog/#full-screen-dialogs)、[Dialog API](https://mui.com/material-ui/api/dialog/) | 分组浏览、边界、焦点与全屏浮层;MUI Core 没有 Image.PreviewGroup 的直接替代品 |
|
|
13
|
+
|
|
14
|
+
AntD 的 Sider 提供折叠状态与响应式配置;MUI 区分常驻与临时 Drawer。两者均可控制临时面板开关和出现边缘。Rhine 保留已有桌面状态协议,新增独立的抽屉控制,以免导航点击无法关闭手机抽屉。[AntD Layout](https://ant.design/components/layout/#layout.sider)、[AntD Drawer](https://ant.design/components/drawer/#api)、[MUI Drawer](https://mui.com/material-ui/api/drawer/)。
|
|
15
|
+
|
|
16
|
+
MUI Card 将主点击区域与补充操作分开;Rhine 的复选框原已独立,本轮新增同样独立的 `actions` 内容区域。AntD Image 的资源载入、变换和 PreviewGroup 集合管理范围更大,不能把提供一个全屏 Dialog 视为完成这些功能。[MUI Card](https://mui.com/material-ui/react-card/#primary-action)、[AntD Image](https://ant.design/components/image/#previewgrouptype)。
|
|
17
|
+
|
|
18
|
+
## 逐项核对与结果
|
|
19
|
+
|
|
20
|
+
| 组件 / 核对项 | 原实现 | 本轮处理 / 判定 |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| Background:主题继承 | 六种语义区域、局部 appearance、单一主题根 | 保留。背景不创建 Provider,不拦截指针,不引入定位上下文 |
|
|
23
|
+
| Background:原生扩展 | HTML 属性、className、style;类型缺少 ref | 补齐 React 19 `ref`,透传到同一 div;样式覆盖优先级不变 |
|
|
24
|
+
| Background:纹理 | auto 仅 canvas;正文默认纯色 | 保留用户已确定的设计;不是 MUI `sx` 的同构实现 |
|
|
25
|
+
| AppShell:布局 | single / double-left / left-right,正文挂载稳定 | 保留,三种布局沿用同一正文树 |
|
|
26
|
+
| AppShell:桌面状态 | `open`、`onOpenChange` 必填 | 保留受控契约。不开辟第二套默认桌面状态,避免持久化状态来源分裂 |
|
|
27
|
+
| AppShell:临时侧栏状态 | 只有内部 state,宿主导航无法关闭 | **补齐** `drawerOpen`、`onDrawerOpenChange`;省略受控值时继续本地管理,并支持 `defaultDrawerOpen` |
|
|
28
|
+
| AppShell:边缘 | 全部复用默认右 Drawer | **修正**:全局导航从左出现;double-left 次栏从左;left-right 次栏从右。共享 Drawer 新增 `side`,其默认右侧保持兼容 |
|
|
29
|
+
| AppShell:尺寸 | 桌面224/256、容器阈值760/1120固定 | **补齐**每栏 `width`(桌面 px)、`breakpoint`(容器 px);省略保持原值;阈值相等时使用桌面栏。以 clientWidth 布局像素判断,不受 CSS zoom/transform 对视觉边界尺寸的放大影响 |
|
|
30
|
+
| AppShell:局部样式 | 根元素无扩展入口 | **补齐** `className` / `style`,不开放替换焦点管理结构的任意 slots |
|
|
31
|
+
| AppShell:焦点 | 栏隐藏/移除返回存活按钮,抽屉由 Radix 管理 | 保留并回归。正文输入不因侧栏折叠而重建 |
|
|
32
|
+
| MediaCard:选择 vs 打开 | 独立 checkbox 与打开按钮 | 保留。禁用同时阻止打开/选择;不会把 loading 自动等同 disabled |
|
|
33
|
+
| MediaCard:错误恢复 | 有加载/错误/空态,但没有次操作位置 | **补齐**独立 `actions`,可以放重试;点击不触发 `onOpen` / 选择 |
|
|
34
|
+
| MediaCard:内容 / 样式 | 预览 ReactNode;缺少根节点属性 | **补齐**原生 article 属性、ref、className、style;修正数值 `0` 预览被误当空态 |
|
|
35
|
+
| MediaViewer:受控 / 默认 | 支持 open、onOpenChange;缺默认打开 | **补齐** `defaultOpen`。受控 open 优先;仍需宿主采纳回调更新状态 |
|
|
36
|
+
| MediaViewer:信息侧栏 | 没有 onMetadataOpenChange 时不能切换 | **补齐**本地状态与 `defaultMetadataOpen`;传入 metadataOpen 时保持受控;按钮与信息区域通过 aria-controls 关联;数值0内容有效 |
|
|
37
|
+
| MediaViewer:上一项 / 下一项 | 按钮动作由宿主提供;缺快捷键 | **补齐** ← / →;`keyboardNavigation=false` 可关闭这些快捷键。Escape 退出由 Radix 保持 |
|
|
38
|
+
| MediaViewer:集合边界 | 缺 callback 即禁用,无独立禁用状态 | **补齐** previousDisabled / nextDisabled;缺 callback 仍禁用;无自动循环,数据索引由宿主维护 |
|
|
39
|
+
| MediaViewer:到达边界后的焦点 | Chrome 将变为 disabled 的已聚焦按钮焦点丢到 body | **修复**:将焦点移到查看器本体,反向方向键仍有效;离开查看器返回实际打开它的媒体卡片 |
|
|
40
|
+
| MediaViewer:快捷键冲突 | 此前没有快捷键 | 新快捷键跳过编辑器、select、视频/音频、slider/tree/listbox/tablist/menu,跳过组合输入、修饰键与嵌套 Portal;不劫持这些控件的方向键 |
|
|
41
|
+
| MediaViewer:内容 / 样式 | children / actions / metadata 已可组合 | 保留这些语义插槽,**补齐** className/style 到全屏 Dialog 内容;不改变暗色媒体画布 |
|
|
42
|
+
| 浮层:主题 / 焦点 / 退出 | Portal 到最近主题根,Radix focus trap、Escape | 保留。点击操作、键盘、嵌套层及实际 opener 返回继续由现有机制负责 |
|
|
43
|
+
|
|
44
|
+
## 新增 API 的边界
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// open 管理桌面折叠;drawerOpen 管理临时浮层,两者互不替代。
|
|
48
|
+
<AppShell
|
|
49
|
+
layout="left-right"
|
|
50
|
+
navigation={{
|
|
51
|
+
label: '导航', content: navigation,
|
|
52
|
+
open: navigationExpanded, onOpenChange: setNavigationExpanded,
|
|
53
|
+
drawerOpen: navigationDrawerOpen, onDrawerOpenChange: setNavigationDrawerOpen,
|
|
54
|
+
width: 240, breakpoint: 800,
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{body}
|
|
58
|
+
</AppShell>
|
|
59
|
+
|
|
60
|
+
<MediaViewer
|
|
61
|
+
trigger={<button>查看媒体</button>}
|
|
62
|
+
title={current.title} description={`${index + 1} / ${items.length}`}
|
|
63
|
+
onPrevious={() => setIndex(index - 1)} previousDisabled={index === 0}
|
|
64
|
+
onNext={() => setIndex(index + 1)} nextDisabled={index === items.length - 1}
|
|
65
|
+
metadata={metadata} defaultMetadataOpen
|
|
66
|
+
>
|
|
67
|
+
{preview}
|
|
68
|
+
</MediaViewer>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- `defaultOpen`、`defaultMetadataOpen`、`defaultDrawerOpen` 只作为初始值;后续控制使用对应受控属性。抽屉保持打开跨过宽度断点时仍可完成当前操作,关闭后恢复桌面布局。
|
|
72
|
+
- `width` 仅管理桌面侧栏,抽屉仍使用现有 `min(440px, 100%)`。`breakpoint` 使用容器宽度,不能直接替换 AntD 的命名视口断点。
|
|
73
|
+
- 主体子树保持挂载;侧栏内容在桌面与 Portal 抽屉之间会重挂载,需保留的表单/查询状态仍由宿主控制。这不等同 AntD `destroyOnHidden=false` 的内容生命周期。
|
|
74
|
+
- `MediaCard actions` 中的业务按钮由宿主管理 busy/disabled;卡片 `disabled` 控制自身打开与选择。预览区域应提供非交互缩略图;完整视频控件放在 Viewer,避免把交互元素嵌进主按钮。
|
|
75
|
+
- MediaViewer 不持有 items/index,不猜测首尾、循环或资源顺序。完整资源的加载/失败重试内容通过 children/actions 组合;MediaCard 的状态不自动代表完整资源状态。
|
|
76
|
+
|
|
77
|
+
## 明确保留的差异
|
|
78
|
+
|
|
79
|
+
1. AntD Image 的缩放、旋转、拖拽、轮播集合 API 未移植;本次解决媒体壳层和已有宿主分页动作。没有对任意 children 强制应用图像变换,以免改变视频及原生媒体交互。
|
|
80
|
+
2. MUI `sx`、任意组件替换、AntD 语义 DOM 样式对象未照搬;Rhine 保留 CSS tokens、className/style 和既有 ReactNode 插槽,避免引入第二套样式引擎。
|
|
81
|
+
3. AppShell 没有侧栏拖拽调整宽度、迷你图标栏、触摸滑出手势;Drawer 只提供当前需要的左右边缘,未新增上下抽屉。需要这些行为时应另立完整交互契约,不能只补同名 props。
|
|
82
|
+
4. Background 不提供 MUI Box 的任意 component/as;根 div 与原生 role/aria 属性已覆盖当前背景容器场景。它不是布局、可点击卡片或新的主题作用域。
|
|
83
|
+
|
|
84
|
+
## 验证证据
|
|
85
|
+
|
|
86
|
+
- `tests/ShellMediaParity.test.tsx` 新增 **10 项**行为回归;与既有 `tests/Extensions.test.tsx` 一起 **25 项通过**。覆盖受控值不被本地修改、默认状态、左右位置、精确断点、自定义宽度、隐藏栏焦点、错误重试分区、metadata、快捷键边界与冲突。类型检查通过。
|
|
87
|
+
- Chromium 真实开发展台,橙趣主题400px:左导航抽屉 `x=0,width=400`,右侧圆角与向右阴影正确,动画从左进入;媒体从卡片打开,方向键连续 `1→2→3→2`,最后一项禁用后焦点仍留在查看器;Escape 回到实际卡片。
|
|
88
|
+
- 520px、减少动态模式,三套主题的左右抽屉共6次:左侧 `x=0,width=440`,右侧 `x=80,right=520`;全部无抽屉内部横向溢出,Escape 关闭成功。
|
|
89
|
+
- 1600px 浏览器窗口的容器测量:正常 `clientWidth=1134 / visualWidth=1136`,两栏常驻;CSS 200% `502 / 1008`,两栏正确转抽屉;CSS `transform:scale(.5)` 后 `1134 / 568`,两栏仍常驻。证明断点依据布局宽度,而不是变换后的视觉尺寸。这里是 CSS zoom/transform 验证,不声称浏览器菜单缩放实测。
|
|
90
|
+
- 首次浏览器复现明确发现「边界按钮禁用导致焦点回 body」,修复后重复验证反向快捷键成功。JSDOM 回归同时检查焦点目标,但不将其视为真实布局证据。
|
|
91
|
+
- 完整跨主题/宽度、构建产物与包安装验证由本轮总体验收统一记录;这里的开发浏览器结果不代替生产 bundle、原生 WebView 或屏幕阅读器实测。
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Background, Button, Dialog, Select, Stack, Text, TextField, type BackgroundDecoration, type BackgroundRole } from '../components';
|
|
3
|
+
import './backgrounds.css';
|
|
4
|
+
|
|
5
|
+
const scenes: { role: BackgroundRole; label: string; description: string }[] = [
|
|
6
|
+
{ role: 'canvas', label: '页面与工作台', description: '主题纹理留在画布上,让工作区有自己的风格。' },
|
|
7
|
+
{ role: 'content', label: '正文与阅读', description: '默认纯色,长段文字与密集信息保持清晰。' },
|
|
8
|
+
{ role: 'sidebar', label: '导航与侧栏', description: '独立的底色层次,帮助辨认当前位置。' },
|
|
9
|
+
{ role: 'surface', label: '卡片与表单', description: '不透明的阅读表面,承载内容和输入。' },
|
|
10
|
+
{ role: 'overlay', label: '弹窗与浮层', description: '表面与遮罩分开配置,突出当前任务。' },
|
|
11
|
+
{ role: 'media', label: '图片与视频', description: '中性深色画布与浅色文字,让媒体成为焦点。' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function SceneSamples({ decoration = 'auto' }: { decoration?: BackgroundDecoration }) {
|
|
15
|
+
return <div className="ds-background-scenes">{scenes.map(scene => <div key={scene.role} className="ds-background-example">
|
|
16
|
+
<div className="ds-background-label"><strong>{scene.label}</strong><code>{scene.role}</code></div>
|
|
17
|
+
<div className={scene.role === 'overlay' ? 'ds-background-backdrop' : undefined}>
|
|
18
|
+
<Background roleTone={scene.role} decoration={decoration} className="ds-background-sample">
|
|
19
|
+
<Text variant="heading">{scene.role === 'media' ? '光影之间 / 16:9' : '专注于眼前的工作'}</Text>
|
|
20
|
+
<Text>{scene.description}</Text>
|
|
21
|
+
</Background>
|
|
22
|
+
</div>
|
|
23
|
+
</div>)}</div>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function BackgroundGallery() {
|
|
27
|
+
const [decoration, setDecoration] = useState<BackgroundDecoration>('auto');
|
|
28
|
+
return <section id="backgrounds">
|
|
29
|
+
<div className="ds-section-title"><span>BACKGROUND</span><h2>背景有层次,阅读有留白</h2></div>
|
|
30
|
+
<p className="ds-caption">主题决定配色,场景决定背景。默认仅画布使用主题纹理;正文、侧栏、卡片、浮层与媒体保持纯色。右上角可切换当前主题。</p>
|
|
31
|
+
<div className="ds-background-controls">
|
|
32
|
+
<Select label="纹理策略" value={decoration} onChange={event => setDecoration(event.target.value as BackgroundDecoration)}>
|
|
33
|
+
<option value="auto">按场景默认</option><option value="theme">全部使用主题纹理</option><option value="none">全部关闭纹理</option>
|
|
34
|
+
</Select>
|
|
35
|
+
<Dialog title="独立的浮层表面" description="弹窗使用主题浮层底色,背后叠加独立遮罩。" trigger={<Button variant="secondary">查看实际弹窗</Button>}>
|
|
36
|
+
<TextField label="浮层草稿" placeholder="试试输入,再按 Escape 返回" />
|
|
37
|
+
</Dialog>
|
|
38
|
+
</div>
|
|
39
|
+
<SceneSamples decoration={decoration} />
|
|
40
|
+
<div className="ds-background-local">
|
|
41
|
+
<Background roleTone="content" appearance={{ color: '#e5ece3', textColor: '#242b28' }} className="ds-background-sample">
|
|
42
|
+
<Stack gap={4}><Text variant="heading">局部覆盖 · 安静的阅读区</Text><Text>仅覆盖这一区域的底色与文字颜色,其他场景继续跟随主题。</Text><TextField label="正文草稿" placeholder="切换主题或纹理,内容仍然保留" /></Stack>
|
|
43
|
+
</Background>
|
|
44
|
+
</div>
|
|
45
|
+
</section>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { AppShell, Background, Button, Checkbox, DataTable, DateField, DateRangePicker, Dialog, EmptyState, MediaCard, MediaViewer, Notice, NumberField, Pagination, RemoteMultiSelect, Select, SideNav, Skeleton, Stack, Surface, TextField, TreeSelect, type AppShellProps, type DateRange, type TreeNode, type TableSort } from '../components';
|
|
3
|
+
import type { Choice } from '../components/components';
|
|
4
|
+
import './extensions.css';
|
|
5
|
+
|
|
6
|
+
const longName = '非常长的工作区名称与跨区域协作资料—Workspace-2026-archive';
|
|
7
|
+
const sampleImage = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="960" height="640" viewBox="0 0 960 640"><rect width="960" height="640" fill="#d2dacf"/><circle cx="720" cy="150" r="65" fill="#f8f2d8"/><path d="M0 520 310 140 620 550 800 280 960 500V640H0Z" fill="#536b5d"/><path d="M0 560 320 400 610 590 960 480V640H0Z" fill="#293f37"/></svg>')}`;
|
|
8
|
+
|
|
9
|
+
function StateControl({ value, onChange }: { value: string; onChange: (value: string) => void }) {
|
|
10
|
+
return <Select label="演示状态" value={value} onChange={event => onChange(event.target.value)}><option value="ready">默认</option><option value="loading">加载 / 保留旧数据</option><option value="error">失败</option><option value="empty">空内容</option><option value="disabled">禁用</option></Select>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ShellGallery() {
|
|
14
|
+
const [layout, setLayout] = useState<AppShellProps['layout']>('double-left');
|
|
15
|
+
const [navOpen, setNavOpen] = useState(true), [secondaryOpen, setSecondaryOpen] = useState(true);
|
|
16
|
+
const [navDrawerOpen, setNavDrawerOpen] = useState(false);
|
|
17
|
+
const [current, setCurrent] = useState('inbox'), [draft, setDraft] = useState('主题切换和侧栏折叠会保留这段内容。');
|
|
18
|
+
const [plain, setPlain] = useState(true), [state, setState] = useState('ready');
|
|
19
|
+
return <section><div className="ds-section-title"><span>LAYOUT</span><h2>画布与应用外壳</h2></div>
|
|
20
|
+
<p className="ds-caption">单侧栏适合简单工具;双左栏适合目录与列表;左右栏适合编辑器与检查器。可用宽度不足 1120px 时次栏转抽屉,不足 760px 时全局导航也转抽屉。正文始终保留。</p>
|
|
21
|
+
<div className="ds-extension-controls"><Select label="布局方案" value={layout} onChange={event => setLayout(event.target.value as AppShellProps['layout'])}><option value="single">单侧栏</option><option value="double-left">双左侧栏</option><option value="left-right">左右侧栏</option></Select><Checkbox label="纯色背景" checked={plain} onChange={event => setPlain(event.target.checked)} /><StateControl value={state} onChange={setState} /></div>
|
|
22
|
+
<div className="ds-shell-stage"><AppShell layout={layout} decoration={plain ? 'none' : 'theme'}
|
|
23
|
+
navigation={{ label: '全局导航', open: navOpen, onOpenChange: setNavOpen, drawerOpen: navDrawerOpen, onDrawerOpenChange: setNavDrawerOpen, content: <SideNav aria-label="应用导航" currentId={current} groups={[{ label: '工作空间', items: [{ id: 'inbox', label: '收件箱', href: '#app-shell' }, { id: 'archive', label: longName, href: '#app-shell' }] }]} onNavigate={(item, event) => { event.preventDefault(); setCurrent(item.id); setNavDrawerOpen(false); }} /> }}
|
|
24
|
+
secondary={{ label: layout === 'left-right' ? '辅助详情' : '上下文目录', open: secondaryOpen, onOpenChange: setSecondaryOpen, content: <Stack gap={4}><strong>{current === 'inbox' ? '收件箱' : longName}</strong>{state === 'loading' ? <Skeleton label="目录加载中" /> : state === 'error' ? <Notice title="目录读取失败" tone="error">可以继续编辑正文。</Notice> : state === 'empty' ? <EmptyState title="目录暂无内容" description="选择其他项目或继续编辑正文。" /> : <><p>当前条目、所属目录和属性。</p><Button variant="secondary" disabled={state === 'disabled'}>上下文操作</Button></>}</Stack> }}
|
|
25
|
+
header={<Dialog title="背景与浮层" description="浮层仍然挂载在唯一主题根;不受局部背景裁剪。" trigger={<Button>打开对话框</Button>}><TextField label="对话框输入" placeholder="试试 Tab 和 Escape" /></Dialog>}>
|
|
26
|
+
<Stack gap={6}><div><p className="ds-kicker">CURRENT TASK / {current}</p><h3>同一主题,独立正文背景</h3><p>正文默认使用纯色背景。关闭“纯色背景”可显式启用主题纹理;纸面主题保持纯净。</p></div>
|
|
27
|
+
<Surface><Stack gap={4}><TextField label="正文草稿" value={draft} onChange={event => setDraft(event.target.value)} /><Button disabled={state === 'disabled'}>继续当前任务</Button></Stack></Surface>
|
|
28
|
+
{Array.from({ length: 5 }, (_, i) => <Surface key={i}><h3>内容段落 {i + 1}</h3><p>滚动正文时顶部操作栏保持可达;每栏仅有一个滚动容器。Tab 可依次访问导航、正文与辅助详情。</p></Surface>)}
|
|
29
|
+
</Stack>
|
|
30
|
+
</AppShell></div>
|
|
31
|
+
<div className="ds-extension-grid ds-gap-top">{(['canvas','content','sidebar'] as const).map(role => <Background key={role} roleTone={role} decoration={plain ? 'none' : 'theme'} className="ds-background-swatch"><strong>{role}</strong><Surface>Surface 始终提供不透明阅读表面。</Surface></Background>)}</div>
|
|
32
|
+
</section>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function AdvancedFormsGallery() {
|
|
36
|
+
const [range, setRange] = useState<DateRange>({ start: '2026-10-01', end: '2026-10-23' }), [date, setDate] = useState('2026-10-23');
|
|
37
|
+
const [number, setNumber] = useState<number | null>(1.5), [query, setQuery] = useState(''), [options, setOptions] = useState<Choice[]>([]), [selected, setSelected] = useState<Choice[]>([]);
|
|
38
|
+
const [pending, setPending] = useState(false), [state, setState] = useState('ready');
|
|
39
|
+
const [readOnly, setReadOnly] = useState(false);
|
|
40
|
+
const [keys, setKeys] = useState<string[]>(['leaf']), [expanded, setExpanded] = useState<string[]>(['root']), [multiple, setMultiple] = useState(true);
|
|
41
|
+
const nodes: TreeNode[] = [{ id: 'root', label: '协作项目', children: [{ id: 'leaf', label: longName }, { id: 'locked', label: '归档项目', disabled: true }] }, { id: 'other', label: '独立项目' }];
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setPending(true);
|
|
44
|
+
// This host mock cancels superseded requests; the component never fetches or filters.
|
|
45
|
+
const timer = setTimeout(() => { setOptions([{ value: `${query || 'all'}-1`, label: `${query || '远程结果'} · ${longName}` }, { value: 'disabled', label: '无访问权限', disabled: true }, { value: `${query || 'all'}-2`, label: `${query || '远程结果'} · 第二项` }]); setPending(false); }, 400);
|
|
46
|
+
return () => clearTimeout(timer);
|
|
47
|
+
}, [query]);
|
|
48
|
+
const disabled = state === 'disabled', error = state === 'error' ? '无法读取,请重试。' : undefined;
|
|
49
|
+
return <section><div className="ds-section-title"><span>FORMS</span><h2>复杂输入,明确的数据所有权</h2></div><div className="ds-extension-controls"><StateControl value={state} onChange={setState} /><Checkbox label="只读输入" checked={readOnly} onChange={event => setReadOnly(event.target.checked)} /></div>
|
|
50
|
+
<p className="ds-caption">日期为 YYYY-MM-DD,时区由宿主解释。数字在失焦或 Enter 时提交,Escape 撤销草稿。树支持方向键、字符定位、空格选择与多选 Ctrl/⌘ A;远程结果可连续选择,空查询按 Backspace 移除最后一个可移除标签。只读时可浏览和复制,不能更改值。</p>
|
|
51
|
+
<div className="ds-extension-grid"><Surface><Stack gap={4}><DateRangePicker label="统计范围" value={range} onValueChange={setRange} min="2026-01-01" max="2026-12-31" disabled={disabled} readOnly={readOnly} error={error} presets={[{ label: '本月演示', value: { start: '2026-10-01', end: '2026-10-23' } }, { label: '最近 7 天', value: { start: '2026-10-17', end: '2026-10-23' } }, { label: '无效预设(禁用)', value: { start: '2026-11-01', end: '2026-10-01' } }]} /><DateField label="单个日期" value={date} onValueChange={setDate} disabled={disabled} readOnly={readOnly} /><p>范围:{range.start || '未选'} — {range.end || '未选'}</p></Stack></Surface>
|
|
52
|
+
<Surface><NumberField label="配额" value={number} onValueChange={setNumber} min={-10} max={100} step={0.25} precision={2} disabled={disabled} readOnly={readOnly} error={error} /><p>已提交数值:{number ?? '空值'}。试输入 - 或 1.,编辑时不会丢失。</p></Surface>
|
|
53
|
+
<Surface><RemoteMultiSelect label="远程项目" query={query} onQueryChange={setQuery} value={selected} onValueChange={setSelected} options={state === 'empty' ? [] : options} loading={pending || state === 'loading'} error={error} disabled={disabled} readOnly={readOnly} hint="已选标签跨查询保留;选择后可继续添加。" /></Surface>
|
|
54
|
+
<Surface><Stack gap={4}><Checkbox label="树多选模式" checked={multiple} onChange={event => { setMultiple(event.target.checked); setKeys([]); }} /><TreeSelect label="项目层级" nodes={state === 'empty' ? [] : nodes} value={keys} onValueChange={setKeys} expandedKeys={expanded} onExpandedChange={setExpanded} multiple={multiple} disabled={disabled} readOnly={readOnly} loading={state === 'loading'} error={error} /><p>选中键:{keys.join(', ') || '无'};父子勾选互不联动。</p></Stack></Surface>
|
|
55
|
+
</div>
|
|
56
|
+
</section>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface DemoRow { id: string; name: string; disabled?: boolean; children?: DemoRow[] }
|
|
60
|
+
const demoRows: DemoRow[] = [{ id: 'one', name: longName, children: [{ id: 'child', name: '子记录 / 保留层级' }] }, { id: 'locked', name: '不可选择的记录', disabled: true }, ...Array.from({ length: 8 }, (_, i) => ({ id: `row-${i}`, name: `记录 ${i + 3}` }))];
|
|
61
|
+
export function AdvancedDataGallery() {
|
|
62
|
+
const [state, setState] = useState('ready'), [keys, setKeys] = useState<string[]>(['off-page']), [expanded, setExpanded] = useState<string[]>(['one']);
|
|
63
|
+
const [page, setPage] = useState(1), [pageSize, setPageSize] = useState(10), [unknown, setUnknown] = useState(false), [compact, setCompact] = useState(false);
|
|
64
|
+
const [single, setSingle] = useState(false), [hideSelectAll, setHideSelectAll] = useState(false), [sort, setSort] = useState<TableSort>();
|
|
65
|
+
return <section><div className="ds-section-title"><span>DATA</span><h2>受控选择、层级与服务端分页</h2></div>
|
|
66
|
+
<div className="ds-extension-controls"><StateControl value={state} onChange={setState} /><Checkbox label="未知总数" checked={unknown} onChange={event => setUnknown(event.target.checked)} /><Checkbox label="紧凑密度" checked={compact} onChange={event => setCompact(event.target.checked)} /><Checkbox label="表格单选" checked={single} onChange={event => { setSingle(event.target.checked); setKeys([]); }} /><Checkbox label="隐藏全选" checked={hideSelectAll} onChange={event => setHideSelectAll(event.target.checked)} /></div>
|
|
67
|
+
<p className="ds-caption">多选表头只作用于当前可见且可选的行,保留跨页键;单选选取新记录会替换旧键。子行与详情独立呈现。右侧两列自动累计固定偏移,表格内部滚动。排序演示仅回传意图,不请求后端。</p>
|
|
68
|
+
<Surface><Stack gap={4}><DataTable caption="项目记录" rows={state === 'empty' ? [] : page === 1 ? demoRows : [{ id: 'next-page', name: '宿主返回的第二页' }]} rowKey={row => row.id} rowLabel={row => row.name} loading={state === 'loading'} error={state === 'error' ? '读取失败,保留上次数据。' : undefined} onRetry={() => setState('ready')}
|
|
69
|
+
selection={{ keys, onChange: setKeys, mode: single ? 'single' : 'multiple', hideSelectAll, isDisabled: row => !!row.disabled || state === 'disabled' }} expansion={{ keys: expanded, onChange: setExpanded, getChildren: row => row.children ?? [], renderDetail: row => <p>独立详情:{row.name}。导出与查询由宿主实现。</p> }} sort={sort} onSortChange={setSort} onSortReset={() => setSort(undefined)} density={compact ? 'compact' : 'comfortable'} stickyHeader maxHeight={420}
|
|
70
|
+
columns={[{ key: 'name', header: '项目名称', width: 300, rowHeader: true, sortable: true, render: row => row.name }, { key: 'id', header: '标识', width: 80, sticky: 'right', render: row => row.id }, { key: 'action', header: '操作(固定)', width: 112, sticky: 'right', render: row => <Button variant="secondary" disabled={state === 'disabled'} onClick={() => setExpanded([...new Set([...expanded, row.id])])}>查看详情</Button> }]} />
|
|
71
|
+
<Pagination label="项目记录分页" page={page} pageSize={pageSize} total={unknown ? undefined : 28} hasNext={page < 3} disabled={state === 'disabled'} loading={state === 'loading'} showFirstButton showLastButton onPageChange={setPage} onPageSizeChange={size => { setPageSize(size); setPage(1); }} />
|
|
72
|
+
<p role="status">已选 {keys.length} 项:{keys.join(', ') || '无'}。排序意图:{sort ? `${sort.key} ${sort.direction}` : '无'};分页只回传意图。</p>
|
|
73
|
+
</Stack></Surface>
|
|
74
|
+
</section>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function MediaGallery() {
|
|
78
|
+
const [selected, setSelected] = useState(false), [open, setOpen] = useState(false), [index, setIndex] = useState(0), [metadata, setMetadata] = useState(true), [version, setVersion] = useState('original');
|
|
79
|
+
const titles = ['山野 · 午后的光', longName, '视频预览示例'];
|
|
80
|
+
const [retried, setRetried] = useState(false);
|
|
81
|
+
return <section><div className="ds-section-title"><span>MEDIA</span><h2>媒体保持比例,画布保持中性</h2></div><p className="ds-caption">照片、RAW、Live 和视频共享卡片契约。中性暗色查看器独立于三套界面主题;媒体请求、视频播放、下载与版本资源均由宿主提供。示例只使用本地内嵌矢量预览。</p>
|
|
82
|
+
<MediaViewer open={open} onOpenChange={setOpen} trigger={<Button>打开全屏查看器</Button>} title={titles[index]} description={`${index + 1} / 3 · 左右方向键切换;输入和播放器保留自身按键`} onPrevious={() => setIndex(current => Math.max(0, current - 1))} onNext={() => setIndex(current => Math.min(2, current + 1))} previousDisabled={index === 0} nextDisabled={index === 2} metadataOpen={metadata} onMetadataOpenChange={setMetadata}
|
|
83
|
+
actions={<><label>资源版本 <select aria-label="资源版本" value={version} onChange={event => setVersion(event.target.value)}><option value="original">原始资源</option><option value="preview">预览资源</option></select></label><a href={sampleImage} download="rhine-demo.svg">下载演示资源</a></>}
|
|
84
|
+
metadata={<><h3>媒体信息</h3><p>{titles[index]}</p><p>类型:{index === 2 ? '视频' : '照片'}</p><p>版本:{version}</p><p>日期:未知</p><Dialog title="资源说明" description="Escape 先关闭此层,再关闭媒体查看器。" trigger={<button type="button">打开资源说明</button>}><p>业务元数据由宿主管理。</p></Dialog></>}
|
|
85
|
+
>{index === 2 ? <video controls poster={sampleImage} aria-label="视频播放器示例" /> : <img src={sampleImage} alt="山峰与太阳构成的演示插画" />}</MediaViewer>
|
|
86
|
+
<div className="ds-extension-grid ds-gap-top">
|
|
87
|
+
<MediaCard title={titles[0]} kind="photo" date="2026-10-23" preview={<img src={sampleImage} alt="山野插画" />} onOpen={() => { setIndex(0); setOpen(true); }} selected={selected} onSelectedChange={setSelected} />
|
|
88
|
+
<MediaCard title={titles[1]} kind="RAW" preview={<img src={sampleImage} alt="原始预览" />} aspectRatio="3 / 2" onOpen={() => { setIndex(1); setOpen(true); }} />
|
|
89
|
+
<MediaCard title="视频,保持原始比例" kind="video" preview={<img src={sampleImage} alt="视频封面" />} onOpen={() => { setIndex(2); setOpen(true); }} />
|
|
90
|
+
<MediaCard title="加载中的 Live 资源" kind="Live" state="loading" onOpen={() => setOpen(true)} />
|
|
91
|
+
<MediaCard title="不可解码的文件" kind="RAW" state={retried ? 'ready' : 'error'} preview={retried ? <img src={sampleImage} alt="重试后恢复的示例预览" /> : undefined} onOpen={() => { setIndex(1); setOpen(true); }} actions={<Button variant="secondary" onClick={() => setRetried(true)} disabled={retried}>{retried ? '预览已恢复' : '重试预览'}</Button>} />
|
|
92
|
+
<MediaCard title="尚无预览的资源" kind="photo" state="empty" onOpen={() => setOpen(true)} />
|
|
93
|
+
</div>
|
|
94
|
+
</section>;
|
|
95
|
+
}
|
package/preview/GalleryShell.tsx
CHANGED
|
@@ -7,9 +7,10 @@ import './theme-neo-brutalism.css';
|
|
|
7
7
|
|
|
8
8
|
export const galleryGroups = [
|
|
9
9
|
{ label: '开始', items: [['top', '设计总览'], ['subscriptions', '订阅管理示例']] },
|
|
10
|
-
{ label: '
|
|
10
|
+
{ label: '应用能力', items: [['app-shell', '背景与多侧栏'], ['advanced-forms', '复杂表单'], ['advanced-data', '受控数据表'], ['media', '媒体卡片与查看器'], ['monitoring', '健康详情与图表']] },
|
|
11
|
+
{ label: '设计语言', items: [['foundation', '视觉基础'], ['backgrounds', '场景背景'], ['spacing', '间距与组合'], ['typography', '字体与文字'], ['brand', '品牌标志']] },
|
|
11
12
|
{ label: '通用组件', items: [['actions', '按钮与输入'], ['selection', '选择与切换'], ['feedback', '状态与加载'], ['disclosure', '展开与组合'], ['data', '数据与分页'], ['forms', '表单与选择'], ['utilities', '操作与布局'], ['overlays', '浮层与反馈'], ['navigation', '导航与步骤'], ['form-workflow', '表单流程']] },
|
|
12
|
-
{ label: '交互与动效', items: [['patterns', '场景与交互指南'], ['motion', '动效实验室'], ['startup', '启动动画']] },
|
|
13
|
+
{ label: '交互与动效', items: [['patterns', '场景与交互指南'], ['motion', '动效实验室'], ['startup', '启动动画'], ['orbit', '轨道星空']] },
|
|
13
14
|
] as const;
|
|
14
15
|
export type GallerySection = typeof galleryGroups[number]['items'][number][0];
|
|
15
16
|
const entries = galleryGroups.flatMap(group => [...group.items]);
|
|
@@ -93,7 +94,7 @@ function GalleryContent({ motion, onMotionChange, children }: GalleryShellProps)
|
|
|
93
94
|
<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
95
|
setMenuOpen(false); if(section===item.id) main.current?.focus({preventScroll:true});
|
|
95
96
|
}}/>
|
|
96
|
-
<div className="ds-sidebar-foot"><span>COMPONENT LIBRARY</span><span>
|
|
97
|
+
<div className="ds-sidebar-foot"><span>COMPONENT LIBRARY</span><span>60 个组件 · 持续演进</span><span className="ak-readout">{readout}</span></div>
|
|
97
98
|
</aside>
|
|
98
99
|
<main className="ds-workspace" id="gallery-content" ref={main} tabIndex={-1} aria-label={label}>
|
|
99
100
|
<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 => {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Select, Stack, StackedBar, StatusTimeline, Surface, TimelineLegend, TrendChart, type ChartPoint, type TimelineSegment } from '../components';
|
|
3
|
+
|
|
4
|
+
export function MonitoringGallery() {
|
|
5
|
+
const [state, setState] = useState('ready');
|
|
6
|
+
const [activePoint, setActivePoint] = useState<string | null>(null);
|
|
7
|
+
const points: ChartPoint[] = state === 'empty' ? [] : [12, 18, null, 0, 24, 16].map((value, index) => ({ id: String(index), label: `10:${String(index * 10).padStart(2, '0')}`, value }));
|
|
8
|
+
const segments: TimelineSegment[] = state === 'empty' ? [] : ['healthy', 'failed', 'unknown', 'recovered'].map((status, index) => ({
|
|
9
|
+
id: String(index), label: `时段 ${index + 1}`, status: status as TimelineSegment['status'], detail: {
|
|
10
|
+
start: `2026-10-23 10:${String(index * 10).padStart(2, '0')} UTC+8`, end: `2026-10-23 10:${String((index + 1) * 10).padStart(2, '0')} UTC+8`,
|
|
11
|
+
reason: status === 'unknown' ? '此时段没有完整采集证据,不能视为健康。' : status === 'failed' ? '存在宿主确认的最终失败。' : '宿主提供的状态原因,组件不计算健康评分。',
|
|
12
|
+
counts: [{ id: 'success', label: '确认输出', value: status === 'unknown' ? null : 12, unit: '次' }, { id: 'failure', label: '最终失败', value: status === 'failed' ? 2 : 0, unit: '次' }, { id: 'pending', label: '待确认', value: null }],
|
|
13
|
+
methodology: '按请求最终结果归类;重试已恢复与最终失败分开计数,缺失采样保持未知。',
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
const feedback = { stale: state === 'stale' ? '最后采集于 20 分钟前,请核对来源。' : undefined, error: state === 'error' ? '本次读取失败,保留上次采样。' : undefined };
|
|
17
|
+
return <section><div className="ds-section-title"><span>MONITORING</span><h2>看见证据,也看见缺口</h2></div>
|
|
18
|
+
<p className="ds-caption">计数与口径由宿主提供。未知保持未知,折线在缺失处断开;图例与数据表提供文字替代,不生成健康评分。时段详情支持鼠标、键盘、触屏和 Escape,并自动避让视口。</p>
|
|
19
|
+
<Select label="监控演示状态" value={state} onChange={event => setState(event.target.value)}><option value="ready">默认 / 含缺失采样</option><option value="empty">空数据</option><option value="stale">陈旧数据</option><option value="error">错误 / 保留旧数据</option><option value="unknown">占比总量未知</option></Select>
|
|
20
|
+
<Surface className="ds-gap-top"><Stack gap={4}><h3>结构化时段详情</h3><StatusTimeline label="结构化健康历史" startLabel="10:00 UTC+8" endLabel="10:40 UTC+8" segments={segments} /><TimelineLegend /><p>方向键与 Home/End 切换时段;轻触健康条查看同一份完整详情。</p><StatusTimeline compact label="紧凑结构化历史" startLabel="10:00" endLabel="10:40" segments={segments} /></Stack></Surface>
|
|
21
|
+
<div className="ds-extension-grid">
|
|
22
|
+
<Surface><TrendChart label="请求量趋势" unit="次" rangeLabel="2026-10-23 10:00–11:00 UTC+8" description="每 10 分钟一个桶;10:20 未采集,10:30 已确认是零。左右键或轻触可查看,并同步右侧图表时段。" points={points} activePointId={activePoint} onActivePointChange={setActivePoint} valueFormatter={value => value === null ? '未知' : value.toLocaleString('zh-CN')} {...feedback} /></Surface>
|
|
23
|
+
<Surface><TrendChart kind="line" label="资源使用历史" unit="%" rangeLabel="2026-10-23 10:00–11:00 UTC+8" description="宿主采集的资源占用;不插值,不跨越缺失点连线。两个图表按时段 ID 同步查看。" points={points} activePointId={activePoint} onActivePointChange={setActivePoint} {...feedback} /></Surface>
|
|
24
|
+
<Surface><StackedBar label="请求结果分布" unit="次" rangeLabel="2026-10-23 10:00–11:00 UTC+8" description="按宿主互斥分类的计数绘图;包含未知计数时不归一化。" parts={state === 'empty' ? [] : [{ id: 'complete', label: '已完成', value: 32, tone: 1 }, { id: 'retry', label: '重试恢复', value: 8, tone: 3 }, { id: 'failed', label: '最终失败', value: state === 'unknown' ? null : 4, tone: 2 }]} {...feedback} /></Surface>
|
|
25
|
+
</div>
|
|
26
|
+
</section>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Badge, Button, Inline, Logo, OrbitBackdrop, Stack, Surface, Switch, Text, TextField } from '../components';
|
|
3
|
+
|
|
4
|
+
export function OrbitGallery() {
|
|
5
|
+
const [replay, setReplay] = useState(0);
|
|
6
|
+
const [reduced, setReduced] = useState(false);
|
|
7
|
+
const [artworkName, setArtworkName] = useState('Orbit workspace');
|
|
8
|
+
const [workspaceName, setWorkspaceName] = useState('Rhine Studio');
|
|
9
|
+
return <section id="orbit">
|
|
10
|
+
<div className="ds-section-title"><span>ORBIT</span><h2>轨道星空 · 品牌艺术背景</h2></div>
|
|
11
|
+
<p className="ds-caption">420 个环节点、185 条空间曲线与发射箭头组成同一幅画面。用于入口和品牌叙事,内容可立即阅读,不表示加载或业务进度。</p>
|
|
12
|
+
<Inline gap={3}>
|
|
13
|
+
<Badge>0.6.0</Badge>
|
|
14
|
+
<Button variant="secondary" onClick={() => setReplay(value => value + 1)}>重播轨道</Button>
|
|
15
|
+
<Switch label="减少轨道动态" checked={reduced} onCheckedChange={setReduced} />
|
|
16
|
+
</Inline>
|
|
17
|
+
<OrbitBackdrop key={replay} className="ds-gap-top" reducedMotion={reduced} style={{ minHeight: 560 }}>
|
|
18
|
+
<Logo variant="lockup" size={40} />
|
|
19
|
+
<Stack gap={3}>
|
|
20
|
+
<Text as="h3" variant="title">连接彼此,探索更多。</Text>
|
|
21
|
+
<Text tone="muted">在有序的轨迹中,留下新的可能。<br />艺术背景不阻挡内容与操作。</Text>
|
|
22
|
+
<TextField label="直接叠加的输入框" value={artworkName} onChange={event => setArtworkName(event.target.value)} hint="提示文字随当前内容前景保持可读。" />
|
|
23
|
+
<Inline gap={2}>
|
|
24
|
+
<Button>开始探索</Button>
|
|
25
|
+
<Button variant="secondary">查看轨道</Button>
|
|
26
|
+
<Button variant="quiet">了解更多</Button>
|
|
27
|
+
<Button variant="quiet" busy busyLabel="示例处理中">示例处理中</Button>
|
|
28
|
+
<Button variant="quiet" disabled>暂不可用</Button>
|
|
29
|
+
</Inline>
|
|
30
|
+
</Stack>
|
|
31
|
+
<Surface>
|
|
32
|
+
<Stack gap={3}>
|
|
33
|
+
<Text as="h3" variant="heading">带底色的内容保持宿主主题</Text>
|
|
34
|
+
<Text tone="muted">也可组合到 Surface,保留完整的前景、背景与状态配色。</Text>
|
|
35
|
+
<TextField label="工作空间名称" value={workspaceName} onChange={event => setWorkspaceName(event.target.value)} hint="这是可编辑的本地展台示例。" />
|
|
36
|
+
<Inline gap={2}>
|
|
37
|
+
<Button>继续探索</Button>
|
|
38
|
+
<Button variant="secondary">查看介绍</Button>
|
|
39
|
+
<Button variant="quiet">更多介绍</Button>
|
|
40
|
+
</Inline>
|
|
41
|
+
</Stack>
|
|
42
|
+
</Surface>
|
|
43
|
+
<Text variant="caption" tone="muted">空间、连接与持续探索。</Text>
|
|
44
|
+
</OrbitBackdrop>
|
|
45
|
+
<p className="ds-caption">暂停后在原画面继续;系统、顶部或本节减少动态均保留静态画面。视口不超过 760px、页面隐藏或画面离屏时停帧。重播只重建这幅艺术,不执行任何业务操作。</p>
|
|
46
|
+
</section>;
|
|
47
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
.ds-background-controls { display:flex; flex-wrap:wrap; align-items:end; gap:16px; margin:24px 0; }
|
|
2
|
+
.ds-background-controls .rhine-select { width:min(100%,260px); }
|
|
3
|
+
.ds-background-scenes { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),1fr)); gap:24px; }
|
|
4
|
+
.ds-background-example { min-width:0; }
|
|
5
|
+
.ds-background-label { display:flex; flex-wrap:wrap; justify-content:space-between; gap:8px; margin-bottom:8px; }
|
|
6
|
+
.ds-background-label code { font:12px/20px var(--rhine-mono); color:var(--rhine-muted); }
|
|
7
|
+
.ds-background-sample { display:grid; align-content:center; gap:8px; min-height:136px; padding:24px; border:1px solid var(--rhine-line); border-radius:var(--rhine-radius-card); overflow-wrap:anywhere; }
|
|
8
|
+
.ds-background-backdrop { padding:16px; background:var(--rhine-backdrop-bg); border-radius:var(--rhine-radius-card); }
|
|
9
|
+
.ds-background-backdrop .ds-background-sample { min-height:104px; padding:16px; }
|
|
10
|
+
.ds-background-local { margin-top:24px; }
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
.ds-extension-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,360px),1fr)); gap:24px; margin-top:24px; }
|
|
2
|
+
.ds-extension-grid>* { min-width:0; }
|
|
3
|
+
.ds-extension-controls { display:flex; flex-wrap:wrap; align-items:center; gap:16px; margin-bottom:24px; }
|
|
4
|
+
.ds-extension-controls .rhine-select { width:auto; max-width:100%; }
|
|
5
|
+
.ds-shell-stage { height:680px; max-height:85dvh; min-height:380px; min-width:0; }
|
|
6
|
+
.ds-background-swatch { display:grid; gap:24px; padding:24px; border:1px solid var(--rhine-line); }
|
package/preview/main.tsx
CHANGED
|
@@ -10,6 +10,10 @@ import { GalleryShell } from './GalleryShell';
|
|
|
10
10
|
import { SpacingGallery } from './SpacingGallery';
|
|
11
11
|
import { LineHeightGallery } from './LineHeightGallery';
|
|
12
12
|
import { SubscriptionGallery } from './SubscriptionGallery';
|
|
13
|
+
import { BackgroundGallery } from './BackgroundGallery';
|
|
14
|
+
import { OrbitGallery } from './OrbitGallery';
|
|
15
|
+
import { AdvancedDataGallery, AdvancedFormsGallery, MediaGallery, ShellGallery } from './ExtensionsGallery';
|
|
16
|
+
import { MonitoringGallery } from './MonitoringGallery';
|
|
13
17
|
|
|
14
18
|
function Gallery() {
|
|
15
19
|
const [motion, setMotion] = useState('system');
|
|
@@ -28,9 +32,16 @@ function Gallery() {
|
|
|
28
32
|
useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
|
|
29
33
|
const simulate = () => { setBusy(true); timer.current = setTimeout(() => { setBusy(false); setNotice(true); }, 900); };
|
|
30
34
|
return <GalleryShell motion={motion} onMotionChange={setMotion}>{section => <>
|
|
35
|
+
{section === 'monitoring' && <MonitoringGallery />}
|
|
36
|
+
{section === 'backgrounds' && <BackgroundGallery />}
|
|
37
|
+
{section === 'app-shell' && <ShellGallery />}
|
|
38
|
+
{section === 'orbit' && <OrbitGallery />}
|
|
39
|
+
{section === 'advanced-forms' && <AdvancedFormsGallery />}
|
|
40
|
+
{section === 'advanced-data' && <AdvancedDataGallery />}
|
|
41
|
+
{section === 'media' && <MediaGallery />}
|
|
31
42
|
{section === 'subscriptions' && <SubscriptionGallery />}
|
|
32
43
|
{section === 'top' && <>
|
|
33
|
-
<div className="ds-hero"><div><p className="ds-kicker">FOUNDATIONS / COMPONENTS / MOTION</p><h1>精密,有序。<br /><span>每一次交互都有回应。</span></h1><p>
|
|
44
|
+
<div className="ds-hero"><div><p className="ds-kicker">FOUNDATIONS / COMPONENTS / MOTION</p><h1>精密,有序。<br /><span>每一次交互都有回应。</span></h1><p>60 个通用组件,共用一套视觉变量和动效规则。<br />这里运行的就是后续业务页面可直接导入的 React 组件。</p></div></div>
|
|
34
45
|
<div className="ds-overview-links">{[
|
|
35
46
|
['foundation','视觉基础','从色彩、间距与材质开始。'],
|
|
36
47
|
['typography','字体与文字','清晰的层级,稳定的阅读节奏。'],
|