@linhey/rhine-design 0.6.0 → 0.8.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 (98) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +19 -8
  3. package/components/Actions.tsx +18 -1
  4. package/components/AppShell.tsx +46 -17
  5. package/components/Media.tsx +7 -7
  6. package/components/NeutronStarBackdrop.tsx +43 -0
  7. package/components/OrbitBackdrop.tsx +4 -2
  8. package/components/Overlays.tsx +37 -8
  9. package/components/README.md +27 -3
  10. package/components/StatusTimeline.tsx +72 -6
  11. package/components/StatusTimelineList.tsx +38 -0
  12. package/components/ThemePicker.tsx +49 -0
  13. package/components/assets/orbit-bird.svg +578 -0
  14. package/components/assets/orbit-crystal.svg +11 -0
  15. package/components/assets/orbit-donut.svg +16 -0
  16. package/components/assets/theme-previews/arknights.png +0 -0
  17. package/components/assets/theme-previews/manifest.json +24 -0
  18. package/components/assets/theme-previews/neo-brutalism.png +0 -0
  19. package/components/assets/theme-previews/rhine.png +0 -0
  20. package/components/common.css +3 -3
  21. package/components/components.tsx +3 -3
  22. package/components/dialog-settings.css +9 -0
  23. package/components/extensions.css +8 -32
  24. package/components/index.ts +11 -5
  25. package/components/media-parity.css +55 -2
  26. package/components/neutron.css +53 -0
  27. package/components/neutronRenderer.ts +327 -0
  28. package/components/orbit.css +79 -1
  29. package/components/orbitBirdFlight.ts +34 -0
  30. package/components/orbitBirdGeometry.ts +158 -0
  31. package/components/orbitDonut.ts +134 -0
  32. package/components/orbitDonutThree.ts +208 -0
  33. package/components/orbitObjectMaterials.ts +76 -0
  34. package/components/orbitObjects.ts +71 -0
  35. package/components/orbitObjectsThree.ts +215 -0
  36. package/components/orbitProjection.ts +21 -0
  37. package/components/orbitRenderer.ts +163 -46
  38. package/components/styles.css +3 -1
  39. package/components/theme-neo-brutalism.css +3 -2
  40. package/components/theme-picker.css +17 -0
  41. package/components/theme.tsx +2 -8
  42. package/components/themeMetadata.ts +7 -0
  43. package/components/timeline.css +17 -6
  44. package/components/useFloatingScale.ts +96 -0
  45. package/docs/APPLICATION_COMPONENTS.md +52 -3
  46. package/docs/COMPACT_ACCOUNT_PATTERNS.md +70 -0
  47. package/docs/DATE_RANGE_FILTER.md +60 -0
  48. package/docs/DESIGN_SYSTEM.md +7 -0
  49. package/docs/FLOATING_OVERLAYS.md +15 -0
  50. package/docs/INTERACTION_PATTERNS.md +5 -1
  51. package/docs/NEUTRON_STAR_BACKDROP.md +122 -0
  52. package/docs/ORBIT_BACKDROP.md +38 -46
  53. package/docs/SETTINGS_DIALOG.md +21 -0
  54. package/docs/THEME_PICKER.md +26 -0
  55. package/docs/THEMING.md +2 -0
  56. package/docs/assets/compact-account-neo.png +0 -0
  57. package/docs/assets/neutron-star-original.png +0 -0
  58. package/docs/assets/neutron-star-theme.png +0 -0
  59. package/docs/assets/orbit-bird.png +0 -0
  60. package/docs/assets/orbit-crystal.png +0 -0
  61. package/docs/assets/orbit-strawberry.png +0 -0
  62. package/package.json +8 -3
  63. package/preview/CompactAccountGallery.tsx +178 -0
  64. package/preview/DateRangeFilterGallery.tsx +109 -0
  65. package/preview/ExtensionsGallery.tsx +27 -18
  66. package/preview/GalleryShell.tsx +3 -3
  67. package/preview/MonitoringGallery.tsx +2 -0
  68. package/preview/NeutronStarGallery.tsx +56 -0
  69. package/preview/OrbitGallery.tsx +35 -32
  70. package/preview/OverlayBoundsGallery.tsx +49 -0
  71. package/preview/SettingsDialogGallery.tsx +33 -0
  72. package/preview/ThemePickerGallery.tsx +24 -0
  73. package/preview/ThemeSample.tsx +14 -0
  74. package/preview/TimelineListGallery.tsx +36 -0
  75. package/preview/extensions.css +9 -0
  76. package/preview/main.tsx +16 -3
  77. package/preview/preview.css +8 -0
  78. package/preview/theme-sample.html +1 -0
  79. package/scripts/check-overlay-bounds.js +76 -0
  80. package/scripts/check-settings-theme.js +88 -0
  81. package/skills/rhine-design/SKILL.md +3 -2
  82. package/skills/rhine-design/references/API_MAP.md +13 -2
  83. package/skills/rhine-design/references/TASK_MAP.md +8 -2
  84. package/skills/rhine-design/references/integration.md +1 -1
  85. package/tests/AppShellControls.test.tsx +82 -0
  86. package/tests/ButtonLink.test.tsx +94 -0
  87. package/tests/CompactAccountGallery.test.tsx +116 -0
  88. package/tests/DateRangeFilterGallery.test.tsx +85 -0
  89. package/tests/NeutronStarBackdrop.test.tsx +288 -0
  90. package/tests/NeutronStarGallery.test.tsx +62 -0
  91. package/tests/NeutronStarImport.test.tsx +15 -0
  92. package/tests/OrbitBackdrop.test.tsx +246 -9
  93. package/tests/OrbitDonutThree.test.tsx +91 -0
  94. package/tests/OrbitObjectsThree.test.tsx +164 -0
  95. package/tests/SettingsDialog.test.tsx +66 -0
  96. package/tests/StatusTimelineList.test.tsx +63 -0
  97. package/tests/ThemePicker.test.tsx +30 -0
  98. package/tests/TimelineLegend.test.tsx +60 -0
@@ -1,81 +1,73 @@
1
- # OrbitBackdrop · 轨道背景(0.6.0)
1
+ # OrbitBackdrop · 主题艺术背景
2
2
 
