@linhey/rhine-design 0.5.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 (100) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +26 -8
  3. package/components/Actions.tsx +18 -1
  4. package/components/AppShell.tsx +46 -17
  5. package/components/MOTION.md +9 -1
  6. package/components/Media.tsx +7 -7
  7. package/components/NeutronStarBackdrop.tsx +43 -0
  8. package/components/OrbitBackdrop.tsx +35 -0
  9. package/components/Overlays.tsx +37 -8
  10. package/components/README.md +30 -2
  11. package/components/StatusTimeline.tsx +72 -6
  12. package/components/StatusTimelineList.tsx +38 -0
  13. package/components/ThemePicker.tsx +49 -0
  14. package/components/assets/orbit-bird.svg +578 -0
  15. package/components/assets/orbit-crystal.svg +11 -0
  16. package/components/assets/orbit-donut.svg +16 -0
  17. package/components/assets/theme-previews/arknights.png +0 -0
  18. package/components/assets/theme-previews/manifest.json +24 -0
  19. package/components/assets/theme-previews/neo-brutalism.png +0 -0
  20. package/components/assets/theme-previews/rhine.png +0 -0
  21. package/components/common.css +3 -3
  22. package/components/components.tsx +3 -3
  23. package/components/dialog-settings.css +9 -0
  24. package/components/extensions.css +8 -32
  25. package/components/index.ts +13 -5
  26. package/components/media-parity.css +55 -2
  27. package/components/neutron.css +53 -0
  28. package/components/neutronRenderer.ts +327 -0
  29. package/components/orbit.css +120 -0
  30. package/components/orbitBirdFlight.ts +34 -0
  31. package/components/orbitBirdGeometry.ts +158 -0
  32. package/components/orbitDonut.ts +134 -0
  33. package/components/orbitDonutThree.ts +208 -0
  34. package/components/orbitObjectMaterials.ts +76 -0
  35. package/components/orbitObjects.ts +71 -0
  36. package/components/orbitObjectsThree.ts +215 -0
  37. package/components/orbitProjection.ts +21 -0
  38. package/components/orbitRenderer.ts +347 -0
  39. package/components/styles.css +4 -1
  40. package/components/theme-neo-brutalism.css +3 -2
  41. package/components/theme-picker.css +17 -0
  42. package/components/theme.tsx +2 -8
  43. package/components/themeMetadata.ts +7 -0
  44. package/components/timeline.css +17 -6
  45. package/components/useFloatingScale.ts +96 -0
  46. package/docs/APPLICATION_COMPONENTS.md +52 -3
  47. package/docs/COMPACT_ACCOUNT_PATTERNS.md +70 -0
  48. package/docs/DATE_RANGE_FILTER.md +60 -0
  49. package/docs/DESIGN_SYSTEM.md +7 -0
  50. package/docs/FLOATING_OVERLAYS.md +15 -0
  51. package/docs/INTERACTION_PATTERNS.md +5 -1
  52. package/docs/NEUTRON_STAR_BACKDROP.md +122 -0
  53. package/docs/ORBIT_BACKDROP.md +73 -0
  54. package/docs/SETTINGS_DIALOG.md +21 -0
  55. package/docs/THEME_PICKER.md +26 -0
  56. package/docs/THEMING.md +2 -0
  57. package/docs/assets/compact-account-neo.png +0 -0
  58. package/docs/assets/neutron-star-original.png +0 -0
  59. package/docs/assets/neutron-star-theme.png +0 -0
  60. package/docs/assets/orbit-backdrop.png +0 -0
  61. package/docs/assets/orbit-bird.png +0 -0
  62. package/docs/assets/orbit-crystal.png +0 -0
  63. package/docs/assets/orbit-strawberry.png +0 -0
  64. package/package.json +8 -3
  65. package/preview/CompactAccountGallery.tsx +178 -0
  66. package/preview/DateRangeFilterGallery.tsx +109 -0
  67. package/preview/ExtensionsGallery.tsx +27 -18
  68. package/preview/GalleryShell.tsx +3 -3
  69. package/preview/MonitoringGallery.tsx +2 -0
  70. package/preview/NeutronStarGallery.tsx +56 -0
  71. package/preview/OrbitGallery.tsx +50 -0
  72. package/preview/OverlayBoundsGallery.tsx +49 -0
  73. package/preview/SettingsDialogGallery.tsx +33 -0
  74. package/preview/ThemePickerGallery.tsx +24 -0
  75. package/preview/ThemeSample.tsx +14 -0
  76. package/preview/TimelineListGallery.tsx +36 -0
  77. package/preview/extensions.css +9 -0
  78. package/preview/main.tsx +19 -4
  79. package/preview/preview.css +8 -0
  80. package/preview/theme-sample.html +1 -0
  81. package/scripts/check-overlay-bounds.js +76 -0
  82. package/scripts/check-settings-theme.js +88 -0
  83. package/skills/rhine-design/SKILL.md +3 -2
  84. package/skills/rhine-design/references/API_MAP.md +14 -1
  85. package/skills/rhine-design/references/TASK_MAP.md +8 -1
  86. package/skills/rhine-design/references/integration.md +1 -1
  87. package/tests/AppShellControls.test.tsx +82 -0
  88. package/tests/ButtonLink.test.tsx +94 -0
  89. package/tests/CompactAccountGallery.test.tsx +116 -0
  90. package/tests/DateRangeFilterGallery.test.tsx +85 -0
  91. package/tests/NeutronStarBackdrop.test.tsx +288 -0
  92. package/tests/NeutronStarGallery.test.tsx +62 -0
  93. package/tests/NeutronStarImport.test.tsx +15 -0
  94. package/tests/OrbitBackdrop.test.tsx +420 -0
  95. package/tests/OrbitDonutThree.test.tsx +91 -0
  96. package/tests/OrbitObjectsThree.test.tsx +164 -0
  97. package/tests/SettingsDialog.test.tsx +66 -0
  98. package/tests/StatusTimelineList.test.tsx +63 -0
  99. package/tests/ThemePicker.test.tsx +30 -0
  100. package/tests/TimelineLegend.test.tsx +60 -0
