@linhey/rhine-design 0.9.1 → 0.11.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 (90) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/CONTRIBUTING.md +3 -1
  3. package/README.md +18 -15
  4. package/components/AdvancedForms.tsx +22 -8
  5. package/components/COMMON_COMPONENTS.md +6 -3
  6. package/components/Forms.tsx +14 -1
  7. package/components/ListFilterBar.tsx +5 -3
  8. package/components/Overlays.tsx +3 -2
  9. package/components/README.md +13 -5
  10. package/components/SPACING.md +9 -0
  11. package/components/TYPOGRAPHY.md +2 -0
  12. package/components/Toast.tsx +10 -4
  13. package/components/checkbox-row.css +12 -0
  14. package/components/common.css +3 -1
  15. package/components/compact-forms.css +20 -0
  16. package/components/components.tsx +13 -7
  17. package/components/index.ts +1 -0
  18. package/components/media-parity.css +4 -4
  19. package/components/patterns.css +3 -3
  20. package/components/styles.css +17 -2
  21. package/components/theme-picker.css +2 -2
  22. package/docs/APPLICATION_COMPONENTS.md +2 -2
  23. package/docs/CHECKBOX_ROWS.md +36 -0
  24. package/docs/COMPACT_FILTER_CONTROLS.md +38 -0
  25. package/docs/COMPONENT_TYPOGRAPHY_AUDIT.md +44 -0
  26. package/docs/DESIGN_SYSTEM.md +10 -1
  27. package/docs/DESIGN_SYSTEM_AUDIT.md +4 -4
  28. package/docs/DISCLOSURE_VERIFICATION.md +49 -0
  29. package/docs/INTERACTION_PATTERNS.md +9 -1
  30. package/docs/LIST_FILTER_BAR.md +5 -1
  31. package/docs/REVIEW_TOOL.md +128 -0
  32. package/docs/REVIEW_VERIFICATION.md +56 -0
  33. package/docs/SUMMARY_TABS.md +77 -0
  34. package/docs/THEMING.md +1 -1
  35. package/docs/assets/disclosure/arknights-400.png +0 -0
  36. package/docs/assets/disclosure/neo-brutalism-400.png +0 -0
  37. package/docs/assets/disclosure/rhine-400.png +0 -0
  38. package/docs/assets/disclosure/rhine-native200.png +0 -0
  39. package/examples/review/http/App.tsx +46 -0
  40. package/examples/review/http/catalog.tsx +44 -0
  41. package/examples/review/http/details.ts +1 -0
  42. package/examples/review/http/vite.config.ts +8 -0
  43. package/examples/review/platform/Workspace.tsx +34 -0
  44. package/examples/review/platform/catalog.tsx +46 -0
  45. package/examples/review/platform/vite.config.ts +8 -0
  46. package/examples/review/product.css +3 -0
  47. package/package.json +17 -5
  48. package/preview/CheckboxRowGallery.tsx +26 -0
  49. package/preview/CommonGallery.tsx +1 -1
  50. package/preview/CompactFilterBarGallery.tsx +19 -0
  51. package/preview/ComponentTypographyGallery.tsx +23 -0
  52. package/preview/DenseFilterControlsGallery.tsx +21 -0
  53. package/preview/GalleryShell.tsx +4 -3
  54. package/preview/OrbitGallery.tsx +1 -1
  55. package/preview/SpacingGallery.tsx +9 -1
  56. package/preview/SummaryTabsGallery.tsx +37 -0
  57. package/preview/main.tsx +14 -3
  58. package/preview/packageInfo.ts +5 -0
  59. package/review/canvas.tsx +107 -0
  60. package/review/dev-client.mjs +17 -0
  61. package/review/feedback.ts +17 -0
  62. package/review/index.ts +2 -0
  63. package/review/isolation.ts +64 -0
  64. package/review/lifecycle.ts +59 -0
  65. package/review/protocol.ts +7 -0
  66. package/review/review.css +41 -0
  67. package/review/shell.tsx +165 -0
  68. package/review/state.ts +41 -0
  69. package/review/types.ts +59 -0
  70. package/review/vite.d.mts +10 -0
  71. package/review/vite.mjs +107 -0
  72. package/scripts/check-checkbox-rows.js +65 -0
  73. package/scripts/check-component-typography.js +76 -0
  74. package/scripts/check-dense-filters.js +60 -0
  75. package/scripts/check-review-package.mjs +11 -0
  76. package/scripts/check-review-recovery.js +47 -0
  77. package/scripts/check-review.js +153 -0
  78. package/skills/rhine-design/SKILL.md +2 -2
  79. package/skills/rhine-design/references/API_MAP.md +6 -0
  80. package/skills/rhine-design/references/TASK_MAP.md +2 -0
  81. package/skills/rhine-design/references/integration.md +1 -1
  82. package/tests/CheckboxRow.test.tsx +53 -0
  83. package/tests/CommonComponents.test.tsx +8 -0
  84. package/tests/CompactFilterBar.test.tsx +50 -0
  85. package/tests/DenseFilterControls.test.tsx +79 -0
  86. package/tests/DesignSystem.test.tsx +32 -0
  87. package/tests/Review.test.tsx +117 -0
  88. package/tests/ReviewBuild.test.tsx +59 -0
  89. package/tests/ReviewShell.test.tsx +63 -0
  90. package/tsconfig.json +5 -2