3
- `OrbitBackdrop` 将授权移交的轨道动画作为通用装饰组件维护:固定深色星空衬底、420 个环节点、185 条曲线、80 个深度桶,以及发射箭头、光晕和透视关系。它适合登录、欢迎等入口区域;不包含产品文案、账号逻辑或网络请求。需求与移交记录见 [Issue #8](https://github.com/linhay/rhine-design/issues/8)。
3
+ `OrbitBackdrop` 是带暂停控制的装饰组件。0.8.0 根据主题展示折纸鸟、源石晶簇或草莓甜甜圈,也可通过 `appearance="original"` 保留初版深色轨道。它适合欢迎、登录和品牌展示区域,不表达业务进度,不包含账号逻辑或外部资源请求。移交来源见 [Issue #8](https://github.com/linhay/rhine-design/issues/8)。
4
4
 
5
- **版本边界:**此组件自 `@linhey/rhine-design@0.6.0` 提供,0.5.0 及以前不包含它。采用前核对安装版本与 `components/index.ts`,固定精确版本并提交 lockfile。组件源于 [OrbitBackdrop.tsx](../components/OrbitBackdrop.tsx),绘制实现在 [orbitRenderer.ts](../components/orbitRenderer.ts);后者是内部实现,不作为公共 API 导入。
5
+ **版本边界:**基础组件自 `@linhey/rhine-design@0.6.0` 提供;`appearance` 与三套主题物品需要 0.8.0。0.6.0 默认固定深色轨道,升级后默认跟随主题;需要保持旧视觉时显式使用 `appearance="original"`。固定精确版本并提交 lockfile。
6
6
 
7
7
  ## 使用与契约
8
8
 
9
9
  ```tsx
10
10
  import { Logo, OrbitBackdrop, Text } from '@linhey/rhine-design';
11
11
 
12
- // 此示例要求 0.6.0 或以上,并放在现有主题根内。
13
12
  export function WelcomeArtwork() {
14
- return (
15
- <OrbitBackdrop className="welcome-artwork" style={{ minHeight: 360 }}>
16
- <Logo decorative />
17
- <Text as="h1" variant="title">开始新的工作</Text>
18
- <Text>从这里进入你的工作空间。</Text>
19
- </OrbitBackdrop>
20
- );
13
+ return <OrbitBackdrop style={{ minHeight: 360 }}>
14
+ <Logo decorative />
15
+ <Text as="h1" variant="title">开始新的工作</Text>
16
+ <Text>从这里进入你的工作空间。</Text>
17
+ </OrbitBackdrop>;
21
18
  }
22
19
  ```
23
20
 
24
21
  | 输入 | 契约 |
25
22
  | --- | --- |
26
- | `HTMLAttributes<HTMLDivElement>` | 接收容器原生属性,如 `className`、`style`、`id`;宿主负责尺寸、位置和相邻内容布局 |
27
- | `reducedMotion?: boolean` | `true` 加强减少动态偏好;`false` 或省略均不能覆盖系统、祖先或环境的停帧条件 |
28
- | `children` | 放入 `.rhine-orbit-content` 内容层,可组合 Logo、标题、正文、表单、动作和页脚,也可用 Surface 分组;宿主 class 控制内容布局,避免遮挡主要艺术或暂停控件 |
23
+ | `HTMLAttributes<HTMLDivElement>` | 宿主负责尺寸、位置、相邻内容布局与可访问名称 |
24
+ | `appearance?: 'theme' \| 'original'` | 默认跟随主题;`original` 固定初版深色轨道,切换保留播放位置与暂停状态 |
25
+ | `reducedMotion?: boolean` | `true` 加强减少动态;`false` 不能覆盖系统或祖先偏好 |
26
+ | `children` | 普通 HTML 内容层,可组合表单、按钮、Logo 与 Surface;不得遮挡暂停控件 |
29
27
 
30
- 组件持有绘制配方、内部类名、样式与暂停状态。宿主不复制 Canvas 实现,不另写同用途动画,不通过覆盖内部样式重建其视觉。没有速度、节点数量、业务进度或主题配色参数;需要新能力时先在设计系统中补契约与展台。
28
+ 组件持有绘制算法、内部类名、主题配方和暂停状态。没有任意速度、粒子数、业务进度或调色板参数。宿主不复制绘制器、不嵌套 ThemeProvider,也不通过覆盖内部样式重建视觉。内部文件 `orbitRenderer.ts`、`orbitObjectsThree.ts`、`orbitObjectMaterials.ts`、`orbitDonutThree.ts` 不作为公共 API 导入。
31
29
 
32
- Canvas 绝对定位在内容层下方。内容层为直接置于星空上的文字提供浅色前景,普通 Text 与非 icon Logo 继承当前前景,muted Text 沿当前前景降低透明度;不修改宿主颜色 tokens,也不新建或嵌套 `ThemeProvider`。Surface、Notice 等有底色的组件继续使用自己的语义前景,表单与按钮保留宿主主题的前景 / 背景组合。默认内置 Rhine `Button`,在独立控件区域提供“暂停 / 继续”的明确操作。Canvas 为 `aria-hidden` 纯装饰,不接收键盘焦点,不提供进度或业务状态。有效信息仍由普通 HTML 内容表达;动画暂停、减少动态或 Canvas 不可用均不影响内容与业务操作。
30
+ ## 四套画面
33
31
 
34
- ## 动效生命周期
35
-
36
- 下列任一条件成立时停止逐帧调度,保留静态画面:
32
+ | 风格 | 物品与细节 |
33
+ | --- | --- |
34
+ | 莱茵 · 纸面 | 折纸鸟:578 个实际三角折面、暖白纤维纸纹、扇形褶皱与叠层双翼、立体细颈与三片尾羽、弧线飞行与振翅转弯、42 片随行纸屑与 120 颗微尘 |
35
+ | 战术终端 | 悬浮源石晶簇:五块不同姿态的倒角晶体、程序矿物纹理、琥珀裂纹、48 块环绕碎片、420 颗余烬与断续约束环 |
36
+ | 橙趣 · 粗描边 | 草莓甜甜圈:烘焙纹理、立体糖霜、280 根糖针、110 颗糖珠、100 根环绕糖粒、460 颗糖尘与奶油色背景 |
37
+ | `original` | 完整初版轨道:420 个环节点、185 条空间曲线、80 个深度桶、箭头、光晕与原始配色 |
37
38
 
38
- - 用户主动暂停;
39
- - 系统 `prefers-reduced-motion: reduce`,或 `reducedMotion` 为 `true`;
40
- - 祖先的 `data-rhine-motion="reduced"` 偏好生效(内层跟随系统不覆盖外层减少动态);
41
- - 当前窗口宽度不超过 760px;
42
- - `document.hidden`、组件离屏或可用绘制尺寸为零。
39
+ 折纸鸟以舒展的翼面与轻盈的飞行呼应莱茵纸面主题;源石晶簇采用坚硬、暗色的矿物质感。原始模式不随主题改变。
43
40
 
44
- “暂停 / 继续”使用随当前动作变化的按钮名称,不同时声明 `aria-pressed` 切换语义。“继续”只解除用户主动暂停,不越过其他停帧条件。恢复时沿冻结的动画时间继续,不累计隐藏期间的时间,也不追赶错过的帧。偏好或可见性改变时即时重新判断;窗口调整会以当前冻结时间重绘静态画面。设备像素比上限为 2,卸载清理 rAF、监听器和 observers,离开页面不会继续后台绘制。
41
+ 莱茵物品在飞行中连续变形成纸蝴蝶:原有 578 个三角折面保持连接,鸟颈收为蝶首、尾羽重塑为触须与腹尖、左右翼展开为四瓣蝶翼;折痕与纸纹随顶点移动,不更换模型或交叉淡化。42 秒有效播放周期为纸鸟 4 秒、折叠变形 14 秒、纸蝴蝶 6 秒、折回 14 秒、纸鸟 4 秒。变形依次收翼、松开折痕、展开为近乎平面的中间纸形,短暂停留后重新折出新形态;翼根、内侧折线与翼尖以独立时间窗依次松开、重新折起,相邻动作错开约 0.8–1 秒;纸面由内向外展平,外缘延后跟随,鸟颈与尾羽绕根部打开。中途暂停扑翼,让折纸动作清晰可见,完成后平滑恢复;蝶形阶段转为较快的扑翼。暂停和减少动态同时冻结形态、翼拍与飞行位置,恢复后接续当前折叠进度。Canvas2D 使用同一组对应顶点与变形函数;静态 SVG 保留纸鸟初始形态。
45
42
 
46
- Canvas 不可用时保留静态艺术 fallback 与 `children`。此降级不表示浏览器或页面业务失败,也不以加载提示掩盖缺失的装饰能力。
43
+ 主题物品按需加载 Three.js,环境反射与表面纹理均在本地生成,无模型下载、CDN 或外部纹理。种子固定,运动由唯一的有效播放时间决定;物品、振翅、纸屑、碎片和着色器共用宿主时钟。切换主题复用已创建的绘制器,未激活的画布隐藏并停止绘制。异步导入完成时复核当前主题及挂载状态。
47
44
 
48
- ## 主题与可读性
45
+ 甜甜圈保留已校准的原版相机与构图:yaw -0.72、tilt 0.22、roll 0.34,透视距离 3.6;中心位于宽度 62%、高度 57%,缩放为 `min(width × .66, height × .39)`。环半径 .84、管半径 .34、糖霜厚度 .022,约 22.4 秒有效播放时间转一圈,不额外摆动。折纸鸟从略高的角度展示宽翼与折面,沿 14 秒弧线往返、起伏并侧倾转弯,双翼以约 2.4 秒周期振动,振幅随飞行阶段变化;纸屑沿稍早的飞行位置散开。Three.js 与 Canvas2D 共用姿态函数,全部运动使用宿主有效播放时间,暂停或减少动态时保持同一姿态。晶簇按竖向轮廓取景。两者都为底部操作区留空间。
49
46
 
50
- 轨道采用同一固定深色美术配方;切换 Rhine、Arknights 或 Neo Brutalism 时保留动画构图。艺术文字采用浅色前景,控件、语义容器、独立操作区与外框继续使用完整宿主主题 tokens;不能只改控件的前景而保留白色背景。直接叠加的提示和透明 quiet 操作继承当前内容前景;quiet 的有底色 hover / disabled 状态保持宿主配色,键盘焦点使用当前文字颜色的内描边。可按产品分组需要使用 Surface,不以它作为控件可读性的前提。宿主应提供足够的布局空间,避免正文与主要曲线重叠,也不能遮住暂停入口。
47
+ ## 动效与降级
51
48
 
52
- 轨道动画不替代 `Progress`、`Skeleton` 或 `Logo`。登录提交、校验失败、数据刷新不重播动画;动画时间不决定业务何时就绪。仅为重播装饰而更换 key 时,将 key 限于 `OrbitBackdrop`,输入状态由重播边界外的宿主持有,不能因重播重置表单或其他业务状态。
49
+ 下列条件停止逐帧调度:用户暂停,系统/prop/祖先减少动态,页面隐藏,组件离屏,或尺寸为零。`original` 在窗口不超过 760px 时还会静态降级;三套主题物品在窄屏继续播放。恢复时沿冻结时间继续,不追赶隐藏期间的时间。静态状态调整尺寸或主题仍会重绘当前帧。
53
50
 
54
- ## 真实组件展台与验收
51
+ Canvas2D 像素比上限 2,WebGL 上限 1.5。Three.js 无独立 rAF;卸载取消调度、断开 observers,移除监听器,释放几何、材质、纹理、实例缓存、renderer 与 WebGL 上下文。
55
52
 
56
- 本地展台 `http://127.0.0.1:18309/#orbit` 复用公开组件,提供暂停 / 继续、局部重播、手动减少动态,以及系统和全局偏好继承。重播只重新挂载装饰组件,不重执行业务动作、不清空外层内容。此条目与 `/#motion` 的 12 组交互配方、`/#startup` 的启动编排分别维护。
53
+ WebGL 不可用、模块加载失败、运行时绘制失败或上下文丢失时,回退到对应物品的 Canvas2D 插画。上下文恢复后沿当前时间继续。Canvas 也不可用时,三主题各展示随包提供的静态 SVG;原始模式保留 CSS 星场。所有降级保留内容与可用业务操作。
57
54
 
58
- 验收应覆盖以下行为;本节是验收要求,不代表浏览器检查已执行:
55
+ ## 内容与可读性
59
56
 
60
- 1. 原动画的环节点、曲线、箭头、光晕与透视仍可辨识;采样不同时间的中间帧,不能只检查单张截图。
61
- 2. 鼠标与键盘可暂停 / 继续;暂停时画面冻结,恢复不跳过冻结期间的时间。
62
- 3. 系统、显式 prop、祖先偏好均可中断动画;将 `reducedMotion` 设为 `false` 不能覆盖系统减少动态。
63
- 4. 检查 400 / 520 / 880 宽度、200% 缩放和三套主题,确认窄窗静态呈现、控件可见、焦点可读且无横向溢出。组合 TextField、主 / 次按钮及 Surface,检查输入文字、placeholder、默认 / hover / disabled 状态和容器内 Text 的前景与背景。
64
- 5. 页面隐藏、离屏、零尺寸时停帧,恢复及 resize 行为一致;卸载无残留调度或监听。
65
- 6. Canvas 不可用时仍显示静态艺术和内容;业务表单、请求与路由不依赖动画。
57
+ Canvas 是 `aria-hidden` 装饰,不接收键盘焦点。有效信息由 HTML 内容表达。内置 Button 提供随动作变化的“暂停动效 / 继续动效”名称,不额外声明 `aria-pressed`。
66
58
 
67
- 宿主接入后仍需验证自己的页面布局与业务交互;单元测试、展台检查不等于宿主原生平台或完整辅助技术验收。
59
+ 直接置于原始深色艺术上的正文与非 icon Logo 继承浅色前景;主题模式继承宿主文本色。Surface、Notice、输入框和有底色按钮保留完整宿主 tokens。透明 quiet 操作和提示继承内容前景,焦点使用内描边;不覆盖全局颜色变量。
68
60
 
69
- ### 2026-10-24 初始源码候选验证
61
+ ## 真实组件展台与验收
70
62
 
71
- `npm ci`、`npm run check` 通过:65 项测试(轨道新增 10 项)、TypeScript 类型检查、展台生产构建及 CSS / SVG 产物检查。轨道测试覆盖完整节点绘制、DPR 上限、暂停时间冻结、系统 / prop / 祖先偏好、隐藏 / 离屏 / 窄窗 / 零尺寸、resize、Strict Mode 清理、Canvas 失败降级和重播保焦点。逻辑绘制尺寸使用 CSS client size,避免 CSS zoom 后暂停或 resize 改变线宽;真实浏览器验证固定画幅在 zoom 1→2→1 及 resize 后静态像素保持一致。
63
+ 入口为 `/#orbit`。主画面只展示艺术与暂停控制,外侧提供主题/原始模式、重播与减少动态。控件组合示例默认折叠,展开可验证直接叠加的表单和 Surface。重播只重挂装饰组件,保留输入草稿和按钮焦点。
72
64
 
73
- 本地 Chromium 真实展台通过 19 项检查:跨时间像素采样可见轨道运动;Space 键暂停 / 继续;系统、显式及祖先减少动态与恢复;三主题 × 400 / 520 / 880 宽度的静态 / 播放和无横向溢出;200% CSS 缩放与局部重播。另验证真实滚动离屏后像素冻结,返回可见区域后继续。页面错误为 0。页面隐藏与卸载由单元测试覆盖,不宣称已验证真实业务入口或原生平台。
65
+ 本轮 Chromium 检查通过 53 项交互检查及 18 组布局(3 主题 × 400/520/880px × 100%/200% CSS zoom),无横向溢出,按钮位于画框内,页面错误为 0。逐帧采样验证了三种物品随有效播放时间产生不同像素。暂停和主题往返保持像素;三种减少动态、离屏、模拟隐藏、上下文丢失/恢复、失败降级、迟到导入、重播草稿与卸载均通过。此处为本地开发构建证据,正式包消费及线上发布身份以发布记录为准。
74
66
 
75
- 0.6.0 发布审阅发现初始候选的局部前景 tokens 会污染嵌入控件;现已移除这组覆盖。非 icon Logo 使用默认写法并继承所在内容的前景,无需按艺术底色另设 `theme="dark"`。上述初始截图与 19 项浏览器证据不代替修正后的组合控件验证。
67
+ CSS zoom 不能替代浏览器原生缩放;`document.hidden` 事件模拟不能代表真实后台切换;未据此宣称原生端、任意 SSR 或宿主业务已验收。历史 0.6.0 轨道和甜甜圈单主题测试只对应当时源码,不代替本次三物品验收。
76
68
 
77
- 合并 0.5.0 应用能力并准备 0.6.0 后,重新执行 `npm ci` 与 `npm run check`:16 个测试文件、136 项测试、TypeScript、展台构建及 CSS / SVG 产物检查全部通过。真实展台已加入 Surface 内的正文、提示、TextField 与主 / 次按钮,供发布时单独验证三主题可读性。
69
+ ![莱茵折纸鸟](assets/orbit-bird.png)
78
70
 
79
- 仓库外生产构建验证进一步发现:动画帧可先于媒体 `change` 事件观察到减少动态,此时只停帧会留下过期的播放状态。帧检查现复用同一同步路径,更新按钮与状态并清空上一帧时间;不增加停帧后的轮询。新增该事件顺序与恢复不追赶的回归后,16 个测试文件、137 项测试(`--maxWorkers=2`)、类型检查及构建通过。
71
+ ![战术源石晶簇](assets/orbit-crystal.png)
80
72
 
81
- ![真实组件展台](assets/orbit-backdrop.png)
73
+ ![草莓甜甜圈](assets/orbit-strawberry.png)
@@ -0,0 +1,21 @@
1
+ # 设置导航 Dialog
2
+
3
+ 自 0.8.0 提供,对应 #18。默认 Dialog、Drawer 和 `sidePanel` 用法保持兼容。
4
+
5
+ ```tsx
6
+ <Dialog title="设置" description="管理工作区偏好" trigger={<Button>设置</Button>}
7
+ navigation={<SideNav aria-label="设置导航" currentId={section} groups={groups}
8
+ onNavigate={(item, event) => { event.preventDefault(); setSection(item.id); }} />}>
9
+ <SettingsSection section={section} />
10
+ </Dialog>
11
+ ```
12
+
13
+ `navigation?: ReactNode` 专门承载左侧导航,`children` 为任务内容;不与辅助 `sidePanel` 同时使用,两者同时提供会抛出明确错误。Drawer 不提供此插槽。
14
+
15
+ 标题、说明和关闭动作在同一头部。内容实际宽度达到 560 CSS px 时分成约 1:3 两栏,否则导航在上、任务内容在下;容器查询会响应宿主宽度和 CSS zoom,而不是仅判断窗口宽度。长内容在弹窗内滚动,保留原字体和点击目标。
16
+
17
+ 应用拥有栏目、数据、权限、请求、草稿、保存和取消。库不改变 URL、不自动保存、也不会因为异步数据变化而关闭弹窗。需要保留切换栏目期间的草稿时,将状态提升到 Dialog 的宿主;关闭后的重置也由宿主明确执行。不要给 Dialog 或 ThemeProvider 使用随栏目/主题变化的 key。
18
+
19
+ 嵌套编辑继续使用 Radix 的焦点陷阱和焦点归还;Dialog 在布局阶段按焦点所属弹窗接管 Escape,避免快速打开内层时旧监听误关外层:先关闭内层并归还其触发器,外层仍保持打开。`onCloseAutoFocus` 仍可处理触发器已被宿主删除的场景。
20
+
21
+ 展台 `#settings-dialog` 包含账号草稿/异步保存/取消、长用户列表、嵌套编辑,以及项目加载/错误/空态。浏览器回归在 `scripts/check-settings-theme.js`,覆盖三主题、400/520/880/1440px、100%/200% CSS zoom、实际按钮边界、嵌套 Escape、焦点与宿主查询/URL 保持。
@@ -0,0 +1,26 @@
1
+ # 主题预览与选择
2
+
3
+ `ThemePicker` 与 `ThemePreview` 自 0.8.0 提供,对应 #20;0.6.0 及未发布的 0.7.0 候选不包含它们。
4
+
5
+ ```tsx
6
+ function Appearance() {
7
+ const { theme, setTheme } = useTheme();
8
+ return <ThemePicker value={theme} onValueChange={setTheme} />;
9
+ }
10
+ ```
11
+
12
+ `ThemePicker` 是受控原生单选组:`value: ThemeName`、`onValueChange(theme)`、可选 `label`(默认“界面主题”)、`disabled` 和 `options`。默认列出三套内置主题;每个 option 的 `value` 必填,`label`、`description`、`disabled`、`previewSrc` 可选。自定义 options 不应包含重复 value。
13
+
14
+ 点击卡片、Space 或方向键选择会回传变更;Tab 聚焦和悬停不会应用主题。当前项同时使用勾选文字和原生 checked 状态。整组禁用及单项禁用遵循原生表单语义。库不访问 localStorage、网络业务接口或权限状态,偏好持久化由宿主决定。
15
+
16
+ `ThemePreview({ theme, src? })` 是装饰性缩略图,需由外部标题描述;默认资源随组件包交付,也可传入业务自己的合成截图。加载时预留 8:5 空间,失败保留文字与选择入口;src 变化会重试。图片中的控制不能点击,也不应成为另一套可交互 UI。
17
+
18
+ ## 预览资源维护
19
+
20
+ 官方图片为 960×600 PNG,由 `preview/theme-sample.html` 使用公开组件、同一个合成场景分别渲染三种主题后生成。源码为 `preview/ThemeSample.tsx`,校验记录在 `components/assets/theme-previews/manifest.json`。不包含用户数据、业务地址或外部媒体请求。
21
+
22
+ 更新 tokens、主题或样本组件视觉时,同时重新生成三张图片、更新 manifest 的源文件/图片 SHA-256,并检查三张完整截图;不能只替换其中一套。使用开发服务器打开 `/preview/theme-sample.html?theme=<name>`,将视口设为 960×600,等待样本按钮和字体完成布局后截图到对应 PNG。预览是随包版本固定的样本,不跟随线上 Pages 自动变化。
23
+
24
+ 静态图片不会继承宿主 CSS 或 Portal 样式,因此可同时比较主题;禁止嵌套 ThemeProvider 来制作实时缩略图。正常切换只更新宿主 Provider 的 theme 属性,不为业务子树添加新的 key。
25
+
26
+ 展台 `#theme-picker` 包含三主题、长名称、加载/失败、禁用和弹窗内切换。浏览器回归覆盖 390/400/520/880/889/1440px、100%/200% CSS zoom、键盘选择、图片失败仍可选择、草稿节点与打开的 Dialog 保持。
package/docs/THEMING.md CHANGED
@@ -143,3 +143,5 @@ import { Background, Surface } from '@linhey/rhine-design';
143
143
  若需全主题配置,在**主题根**通过 CSS 变量覆盖对应场景的底色、文字与纹理;局部调整优先用 `appearance`。局部背景不会传递给挂在主题根的 Portal,浮层仍使用主题的浮层变量。
144
144
 
145
145
  兼容已有源码调用:`roleTone` 的原有三种值与显式 `theme` / `none` 均保留;省略 `decoration` 的正文及 `AppShell` 改为 `auto`,默认纯色。需要旧版正文纹理的调用方显式传入 `decoration="theme"`。侧栏显式传入 `theme` 现在也会开启纹理;默认仍关闭。以上能力自 0.5.0 提供,不属于 npm 0.4.0。
146
+
147
+ 主题选择的新候选组合见 [ThemePicker / ThemePreview](THEME_PICKER.md);静态预览避免嵌套 Provider,未发布能力需核对源码与实际安装包。
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linhey/rhine-design",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -8,7 +8,10 @@
8
8
  },
9
9
  "type": "module",
10
10
  "description": "Rhine Design — independent design system, React components and motion patterns",
11
- "repository": { "type": "git", "url": "git+https://github.com/linhay/rhine-design.git" },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/linhay/rhine-design.git"
14
+ },
12
15
  "homepage": "https://github.com/linhay/rhine-design#readme",