@@ -0,0 +1,73 @@
1
+ # OrbitBackdrop · 主题艺术背景
2
+
3
+ `OrbitBackdrop` 是带暂停控制的装饰组件。0.8.0 根据主题展示折纸鸟、源石晶簇或草莓甜甜圈,也可通过 `appearance="original"` 保留初版深色轨道。它适合欢迎、登录和品牌展示区域,不表达业务进度,不包含账号逻辑或外部资源请求。移交来源见 [Issue #8](https://github.com/linhay/rhine-design/issues/8)。
4
+
5
+ **版本边界:**基础组件自 `@linhey/rhine-design@0.6.0` 提供;`appearance` 与三套主题物品需要 0.8.0。0.6.0 默认固定深色轨道,升级后默认跟随主题;需要保持旧视觉时显式使用 `appearance="original"`。固定精确版本并提交 lockfile。
6
+
7
+ ## 使用与契约
8
+
9
+ ```tsx
10
+ import { Logo, OrbitBackdrop, Text } from '@linhey/rhine-design';
11
+
12
+ export function WelcomeArtwork() {
13
+ return <OrbitBackdrop style={{ minHeight: 360 }}>
14
+ <Logo decorative />
15
+ <Text as="h1" variant="title">开始新的工作</Text>
16
+ <Text>从这里进入你的工作空间。</Text>
17
+ </OrbitBackdrop>;
18
+ }
19
+ ```
20
+
21
+ | 输入 | 契约 |
22
+ | --- | --- |
23
+ | `HTMLAttributes<HTMLDivElement>` | 宿主负责尺寸、位置、相邻内容布局与可访问名称 |
24
+ | `appearance?: 'theme' \| 'original'` | 默认跟随主题;`original` 固定初版深色轨道,切换保留播放位置与暂停状态 |
25
+ | `reducedMotion?: boolean` | `true` 加强减少动态;`false` 不能覆盖系统或祖先偏好 |
26
+ | `children` | 普通 HTML 内容层,可组合表单、按钮、Logo 与 Surface;不得遮挡暂停控件 |
27
+
28
+ 组件持有绘制算法、内部类名、主题配方和暂停状态。没有任意速度、粒子数、业务进度或调色板参数。宿主不复制绘制器、不嵌套 ThemeProvider,也不通过覆盖内部样式重建视觉。内部文件 `orbitRenderer.ts`、`orbitObjectsThree.ts`、`orbitObjectMaterials.ts`、`orbitDonutThree.ts` 不作为公共 API 导入。
29
+
30
+ ## 四套画面
31
+
32
+ | 风格 | 物品与细节 |
33
+ | --- | --- |
34
+ | 莱茵 · 纸面 | 折纸鸟:578 个实际三角折面、暖白纤维纸纹、扇形褶皱与叠层双翼、立体细颈与三片尾羽、弧线飞行与振翅转弯、42 片随行纸屑与 120 颗微尘 |
35
+ | 战术终端 | 悬浮源石晶簇:五块不同姿态的倒角晶体、程序矿物纹理、琥珀裂纹、48 块环绕碎片、420 颗余烬与断续约束环 |
36
+ | 橙趣 · 粗描边 | 草莓甜甜圈:烘焙纹理、立体糖霜、280 根糖针、110 颗糖珠、100 根环绕糖粒、460 颗糖尘与奶油色背景 |
37
+ | `original` | 完整初版轨道:420 个环节点、185 条空间曲线、80 个深度桶、箭头、光晕与原始配色 |
38
+
39
+ 折纸鸟以舒展的翼面与轻盈的飞行呼应莱茵纸面主题;源石晶簇采用坚硬、暗色的矿物质感。原始模式不随主题改变。
40
+
41
+ 莱茵物品在飞行中连续变形成纸蝴蝶:原有 578 个三角折面保持连接,鸟颈收为蝶首、尾羽重塑为触须与腹尖、左右翼展开为四瓣蝶翼;折痕与纸纹随顶点移动,不更换模型或交叉淡化。42 秒有效播放周期为纸鸟 4 秒、折叠变形 14 秒、纸蝴蝶 6 秒、折回 14 秒、纸鸟 4 秒。变形依次收翼、松开折痕、展开为近乎平面的中间纸形,短暂停留后重新折出新形态;翼根、内侧折线与翼尖以独立时间窗依次松开、重新折起,相邻动作错开约 0.8–1 秒;纸面由内向外展平,外缘延后跟随,鸟颈与尾羽绕根部打开。中途暂停扑翼,让折纸动作清晰可见,完成后平滑恢复;蝶形阶段转为较快的扑翼。暂停和减少动态同时冻结形态、翼拍与飞行位置,恢复后接续当前折叠进度。Canvas2D 使用同一组对应顶点与变形函数;静态 SVG 保留纸鸟初始形态。
42
+
43
+ 主题物品按需加载 Three.js,环境反射与表面纹理均在本地生成,无模型下载、CDN 或外部纹理。种子固定,运动由唯一的有效播放时间决定;物品、振翅、纸屑、碎片和着色器共用宿主时钟。切换主题复用已创建的绘制器,未激活的画布隐藏并停止绘制。异步导入完成时复核当前主题及挂载状态。
44
+
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 共用姿态函数,全部运动使用宿主有效播放时间,暂停或减少动态时保持同一姿态。晶簇按竖向轮廓取景。两者都为底部操作区留空间。
46
+
47
+ ## 动效与降级
48
+
49
+ 下列条件停止逐帧调度:用户暂停,系统/prop/祖先减少动态,页面隐藏,组件离屏,或尺寸为零。`original` 在窗口不超过 760px 时还会静态降级;三套主题物品在窄屏继续播放。恢复时沿冻结时间继续,不追赶隐藏期间的时间。静态状态调整尺寸或主题仍会重绘当前帧。
50
+
51
+ Canvas2D 像素比上限 2,WebGL 上限 1.5。Three.js 无独立 rAF;卸载取消调度、断开 observers,移除监听器,释放几何、材质、纹理、实例缓存、renderer 与 WebGL 上下文。
52
+
53
+ WebGL 不可用、模块加载失败、运行时绘制失败或上下文丢失时,回退到对应物品的 Canvas2D 插画。上下文恢复后沿当前时间继续。Canvas 也不可用时,三主题各展示随包提供的静态 SVG;原始模式保留 CSS 星场。所有降级保留内容与可用业务操作。
54
+
55
+ ## 内容与可读性
56
+
57
+ Canvas 是 `aria-hidden` 装饰,不接收键盘焦点。有效信息由 HTML 内容表达。内置 Button 提供随动作变化的“暂停动效 / 继续动效”名称,不额外声明 `aria-pressed`。
58
+
59
+ 直接置于原始深色艺术上的正文与非 icon Logo 继承浅色前景;主题模式继承宿主文本色。Surface、Notice、输入框和有底色按钮保留完整宿主 tokens。透明 quiet 操作和提示继承内容前景,焦点使用内描边;不覆盖全局颜色变量。
60
+
61
+ ## 真实组件展台与验收
62
+
63
+ 入口为 `/#orbit`。主画面只展示艺术与暂停控制,外侧提供主题/原始模式、重播与减少动态。控件组合示例默认折叠,展开可验证直接叠加的表单和 Surface。重播只重挂装饰组件,保留输入草稿和按钮焦点。
64
+
65
+ 本轮 Chromium 检查通过 53 项交互检查及 18 组布局(3 主题 × 400/520/880px × 100%/200% CSS zoom),无横向溢出,按钮位于画框内,页面错误为 0。逐帧采样验证了三种物品随有效播放时间产生不同像素。暂停和主题往返保持像素;三种减少动态、离屏、模拟隐藏、上下文丢失/恢复、失败降级、迟到导入、重播草稿与卸载均通过。此处为本地开发构建证据,正式包消费及线上发布身份以发布记录为准。
66
+
67
+ CSS zoom 不能替代浏览器原生缩放;`document.hidden` 事件模拟不能代表真实后台切换;未据此宣称原生端、任意 SSR 或宿主业务已验收。历史 0.6.0 轨道和甜甜圈单主题测试只对应当时源码,不代替本次三物品验收。
68
+
69
+ ![莱茵折纸鸟](assets/orbit-bird.png)
70
+
71
+ ![战术源石晶簇](assets/orbit-crystal.png)
72
+
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
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linhey/rhine-design",
3
- "version": "0.5.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
+ }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
- import { AppShell, Background, Button, Checkbox, DataTable, DateField, DateRangePicker, Dialog, EmptyState, MediaCard, MediaViewer, Notice, NumberField, Pagination, RemoteMultiSelect, Select, SideNav, Skeleton, Stack, Surface, TextField, TreeSelect, type AppShellProps, type DateRange, type TreeNode, type TableSort } from '../components';
2
+ import { AppShell, AppShellToggle, IconButton, Inline, Background, Button, Checkbox, DataTable, DateField, DateRangePicker, Dialog, EmptyState, MediaCard, MediaViewer, Notice, NumberField, Pagination, RemoteMultiSelect, Select, SideNav, Skeleton, Stack, Surface, TextField, TreeSelect, type AppShellProps, type DateRange, type TreeNode, type TableSort } from '../components';
3
3
  import type { Choice } from '../components/components';
