@linhey/rhine-design 0.10.0 → 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 (62) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/CONTRIBUTING.md +3 -1
  3. package/README.md +17 -14
  4. package/components/COMMON_COMPONENTS.md +5 -3
  5. package/components/README.md +13 -5
  6. package/components/SPACING.md +9 -0
  7. package/components/Toast.tsx +10 -4
  8. package/components/common.css +2 -0
  9. package/components/components.tsx +13 -7
  10. package/components/styles.css +12 -0
  11. package/docs/DESIGN_SYSTEM.md +10 -1
  12. package/docs/DESIGN_SYSTEM_AUDIT.md +4 -4
  13. package/docs/DISCLOSURE_VERIFICATION.md +49 -0
  14. package/docs/INTERACTION_PATTERNS.md +9 -1
  15. package/docs/REVIEW_TOOL.md +128 -0
  16. package/docs/REVIEW_VERIFICATION.md +56 -0
  17. package/docs/SUMMARY_TABS.md +77 -0
  18. package/docs/THEMING.md +1 -1
  19. package/docs/assets/disclosure/arknights-400.png +0 -0
  20. package/docs/assets/disclosure/neo-brutalism-400.png +0 -0
  21. package/docs/assets/disclosure/rhine-400.png +0 -0
  22. package/docs/assets/disclosure/rhine-native200.png +0 -0
  23. package/examples/review/http/App.tsx +46 -0
  24. package/examples/review/http/catalog.tsx +44 -0
  25. package/examples/review/http/details.ts +1 -0
  26. package/examples/review/http/vite.config.ts +8 -0
  27. package/examples/review/platform/Workspace.tsx +34 -0
  28. package/examples/review/platform/catalog.tsx +46 -0
  29. package/examples/review/platform/vite.config.ts +8 -0
  30. package/examples/review/product.css +3 -0
  31. package/package.json +17 -5
  32. package/preview/CommonGallery.tsx +1 -1
  33. package/preview/GalleryShell.tsx +3 -2
  34. package/preview/OrbitGallery.tsx +1 -1
  35. package/preview/SpacingGallery.tsx +9 -1
  36. package/preview/SummaryTabsGallery.tsx +37 -0
  37. package/preview/main.tsx +7 -3
  38. package/preview/packageInfo.ts +5 -0
  39. package/review/canvas.tsx +107 -0
  40. package/review/dev-client.mjs +17 -0
  41. package/review/feedback.ts +17 -0
  42. package/review/index.ts +2 -0
  43. package/review/isolation.ts +64 -0
  44. package/review/lifecycle.ts +59 -0
  45. package/review/protocol.ts +7 -0
  46. package/review/review.css +41 -0
  47. package/review/shell.tsx +165 -0
  48. package/review/state.ts +41 -0
  49. package/review/types.ts +59 -0
  50. package/review/vite.d.mts +10 -0
  51. package/review/vite.mjs +107 -0
  52. package/scripts/check-review-package.mjs +11 -0
  53. package/scripts/check-review-recovery.js +47 -0
  54. package/scripts/check-review.js +153 -0
  55. package/skills/rhine-design/references/API_MAP.md +4 -0
  56. package/skills/rhine-design/references/TASK_MAP.md +2 -0
  57. package/tests/CommonComponents.test.tsx +8 -0
  58. package/tests/DesignSystem.test.tsx +32 -0
  59. package/tests/Review.test.tsx +117 -0
  60. package/tests/ReviewBuild.test.tsx +59 -0
  61. package/tests/ReviewShell.test.tsx +63 -0
  62. package/tsconfig.json +5 -2