@@ -0,0 +1,56 @@
1
+ # 产品评审工具验收(2026-10-27)
2
+
3
+ 对应 [#40](https://github.com/linhay/rhine-design/issues/40),验收时为未提交、未发布候选;后续提交进展见关联 issue。验证基于 Web `cfcb400fa213d9905eae1cab5dcb8ab8f817e03f` 加本任务工作区变更。版本字段仍为 0.10.0,不等于 npm 0.10.0 已具有新入口。
4
+
5
+ ## 证据范围
6
+
7
+ - 合成数据;源码构建/测试使用 Node 24.21.0。源码工作区使用 React 19.3.1、Vite 8.3.4、TypeScript 5.9.3。
8
+ - 安装包复验覆盖 React/ReactDOM 19.2.0 + Vite 6.4.5 + plugin-react 4.7.0,以及 React/ReactDOM 19.3.1 + Vite 8.3.4 + plugin-react 6.1.2。后续真实 App 隔离试接另覆盖 React 19.2.0 + Vite 6.4.3。仅声明这些实测组合;Vite 7、其他构建器、SSR 未测。
9
+ - Chromium 155 与 WebKit 26.6 完整回归;Firefox 157(Playwright/core 1.64.1,Debian 13 arm64 隔离容器,CLI Node 20.19.2)覆盖静态制品 25 组及故障恢复,未测 Firefox dev。Safari 27.0.1 原生窗口完成草稿/Portal 跨主题保持、保存、400px 画布与 200% CSS zoom smoke。WebKit 引擎结果不冒充 Safari 完整矩阵;CSS zoom 不代表原生浏览器缩放。
10
+ - 本地源码文件摘要、候选 tarball SHA-256、安装目录逐文件比对和原始日志保存在管理工作区的忽略证据目录。没有把候选当作正式版本发布。
11
+
12
+ ## 已执行检查
13
+
14
+ | 项目 | 结果与范围 |
15
+ | --- | --- |
16
+ | 状态与生命周期 | PASS:URL 多维状态/非法回退,慢 setup 取消、早期资源清理、串行 dispose、reset、异常反馈 |
17
+ | HTTP 模拟 | PASS:Request method/body、credentials omit、调用方/场景 signal、未匹配失败、内存存储 reset |
18
+ | 构建边界 | PASS:静态 HTML 的真实 CSS 引用、非根 base、preview 目录入口、拒绝业务代理、生产目录精确匹配及官方 runtime 排除 |
19
+ | 全库检查 | PASS:287 测试、类型检查、原组件展台构建/CSS/SVG 检查、两个产品评审示例构建 |
20
+ | 真实浏览器 | PASS:Chromium、WebKit 分别执行开发/静态 HTTP 交互与 24 组(三主题 × 400/520/880/1280 宽 × 100%/200% CSS zoom),各 26 组,Firefox 另执行同一静态矩阵 25 组;草稿/Portal/DOM 焦点保留、表单保存、键盘关闭与焦点返回、长文、状态/角色、hash 刷新与前进后退 |
21
+ | 故障恢复与错误留存 | PASS:Chromium/WebKit/Firefox 注入首次画布请求失败、永不完成的 dispose;10 秒后可显式重建 iframe,评审条件保留且旧上下文不继续 setup;旧请求确认不覆盖新状态,错误跨就绪/主题/重载保留并标记原场景,显式确认才清除 |
22
+ | 动效生命周期 | PASS:平台 client 保存、Canvas 播放/暂停/恢复、系统/手动减少动态及系统恢复;切场景旧 Canvas 帧计数停止,慢初始化不覆盖新场景,渲染错误可见且可离开恢复 |
23
+ | 网络/导航 | PASS:上述浏览器运行无真实业务请求/外部出站;未实现请求、同源整页链接/下载路径和外链被阻止,当前文档 hash 可用 |
24
+ | 固定制品消费 | PASS:两套实际安装包分别构建 HTTP/平台两例;Vite 6 dev/preview 与 Vite 8 preview 中样式、主题切换保留、表单提交、平台保存及暂停通过,console/page error 为零 |
25
+ | 真实 App 隔离试接 | PASS:两类真实 App 的固定源码副本安装同一候选,256 个包文件逐一匹配;共 18 组三主题/三宽度渲染与唯一 Provider 检查,另测 Portal/草稿原位保留、模拟保存/reset、产品路由、200% CSS zoom、减少动态与显式重载。178 次请求全为本地静态资源,pageerror 为零;HTTP 产品 619 业务 + 17 设计测试,平台产品 18 项 App/预览测试与类型检查,两者正式/预览构建和生产排除检查通过。仅副本接入,原产品未改;真实产品跨浏览器矩阵未覆盖 |
26
+ | 发布与消费迁移 | SKIPPED:未获本轮发布/消费升级授权;没有 npm 发布、Pages 部署或真实产品代码迁移 |
27
+ | 原生运行 | N/A:工具为 Web 评审,不能证明桌面原生或 UIKit 行为 |
28
+
29
+ 复验入口:`npm run check`;浏览器启动 HTTP dev(18340),以 Python 静态服务在 18342 提供 dist,再运行 `playwright-cli run-code --filename scripts/check-review.js` 与 `playwright-cli run-code --filename scripts/check-review-recovery.js`。用 CLI `open --browser=chrome` / `--browser=webkit` 选择独立会话。默认源码端口与脚本中的固定回归端口一致,避免误测其他页面。截图在 `output/playwright/review-*.png`(忽略目录)。
30
+
31
+ ## 失败与修正保留
32
+
33
+ 1. 早期开发子路径 404:中间件未正确处理 base,随后 HTML transform 又重复拼接 base。已修正为中间件识别 base、由 Vite 转换单一模块绝对路径。
34
+ 2. 第一轮静态截图没有样式:生成 HTML 时 CSS 尚未加入 bundle。改为 post generateBundle 后链接最终 CSS,补构建断言、计算样式和实际截图审阅;原交互可运行不能证明视觉通过。
35
+ 3. 开发模式出现一次 504 Outdated Optimize Dep,以及 CSS 模块引入 Vite client 的 CSP WebSocket 错误。调整独立优化缓存/扫描入口,并提供无连接的样式客户端。修正后浏览器 console error 为零;单次 504 的其他触发条件未穷尽。
36
+ 4. 第一轮焦点返回采样有 4 项瞬时失败。改为有界等待真实 activeElement 恢复,第二轮通过;未修改 Dialog 行为,也不把瞬时观察写成已定位组件缺陷。
37
+ 5. 首次 tarball/Vite 6 构建失败,Node 不支持 node_modules 中 TypeScript 插件自动剥离。Vite 入口改为 `.mjs` 运行文件和 `.d.mts` 声明,随后两套制品构建通过。
38
+ 6. 最终浏览器回归曾在 reset 消息尚未处理时读取旧输入,出现两项采样失败。脚本改为先确认旧会话 DOM 卸载,再检查新输入;保留原结果,不用可见性冒充 reset 已完成。
39
+ 7. 打包清单检查发现宽泛 examples 目录规则会收入嵌套 Vite 缓存。改为精确源码 glob,并加入包清单检查,最终制品排除 node_modules/缓存;所有早期包都仅留本地,未发布。
40
+ 8. 首次 `vite preview` 访问目录 URL 404,显式 index.html 可用。将 appType 改为 mpa,补真实 preview HTTP 回归,不用源码 dev 成功代替产物 preview。
41
+
42
+ 9. Firefox 156 在本机 headless 启动报 `Could not find profile folder`,独立 profile 重试仍失败,headed 显示 Profile Missing。与 [Playwright 上游 macOS 27 报告](https://github.com/microsoft/playwright/issues/42768) 的症状一致,尚未独立确认本机根因;未授予额外磁盘访问或更改隐私设置。Safari WebDriver 因远程自动化未启用而拒绝启动,改用原生 UI smoke,未更改该设置。
43
+ 10. Linux 补验首次安装器与 CLI 的 Playwright core 版本不一致,下载了 Firefox 1555 构建但 CLI 查找 1553,浏览器未启动;统一 Playwright 与 core 到 1.64.1 后重跑,原启动失败保留。
44
+ 11. 真实消费试接保留了合成时间类型错误、快照漏配导致 ENOENT、定位器/滚动/残留浮层导致的首次脚本失败;修正副本 fixture/必要配置及脚本后最终通过。既有测试警告和开发依赖扫描日志保留,不宣称所有日志为零。
45
+
46
+ 12. Firefox 首轮矩阵有两项脚本失败:同步读取 frame URL 早于 hash 跳转,以及预期合成渲染异常的 console 文本仅为 `Error`。诊断确认 hash 随后完成,Error 参数的 message 为预期合成异常;脚本改为有界等待 hash,并读取实际 Error 参数,未泛化忽略所有 Error。修正后 Firefox 静态 25 组通过;Chromium/WebKit 用同版脚本复核。
47
+
48
+ ## 合并前增量验证
49
+
50
+ 2026-10-27 在 `9f010f7` 基础上核对现有 Pages 工作流,发现示例的绝对 `/review-http/`、`/review-platform/` 资源路径不适用于项目站点 `/rhine-design/`。示例改为构建时使用 `./` 相对资源,dev/preview 保留各自目录前缀;工具运行代码和场景协议未改。构建集成测试同时覆盖绝对嵌套 base 与相对 base,全库 `npm run check` 为 288 项测试及全部构建/包清单 PASS。
51
+
52
+ 最终静态产物放在 `/rhine-design/review-http/`、`/rhine-design/review-platform/` 后,Chromium/WebKit 各 25 组矩阵 PASS:样式、动态模块、主题/Portal/草稿、表单、hash 刷新、动效和无业务出站。该增量验证不把此前固定 tarball 摘要改称新源码的包摘要;尚未重新发布 npm 制品。实际线上部署另以对应主线工作流和真实 URL 复验为准。
53
+
54
+ ## 交付边界
55
+
56
+ 评审接口和通用壳已有本地可运行候选,两个示例无需复制壳、URL 或工具栏。尚不能关闭 #40:固定版本发布、稳定在线部署与真实消费项目迁移仍需各自授权和证据。专用静态源、合成数据、host Provider 所有权、显式动效适配和异步关闭边界见[工具契约](REVIEW_TOOL.md)。
@@ -0,0 +1,77 @@
1
+ # 带摘要的页签 · Summary Tabs
2
+
3
+ 版本边界:自 0.11.0 提供;npm 0.10.0 不包含这些字段,见[版本记录](../CHANGELOG.md#01102026-10-27)。实现使用现有 `Tabs` 的 `description` / `status`,不新增另一套组件。规范、组件和展台必须一起验收;消费方不得用后代选择器另改间距。
4
+
5
+ ## 适用范围与结构
6
+
7
+ 同一任务中切换少量独立对象,同时需要知道每个对象当前身份和状态,例如不同客户端、工作区或运行环境。建议2–3项;大量对象优先简洁Tabs或导航列表,完整身份留在内容区,不将大量摘要挤进一排。
8
+
9
+ 每个页签只有两组信息:第一组为名称及一个只读状态;第二组为身份摘要。状态即使未选中也保持可见,不暗示“选中才运行”。详细说明、路径、刷新按钮、停止同步等操作放在面板或操作栏,不能嵌套在Tab按钮中。
10
+
11
+ ```text
12
+ ┌── 命中区域:上12 / 下12,水平0 ──────────┐
13
+ │ 名称 ← 8 → 状态 │ 14/22;状态复用Badge
14
+ │ ↓ 4 │
15
+ │ 类型 · 当前身份 │ 12/18
16
+ └────────────── 2px选中线 ────────────────┘
17
+ ↓ 24
18
+ 面板第一组内容
19
+ ```
20
+
21
+ 图示尺寸是CSS像素;文字按行盒对齐,不能以中文字形的上下空白反推padding。
22
+
23
+ ## 间距与排版
24
+
25
+ | 部位 | 值 / token | 说明 |
26
+ | --- | --- | --- |
27
+ | 名称 | 14/22,600;body / leading-body / weight-heading | 左对齐;不是页面标题 |
28
+ | 摘要 | 12/18,400;caption / leading-caption / weight-regular | muted语义色;不得继承名称加粗 |
29
+ | 状态 | 复用Badge契约 | 当前Badge为12/20、上下2px内距;各主题保留自己的边框、字重和内距,不在Tabs重画 |
30
+ | 名称到状态 | 8,space-2 | 同一flex行;空间不足状态自然换行 |
31
+ | 第一组到摘要 | 4,space-1 | 紧密身份组合;不同于普通卡片标题/说明的8px配方 |
32
+ | 命中区上下内距 | 各12,space-3 | 水平0;首组与末组分别到上下边界 |
33
+ | 相邻页签间距 | 16,space-4 | 不再叠加水平padding |
34
+ | 页签分隔线到面板内容 | 24,space-6 | 由Tabs面板负责;调用方首项不得再加顶部margin |
35
+ | 命中区域 | 最小高度44,自然增高 | 默认莱茵单行名称/状态、单行摘要70px(24+4+18+12×2);Badge或文字换行可增高 |
36
+ | 底部线 | 普通1px,选中2px | 选中线覆盖该页签命中区宽度;状态徽标不替代选中线 |
37
+
38
+ 带摘要的一组Tab等分可用宽度,均从顶部对齐;第一项与面板正文左缘对齐。容器保持至少必要的焦点外延空间,不加阴影、整块选中底色、额外卡片边框或圆角胶囊。
39
+
40
+ ## 状态、主题与动效
41
+
42
+ | 状态 | 外观 / 行为 |
43
+ | --- | --- |
44
+ | 未选中 | 名称muted,摘要muted;状态仍用其真实语义色 |
45
+ | 选中 | 名称ink与2px指示线;摘要不加粗、不变为成功色 |
46
+ | 悬停 | 可用页签名称变ink;无抬升、阴影或位移 |
47
+ | 键盘焦点 | 沿用库2px焦点环与3px偏移,和选中线分开 |
48
+ | 禁用 | 沿用库禁用透明度;不能点击,方向键跳过;不是错误状态的替代品 |
49
+ | 加载 | 保留已知身份,状态写“读取中”;未知身份写“尚未读取”,不编造账号 |
50
+ | 错误 / 未知 | 明确文字徽标;保留可信旧值或显示未匹配,详情在面板 |
51
+
52
+ 状态是对象事实,选中是浏览位置;两者不能互相驱动。三主题只通过现有语义tokens、Badge和主题规则变化;橙趣等主题Badge较高时自然抬高整行,不能压缩字高去维持70px。状态外层使用inline-flex,消除额外的行内基线空白。
53
+
54
+ 选中线与面板沿用180ms的库动效,状态文字即时更新,不因标签变化循环动画;系统或手动减少动态时立即呈现。动画、状态与请求成功互不代替。
55
+
56
+ ## 窄窗与可访问性
57
+
58
+ - 320px局部容器、400/520/880视口和200%缩放保持字号及间距;名称、状态和摘要自然换行,页签等高顶对齐,不固定高度、不裁剪身份,不使用省略号隐藏唯一辨识信息。
59
+ - `label`始终是稳定可访问名称;`description`与`status`通过`aria-describedby`关联。状态更新不改`value`、焦点或当前页签。
60
+ - 左右方向键循环,Home/End到首末可用项;跳过禁用项。隐藏面板保留挂载但退出可访问树。新增摘要不改变这些行为。
61
+ - 状态仅允许纯文本或只读Badge,不允许按钮、链接、菜单、Switch、live region;异步结果由面板反馈,避免多个Tab反复播报。
62
+ - 首次读取、错误及后台状态变化均不自动切页;宿主保存每个对象的状态,不把当前对象复制给其他页签。
63
+
64
+ ## 接入与展台
65
+
66
+ ```tsx
67
+ <Tabs label="工作区" value={selected} onValueChange={setSelected} items={[
68
+ { value: 'primary', label: '主工作区', description: '研发 · 研究项目',
69
+ status: <Badge tone="success">同步中</Badge>, content: <PrimaryPanel /> },
70
+ { value: 'team', label: '协作工作区', description: '协作 · 设计项目',
71
+ status: <Badge>已停止</Badge>, content: <TeamPanel /> },
72
+ ]} />
73
+ ```
74
+
75
+ 独立展台 `/#summary-tabs` 提供局部320/520/自适应宽度、正常/加载/错误/禁用/长文本、尺寸说明和可编辑面板草稿。保留 `/#selection` 普通Tabs作对照;两个变体不能共用临时业务CSS。包内入口见[组件契约](../components/README.md),间距含义见[间距规范](../components/SPACING.md)。
76
+
77
+ 验收同时检查计算样式与实际像素:12/8/4/16/24、名称14/22/600、摘要12/18/400;三主题长文本、键盘、禁用跳过、焦点保留、隐藏面板、状态变化、系统/手动减少动态。局部无横向溢出不替代视觉检查。正式消费需固定包含此能力的版本或提交;本地候选展台不代表能力已经发布。
package/docs/THEMING.md CHANGED
@@ -144,4 +144,4 @@ import { Background, Surface } from '@linhey/rhine-design';
144
144
 
145
145
  兼容已有源码调用:`roleTone` 的原有三种值与显式 `theme` / `none` 均保留;省略 `decoration` 的正文及 `AppShell` 改为 `auto`,默认纯色。需要旧版正文纹理的调用方显式传入 `decoration="theme"`。侧栏显式传入 `theme` 现在也会开启纹理;默认仍关闭。以上能力自 0.5.0 提供,不属于 npm 0.4.0。
146
146
 
147
- 主题选择的新候选组合见 [ThemePicker / ThemePreview](THEME_PICKER.md);静态预览避免嵌套 Provider,未发布能力需核对源码与实际安装包。
147
+ 主题选择组件 [ThemePicker / ThemePreview](THEME_PICKER.md) 自 0.8.0 提供;静态预览避免嵌套 Provider。接入时核对源码与实际安装包,不由 main 展台推断发布范围。
@@ -0,0 +1,46 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Button, Dialog, Notice, Stack, Surface, Text, TextField } from '@linhey/rhine-design';
3
+ import { ThemeProvider } from '@linhey/rhine-design/theme';
4
+ import type { ReviewEnvironment } from '@linhey/rhine-design/review';
5
+ import '../product.css';
6
+ type Project = { id: number; name: string };
7
+ export function App({ environment, signal, role, report }: { environment: ReviewEnvironment; signal: AbortSignal; role: string; report(message: string, kind?: 'info' | 'error'): void }) {
8
+ const [items, setItems] = useState<Project[]>([]);
9
+ const [status, setStatus] = useState('loading');
10
+ const [query, setQuery] = useState('');
11
+ const [draft, setDraft] = useState('');
12
+ const [open, setOpen] = useState(false);
13
+ const [saving, setSaving] = useState(false);
14
+ const [details, setDetails] = useState('');
15
+ useEffect(() => {
16
+ const abort = new AbortController();
17
+ setStatus('loading');
18
+ fetch(`/api/projects?q=${encodeURIComponent(query)}`, { signal: AbortSignal.any([signal, abort.signal]) }).then(async response => {
19
+ if (!response.ok) throw new Error('合成服务暂不可用');
20
+ const next = await response.json() as Project[];
21
+ if (!abort.signal.aborted && !signal.aborted) { setItems(next); setStatus('ready'); }
22
+ }).catch(error => { if (error.name !== 'AbortError') setStatus('error'); });
23
+ return () => abort.abort();
24
+ }, [query, signal]);
25
+ return <ThemeProvider theme={environment.theme} data-rhine-motion={environment.reducedMotion ? 'reduced' : environment.paused ? 'paused' : 'full'}>
26
+ <div className="review-product"><Stack gap={4}><Text as="h1" variant="title">项目工作台</Text><Text tone="muted">合成数据 · {role === 'editor' ? '可编辑' : '只读'} · 独立产品路由</Text>
27
+ <TextField label="搜索项目" value={query} onChange={event => setQuery(event.target.value)} />
28
+ <TextField label="未保存草稿" value={draft} onChange={event => setDraft(event.target.value)} />
29
+ <Button variant="secondary" onClick={async () => {
30
+ try { const { message } = await import('./details'); setDetails(message); }
31
+ catch (error) { report(String(error), 'error'); }
32
+ }}>加载延迟模块</Button>{details && <p>{details}</p>}
33
+ {status === 'loading' ? <p role="status">正在加载合成项目…</p> : status === 'error' ? <Notice title="合成服务暂不可用">请切换状态重试。</Notice> : items.length ? items.map(item => <Surface key={item.id}><Text>{item.name}</Text></Surface>) : <p>暂无项目</p>}
34
+ <Button variant="secondary" onClick={() => { void fetch('/api/unimplemented').catch(error => report(error.message, 'error')); }}>触发未实现操作</Button>
35
+ <Dialog trigger={<Button disabled={role !== 'editor'}>新建项目</Button>} open={open} onOpenChange={setOpen} title="新建合成项目" description="仅保存到当前合成场景的内存数据。"><form onSubmit={async event => {
36
+ event.preventDefault(); setSaving(true);
37
+ try {
38
+ const response = await fetch(new Request(`${location.origin}/api/projects`, { method: 'POST', body: JSON.stringify({ name: draft || '新合成项目' }), signal }));
39
+ if (!response.ok) throw new Error('模拟保存失败');
40
+ setItems(await response.json() as Project[]); setOpen(false);
41
+ } catch (error) { report(String(error), 'error'); }
42
+ finally { setSaving(false); }
43
+ }}><Stack gap={3}><TextField label="项目名称" value={draft} onChange={event => setDraft(event.target.value)} /><Button type="submit" disabled={saving}>保存项目</Button></Stack></form></Dialog>
44
+ </Stack></div>
45
+ </ThemeProvider>;
46
+ }
@@ -0,0 +1,44 @@
1
+ import { defineReview } from '@linhey/rhine-design/review';
2
+ export default defineReview({
3
+ title: '项目工作台 · HTTP 示例',
4
+ scenes: [{
5
+ id: 'projects', title: '项目与编辑流程', group: '工作台', themeRoot: 'host',
6
+ source: 'examples/review/http/App.tsx', issue: 'https://github.com/linhay/rhine-design/issues/40',
7
+ description: '真实表单、弹窗与路由;HTTP 适配使用内存数据。主题与宽度切换保留草稿,状态或角色切换重新初始化。',
8
+ dimensions: [
9
+ { id: 'status', label: '列表状态', options: ['normal', 'loading', 'empty', 'error', 'dense'].map(id => ({ id, label: ({ normal: '正常', loading: '加载', empty: '空', error: '错误', dense: '密集长文' })[id]! })) },
10
+ { id: 'role', label: '角色', options: [{ id: 'editor', label: '编辑者' }, { id: 'reader', label: '只读成员' }] },
11
+ ],
12
+ async setup(context) {
13
+ const items = context.dimensions.status === 'empty' ? [] : Array.from({ length: context.dimensions.status === 'dense' ? 25 : 3 }, (_, index) => ({ id: index + 1, name: context.dimensions.status === 'dense' ? `合成项目 ${index + 1} · 跨团队设计评审与持续验证的长标题示例` : `合成项目 ${index + 1}` }));
14
+ context.mockHttp(async request => {
15
+ const url = new URL(request.url);
16
+ if (url.pathname !== '/api/projects') return;
17
+ if (request.method === 'GET') {
18
+ if (context.dimensions.status === 'loading') return new Promise<Response>((_resolve, reject) => request.signal.addEventListener('abort', () => reject(request.signal.reason), { once: true }));
19
+ if (context.dimensions.status === 'error') return Response.json({ message: '合成服务暂不可用' }, { status: 503 });
20
+ return Response.json(items.filter(item => item.name.includes(url.searchParams.get('q') ?? '')));
21
+ }
22
+ if (request.method === 'POST') {
23
+ if (context.dimensions.role !== 'editor') return Response.json({ message: '只读角色' }, { status: 403 });
24
+ const body = await request.json() as { name: string };
25
+ items.push({ id: items.length + 1, name: body.name });
26
+ context.report('模拟项目已保存');
27
+ return Response.json(items);
28
+ }
29
+ });
30
+ // The adapter is installed before any product module is evaluated.
31
+ const { App } = await import('./App');
32
+ context.signal.throwIfAborted();
33
+ const oldUrl = location.href;
34
+ history.replaceState(null, '', `${import.meta.env.BASE_URL}projects`);
35
+ window.dispatchEvent(new PopStateEvent('popstate'));
36
+ context.onDispose(() => { history.replaceState(null, '', oldUrl); });
37
+ return {
38
+ render: environment => <App environment={environment} signal={context.signal} role={context.dimensions.role} report={context.report} />,
39
+ reset: () => context.report('模拟数据已恢复初始值'),
40
+ dispose() { items.length = 0; },
41
+ };
42
+ },
43
+ }],
44
+ });
@@ -0,0 +1 @@
1
+ export const message = '延迟模块已加载 · 资源 base 与产品路由互相独立';
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import { reviewPlugin } from '@linhey/rhine-design/review/vite';
4
+ export default defineConfig(({ command }) => ({
5
+ root: import.meta.dirname, base: command === 'serve' ? '/review-http/' : './',
6
+ plugins: [react(), reviewPlugin({ catalog: './catalog.tsx', routerBase: '/product/' })],
7
+ build: { outDir: '../../../dist/review-http', emptyOutDir: true },
8
+ }));
@@ -0,0 +1,34 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { Button, Notice, Stack, Text, TextField } from '@linhey/rhine-design';
3
+ import type { ReviewEnvironment } from '@linhey/rhine-design/review';
4
+ import '../product.css';
5
+ interface Client { read(): string; save(value: string): Promise<void> }
6
+ export function Workspace({ client, environment, dimensions }: { client: Client; environment: ReviewEnvironment; dimensions: Readonly<Record<string, string>> }) {
7
+ const [draft, setDraft] = useState(client.read);
8
+ const [saved, setSaved] = useState(false);
9
+ const canvas = useRef<HTMLCanvasElement>(null);
10
+ const count = useRef(0);
11
+ useEffect(() => {
12
+ const target = canvas.current!;
13
+ const context = target.getContext('2d')!;
14
+ let frame = 0;
15
+ const draw = () => {
16
+ count.current++;
17
+ target.dataset.frames = String(count.current);
18
+ context.clearRect(0, 0, 320, 100);
19
+ context.fillStyle = environment.theme === 'arknights' ? '#d0ef82' : environment.theme === 'neo-brutalism' ? '#e95725' : '#507058';
20
+ context.beginPath(); context.arc(160 + Math.sin(count.current / 25) * 110, 50, 14, 0, Math.PI * 2); context.fill();
21
+ if (!environment.paused && !environment.reducedMotion) frame = requestAnimationFrame(draw);
22
+ };
23
+ draw();
24
+ return () => cancelAnimationFrame(frame);
25
+ }, [environment.paused, environment.reducedMotion, environment.theme]);
26
+ return <div className="review-product"><Stack gap={4}><Text as="h1" variant="title">桌面工作区</Text>
27
+ {dimensions.connection === 'error' ? <Notice title="合成连接失败" /> : dimensions.connection === 'waiting' ? <p role="status">等待模拟连接…</p> : <p>平台模拟已连接 · {dimensions.quota === 'empty' ? '暂无用量' : '合成用量 42'}</p>}
28
+ <TextField label="工作区名称" value={draft} onChange={event => { setDraft(event.target.value); setSaved(false); }} />
29
+ <Button disabled={dimensions.connection !== 'ready'} onClick={async () => { await client.save(draft); setSaved(true); }}>保存工作区</Button>
30
+ {saved && <p role="status">内存工作区已保存</p>}
31
+ <canvas ref={canvas} width={320} height={100} aria-label="合成动态画布" />
32
+ <p>{environment.reducedMotion ? '减少动态' : environment.paused ? '已暂停' : '动态播放'}</p>
33
+ </Stack></div>;
34
+ }
@@ -0,0 +1,46 @@
1
+ import { defineReview, type ReviewScene } from '@linhey/rhine-design/review';
2
+ const workspace: ReviewScene = {
3
+ id: 'workspace', title: '桌面工作区', group: '平台接口',
4
+ motionControls: ['full', 'paused'],
5
+ source: 'examples/review/platform/Workspace.tsx',
6
+ description: '仅注入内存平台 client,不调用原生配置、下载或业务剪贴板。Canvas 通过显式环境属性暂停、减少动态和恢复。',
7
+ dimensions: [
8
+ { id: 'connection', label: '连接阶段', options: [{ id: 'ready', label: '已连接' }, { id: 'waiting', label: '等待连接' }, { id: 'error', label: '连接失败' }] },
9
+ { id: 'quota', label: '用量状态', options: [{ id: 'normal', label: '正常用量' }, { id: 'empty', label: '暂无用量' }] },
10
+ ],
11
+ async setup(context) {
12
+ const { Workspace } = await import('./Workspace');
13
+ context.signal.throwIfAborted();
14
+ let current = '合成工作区';
15
+ const client = {
16
+ read: () => current,
17
+ save: async (value: string) => { context.signal.throwIfAborted(); current = value; context.report('已更新内存工作区名称'); },
18
+ };
19
+ return {
20
+ render: environment => <Workspace client={client} environment={environment} dimensions={context.dimensions} />,
21
+ dispose() { current = ''; },
22
+ };
23
+ },
24
+ };
25
+ export default defineReview({
26
+ title: '桌面工作区 · 平台示例', scenes: [workspace, {
27
+ id: 'slow', title: '慢速初始化', group: '生命周期', description: '等待期间可以切回工作区;定时器与迟到结果必须清理。',
28
+ async setup(context) {
29
+ await new Promise<void>((resolve, reject) => {
30
+ const timer = setTimeout(resolve, 4000);
31
+ context.onDispose(() => { clearTimeout(timer); reject(new DOMException('Disposed', 'AbortError')); });
32
+ });
33
+ context.signal.throwIfAborted();
34
+ return { render: () => <p>慢速初始化完成</p>, dispose() {} };
35
+ },
36
+ }, {
37
+ id: 'stuck-cleanup', title: '清理挂起恢复', group: '故障恢复', description: '离开此场景会遇到永不结束的合成清理;10 秒后可在外层重新载入画布恢复。',
38
+ setup: () => ({ render: () => <p>此场景模拟清理挂起,请切换到桌面工作区验证恢复。</p>, dispose: () => new Promise<void>(() => {}) }),
39
+ }, {
40
+ id: 'cleanup-error', title: '清理错误留存', group: '故障恢复',
41
+ setup: () => ({ render: () => <p>离开此场景后,合成清理错误应保留到显式确认。</p>, dispose() { throw new Error('合成清理失败'); } }),
42
+ }, {
43
+ id: 'failure', title: '渲染错误边界', group: '生命周期',
44
+ setup: () => ({ render() { throw new Error('合成渲染失败'); }, dispose() {} }),
45
+ }],
46
+ });
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import { reviewPlugin } from '@linhey/rhine-design/review/vite';
4
+ export default defineConfig(({ command }) => ({
5
+ root: import.meta.dirname, base: command === 'serve' ? '/review-platform/' : './',
6
+ plugins: [react(), reviewPlugin({ catalog: './catalog.tsx' })],
7
+ build: { outDir: '../../../dist/review-platform', emptyOutDir: true },
8
+ }));
@@ -0,0 +1,3 @@
1
+ .review-product { padding: 24px; max-width: 840px; margin: 0 auto; overflow-wrap: anywhere; }
2
+ .review-product canvas { display: block; max-width: 100%; border: 1px solid var(--rhine-line); }
3
+ @media (max-width: 400px) { .review-product { padding: 12px; } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linhey/rhine-design",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -23,7 +23,12 @@
23
23
  "./styles.css": "./components/styles.css",
24
24
  "./theme-arknights.css": "./components/theme-arknights.css",
25
25
  "./theme-neo-brutalism.css": "./components/theme-neo-brutalism.css",
26
- "./logo.svg": "./components/assets/logo.svg"
26
+ "./logo.svg": "./components/assets/logo.svg",
27
+ "./review": "./review/index.ts",
28
+ "./review/vite": {
29
+ "types": "./review/vite.d.mts",
30
+ "import": "./review/vite.mjs"
31
+ }
27
32
  },
28
33
  "sideEffects": [
29
34
  "./components/index.ts",
@@ -47,15 +52,22 @@
47
52
  "vitest.config.ts",
48
53
  "package-lock.json",
49
54
  "scripts",
50
- "skills"
55
+ "skills",
56
+ "review",
57
+ "examples/review/*/*.ts",
58
+ "examples/review/*/*.tsx",
59
+ "examples/review/*.css"
51
60
  ],
52
61
  "scripts": {
53
62
  "dev": "vite --host 127.0.0.1 --port 18309 --strictPort",
54
63
  "typecheck": "tsc --noEmit",
55
64
  "test": "vitest run",
56
65
  "build": "npm run typecheck && vite build && node scripts/check-build.mjs",
57
- "check": "npm run test && npm run build",
58
- "prepublishOnly": "npm run check"
66
+ "check": "npm run test && npm run build && npm run review:build && node scripts/check-review-package.mjs",
67
+ "prepublishOnly": "npm run check",
68
+ "review:http": "vite --config examples/review/http/vite.config.ts --host 127.0.0.1 --port 18340 --strictPort",
69
+ "review:platform": "vite --config examples/review/platform/vite.config.ts --host 127.0.0.1 --port 18341 --strictPort",
70
+ "review:build": "vite build --config examples/review/http/vite.config.ts && vite build --config examples/review/platform/vite.config.ts"
59
71
  },
60
72
  "peerDependencies": {
61
73
  "react": "^19.0.0",
@@ -0,0 +1,26 @@
1
+ import { useState } from 'react';
2
+ import { Checkbox, Stack, Text } from '../components';
3
+
4
+ const choices = [
5
+ { id: 'a', label: '示例项目 A', hint: '内存卡 · 14 项' },
6
+ { id: 'b', label: '跨团队拍摄与素材整理的较长示例项目名称', hint: '移动设备 · 128 项,包含待整理素材' },
7
+ { id: 'c', label: 'Archive_With_A_Very_Long_Unbroken_Synthetic_Project_Name' },
8
+ { id: 'd', label: '其他来源' },
9
+ ];
10
+ export function CheckboxRowGallery() {
11
+ const [selected, setSelected] = useState(['a']);
12
+ const [width, setWidth] = useState('240');
13
+ return <Stack gap={4}>
14
+ <Text as="h2" variant="heading">侧栏双行多选</Text>
15
+ <Text tone="muted">纯合成项目。整行、说明和留白均可选择;长名称自然换行。</Text>
16
+ <label>侧栏宽度 <select value={width} onChange={event => setWidth(event.target.value)}>{['160', '240', '320'].map(value => <option key={value} value={value}>{value}px</option>)}</select></label>
17
+ <div data-checkbox-rows style={{ width: Number(width), maxWidth: '100%' }}><Stack gap={2}>
18
+ <Checkbox layout="row" label="全部项目" hint="选择四个示例项目" checked={selected.length === choices.length} indeterminate={selected.length > 0 && selected.length < choices.length} onChange={event => setSelected(event.target.checked ? choices.map(item => item.id) : [])} />
19
+ {choices.map(item => <Checkbox key={item.id} layout="row" label={item.label} hint={item.hint} checked={selected.includes(item.id)} onChange={event => setSelected(current => event.target.checked ? [...current, item.id] : current.filter(id => id !== item.id))} />)}
20
+ <Checkbox layout="row" label="不可用项目" hint="当前无法读取" disabled />
21
+ <Checkbox layout="row" label="已选但不可用" hint="等待重新连接" disabled checked />
22
+ </Stack></div>
23
+ <Text variant="caption">已选 {selected.length} 项</Text>
24
+ <Checkbox label="默认表单布局" hint="保留旧表单说明的位置和命中范围。" />
25
+ </Stack>;
26
+ }
@@ -30,6 +30,6 @@ export function CommonGallery({ section }: { section?: string }) {
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 placement="floating" open={toast} onOpenChange={setToast} tone={toastTone} title={toastTone==='success'?'演示操作已完成':'演示操作失败'} description={toastTone==='success'?'5秒后关闭;悬停、聚焦或页面隐藏时暂停。':'错误通知保持显示,请重试或手动关闭。'}/></Stack></Surface></section>}
34
34
  </>;
35
35
  }
@@ -0,0 +1,19 @@
1
+ import { useState } from 'react';
2
+ import { Button, Inline, ListFilterBar, SegmentedControl, Select, Stack, Text } from '../components';
3
+
4
+ export function CompactFilterBarGallery() {
5
+ const [range, setRange] = useState('today');
6
+ const [feedback, setFeedback] = useState('normal');
7
+ return <Stack gap={4}>
8
+ <Text>筛选抽屉仅保留必要字段;进行中与失败仍然可见。</Text>
9
+ <Inline gap={2}>
10
+ {['normal', 'pending', 'error'].map(state => <Button key={state} variant="secondary" aria-pressed={feedback === state} onClick={() => setFeedback(state)}>{({ normal: '正常', pending: '加载', error: '失败' })[state]}</Button>)}
11
+ </Inline>
12
+ <ListFilterBar label="紧凑筛选示例" result="120 条合成记录" drawerDescription={null} drawerResult={null}
13
+ pending={feedback === 'pending'} error={feedback === 'error' ? '暂时无法更新合成结果,请重试。' : undefined}
14
+ primary={[{ id: 'platform', label: '平台', control: <Select label="平台" defaultValue="all"><option value="all">全部平台</option></Select> }]}
15
+ filters={[{ id: 'range', label: '时间快捷', control: <SegmentedControl fullWidth label="时间快捷" value={range} onValueChange={setRange} items={[{ value: 'today', label: '今天' }, { value: 'yesterday', label: '昨天' }, { value: 'week', label: '本周' }]} /> }]}
16
+ />
17
+ <SegmentedControl fullWidth label="等宽长文本与禁用示例" value="first" onValueChange={() => {}} items={[{ value: 'first', label: '较长的第一个范围' }, { value: 'second', label: '第二范围' }, { value: 'third', label: '不可选择', disabled: true }]} />
18
+ </Stack>;
19
+ }
@@ -0,0 +1,23 @@
1
+ import { useState } from 'react';
2
+ import { Button, CopyButton, EmptyState, ErrorSummary, Fieldset, MediaCard, MediaViewer, Stack, Steps, Text, TextField, ThemePicker } from '../components';
3
+
4
+ const description = '示例辅助说明:这是一段用于检查长文本自然换行的合成内容,来源、时间与读取状态应保持完整可读。';
5
+ export function ComponentTypographyGallery() {
6
+ const [open, setOpen] = useState(false);
7
+ const [next, setNext] = useState(false);
8
+ return <Stack gap={4}>
9
+ <Text as="h2" variant="heading">公共组件文字配对</Text>
10
+ <EmptyState title="没有匹配结果:请检查名称、日期与当前范围" description={description} action={<Button onClick={() => setOpen(true)}>打开文字检查</Button>} />
11
+ <MediaViewer open={open} onOpenChange={setOpen} trigger={<Button>打开媒体示例</Button>} title={next ? '下一项合成媒体' : '示例媒体与较长标题:检查字号、行高和字重'} description={description}
12
+ onNext={() => setNext(true)} onPrevious={() => setNext(false)}
13
+ actions={<label>显示方式 <select aria-label="显示方式"><option>适应窗口</option><option>原始尺寸</option></select></label>}>
14
+ <Text>合成画布:不加载真实媒体。</Text>
15
+ </MediaViewer>
16
+ <MediaCard title="读取状态" kind="photo" state="loading" onOpen={() => setOpen(true)} />
17
+ <CopyButton value="synthetic-example" label="模拟复制" copy={async () => {}} />
18
+ <Steps current={0} items={[{ id:'one',label:'检查说明',description }]} />
19
+ <Fieldset legend="分组提示" hint={description}><TextField id="typography-example" label="示例字段" /></Fieldset>
20
+ <ErrorSummary title="请检查以下字段的完整内容" errors={[{fieldId:'typography-example',message:'定位示例字段'}]} />
21
+ <ThemePicker value="rhine" onValueChange={() => {}} options={[{value:'rhine',label:'仅展示文字状态',previewSrc:'data:image/png;base64,invalid'}]} />
22
+ </Stack>;
23
+ }
@@ -0,0 +1,21 @@
1
+ import { useState } from 'react';
2
+ import { Button, DateRangePicker, Inline, RemoteMultiSelect, Stack, Surface, Text, type Choice, type DateRange } from '../components';
3
+
4
+ const departments: Choice[] = Array.from({ length: 100 }, (_, i) => ({ value: `synthetic-${i}`, label: `合成部门 ${i + 1}${i % 8 === 0 ? ' · 跨团队协作与长期计划' : ''}` }));
5
+
6
+ export function DenseFilterControlsGallery() {
7
+ const [selected, setSelected] = useState(departments.slice(0, 90));
8
+ const [query, setQuery] = useState('');
9
+ const [date, setDate] = useState<DateRange>({ start: '2026-10-01', end: '2026-10-24' });
10
+ const [error, setError] = useState<string | null>(null);
11
+ // Fixture filtering is local; production hosts keep server-side query ownership.
12
+ const options = departments.filter(item => item.label.includes(query));
13
+ return <Surface><Stack gap={4}>
14
+ <Text as="h2" variant="heading">密集筛选控件</Text>
15
+ <Text tone="muted">100 个合成部门、90 个已选项;可搜索、展开移除或清空。日期保留原生键盘和日历。</Text>
16
+ <Inline gap={2}><Button variant="secondary" onClick={() => setSelected(departments)}>全选部门</Button><Button variant="quiet" onClick={() => setSelected([])}>清空部门</Button></Inline>
17
+ <RemoteMultiSelect label="部门" query={query} onQueryChange={setQuery} value={selected} onValueChange={setSelected} options={options} maxVisibleTags={2} placeholder="搜索部门" />
18
+ <DateRangePicker compact label="日期区间" value={date} onValueChange={setDate} min="2026-01-01" max="2026-12-31" clearable={false} required onValidationChange={setError} />
19
+ <Text variant="caption" tone="muted">{error || `${date.start} 至 ${date.end}`}</Text>
20
+ </Stack></Surface>;
21
+ }
@@ -1,15 +1,16 @@
1
1
  import { useEffect, useRef, useState, type ReactNode } from 'react';
2
2
  import { Breadcrumbs, Button, Logo, SegmentedControl, Select, SideNav } from '../components';
3
3
  import { ThemeProvider, themes, useTheme, type ThemeName } from '../components/theme';
4
+ import { componentCount, packageVersion } from './packageInfo';
4
5
  import './shell.css';
5
6
  import './theme-arknights.css';
6
7
  import './theme-neo-brutalism.css';
7
8
 
8
9
  export const galleryGroups = [
9
- { label: '开始', items: [['top', '设计总览'], ['subscriptions', '订阅管理示例']] },
10
+ { label: '开始', items: [['top', '设计总览'], ['subscriptions', '订阅管理示例'], ['compact-filters', '紧凑与大量筛选'], ['checkbox-rows', '侧栏双行多选'], ['component-typography', '组件文字配对']] },
10
11
  { label: '应用能力', items: [['app-shell', '背景与多侧栏'], ['advanced-forms', '复杂表单'], ['advanced-data', '受控数据表'], ['media', '媒体卡片与查看器'], ['monitoring', '健康详情与图表']] },
11
12
  { label: '设计语言', items: [['foundation', '视觉基础'], ['backgrounds', '场景背景'], ['spacing', '间距与组合'], ['typography', '字体与文字'], ['brand', '品牌标志']] },
12
- { label: '通用组件', items: [['actions', '按钮与输入'], ['selection', '选择与切换'], ['feedback', '状态与加载'], ['disclosure', '展开与组合'], ['data', '数据与分页'], ['forms', '表单与选择'], ['utilities', '操作与布局'], ['overlays', '浮层与反馈'], ['overlay-bounds', '浮层缩放与边界'], ['settings-dialog', '设置导航弹窗'], ['theme-picker', '主题预览与选择'], ['navigation', '导航与步骤'], ['form-workflow', '表单流程']] },
13
+ { label: '通用组件', items: [['actions', '按钮与输入'], ['selection', '选择与切换'], ['summary-tabs', '带摘要的页签'], ['feedback', '状态与加载'], ['disclosure', '展开与组合'], ['data', '数据与分页'], ['forms', '表单与选择'], ['utilities', '操作与布局'], ['overlays', '浮层与反馈'], ['overlay-bounds', '浮层缩放与边界'], ['settings-dialog', '设置导航弹窗'], ['theme-picker', '主题预览与选择'], ['navigation', '导航与步骤'], ['form-workflow', '表单流程']] },
13
14
  { label: '交互与动效', items: [['patterns', '场景与交互指南'], ['compact-account', '紧凑账号与凭证'], ['list-filter-bar', '列表筛选栏'], ['date-range-filter', '日期筛选组合'], ['motion', '动效实验室'], ['startup', '启动动画'], ['orbit', '轨道星空'], ['neutron-star', '中子星']] },
14
15
  ] as const;
15
16
  export type GallerySection = typeof galleryGroups[number]['items'][number][0];
@@ -94,7 +95,7 @@ function GalleryContent({ motion, onMotionChange, children }: GalleryShellProps)
94
95
  <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=>{
95
96
  setMenuOpen(false); if(section===item.id) main.current?.focus({preventScroll:true});
96
97
  }}/>
97
- <div className="ds-sidebar-foot"><span>COMPONENT LIBRARY</span><span>66 个组件 · 0.8.0</span><span className="ak-readout">{readout}</span></div>
98
+ <div className="ds-sidebar-foot"><span>COMPONENT LIBRARY</span><span>{componentCount} 个组件 · 源码包版本 {packageVersion}</span><span>发布范围见版本记录</span><span className="ak-readout">{readout}</span></div>
98
99
  </aside>
99
100
  <main className="ds-workspace" id="gallery-content" ref={main} tabIndex={-1} aria-label={label}>
100
101
  <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 => {
@@ -12,7 +12,7 @@ export function OrbitGallery() {
12
12
  <div className="ds-section-title"><span>ORBIT</span><h2>轨道星空 · 品牌艺术背景</h2></div>
13
13
  <p className="ds-caption">莱茵的折纸鸟、战术的悬浮源石晶簇、橙趣的草莓甜甜圈。每个主题都有自己的物件,也可以切换到原始深色星空。</p>
14
14
  <Inline gap={3}>
15
- <Badge>0.7.0</Badge>
15
+ <Badge>主题艺术自 0.8.0 提供</Badge>
16
16
  <SegmentedControl label="画面风格" value={appearance} onValueChange={value => setAppearance(value as 'theme' | 'original')} items={[{ value: 'theme', label: '跟随主题' }, { value: 'original', label: '原始深色' }]} />
17
17
  <Button variant="secondary" onClick={() => setReplay(value => value + 1)}>重播轨道</Button>
18
18
  <Switch label="减少轨道动态" checked={reduced} onCheckedChange={setReduced} />
@@ -1,10 +1,11 @@
1
1
  import { useState } from 'react';
2
- import { Button, DataTable, Grid, Inline, Notice, Select, Stack, Surface, Text, type Spacing } from '../components';
2
+ import { Button, DataTable, Grid, Inline, Notice, SegmentedControl, Select, Stack, Surface, Text, type Spacing } from '../components';
3
3
  import './spacing.css';
4
4
 
5
5
  const scale: Spacing[] = [1, 2, 3, 4, 6, 8, 12];
6
6
  export function SpacingGallery() {
7
7
  const [state, setState] = useState('normal');
8
+ const [metric, setMetric] = useState('volume');
8
9
  const [selected, setSelected] = useState<string[]>(['research']);
9
10
  return <section id="spacing">
10
11
  <div className="ds-section-title"><span>SPACE</span><h2>间距与组合 · 让关系可见</h2></div>
@@ -12,6 +13,13 @@ export function SpacingGallery() {
12
13
  <Text tone="muted">同组紧、组间松。间距使用已有 token;gap 的数字是后缀,例如 gap=4 对应16px。</Text>
13
14
  <Surface><Stack gap={4}>{scale.map(space => <div className="ds-spacing-scale" key={space}><Text variant="code">space-{space}</Text><span className="ds-spacing-bar" style={{width:`var(--rhine-space-${space})`}} aria-hidden="true"/><Text numeric variant="caption">{space * 4}px</Text></div>)}</Stack></Surface>
14
15
  <Select label="卡片内容状态" value={state} onChange={event=>setState(event.target.value)}><option value="normal">默认</option><option value="loading">检查中</option><option value="error">读取失败</option><option value="long">长文案</option></Select>
16
+ <Surface><Stack gap={4}>
17
+ <Inline gap={4} style={{justifyContent:'space-between'}}>
18
+ <Stack gap={2}><Text as="h3" variant="heading">{state==='long'?'长标题示例:按业务维度查看统计分布':'统计分布'}</Text></Stack>
19
+ <SegmentedControl size="compact" label="统计维度" items={[{value:'volume',label:'用量'},{value:'count',label:'次数',disabled:state==='loading'}]} value={metric} onValueChange={setMetric}/>
20
+ </Inline>
21
+ {state==='error'?<Notice tone="error" title="统计暂不可用">保留所选维度,可重新读取。</Notice>:<Text tone="muted">{state==='loading'?'正在读取统计…':`当前维度:${metric==='volume'?'用量':'次数'}。紧凑视觉 32px,点击区域至少 44px;标题与操作居中对齐,窄窗自然换行。`}</Text>}
22
+ </Stack></Surface>
15
23
  <Grid gap={6} minWidth={280}>
16
24
  <Stack gap={3}>
17
25
  <Text variant="caption" tone="muted">24px 内边距 · 8px 标题/说明 · 16px 内容/操作</Text>