4
4
  import './extensions.css';
5
5
 
@@ -16,16 +16,24 @@ export function ShellGallery() {
16
16
  const [navDrawerOpen, setNavDrawerOpen] = useState(false);
17
17
  const [current, setCurrent] = useState('inbox'), [draft, setDraft] = useState('主题切换和侧栏折叠会保留这段内容。');
18
18
  const [plain, setPlain] = useState(true), [state, setState] = useState('ready');
19
+ const [inlineControls, setInlineControls] = useState(true);
19
20
  return <section><div className="ds-section-title"><span>LAYOUT</span><h2>画布与应用外壳</h2></div>
20
21
  <p className="ds-caption">单侧栏适合简单工具;双左栏适合目录与列表;左右栏适合编辑器与检查器。可用宽度不足 1120px 时次栏转抽屉,不足 760px 时全局导航也转抽屉。正文始终保留。</p>
21
- <div className="ds-extension-controls"><Select label="布局方案" value={layout} onChange={event => setLayout(event.target.value as AppShellProps['layout'])}><option value="single">单侧栏</option><option value="double-left">双左侧栏</option><option value="left-right">左右侧栏</option></Select><Checkbox label="纯色背景" checked={plain} onChange={event => setPlain(event.target.checked)} /><StateControl value={state} onChange={setState} /></div>
22
- <div className="ds-shell-stage"><AppShell layout={layout} decoration={plain ? 'none' : 'theme'}
23
- navigation={{ label: '全局导航', open: navOpen, onOpenChange: setNavOpen, drawerOpen: navDrawerOpen, onDrawerOpenChange: setNavDrawerOpen, content: <SideNav aria-label="应用导航" currentId={current} groups={[{ label: '工作空间', items: [{ id: 'inbox', label: '收件箱', href: '#app-shell' }, { id: 'archive', label: longName, href: '#app-shell' }] }]} onNavigate={(item, event) => { event.preventDefault(); setCurrent(item.id); setNavDrawerOpen(false); }} /> }}
24
- secondary={{ label: layout === 'left-right' ? '辅助详情' : '上下文目录', open: secondaryOpen, onOpenChange: setSecondaryOpen, content: <Stack gap={4}><strong>{current === 'inbox' ? '收件箱' : longName}</strong>{state === 'loading' ? <Skeleton label="目录加载中" /> : state === 'error' ? <Notice title="目录读取失败" tone="error">可以继续编辑正文。</Notice> : state === 'empty' ? <EmptyState title="目录暂无内容" description="选择其他项目或继续编辑正文。" /> : <><p>当前条目、所属目录和属性。</p><Button variant="secondary" disabled={state === 'disabled'}>上下文操作</Button></>}</Stack> }}
25
- header={<Dialog title="背景与浮层" description="浮层仍然挂载在唯一主题根;不受局部背景裁剪。" trigger={<Button>打开对话框</Button>}><TextField label="对话框输入" placeholder="试试 Tab 和 Escape" /></Dialog>}>
26
- <Stack gap={6}><div><p className="ds-kicker">CURRENT TASK / {current}</p><h3>同一主题,独立正文背景</h3><p>正文默认使用纯色背景。关闭“纯色背景”可显式启用主题纹理;纸面主题保持纯净。</p></div>
22
+ <div className="ds-extension-controls"><Select label="布局方案" value={layout} onChange={event => setLayout(event.target.value as AppShellProps['layout'])}><option value="single">单侧栏</option><option value="double-left">双左侧栏</option><option value="left-right">左右侧栏</option></Select><Checkbox label="使用正文导航栏" checked={inlineControls} onChange={event => setInlineControls(event.target.checked)} /><Checkbox label="纯色背景" checked={plain} onChange={event => setPlain(event.target.checked)} /><StateControl value={state} onChange={setState} /></div>
23
+ <div className="ds-shell-stage"><AppShell controls={inlineControls ? 'inline' : 'header'} layout={layout} decoration={plain ? 'none' : 'theme'}
24
+ navigation={{ label: '全局导航', open: navOpen, onOpenChange: setNavOpen, drawerOpen: navDrawerOpen, onDrawerOpenChange: setNavDrawerOpen, content: <Stack gap={4}>{inlineControls && <Inline><strong>Rhine Workspace</strong><AppShellToggle placement="panel" iconOnly /></Inline>}<SideNav aria-label="应用导航" currentId={current} groups={[{ label: '工作空间', items: [{ id: 'inbox', label: '收件箱', href: '#app-shell' }, { id: 'archive', label: longName, href: '#app-shell' }] }]} onNavigate={(item, event) => { event.preventDefault(); setCurrent(item.id); setNavDrawerOpen(false); }} /></Stack> }}
25
+ secondary={{ label: layout === 'left-right' ? '辅助详情' : '上下文目录', open: secondaryOpen, onOpenChange: setSecondaryOpen, content: <Stack gap={4}>{inlineControls && <AppShellToggle panel="secondary" placement="panel" />}<strong>{current === 'inbox' ? '收件箱' : longName}</strong>{state === 'loading' ? <Skeleton label="目录加载中" /> : state === 'error' ? <Notice title="目录读取失败" tone="error">可以继续编辑正文。</Notice> : state === 'empty' ? <EmptyState title="目录暂无内容" description="选择其他项目或继续编辑正文。" /> : <><p>当前条目、所属目录和属性。</p><Button variant="secondary" disabled={state === 'disabled'}>上下文操作</Button></>}</Stack> }}
26
+ contentHeader={inlineControls ? <nav aria-label="工作区导航"><Inline>
27
+ <AppShellToggle placement="content" iconOnly />
28
+ <h1>{current === 'inbox' ? '收件箱' : longName}</h1>
29
+ <div className="rhine-shell__navigation-actions"><AppShellToggle panel="secondary" placement="content" />
30
+ <Dialog title="工作区信息" description="查看当前工作区的信息。关闭后返回导航入口。" trigger={<IconButton icon="info" label="工作区信息" title="工作区信息" />}><TextField label="对话框输入" placeholder="试试 Tab 和 Escape" /></Dialog>
31
+ </div>
32
+ </Inline></nav> : undefined}
33
+ header={inlineControls ? undefined : <Dialog title="背景与浮层" description="浮层仍然挂载在唯一主题根;不受局部背景裁剪。" trigger={<Button>打开对话框</Button>}><TextField label="对话框输入" placeholder="试试 Tab 和 Escape" /></Dialog>}>
34
+ <Stack gap={6}>{!inlineControls && <h1 className="rhine-sr-only">{current === 'inbox' ? '收件箱' : longName}</h1>}<p>本地草稿 · 整理工作区,继续你的记录。</p>
27
35
  <Surface><Stack gap={4}><TextField label="正文草稿" value={draft} onChange={event => setDraft(event.target.value)} /><Button disabled={state === 'disabled'}>继续当前任务</Button></Stack></Surface>
28
- {Array.from({ length: 5 }, (_, i) => <Surface key={i}><h3>内容段落 {i + 1}</h3><p>滚动正文时顶部操作栏保持可达;每栏仅有一个滚动容器。Tab 可依次访问导航、正文与辅助详情。</p></Surface>)}
36
+ {Array.from({ length: 5 }, (_, i) => <Surface key={i}><h3>内容段落 {i + 1}</h3><p>滚动正文时导航栏保持可达;每栏仅有一个滚动容器。Tab 可依次访问导航、正文与辅助详情。</p></Surface>)}
29
37
  </Stack>
30
38
  </AppShell></div>
31
39
  <div className="ds-extension-grid ds-gap-top">{(['canvas','content','sidebar'] as const).map(role => <Background key={role} roleTone={role} decoration={plain ? 'none' : 'theme'} className="ds-background-swatch"><strong>{role}</strong><Surface>Surface 始终提供不透明阅读表面。</Surface></Background>)}</div>
@@ -76,20 +84,21 @@ export function AdvancedDataGallery() {
76
84
 
77
85
  export function MediaGallery() {
78
86
  const [selected, setSelected] = useState(false), [open, setOpen] = useState(false), [index, setIndex] = useState(0), [metadata, setMetadata] = useState(true), [version, setVersion] = useState('original');
79
- const titles = ['山野 · 午后的光', longName, '视频预览示例'];
87
+ const titles = ['山野 · 午后的光', '远山手记 · 原始影像与拍摄记录', '暮色之间 · 动态影像'];
80
88
  const [retried, setRetried] = useState(false);
81
- return <section><div className="ds-section-title"><span>MEDIA</span><h2>媒体保持比例,画布保持中性</h2></div><p className="ds-caption">照片、RAW、Live 和视频共享卡片契约。中性暗色查看器独立于三套界面主题;媒体请求、视频播放、下载与版本资源均由宿主提供。示例只使用本地内嵌矢量预览。</p>
82
- <MediaViewer open={open} onOpenChange={setOpen} trigger={<Button>打开全屏查看器</Button>} title={titles[index]} description={`${index + 1} / 3 · 左右方向键切换;输入和播放器保留自身按键`} onPrevious={() => setIndex(current => Math.max(0, current - 1))} onNext={() => setIndex(current => Math.min(2, current + 1))} previousDisabled={index === 0} nextDisabled={index === 2} metadataOpen={metadata} onMetadataOpenChange={setMetadata}
89
+ return <section className="ds-media-gallery"><div className="ds-section-title"><span>MEDIA</span><h2>影像与收藏</h2></div><p className="ds-media-intro">浏览、选择,或打开一张影像。保留画面的原始比例,让每一份作品有自己的位置。</p><div className="ds-media-toolbar"><span role="status">3 份资源 · {selected ? '已选 1 项' : '未选择'}</span>
90
+ <MediaViewer open={open} onOpenChange={setOpen} trigger={<Button>打开全屏查看器</Button>} title={titles[index]} description={`${index + 1} / 3 · 左右方向键切换影像`} onPrevious={() => setIndex(current => Math.max(0, current - 1))} onNext={() => setIndex(current => Math.min(2, current + 1))} previousDisabled={index === 0} nextDisabled={index === 2} metadataOpen={metadata} onMetadataOpenChange={setMetadata}
83
91
  actions={<><label>资源版本 <select aria-label="资源版本" value={version} onChange={event => setVersion(event.target.value)}><option value="original">原始资源</option><option value="preview">预览资源</option></select></label><a href={sampleImage} download="rhine-demo.svg">下载演示资源</a></>}
84
92
  metadata={<><h3>媒体信息</h3><p>{titles[index]}</p><p>类型:{index === 2 ? '视频' : '照片'}</p><p>版本:{version}</p><p>日期:未知</p><Dialog title="资源说明" description="Escape 先关闭此层,再关闭媒体查看器。" trigger={<button type="button">打开资源说明</button>}><p>业务元数据由宿主管理。</p></Dialog></>}
85
93
  >{index === 2 ? <video controls poster={sampleImage} aria-label="视频播放器示例" /> : <img src={sampleImage} alt="山峰与太阳构成的演示插画" />}</MediaViewer>
86
- <div className="ds-extension-grid ds-gap-top">
87
- <MediaCard title={titles[0]} kind="photo" date="2026-10-23" preview={<img src={sampleImage} alt="山野插画" />} onOpen={() => { setIndex(0); setOpen(true); }} selected={selected} onSelectedChange={setSelected} />
94
+ </div><div className="ds-media-grid">
95
+ <MediaCard title={titles[0]} kind="photo" date="2026-10-23" preview={<img src={sampleImage} alt="山野插画" />} onOpen={() => { setIndex(0); setOpen(true); }} selected={selected} onSelectedChange={setSelected} aspectRatio="3 / 2" />
88
96
  <MediaCard title={titles[1]} kind="RAW" preview={<img src={sampleImage} alt="原始预览" />} aspectRatio="3 / 2" onOpen={() => { setIndex(1); setOpen(true); }} />
89
- <MediaCard title="视频,保持原始比例" kind="video" preview={<img src={sampleImage} alt="视频封面" />} onOpen={() => { setIndex(2); setOpen(true); }} />
90
- <MediaCard title="加载中的 Live 资源" kind="Live" state="loading" onOpen={() => setOpen(true)} />
91
- <MediaCard title="不可解码的文件" kind="RAW" state={retried ? 'ready' : 'error'} preview={retried ? <img src={sampleImage} alt="重试后恢复的示例预览" /> : undefined} onOpen={() => { setIndex(1); setOpen(true); }} actions={<Button variant="secondary" onClick={() => setRetried(true)} disabled={retried}>{retried ? '预览已恢复' : '重试预览'}</Button>} />
92
- <MediaCard title="尚无预览的资源" kind="photo" state="empty" onOpen={() => setOpen(true)} />
93
- </div>
97
+ <MediaCard title={titles[2]} date="2026-10-21" aspectRatio="3 / 2" kind="video" preview={<img src={sampleImage} alt="视频封面" />} onOpen={() => { setIndex(2); setOpen(true); }} />
98
+ </div><div className="ds-media-section-heading"><h3>状态与反馈</h3><p>加载、读取失败和缺少预览时,资源信息仍然保留。</p></div><div className="ds-media-grid ds-media-states">
99
+ <MediaCard aspectRatio="3 / 2" title="加载中的 Live 资源" kind="Live" state="loading" onOpen={() => setOpen(true)} />
100
+ <MediaCard aspectRatio="3 / 2" title="不可解码的文件" kind="RAW" state={retried ? 'ready' : 'error'} preview={retried ? <img src={sampleImage} alt="重试后恢复的示例预览" /> : undefined} onOpen={() => { setIndex(1); setOpen(true); }} actions={<Button variant="secondary" onClick={() => setRetried(true)} disabled={retried}>{retried ? '预览已恢复' : '重试预览'}</Button>} />
101
+ <MediaCard aspectRatio="3 / 2" title="尚无预览的资源" kind="photo" state="empty" onOpen={() => setOpen(true)} />
102
+ </div><p className="ds-caption ds-media-note">展台使用本地矢量插画;视频项展示封面与控件,不包含可播放文件。</p>
94
103
  </section>;
95
104
  }
@@ -9,8 +9,8 @@ export const galleryGroups = [
9
9
  { label: '开始', items: [['top', '设计总览'], ['subscriptions', '订阅管理示例']] },
10
10
  { label: '应用能力', items: [['app-shell', '背景与多侧栏'], ['advanced-forms', '复杂表单'], ['advanced-data', '受控数据表'], ['media', '媒体卡片与查看器'], ['monitoring', '健康详情与图表']] },
11
11
  { label: '设计语言', items: [['foundation', '视觉基础'], ['backgrounds', '场景背景'], ['spacing', '间距与组合'], ['typography', '字体与文字'], ['brand', '品牌标志']] },
12
- { label: '通用组件', items: [['actions', '按钮与输入'], ['selection', '选择与切换'], ['feedback', '状态与加载'], ['disclosure', '展开与组合'], ['data', '数据与分页'], ['forms', '表单与选择'], ['utilities', '操作与布局'], ['overlays', '浮层与反馈'], ['navigation', '导航与步骤'], ['form-workflow', '表单流程']] },
13
- { label: '交互与动效', items: [['patterns', '场景与交互指南'], ['motion', '动效实验室'], ['startup', '启动动画']] },
12
+ { label: '通用组件', items: [['actions', '按钮与输入'], ['selection', '选择与切换'], ['feedback', '状态与加载'], ['disclosure', '展开与组合'], ['data', '数据与分页'], ['forms', '表单与选择'], ['utilities', '操作与布局'], ['overlays', '浮层与反馈'], ['overlay-bounds', '浮层缩放与边界'], ['settings-dialog', '设置导航弹窗'], ['theme-picker', '主题预览与选择'], ['navigation', '导航与步骤'], ['form-workflow', '表单流程']] },
13
+ { label: '交互与动效', items: [['patterns', '场景与交互指南'], ['date-range-filter', '日期筛选组合'], ['compact-account', '紧凑账号与凭证'], ['motion', '动效实验室'], ['startup', '启动动画'], ['orbit', '轨道星空'], ['neutron-star', '中子星']] },
14
14
  ] as const;
15
15
  export type GallerySection = typeof galleryGroups[number]['items'][number][0];
16
16
  const entries = galleryGroups.flatMap(group => [...group.items]);
@@ -94,7 +94,7 @@ function GalleryContent({ motion, onMotionChange, children }: GalleryShellProps)
94
94
  <SideNav id="gallery-navigation" className="ds-sidebar-nav" aria-label="组件目录" currentId={section} groups={galleryGroups.map(group=>({label:group.label,items:group.items.map(([id,label])=>({id,label,href:`#${id}`}))}))} onNavigate={item=>{
95
95
  setMenuOpen(false); if(section===item.id) main.current?.focus({preventScroll:true});
96
96
  }}/>
97
- <div className="ds-sidebar-foot"><span>COMPONENT LIBRARY</span><span>59 个组件 · 持续演进</span><span className="ak-readout">{readout}</span></div>
97
+ <div className="ds-sidebar-foot"><span>COMPONENT LIBRARY</span><span>66 个组件 · 0.8.0</span><span className="ak-readout">{readout}</span></div>
98
98
  </aside>
99
99
  <main className="ds-workspace" id="gallery-content" ref={main} tabIndex={-1} aria-label={label}>
100
100
  <header className="ds-workspace-head"><Breadcrumbs label="展台位置" items={[{label:"组件展台",href:"#top"},{label}]} /><div className="ds-head-controls"><SegmentedControl label="动效偏好" items={[{value:'system',label:'跟随系统'},{value:'reduced',label:'减少动态'}]} value={motion} onValueChange={onMotionChange} /><Select label="界面主题" value={theme} onChange={event => {