@@ -0,0 +1,128 @@
1
+ # 产品设计稿评审工具(自 0.11.0)
2
+
3
+ 对应 [#40](https://github.com/linhay/rhine-design/issues/40)。`@linhey/rhine-design/review` 与 `/review/vite` 自 **0.11.0** 提供;npm 0.10.0 不包含它们。安装时固定 `@linhey/rhine-design@0.11.0` 并提交 lockfile,确认 registry 已提供该版本。历史同为 0.10.0 字段的本地候选快照不等同 npm 正式制品。
4
+
5
+ 工具统一维护目录、搜索、评审控件、深链接、独立 iframe、模拟边界和静态构建。产品维护页面、路由、权限、状态维度、业务模拟、说明和验收记录。普通 React 设计稿、真实 App + HTTP mock、真实组件 + 平台 client 均可接入,不要求把平台接口转换成 HTTP。
6
+
7
+ ## 运行两个合成例子
8
+
9
+ 在源码仓使用 Node 24 安装依赖。两个 dev 命令分别在独立终端运行;构建/静态服务是另一种运行方式:
10
+
11
+ ```sh
12
+ npm run review:http # http://127.0.0.1:18340/review-http/
13
+ npm run review:platform # http://127.0.0.1:18341/review-platform/
14
+ npm run review:build # dist/review-http 与 dist/review-platform
15
+ python3 -m http.server 18342 --bind 127.0.0.1 --directory dist
16
+ ```
17
+
18
+ 示例开发服务保留各自 `/review-http/`、`/review-platform/` 路径;构建产物使用相对资源路径,可放在任意嵌套目录(例如项目型 GitHub Pages)。构建后访问 `/review-http/`、`/review-platform/`。这是本地进程地址,不是已部署的持久在线服务。关闭进程后链接失效。也可以使用 `vite preview --config examples/review/http/vite.config.ts --host 127.0.0.1` 预览对应制品;以启动输出为准。
19
+
20
+ HTTP 示例有正常、加载、空、错误、密集长文及只读角色;支持搜索、弹窗保存与延迟模块。平台示例注入内存 client,覆盖独立连接/用量维度、慢初始化、渲染异常、清理错误/挂起恢复和 Canvas 动效。示例只展示工具契约,不代表桌面原生或 iOS 验收。
21
+
22
+ 开发服务没有 HMR/WebSocket:Vite 6/8 的 CSS 导入由官方样式客户端处理,修改页面后手动刷新;修改配置或工具插件后重启服务。不要用放开业务代理来换取热更新。
23
+
24
+ ## 最小消费配置
25
+
26
+ 在消费项目安装固定的 0.11.0 正式制品,并把 `vite`、`@vitejs/plugin-react` 放在开发依赖中。只在独立预览配置中导入 Node/Vite 入口,正式 App 不导入它:
27
+
28
+ ```ts
29
+ // design/vite.config.ts
30
+ import { defineConfig } from 'vite';
31
+ import react from '@vitejs/plugin-react';
32
+ import { reviewPlugin } from '@linhey/rhine-design/review/vite';
33
+
34
+ export default defineConfig({
35
+ root: import.meta.dirname,
36
+ base: '/product-review/',
37
+ plugins: [react(), reviewPlugin({ catalog: './catalog.tsx', routerBase: '/app/' })],
38
+ build: { outDir: '../dist-review', emptyOutDir: true },
39
+ });
40
+ ```
41
+
42
+ 运行 `vite --config design/vite.config.ts`、`vite build --config design/vite.config.ts`、`vite preview --config design/vite.config.ts`。插件生成 `index.html` 和 `canvas.html`,消费方不复制评审外壳、URL 状态代码或工具栏。只复制/编写自身目录与页面;两个合成例子分别在 `examples/review/http`、`examples/review/platform`,共同的示例样式在 `examples/review/product.css`。
43
+
44
+ `base` 是静态资源部署路径;可用相对 `./` 或固定子路径。`routerBase` 仅覆盖产品使用的 `import.meta.env.BASE_URL`,不会改写资源路径,也不替产品建立路由。产品可在 setup 中设置 frame 内 history 并触发自己的路由更新;外层评审 hash 与产品路由各自独立。静态服务器提供带尾斜杠的目录 URL,不必为每个场景配置服务端路由。
45
+
46
+ ## 场景与生命周期
47
+
48
+ ```tsx
49
+ import { defineReview } from '@linhey/rhine-design/review';
50
+
51
+ export default defineReview({
52
+ title: '产品评审',
53
+ widths: [400, 520, 880, 1280],
54
+ scenes: [{
55
+ id: 'workspace', title: '工作区', group: '产品页面',
56
+ themeRoot: 'host', // 真实 App 已有 ThemeProvider 时必须声明
57
+ source: 'src/Workspace.tsx', // 产品仓相对路径
58
+ dimensions: [{ id: 'role', label: '角色', options: [
59
+ { id: 'editor', label: '编辑者' }, { id: 'reader', label: '只读成员' },
60
+ ] }],
61
+ async setup(context) {
62
+ const client = createSyntheticClient(context.signal); // 产品实现
63
+ context.onDispose(() => client.dispose());
64
+ // 必须在模拟适配与清理回调安装后,再求值真实 App 模块。
65
+ const { App } = await import('../src/App');
66
+ context.signal.throwIfAborted();
67
+ return {
68
+ render: environment => <App client={client} theme={environment.theme}
69
+ reducedMotion={environment.reducedMotion} paused={environment.paused} />,
70
+ dispose() {}, // 已登记的清理由 onDispose 承担,避免重复释放
71
+ };
72
+ },
73
+ }],
74
+ });
75
+ ```
76
+
77
+ 这是接入结构示例;`createSyntheticClient` 和 `App` 属性由产品定义,完整可运行代码见两个合成例子。
78
+
79
+ - 目录模块必须无业务副作用。App、真实平台模块、首次 API 请求放在 `setup` 安装适配之后的动态 import 内。静态 import App 会先于场景 mock 求值,属于错误接入。
80
+ - `setup(context)` 可异步。`context` 提供只读维度、`signal`、`reason`、`onDispose`、`mockHttp` 和 `report`。初始化前期获得的监听器、计时器、连接、动画和临时路由状态立即登记 `onDispose`,不能等 Promise 返回后才登记。
81
+ - `render(environment)` 返回普通 React 树;组件类型和 key 要保持稳定。主题、宽度、CSS zoom、动态偏好更新只重渲染同一实例,不重新 setup;用户自然将焦点移到外层控件时,工具不会抢回焦点。
82
+ - 场景或任一维度变化:先卸载 React 树、abort 旧请求,依次执行清理与 session.dispose,再 setup 新场景。快速切换只接纳最新选择;迟到 session 不挂载并执行 dispose。
83
+ - 显式重置在上述过程中额外调用旧 session.reset(若提供),随后重新 setup、重置内存存储与模拟状态。`reset` 是通知,不是任意业务交互的历史恢复。
84
+ - setup/渲染/异步操作失败在评审反馈区显示;不要用失败 mock 返回业务成功。清理失败保留原场景归属并继续其余清理。界面就绪与错误记录独立:新场景就绪、主题更新和重新载入都不会抹掉此前错误,需用户“确认并清除反馈”。显示最近 20 条反馈;更早错误仍计入待确认数量。
85
+ - 首次连接或场景初始化/清理超过 10 秒时显示恢复入口;超时只说明尚未确认完成,可继续等待。显式“重新载入画布”销毁旧 iframe 上下文,清空未保存草稿并保留 URL 评审条件;不会绕过挂起的清理,在旧上下文继续 setup。就绪确认绑定选择序号和 React 实际提交,旧画布消息及迟到确认不更新新状态。
86
+ - 产品清理应尽快完成且幂等;浏览器关闭/整页卸载无法保证异步清理完成,持久远端资源不属于本工具的生命周期保障。重新载入也不是远端资源清理保证。
87
+ - 产品未遵守 AbortSignal 的任意后台副作用无法被框架自动撤销;框架拒绝迟到挂载和 fetch 结果,不等于能拦截所有用户代码。目录不是第二套路由/权限系统,业务列表仍经产品模拟服务按原筛选契约查询。
88
+
89
+ ## HTTP、主题与动效
90
+
91
+ `context.mockHttp(handler)` 在当前场景替换 fetch。handler 接收标准 `Request`,保留 method、body、调用方 signal,并合并场景取消,credentials 固定 omit;返回 `Response` 或 Promise。未匹配返回 undefined 会 reject 并显示失败,永不回退真实 fetch。产品负责 URL/method 白名单和合成增删改;不支持 XHR 的真实请求回退。旧 mock 若采用 `(input, options) => Response`,需薄适配传入 `request.url`、method、headers、signal,以及非 GET/HEAD 时的 `await request.text()`,或改为直接消费 Request;不能用类型断言把旧函数直接接上。
92
+
93
+ 默认 `themeRoot: 'runtime'` 为普通组件提供单一 Rhine ThemeProvider。真实 App 已有 Provider 时使用 `themeRoot: 'host'`,通过 render environment 更新自己的受控 Provider,并传播减少动态属性。现有宿主若只支持内部持久化主题,需要先暴露受控主题或桥接入口;`themeRoot: host` 不自动提供该能力,示意中的 App 属性必须按真实产品接口适配。不要用写 localStorage、重建 App 或再嵌套 Provider 来冒充原位切换。主题和尺寸不进入 iframe key,Portal 与表单保留同一 DOM 实例。
94
+
95
+ 默认动态选项只有 system/reduced。只有完整适配了场景中全部动画的产品才声明 `motionControls: ['full', 'paused']`,对应消费 `environment.reducedMotion`、`environment.paused`。Canvas/WebGL 必须暂停帧循环,清理时取消 rAF/监听器并释放 GPU 资源;只禁 CSS 动画不够。
96
+
97
+ 平台例子证明自有 Canvas 适配,不证明 OrbitBackdrop/NeutronStarBackdrop 全局受控暂停。它们仍遵循各自契约并尊重系统减少动态;未完成适配的场景不要声明 full/paused。工具不改变这两个现有组件的公开 API。
98
+
99
+ ## 分享、隔离与正式产物
100
+
101
+ 主工具栏保留主题、宽度、场景维度与重置;CSS 缩放、动态偏好和手动重新载入收在“更多设置”。画布尺寸显示 iframe 可用区域(不含边框)的实际宽高,以及按 CSS zoom 折算的内容宽度;例如 400px 外宽有 2px 边框时,可用宽度为 398px,200% 下约 199 CSS px。
102
+
103
+ 外层 URL hash 只允许稳定 scene ID、声明的维度值、三主题、宽度预设/fit、1/2 倍 CSS zoom、动态选项。刷新、前进/后退恢复这些评审条件;未知值稳定回退,未知字段和任意表单内容不进入 URL。切换后保留的草稿不等于可序列化业务会话。CSS zoom 不代表浏览器原生缩放。
104
+
105
+ 预览配置禁止读取项目 dotenv/public 目录及业务代理;环境前缀限定为显式公开的 `RHINE_REVIEW_PUBLIC_`。不要继承带秘密 define 的生产配置。iframe 先安装内存 localStorage/sessionStorage、空 Cookie 视图和请求/平台防护,再加载目录;XHR、WebSocket、Worker、IndexedDB、CacheStorage、ServiceWorker、Beacon、业务剪贴板等均拒绝或报告未实现。需要这些能力的产品必须提供自己的内存适配。
106
+
107
+ CSP 使用 `connect-src 'none'`,脚本/样式/媒体仅本地资源及允许的 data/blob;真实网络请求没有兜底。表单使用 `allow-forms` 并由 React onSubmit preventDefault,CSP 仍禁止原生提交。普通链接仅允许当前文档 hash;SPA 路由需由产品 preventDefault 并操作 history。下载、外链、整页导航、window.open 被阻止并报告。外层“复制链接”是用户主动执行的评审工具操作,不属于业务剪贴板。
108
+
109
+ **这不是执行任意不可信代码的安全沙箱。** `allow-scripts + allow-same-origin` 用于保留正常 React/路由/浮层,浏览器会提示该组合不能约束恶意同源脚本。应在专用静态站点/源托管,只装合成数据,不接真实会话;产品主动绕过防护、直接 location 导航、服务端同源 Cookie 等不由 JS 替换 API 全面隔绝。不得将产品秘密或真实后端环境注入预览。
110
+
111
+ 正式构建可启用独立排除检查:
112
+
113
+ ```ts
114
+ import { reviewProductionGuard } from '@linhey/rhine-design/review/vite';
115
+ // 加入正式 App 的 Vite plugins;这些是相对正式 root 的精确文件/目录。
116
+ reviewProductionGuard({ forbidden: ['./design', './src/preview-fixtures'] });
117
+ ```
118
+
119
+ 默认只拒绝官方 review runtime;不会把所有名为 design/fixtures 的正常生产目录误判。消费方必须登记自己的预览目录。检查基于实际输出 chunk 的模块图,随包附带的 tests/examples 不会因主入口引用自动进入 App。生产与预览使用不同 outDir;静态托管只上传对应 dist-review,不上传源码目录、环境文件或本地日志。
120
+
121
+ ## 静态托管与迁移
122
+
123
+ 1. 保留现有可用预览,先固定并安装通过验证的官方候选/发布制品。
124
+ 2. 将产品场景与多维状态映射到 catalog;先接 HTTP/平台模拟,再动态载入页面。保留 router base、Provider 所有权与已有视觉。
125
+ 3. 通过主题切换保状态、重置/清理、深链接、键盘、状态矩阵、出站检查与正式产物排除后,删除本地评审外壳/工具栏/URL 同步实现。
126
+ 4. 用独立 CI 构建,在静态服务器或 GitHub Pages 上传对应 outDir;设置与部署路径一致的 base。用实际部署 URL 重验资源、画布与 hash 刷新,才能称为“已上线”。私有设计稿由消费方管理访问控制;首版没有托管 SaaS 或权限中心。
127
+
128
+ 兼容与实测范围见 [验收记录](REVIEW_VERIFICATION.md)。SSR、其他构建器、实时多人协作、自动截图差异、原生平台验证均不属于首版。最终关闭 #40 还需要固定版本发布及真实消费迁移证据,不能用本地工具通过代替。
@@ -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.10.0",
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",
@@ -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
  }
@@ -1,6 +1,7 @@
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';
@@ -9,7 +10,7 @@ export const galleryGroups = [
9
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} />