13
16
  "engines": {
14
17
  "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
@@ -63,7 +66,9 @@
63
66
  "@radix-ui/react-dialog": "1.3.1",
64
67
  "@radix-ui/react-dropdown-menu": "2.2.1",
65
68
  "@radix-ui/react-popover": "1.3.1",
66
- "@radix-ui/react-tooltip": "1.4.1"
69
+ "@radix-ui/react-tooltip": "1.4.1",
70
+ "@types/three": "0.186.0",
71
+ "three": "0.186.1"
67
72
  },
68
73
  "devDependencies": {
69
74
  "@testing-library/jest-dom": "7.0.1",
@@ -0,0 +1,178 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ Badge, Button, ButtonLink, Checkbox, Disclosure, Divider, Inline, KeyValue,
4
+ Notice, Progress, Select, Skeleton, Stack, StatusTimelineList, Surface, Text,
5
+ TextField, TimelineLegend, Tooltip, type TimelineSegment, type TimelineStatus,
6
+ } from '../components';
7
+
8
+ type AccountState = 'normal' | 'zero' | 'unknown' | 'long';
9
+ type SubscriptionState = 'ready' | 'loading' | 'error' | 'no-link';
10
+
11
+ export const compactAccountLegend = [
12
+ { status: 'healthy', label: '有输出', description: '有已确认输出,且此时段未见异常;历史记录不保证账号当前可用。' },
13
+ { status: 'recovered', label: '已恢复', description: '此时段发生过异常或重试,后续已有确认输出,且没有最终失败。' },
14
+ { status: 'degraded', label: '待确认', description: '此时段出现异常,但最终输出或恢复结果尚未确认;不能视作成功。' },
15
+ { status: 'failed', label: '失败', description: '此时段存在已确认的最终失败;其他请求有输出也不会覆盖失败证据。' },
16
+ { status: 'unknown', label: '未知', description: '没有可用记录、记录不完整或结果未知;未知不等于零,也不等于健康。' },
17
+ ] satisfies { status: TimelineStatus; label: string; description: string }[];
18
+
19
+ const segments: TimelineSegment[] = Array.from({ length: 96 }, (_, index) => {
20
+ const status = compactAccountLegend[index % 13 === 0 ? 3 : index % 11 === 0 ? 2 : index % 7 === 0 ? 1 : index % 5 === 0 ? 4 : 0];
21
+ const start = `${String(Math.floor(index / 4)).padStart(2, '0')}:${String(index % 4 * 15).padStart(2, '0')}`;
22
+ const end = `${String(Math.floor((index + 1) / 4) % 24).padStart(2, '0')}:${String((index + 1) % 4 * 15).padStart(2, '0')}`;
23
+ return {
24
+ id: String(index), label: `10/23 ${start} · UTC+8`, status: status.status,
25
+ detail: {
26
+ start: `2026-10-23 ${start} · UTC+8`, end: `2026-10-${index === 95 ? '24' : '23'} ${end} · UTC+8`, reason: status.description,
27
+ counts: [{ id: 'output', label: '确认输出', value: status.status === 'unknown' ? null : status.status === 'failed' ? 0 : 12, unit: '次' }],
28
+ methodology: '按 15 分钟聚合的合成记录;缺失值保留未知,不计算健康分数。',
29
+ },
30
+ };
31
+ });
32
+
33
+ export function CompactAccountExample({ state = 'normal' }: { state?: AccountState }) {
34
+ const unknown = state === 'unknown', zero = state === 'zero', long = state === 'long';
35
+ return <Surface data-testid="compact-account-card"><Stack gap={4}>
36
+ <Inline gap={2} style={{ justifyContent: 'space-between' }}>
37
+ <Text as="h3" variant="heading">{long ? '跨区域协作工作空间 · LongResourceNameForWrappingAndReview' : '研究资源 A'}</Text>
38
+ <Badge tone={unknown ? 'warning' : 'success'}>{unknown ? '状态未知' : '可调度'}</Badge>
39
+ </Inline>
40
+ <Stack gap={2} data-testid="compact-quota">
41
+ {unknown ? <Text variant="caption">7D 剩余 — · 额度暂不可用</Text> : <Progress label="7D 剩余" value={zero ? 0 : 78} />}
42
+ <Text variant="caption" tone="muted">重置 {unknown ? '—' : '10/27 08:30 · 上海'}</Text>
43
+ <Divider />
44
+ <Text variant="caption" tone="muted">预测暂不可用</Text>
45
+ </Stack>
46
+ <Stack gap={1}>
47
+ <StatusTimelineList label="资源用量与异常记录" items={[{
48
+ id: 'resource-a', title: '用量与异常记录',
49
+ note: unknown ? '历史记录不完整,仅展示已确认的时段。' : undefined,
50
+ timeline: { startLabel: '10/23 00:00 · 上海', endLabel: '10/24 00:00 · 上海', segments: unknown ? segments.slice(0, 48) : segments },
51
+ }]} />
52
+ <TimelineLegend compact label="历史状态图例" items={compactAccountLegend} />
53
+ </Stack>
54
+ <Stack gap={2} data-testid="compact-account-facts">
55
+ <Inline gap={4}>
56
+ <Tooltip content="数值越小,调度优先级越高;这不是健康评分。"><Text as="span" variant="caption" tabIndex={0}>优先级 <strong>0</strong></Text></Tooltip>
57
+ <Text as="span" variant="caption">API-Key <strong>{unknown ? '—' : zero ? '0' : '3'}</strong></Text>
58
+ </Inline>
59
+ <KeyValue items={[
60
+ { key: 'expires', label: '到期', value: long ? '2027-10-24 23:59 · 上海(运营设置的账号期限)' : '未设置' },
61
+ ...(long ? [{ key: 'subscription-expires', label: '订阅到期', value: '2026-11-24 08:30 · 上海(当前服务套餐期限)' }] : []),
62
+ ]} />
63
+ <Inline gap={4} className="compact-gallery-fact-pair">
64
+ <Text as="span" variant="caption">今日消费 <strong>{unknown ? '—' : zero ? '$0.00' : long ? '$12,345.67 / $24,691.34' : '$2.40'}</strong></Text>
65
+ <Text as="span" variant="caption">本周期 <strong>{unknown ? '—' : zero ? '$0.00' : long ? '$123,456.78 / $246,913.56' : '$18.50'}</strong></Text>
66
+ </Inline>
67
+ <Inline gap={4} className="compact-gallery-fact-pair">
68
+ <Text as="span" variant="caption">今日生图 <strong>{unknown ? '—' : zero ? '0' : '8'}</strong></Text>
69
+ <Text as="span" variant="caption">本周期生图 <strong>{unknown ? '—' : zero ? '0' : '24'}</strong></Text>
70
+ </Inline>
71
+ </Stack>
72
+ </Stack></Surface>;
73
+ }
74
+
75
+ // Deliberately inert sample address and harmless download content: no service or credential is involved.
76
+ const demoAddress = 'https://example.invalid/subscriptions/demo';
77
+ const demoDownload = 'data:application/json;charset=utf-8,%7B%22demo%22%3Atrue%7D';
78
+
79
+ export function CompactSubscriptionExample({ initialState = 'ready', showHistory = false, longValue = false }: {
80
+ initialState?: SubscriptionState; showHistory?: boolean; longValue?: boolean;
81
+ }) {
82
+ const [state, setState] = useState(initialState);
83
+ const [revision, setRevision] = useState(1);
84
+ const [history, setHistory] = useState(showHistory ? ['2026-10-22 08:30 · 上海'] : []);
85
+ const [feedback, setFeedback] = useState('');
86
+ const [copyError, setCopyError] = useState(false);
87
+ const address = `${demoAddress}/${longValue ? 'cross-region-research-workspace-with-a-long-resource-name/' : ''}${revision}`;
88
+ const busy = state === 'loading';
89
+ const ready = state === 'ready';
90
+ function create() {
91
+ if (busy) return;
92
+ if (ready) setHistory(previous => ['2026-10-24 08:30 · 上海', ...previous]);
93
+ setRevision(previous => previous + 1); setState('ready'); setCopyError(false);
94
+ setFeedback(ready ? '已重置演示链接,旧链接失效。' : '已生成演示链接。');
95
+ }
96
+ async function copy() {
97
+ if (!ready) return;
98
+ try { await navigator.clipboard.writeText(address); setCopyError(false); setFeedback('已复制演示链接。'); }
99
+ catch { setCopyError(true); setFeedback('未能复制,请选择链接后手动复制。'); }
100
+ }
101
+ return <Surface data-testid="compact-subscription"><Stack gap={3}>
102
+ <Inline gap={2}><Text as="h3" variant="heading">我的订阅</Text><Badge>研究资源 A</Badge></Inline>
103
+ <Text variant="caption" tone="muted">链接可下载凭证,请勿分享。</Text>
104
+ {busy ? <Skeleton label="正在读取订阅链接" lines={2} /> : state === 'error' ?
105
+ <Notice tone="error" title="订阅链接读取失败" action={<Button variant="secondary" onClick={() => { setState('ready'); setFeedback('已重新读取演示链接。'); }}>重试</Button>} /> :
106
+ ready ? <TextField label="订阅链接" readOnly value={address} onFocus={event => event.currentTarget.select()} /> :
107
+ <Text>尚未生成订阅链接</Text>}
108
+ <Inline gap={2}>
109
+ {state === 'no-link' ? <Button onClick={create}>获取订阅链接</Button> : <>
110
+ <Button disabled={!ready} onClick={copy}>复制链接</Button>
111
+ <ButtonLink href={demoDownload} download="rhine-demo.json" rel="noreferrer" variant="secondary" disabled={!ready}>下载文件</ButtonLink>
112
+ </>}
113
+ </Inline>
114
+ {ready && <Stack gap={1}>
115
+ <Inline gap={2}>
116
+ <Button variant="quiet" onClick={create}>重置链接</Button>
117
+ <Button variant="quiet" onClick={() => { setHistory(previous => ['2026-10-24 08:30 · 上海', ...previous]); setState('no-link'); setCopyError(false); setFeedback('演示链接已撤销。'); }}>撤销链接</Button>
118
+ </Inline>
119
+ <Text variant="caption" tone="muted">重置或撤销后,旧链接失效。</Text>
120
+ </Stack>}
121
+ {!!history.length && <Disclosure title={`历史链接(${history.length})`}>
122
+ <Stack gap={3}>{history.map((date, index) => <Stack gap={1} key={`${date}-${index}`}>
123
+ <Text variant="caption" tone="muted">{date} · 已失效</Text>
124
+ <Text variant="code">{demoAddress}/retired-{index + 1}</Text>
125
+ </Stack>)}</Stack>
126
+ </Disclosure>}
127
+ {feedback && <Text variant="caption" tone={copyError ? 'error' : 'success'} role={copyError ? 'alert' : 'status'}>{feedback}</Text>}
128
+ </Stack></Surface>;
129
+ }
130
+
131
+ export function CompactAccountGallery() {
132
+ const [accountState, setAccountState] = useState<AccountState>('normal');
133
+ const [subscriptionState, setSubscriptionState] = useState<SubscriptionState>('ready');
134
+ const [history, setHistory] = useState(false);
135
+ const [longValue, setLongValue] = useState(false);
136
+ const [width, setWidth] = useState('880');
137
+ const [zoom, setZoom] = useState('1');
138
+ return <div id="compact-account" className="compact-gallery"><Stack gap={6}>
139
+ <Stack gap={2}>
140
+ <Text as="h2" variant="section">紧凑账号摘要与凭证操作</Text>
141
+ <Text tone="muted">去掉重复边框和说明,保留判断与操作所需的事实。所有内容均为合成数据。</Text>
142
+ </Stack>
143
+ <Surface><Stack gap={3}>
144
+ <Inline gap={3}>
145
+ <Select label="账号读数" value={accountState} onChange={event => setAccountState(event.target.value as AccountState)}>
146
+ <option value="normal">正常</option><option value="zero">有效零值</option><option value="unknown">未知 / 不完整</option><option value="long">长名称 / 双成本 / 两种到期</option>
147
+ </Select>
148
+ <Select label="订阅状态" value={subscriptionState} onChange={event => setSubscriptionState(event.target.value as SubscriptionState)}>
149
+ <option value="ready">有链接</option><option value="loading">读取中</option><option value="error">读取失败</option><option value="no-link">尚未生成</option>
150
+ </Select>
151
+ <Select label="样机宽度" value={width} onChange={event => setWidth(event.target.value)}>
152
+ <option value="400">400px</option><option value="520">520px</option><option value="880">880px</option>
153
+ </Select>
154
+ <Select label="内容缩放" value={zoom} onChange={event => setZoom(event.target.value)}><option value="1">100%</option><option value="2">200%</option></Select>
155
+ </Inline>
156
+ <Inline gap={4}>
157
+ <Checkbox label="包含历史链接" checked={history} onChange={event => setHistory(event.target.checked)} />
158
+ <Checkbox label="长订阅链接" checked={longValue} onChange={event => setLongValue(event.target.checked)} />
159
+ </Inline>
160
+ </Stack></Surface>
161
+ <div className="compact-gallery-stage" style={{ width: Number(width) }} data-testid="compact-stage">
162
+ <div className="compact-gallery-examples" style={{ zoom: Number(zoom) }}>
163
+ <CompactAccountExample state={accountState} />
164
+ <CompactSubscriptionExample key={`${subscriptionState}-${history}-${longValue}`} initialState={subscriptionState} showHistory={history} longValue={longValue} />
165
+ </div>
166
+ </div>
167
+ <Disclosure title="按钮式链接状态"><Inline gap={3} data-testid="compact-link-states">
168
+ <ButtonLink href={demoDownload} download="rhine-demo.json" busy busyLabel="正在准备演示文件">准备中的下载</ButtonLink>
169
+ <ButtonLink href={demoDownload} download="rhine-demo.json" disabled variant="secondary">不可用的下载</ButtonLink>
170
+ <ButtonLink href="#compact-account" target="_blank" rel="noreferrer" variant="secondary">在新标签页打开样机</ButtonLink>
171
+ </Inline></Disclosure>
172
+ <Disclosure title="跨卡历史交互"><Stack gap={6} data-testid="compact-independent-timelines">
173
+ {['独立卡片 A', '独立卡片 B'].map(title => <Surface key={title}><StatusTimelineList label={`${title}历史`} items={[{
174
+ id: title, title, timeline: { startLabel: '10/23 00:00', endLabel: '10/24 00:00', segments },
175
+ }]} /></Surface>)}
176
+ </Stack></Disclosure>
177
+ </Stack></div>;
178
+ }
@@ -0,0 +1,109 @@
1
+ import { useState } from 'react';
2
+ import { Button, DateRangePicker, Divider, Icon, Inline, Popover, SegmentedControl, Stack, Surface, Text, type DateRange } from '../components';
3
+
4
+ const quickRanges = [
5
+ { value: 'today', label: '今天', range: { start: '2026-10-24', end: '2026-10-24' } },
6
+ { value: 'yesterday', label: '昨天', range: { start: '2026-10-23', end: '2026-10-23' } },
7
+ { value: 'week', label: '本周', range: { start: '2026-10-19', end: '2026-10-24' } },
8
+ ];
9
+ const moreRanges = [
10
+ { label: '最近 7 天', range: { start: '2026-10-18', end: '2026-10-24' } },
11
+ { label: '最近 30 天', range: { start: '2026-09-25', end: '2026-10-24' } },
12
+ { label: '本月', range: { start: '2026-10-01', end: '2026-10-24' } },
13
+ { label: '上月', range: { start: '2026-09-01', end: '2026-09-30' } },
14
+ ];
15
+ const min = '2025-01-01', max = '2026-12-31';
16
+ const rangeLabel = ({ start, end }: DateRange) => `${start} — ${end}`;
17
+
18
+ // Preview composition only: the host owns presets, calendar boundaries and data queries.
19
+ export function DateRangeFilterExample({ label = '时间范围', initialValue = quickRanges[0].range, disabled = false, longLabels = false }: {
20
+ label?: string; initialValue?: DateRange; disabled?: boolean; longLabels?: boolean;
21
+ }) {
22
+ const [value, setValue] = useState(initialValue);
23
+ const [draft, setDraft] = useState(value);
24
+ const [open, setOpen] = useState(false);
25
+ const [error, setError] = useState<string | null>(null);
26
+ const [applied, setApplied] = useState(0);
27
+ const selected = quickRanges.find(item => item.range.start === value.start && item.range.end === value.end)?.value ?? '';
28
+ const canApply = !disabled && !!draft.start && !!draft.end && draft.start <= draft.end && draft.start >= min && draft.end <= max && !error;
29
+ const commit = (range: DateRange) => {
30
+ if (disabled) return;
31
+ setValue(range); setApplied(count => count + 1); setOpen(false);
32
+ };
33
+ const changeOpen = (next: boolean) => {
34
+ if (next && disabled) return;
35
+ if (next) { setDraft(value); setError(null); }
36
+ setOpen(next);
37
+ };
38
+ return <Stack gap={3}>
39
+ <Inline gap={2} role="group" aria-label={label}>
40
+ <Text variant="caption" tone="muted">{label}</Text>
41
+ <SegmentedControl label={`${label}快捷选择`} items={quickRanges.map(item => ({
42
+ value: item.value, label: longLabels ? `${item.label}的完整自然日范围` : item.label, disabled,
43
+ }))} value={selected} onValueChange={key => {
44
+ const preset = quickRanges.find(item => item.value === key);
45
+ if (preset) commit(preset.range);
46
+ }} />
47
+ <Popover title={`${label}设置`} open={open} onOpenChange={changeOpen} trigger={
48
+ <Button variant="secondary" disabled={disabled} aria-label={`${label}:${rangeLabel(value)},选择日期`}>
49
+ {rangeLabel(value)} <Icon name="chevronDown" size={16} />
50
+ </Button>
51
+ }>
52
+ <Stack gap={4}>
53
+ <Stack gap={2}>
54
+ <Text variant="caption" tone="muted">选择预设后立即应用</Text>
55
+ <Inline gap={2} aria-label="更多时间预设">
56
+ {moreRanges.map(item => <Button key={item.label} variant="secondary" onClick={() => commit(item.range)}>{item.label}</Button>)}
57
+ </Inline>
58
+ </Stack>
59
+ <Divider />
60
+ <form onSubmit={event => {
61
+ event.preventDefault();
62
+ if (canApply && event.currentTarget.checkValidity()) commit(draft);
63
+ }}>
64
+ <Stack gap={3}>
65
+ <DateRangePicker label="自定义日期" value={draft} onValueChange={setDraft} onValidationChange={setError}
66
+ min={min} max={max} required clearable={false} hint="填写完整日期后,点击应用。" />
67
+ <Inline gap={2}>
68
+ <Button variant="secondary" onClick={() => setOpen(false)}>取消</Button>
69
+ <Button type="submit" disabled={!canApply}>应用日期</Button>
70
+ </Inline>
71
+ </Stack>
72
+ </form>
73
+ </Stack>
74
+ </Popover>
75
+ </Inline>
76
+ <Text variant="caption" tone="muted" role="status">已应用:{rangeLabel(value)} · 本次操作 {applied} 次</Text>
77
+ </Stack>;
78
+ }
79
+
80
+ export function DateRangeFilterGallery() {
81
+ return <section id="date-range-filter">
82
+ <div className="ds-section-title"><span>RANGE</span><h2>日期筛选 · 快捷选择与自定义</h2></div>
83
+ <p className="ds-caption">常用日期是一组互斥选项;具体日期持续可见。更多预设即选即用,自定义日期在确认后生效。</p>
84
+ <Stack gap={4}>
85
+ <Surface>
86
+ <Stack gap={4}>
87
+ <Text as="h3" variant="heading">日常筛选</Text>
88
+ <DateRangeFilterExample />
89
+ <Text variant="caption" tone="muted">固定演示日期为 2026-10-24。选择只更新本地样例,不请求业务数据。</Text>
90
+ </Stack>
91
+ </Surface>
92
+ <Surface>
93
+ <Stack gap={4}>
94
+ <Text as="h3" variant="heading">跨年范围与长标签</Text>
95
+ <DateRangeFilterExample label="对比时间范围" initialValue={{ start: '2025-12-28', end: '2026-01-06' }} longLabels />
96
+ <Text variant="caption" tone="muted">超出快捷项的日期不冒充任何预设。窄容器允许换行,完整年份和操作保留。</Text>
97
+ </Stack>
98
+ </Surface>
99
+ <Surface>
100
+ <Stack gap={4}>
101
+ <Text as="h3" variant="heading">暂不可更改</Text>
102
+ <DateRangeFilterExample label="归档日期范围" disabled />
103
+ <Text variant="caption" tone="muted">归档结果的统计范围固定,保持日期可读。</Text>
104
+ </Stack>
105
+ </Surface>
106
+ <Text variant="caption" tone="muted">方向键切换快捷项;Escape 或取消退出草稿并归还焦点,点击外部控件按原生焦点行为继续。所有主题使用同一组公开组件。</Text>
107
+ </Stack>
108
+ </section>;
109
+ }