@linhey/rhine-design 0.6.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +19 -8
- package/components/Actions.tsx +18 -1
- package/components/AdvancedForms.tsx +38 -8
- package/components/AppShell.tsx +46 -17
- package/components/ListFilterBar.tsx +104 -0
- package/components/Media.tsx +7 -7
- package/components/NeutronStarBackdrop.tsx +43 -0
- package/components/OrbitBackdrop.tsx +11 -3
- package/components/Overlays.tsx +37 -8
- package/components/README.md +28 -3
- package/components/StatusTimeline.tsx +72 -6
- package/components/StatusTimelineList.tsx +38 -0
- package/components/ThemePicker.tsx +49 -0
- package/components/assets/orbit-bird.svg +578 -0
- package/components/assets/orbit-crystal.svg +11 -0
- package/components/assets/orbit-donut.svg +16 -0
- package/components/assets/theme-previews/arknights.png +0 -0
- package/components/assets/theme-previews/manifest.json +24 -0
- package/components/assets/theme-previews/neo-brutalism.png +0 -0
- package/components/assets/theme-previews/rhine.png +0 -0
- package/components/common.css +3 -3
- package/components/components.tsx +3 -3
- package/components/dialog-settings.css +9 -0
- package/components/extensions.css +8 -32
- package/components/index.ts +12 -5
- package/components/list-filter-bar.css +33 -0
- package/components/media-parity.css +55 -2
- package/components/neutron.css +53 -0
- package/components/neutronRenderer.ts +327 -0
- package/components/orbit.css +100 -1
- package/components/orbitBirdFlight.ts +34 -0
- package/components/orbitBirdGeometry.ts +158 -0
- package/components/orbitDonut.ts +134 -0
- package/components/orbitDonutThree.ts +208 -0
- package/components/orbitObjectMaterials.ts +76 -0
- package/components/orbitObjects.ts +71 -0
- package/components/orbitObjectsThree.ts +215 -0
- package/components/orbitProjection.ts +21 -0
- package/components/orbitRenderer.ts +163 -46
- package/components/styles.css +3 -1
- package/components/theme-neo-brutalism.css +3 -2
- package/components/theme-picker.css +17 -0
- package/components/theme.tsx +2 -8
- package/components/themeMetadata.ts +7 -0
- package/components/timeline.css +17 -6
- package/components/useFloatingScale.ts +96 -0
- package/docs/APPLICATION_COMPONENTS.md +52 -3
- package/docs/BACKGROUND_FILTER_VERIFICATION.md +12 -0
- package/docs/COMPACT_ACCOUNT_PATTERNS.md +70 -0
- package/docs/DATE_RANGE_FILTER.md +60 -0
- package/docs/DESIGN_SYSTEM.md +11 -0
- package/docs/FLOATING_OVERLAYS.md +15 -0
- package/docs/INTERACTION_PATTERNS.md +10 -2
- package/docs/LIST_FILTER_BAR.md +73 -0
- package/docs/NEUTRON_STAR_BACKDROP.md +122 -0
- package/docs/ORBIT_BACKDROP.md +61 -44
- package/docs/SETTINGS_DIALOG.md +21 -0
- package/docs/THEME_PICKER.md +26 -0
- package/docs/THEMING.md +2 -0
- package/docs/assets/compact-account-neo.png +0 -0
- package/docs/assets/neutron-star-original.png +0 -0
- package/docs/assets/neutron-star-theme.png +0 -0
- package/docs/assets/orbit-bird.png +0 -0
- package/docs/assets/orbit-crystal.png +0 -0
- package/docs/assets/orbit-strawberry.png +0 -0
- package/package.json +8 -3
- package/preview/BackgroundFiltersFixture.tsx +28 -0
- package/preview/CompactAccountGallery.tsx +178 -0
- package/preview/DateRangeFilterGallery.tsx +109 -0
- package/preview/ExtensionsGallery.tsx +27 -18
- package/preview/GalleryShell.tsx +3 -3
- package/preview/ListFilterBarGallery.tsx +170 -0
- package/preview/MonitoringGallery.tsx +2 -0
- package/preview/NeutronStarGallery.tsx +56 -0
- package/preview/OrbitGallery.tsx +49 -32
- package/preview/OverlayBoundsGallery.tsx +49 -0
- package/preview/SettingsDialogGallery.tsx +33 -0
- package/preview/ThemePickerGallery.tsx +24 -0
- package/preview/ThemeSample.tsx +14 -0
- package/preview/TimelineListGallery.tsx +36 -0
- package/preview/extensions.css +9 -0
- package/preview/main.tsx +19 -3
- package/preview/preview.css +23 -0
- package/preview/theme-sample.html +1 -0
- package/scripts/check-background-filters.js +58 -0
- package/scripts/check-background-motion.js +169 -0
- package/scripts/check-filter-native-date.js +47 -0
- package/scripts/check-overlay-bounds.js +76 -0
- package/scripts/check-settings-theme.js +88 -0
- package/skills/rhine-design/SKILL.md +3 -2
- package/skills/rhine-design/references/API_MAP.md +13 -2
- package/skills/rhine-design/references/TASK_MAP.md +8 -2
- package/skills/rhine-design/references/integration.md +1 -1
- package/tests/AppShellControls.test.tsx +82 -0
- package/tests/ButtonLink.test.tsx +94 -0
- package/tests/CompactAccountGallery.test.tsx +116 -0
- package/tests/DateNativeInput.test.tsx +147 -0
- package/tests/DateRangeFilterGallery.test.tsx +85 -0
- package/tests/ListFilterBar.test.tsx +198 -0
- package/tests/NeutronStarBackdrop.test.tsx +288 -0
- package/tests/NeutronStarGallery.test.tsx +62 -0
- package/tests/NeutronStarImport.test.tsx +15 -0
- package/tests/OrbitBackdrop.test.tsx +273 -9
- package/tests/OrbitDonutThree.test.tsx +91 -0
- package/tests/OrbitObjectsThree.test.tsx +164 -0
- package/tests/SettingsDialog.test.tsx +66 -0
- package/tests/StatusTimelineList.test.tsx +63 -0
- package/tests/ThemePicker.test.tsx +30 -0
- package/tests/TimelineLegend.test.tsx +60 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# NeutronStarBackdrop · 中子星背景
|
|
2
|
+
|
|
3
|
+
`NeutronStarBackdrop` 将授权移交的中子星背景维护为通用艺术组件。它保留 28 条磁场线、560 个沿磁场流动的粒子、700 颗星尘、极向喷流与星核;星系以 28 秒有效播放时间完成一轮深度公转。镜头固定,星系整体沿三维方向移动,由透视产生远近与尺寸变化。原始构图与运动关系由库维护;主题仅改变艺术配色。移交需求见 [Issue #12](https://github.com/linhay/rhine-design/issues/12)。
|
|
4
|
+
|
|
5
|
+
**版本边界:**本组件自 `@linhey/rhine-design@0.8.0` 提供,0.6.0 及以前不包含它。接入前核对实际 `components/index.ts` 与安装版本,固定精确版本并提交 lockfile。组件与 Three.js 渲染实现随包分发,渲染模块按需加载,不要求宿主复制算法或自行加载 CDN 脚本。固定依赖 `three@0.186.1` 与 `@types/three@0.186.0`;后者随依赖分发,以便消费本包 TypeScript 源码的工程解析类型,不进入浏览器运行代码。
|
|
6
|
+
|
|
7
|
+
## 使用与职责
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { NeutronStarBackdrop, Stack, Surface, Text } from '@linhey/rhine-design';
|
|
11
|
+
|
|
12
|
+
// 要求包含此 API 的精确源码候选;放在已有的主题根内。
|
|
13
|
+
export function Observatory() {
|
|
14
|
+
return (
|
|
15
|
+
<NeutronStarBackdrop style={{ minHeight: 480 }}>
|
|
16
|
+
<Stack style={{ padding: 'var(--rhine-space-6)' }}>
|
|
17
|
+
<Surface>
|
|
18
|
+
<Stack gap={2}>
|
|
19
|
+
<Text as="h1" variant="title">探索新的可能</Text>
|
|
20
|
+
<Text>内容与操作始终可用,不等待艺术初始化。</Text>
|
|
21
|
+
</Stack>
|
|
22
|
+
</Surface>
|
|
23
|
+
</Stack>
|
|
24
|
+
</NeutronStarBackdrop>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| 输入 | 契约 |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `HTMLAttributes<HTMLDivElement>` | 原生容器属性;宿主负责页面位置、`height` / `minHeight`、内容分组和相邻布局 |
|
|
32
|
+
| `appearance?: 'theme' \| 'original'` | 默认 `theme`,读取当前主题语义色;`original` 保留原始深空配色,只影响艺术层与直接叠加内容的可读前景,不建立新的主题根 |
|
|
33
|
+
| `reducedMotion?: boolean` | `true` 加强减少动态;`false` 或省略不能覆盖系统、祖先或环境的停帧条件 |
|
|
34
|
+
| `children` | 可选普通 HTML / React 内容,置于 `.rhine-neutron-content`;表单、请求与业务状态仍归宿主 |
|
|
35
|
+
|
|
36
|
+
公开入口是 `NeutronStarBackdrop` 与 `NeutronStarBackdropProps`,内部绘制模块不是公共 API。无速度、粒子数量、进度或业务参数;需要改变艺术配方时先补库内契约与展台。根容器是 `.rhine-neutron`,装饰层 `.rhine-neutron-art` 为 `aria-hidden`,渲染成功后有 `.rhine-neutron-canvas`。这些类名用于识别与调试,不授权宿主覆盖渲染、暂停或降级状态。
|
|
37
|
+
|
|
38
|
+
内容层仅在底部预留暂停控件安全区:`44px + 2 × --rhine-space-4`,当前为 76px;不强制正文四边内距。宿主决定内容边距,可像上例在自己的 Stack 上使用 24px 的 `--rhine-space-6`,或直接沿用已有页面边距,避免叠加两层内距。保留底部安全区,不覆盖内部 `.rhine-neutron-content` 样式来重排按钮。
|
|
39
|
+
|
|
40
|
+
内容与控制区域占用各自布局空间,暂停按钮不悬盖正文。宿主可用 Stack、Inline、Surface 组合内容,不给整棵内容树加透明度或模糊滤镜。主题切换、艺术加载失败与暂停均不隐藏文字、禁用业务操作或清空输入。画面只是装饰,不是 `Progress`、`Skeleton`、连接状态或科学数据可视化。
|
|
41
|
+
|
|
42
|
+
## 播放、暂停与减少动态
|
|
43
|
+
|
|
44
|
+
组件自带“暂停 / 继续”按钮。使用随动作改变的名称,不同时附加 `aria-pressed` 切换语义;键盘焦点与按钮状态使用宿主主题。点击继续仅取消主动暂停,不越过以下限制:
|
|
45
|
+
|
|
46
|
+
- 系统 `prefers-reduced-motion: reduce`;
|
|
47
|
+
- `reducedMotion` 为 `true`;
|
|
48
|
+
- 任一祖先的 `data-rhine-motion="reduced"`,内层 `system` 不能覆盖它;
|
|
49
|
+
- 页面隐藏、组件离屏或可用绘制尺寸为零。
|
|
50
|
+
|
|
51
|
+
任一停帧条件成立时停止请求新帧,而不是只用 CSS 隐藏 Canvas。恢复时从冻结的动画时间继续,不补算隐藏期间的时间,不追赶错过的帧。单帧有效时间增量上限为 0.05 秒;后台停帧或前台卡顿后不追赶墙钟时间,因此 28 秒指有效播放时间。尺寸变化以当前时间重新绘制;主题或 `appearance` 改变只更新颜色,不重新启动公转。宿主无需为主题切换更换 key。
|
|
52
|
+
|
|
53
|
+
此组件不按窄屏宽度自动关闭;窄屏仍遵循相同的系统、显式及环境偏好。尺寸与像素比由内部渲染器处理,宿主不得以 CSS 缩放后的测量值重复放大绘制尺寸。检查实际画布是否持续播放或冻结,不能把截图相同或 `animation: none` 当作停帧证据。
|
|
54
|
+
|
|
55
|
+
艺术层与正文高度分开:艺术区域最多 740px,视口不超过 600px 时最多 620px,并在上下渐隐;正文和控制区仍使用完整页面空间。窄而高的区域使用适配取景,相机横向对准公转中心并保留足够的水平视野,确保整个星核在完整周期中可见。标准宽幅继续原始取景,磁场几何、粒子、旋转和公转公式不变。宿主不得通过覆盖内部画布尺寸修补长页,须用真实页面高度和多个公转相位验收。
|
|
56
|
+
|
|
57
|
+
## 加载与资源释放
|
|
58
|
+
|
|
59
|
+
服务端渲染不访问 `window`、`document` 或 WebGL;挂载后再异步加载绘制模块。SSR 可安全输出组件静态结构,不等于完整应用的 SSR 构建已经验收。装饰初始化不阻塞 `children`。组件在异步加载完成前卸载时,迟到结果不创建脱离页面的渲染器。
|
|
60
|
+
|
|
61
|
+
没有 WebGL、模块加载失败或 WebGL 上下文丢失时显示静态艺术与原有内容;上下文恢复后重新建立可用绘制并遵循当前停帧条件。静态降级不表示业务失败,也不无限显示“加载中”。卸载取消逐帧任务,移除监听器与 observers,释放已创建的几何、材质与渲染器等 GPU 资源。
|
|
62
|
+
|
|
63
|
+
静态 fallback 使用当前艺术配色保留星核与磁场的视觉意图,不保证复现 WebGL 的三维透视、粒子数量或某一时刻的精确画面。减少动态但 WebGL 可用时保留实际冻结帧;能力不可用时才以这幅静态替代图承接。
|
|
64
|
+
|
|
65
|
+
根的 `data-rhine-neutron-state` 可观察当前装饰状态:`loading`、`playing`、`paused`、`reduced`、`hidden`、`offscreen`、`empty` 或 `unavailable`。这些不是业务状态;`empty` 表示绘制区域无有效尺寸,不能据此把业务内容当作空数据。宿主不要用它控制请求或完成状态。
|
|
66
|
+
|
|
67
|
+
## 主题与内容组合
|
|
68
|
+
|
|
69
|
+
默认 `appearance="theme"` 从 Rhine、Arknights、Neo Brutalism 的现有语义 tokens 取得配色,不新增一套产品色值或公共主题 tokens。切换主题时保持磁场构图与动画时间;默认展台先展示完整艺术,便于核对三主题的表达。
|
|
70
|
+
|
|
71
|
+
`appearance="original"` 保留原始深空艺术,直接叠加的文字使用可读前景;Surface、Button、表单等实体控件仍使用宿主主题的前景与背景组合。不覆写整个子树的主题 tokens,不创建嵌套 ThemeProvider。优先把需要持续阅读的长文或表单组合到 Surface,背景不能模糊或遮住正文与操作。
|
|
72
|
+
|
|
73
|
+
重播是展台或宿主主动发起的装饰动作,不能执行业务请求。需要重建艺术时仅改变装饰边界的 key;表单草稿应由边界外宿主持有,或放在不参与重播的独立组合实例中。不要给包含整页表单的页面根更换 key。
|
|
74
|
+
|
|
75
|
+
## 真实展台与验收
|
|
76
|
+
|
|
77
|
+
本地 `http://127.0.0.1:18309/#neutron-star` 复用公开组件;在线展台跟随 main,未合并前不保证此章节可访问。主视图只放艺术,默认收起“演示控制”和“查看内容组合”。展开后可选择跟随主题 / 原始深空、手动减少动态和重播,并检查 Surface、输入及按钮组合。重播主艺术不会重建下方组合或清空观测名称。
|
|
78
|
+
|
|
79
|
+
以下是验收要求,不是已执行的验证结论:
|
|
80
|
+
|
|
81
|
+
1. 在不同时间采样完整中子星画面,确认磁场线、流粒、星尘、喷流与星核可辨,星系以 28 秒有效播放时间完成一轮深度公转,保留原视觉意图。
|
|
82
|
+
2. 鼠标和键盘暂停 / 继续;暂停后实际帧循环停止,继续不跳过冻结时间。
|
|
83
|
+
3. 分别验证系统、prop、祖先减少动态及恢复;内层 `system`、`reducedMotion={false}` 和继续按钮均不能覆盖更强偏好。
|
|
84
|
+
4. 三主题与两种 appearance,400 / 520 / 880 宽度及 200% 缩放;内容、提示、按钮默认 / hover / disabled / 焦点清晰,无横向溢出或控制入口遮挡。
|
|
85
|
+
5. 主题改变与 resize 保留当前时间;页面隐藏、离屏、零尺寸停帧后可恢复,未发生时间追赶。
|
|
86
|
+
6. 初始化前卸载、正常卸载、Strict Mode 重建与异步迟到结果不遗留 rAF、监听或 GPU 资源。
|
|
87
|
+
7. 模块失败、无 WebGL、context lost 均保留静态艺术与可用内容;context restored 后按当前偏好恢复。
|
|
88
|
+
8. 展台重播保留用户输入与外部按钮焦点;隐藏在 Disclosure 内的实例不能持续后台绘制。
|
|
89
|
+
|
|
90
|
+
单元测试不代替 WebGL 真实浏览器画面;库展台也不代替消费页面、原生 WebView 或完整辅助技术验收。接回宿主后仍须检查实际入口和业务操作。
|
|
91
|
+
|
|
92
|
+
### 本候选的验证记录
|
|
93
|
+
|
|
94
|
+
2026-10-24:`npm run check` 通过 153 项测试、TypeScript 检查、Vite 生产构建及 CSS/SVG 制品检查。新增测试使用真实 Three 场景结构并替换 WebGLRenderer,检查完整场景数量与 28 秒有效播放时间、系统与显式减少动态、暂停恢复、CSS zoom / DPR 尺寸、上下文丢失恢复、异常与卸载清理;同时覆盖 SSR 静态输出、异步导入失败/迟到、Strict Mode 与展台草稿保持。这些单测不代表真实 GPU 或浏览器画面已经验收。
|
|
95
|
+
|
|
96
|
+
生产展台构建将 Three 与绘制实现保留在独立异步块:渲染块约 538 kB(gzip 134 kB),初始入口不会静态导入该块。是否挂载本组件由消费端决定,加载失败始终保留静态艺术。
|
|
97
|
+
|
|
98
|
+
本地 Chromium 真实 WebGL 展台通过 28 项动态及生命周期断言,另有 2 项原始配方下的主题控件组合检查,页面错误和非本地请求为 0。三主题 × 两种 appearance × 400 / 520 / 880 宽度 × 100% / 200% CSS zoom 共 36 组布局通过;无页面横向溢出,暂停入口保留在画幅内。CSS zoom 不等于浏览器原生页面缩放。
|
|
99
|
+
|
|
100
|
+
约 28.61 秒墙钟内观察到 28.58 秒有效播放、637 个绘制帧及 147 个不同的像素采样,3D 投影矩阵随公转变化;不是仅凭截图判断动画存在。键盘暂停 / 恢复、系统 / prop / 祖先减少动态、主题和 appearance 切换、真实离屏、零尺寸、上下文丢失及恢复、表单草稿保持均通过。卸载后无待执行 RAF 或继续绘制,相关监听器与 observers 清理;已分配的绘制 buffers / VAO 释放,program 删除并丢失 WebGL context,剩余驱动资源随 context 释放。
|
|
101
|
+
|
|
102
|
+
`document.hidden` 使用 getter 覆盖并派发 `visibilitychange` 验证停帧分支;headless 后台标签没有产生真实 hidden 状态,因此不把这项算作浏览器后台标签验收。无 WebGL 分支在浏览器中通过移除 `WebGL2RenderingContext` 入口模拟,确认没有 canvas、静态艺术可见且动效按钮禁用。GPU 初始化中途异常及异步导入失败由单元测试覆盖。
|
|
103
|
+
|
|
104
|
+
### 长页取景回归
|
|
105
|
+
|
|
106
|
+
后续真实消费页在 400px 宽、正文约 1008px 高时暴露初始取景缺陷:艺术层随正文增高,垂直视场不变导致水平视场过窄,星核在部分公转相位移出画面。此前固定 560px 展台的布局与动态通过不能证明这种长页画幅正确。现已在库内独立限制艺术高度,并对窄画幅适配相机取景,宿主无需覆盖组件内部 CSS。
|
|
107
|
+
|
|
108
|
+
新增单元回归逐一投影星核几何顶点,在 400×1008、400×620、160×620 和 880×740 四种画幅的九个公转相位验证完整星核留在画幅内。包含此回归的 `npm run check` 通过 157 项测试、类型检查、构建及制品门禁;几何、粒子和 28 秒时间公式保持不变。
|
|
109
|
+
|
|
110
|
+
真实 WebGL 追验采用 400×1008 的宿主,因测试视口为 648px,实际艺术层上限为 740px。三主题共 27 个公转相位均能完整显示核心:有效时间覆盖 0.17–28.30 秒,320px 宽像素采样中的核心横向范围为 70–247,无贴边或消失。此证据不冒充实际 400px 视口的 620px 上限消费页验收;接入页面须另查其真实 viewport 和艺术尺寸。
|
|
111
|
+
|
|
112
|
+
真实展台截帧仅展示当时画面;动态与资源验收按上述独立检查记录。
|
|
113
|
+
|
|
114
|
+
### 合入 main 后的交付复核
|
|
115
|
+
|
|
116
|
+
2026-10-24:同步已合并的日期筛选展台及长页取景修复后,修正外层流粒缺失的 0.15 弧度偏移,使其跟随对应磁场线;流粒的 position 属性实际存放生成参数,因此关闭基于这些参数的错误 CPU 视锥裁剪。暂停控件的焦点框改在实体按钮内部绘制,避免原始深空下深色外框无法辨认。两条新增回归分别检查轨迹对齐与裁剪边界,修正前失败、修正后通过;`npm run check` 共 165 项测试及类型、生产构建、制品检查通过。
|
|
117
|
+
|
|
118
|
+
对最终生产构建重跑真实 Chromium WebGL:31 项动态、清理、内容组合及长页尺寸断言、36 组布局全部通过,无页面异常或非本地请求。完整周期采用实际 400px 视口、1008px 宿主和 620px 艺术层;28.47 秒墙钟内记录到 28.43 秒有效播放、632 帧和 146 个不同像素采样。另有 13 项专项检查通过:初始入口没有请求渲染块,进入中子星展台后才请求;三主题 × 两种 appearance 在 400px 下持续提交 560 粒子的绘制;六种组合均有可见的内部键盘焦点框,与按钮表面的对比度为 14.07–15.84。上述 CSS zoom、模拟隐藏状态与消费端验收的证据边界仍适用。
|
|
119
|
+
|
|
120
|
+

|
|
121
|
+
|
|
122
|
+

|
package/docs/ORBIT_BACKDROP.md
CHANGED
|
@@ -1,81 +1,98 @@
|
|
|
1
|
-
# OrbitBackdrop ·
|
|
1
|
+
# OrbitBackdrop · 主题艺术背景
|
|
2
2
|
|
|
3
|
-
`OrbitBackdrop`
|
|
3
|
+
`OrbitBackdrop` 是带暂停控制的装饰组件。0.8.0 根据主题展示折纸鸟、源石晶簇或草莓甜甜圈,也可通过 `appearance="original"` 保留初版深色轨道。它适合欢迎、登录和品牌展示区域,不表达业务进度,不包含账号逻辑或外部资源请求。移交来源见 [Issue #8](https://github.com/linhay/rhine-design/issues/8)。
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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>` |
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
23
|
+
| `HTMLAttributes<HTMLDivElement>` | 宿主负责尺寸、位置、相邻内容布局与可访问名称 |
|
|
24
|
+
| `appearance?: 'theme' \| 'original'` | 默认跟随主题;`original` 固定初版深色轨道,切换保留播放位置与暂停状态 |
|
|
25
|
+
| `reducedMotion?: boolean` | `true` 加强减少动态;`false` 不能覆盖系统或祖先偏好 |
|
|
26
|
+
| `children` | 普通 HTML 内容层,可组合表单、按钮、Logo 与 Surface;不得遮挡暂停控件 |
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
组件持有绘制算法、内部类名、主题配方和暂停状态。没有任意速度、粒子数、业务进度或调色板参数。宿主不复制绘制器、不嵌套 ThemeProvider,也不通过覆盖内部样式重建视觉。内部文件 `orbitRenderer.ts`、`orbitObjectsThree.ts`、`orbitObjectMaterials.ts`、`orbitDonutThree.ts` 不作为公共 API 导入。
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
## 四套画面
|
|
33
31
|
|
|
34
|
-
|
|
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 保留纸鸟初始形态。
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
主题物品按需加载 Three.js,环境反射与表面纹理均在本地生成,无模型下载、CDN 或外部纹理。种子固定,运动由唯一的有效播放时间决定;物品、振翅、纸屑、碎片和着色器共用宿主时钟。切换主题复用已创建的绘制器,未激活的画布隐藏并停止绘制。异步导入完成时复核当前主题及挂载状态。
|
|
37
44
|
|
|
38
|
-
-
|
|
39
|
-
- 系统 `prefers-reduced-motion: reduce`,或 `reducedMotion` 为 `true`;
|
|
40
|
-
- 祖先的 `data-rhine-motion="reduced"` 偏好生效(内层跟随系统不覆盖外层减少动态);
|
|
41
|
-
- 当前窗口宽度不超过 760px;
|
|
42
|
-
- `document.hidden`、组件离屏或可用绘制尺寸为零。
|
|
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 共用姿态函数,全部运动使用宿主有效播放时间,暂停或减少动态时保持同一姿态。晶簇按竖向轮廓取景。两者都为底部操作区留空间。
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
## 动效与降级
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
下列条件停止逐帧调度:用户暂停,系统/prop/祖先减少动态,页面隐藏,组件离屏,或尺寸为零。`original` 在窗口不超过 760px 时还会静态降级;三套主题物品在窄屏继续播放。恢复时沿冻结时间继续,不追赶隐藏期间的时间。静态状态调整尺寸或主题仍会重绘当前帧。
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
Canvas2D 像素比上限 2,WebGL 上限 1.5。Three.js 无独立 rAF;卸载取消调度、断开 observers,移除监听器,释放几何、材质、纹理、实例缓存、renderer 与 WebGL 上下文。
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
WebGL 不可用、模块加载失败、运行时绘制失败或上下文丢失时,回退到对应物品的 Canvas2D 插画。上下文恢复后沿当前时间继续。Canvas 也不可用时,三主题各展示随包提供的静态 SVG;原始模式保留 CSS 星场。所有降级保留内容与可用业务操作。
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
## 内容与可读性
|
|
56
|
+
|
|
57
|
+
Canvas 是 `aria-hidden` 装饰,不接收键盘焦点。有效信息由 HTML 内容表达。内置 Button 提供随动作变化的“暂停动效 / 继续动效”名称,不额外声明 `aria-pressed`。
|
|
58
|
+
|
|
59
|
+
直接置于原始深色艺术上的正文与非 icon Logo 继承浅色前景;主题模式继承宿主文本色。Surface、Notice、输入框和有底色按钮保留完整宿主 tokens。透明 quiet 操作和提示继承内容前景,焦点使用内描边;不覆盖全局颜色变量。
|
|
53
60
|
|
|
54
61
|
## 真实组件展台与验收
|
|
55
62
|
|
|
56
|
-
|
|
63
|
+
入口为 `/#orbit`。主画面只展示艺术与暂停控制,外侧提供主题/原始模式、重播与减少动态。控件组合示例默认折叠,展开可验证直接叠加的表单和 Surface。重播只重挂装饰组件,保留输入草稿和按钮焦点。
|
|
57
64
|
|
|
58
|
-
|
|
65
|
+
本轮 Chromium 检查通过 53 项交互检查及 18 组布局(3 主题 × 400/520/880px × 100%/200% CSS zoom),无横向溢出,按钮位于画框内,页面错误为 0。逐帧采样验证了三种物品随有效播放时间产生不同像素。暂停和主题往返保持像素;三种减少动态、离屏、模拟隐藏、上下文丢失/恢复、失败降级、迟到导入、重播草稿与卸载均通过。此处为本地开发构建证据,正式包消费及线上发布身份以发布记录为准。
|
|
59
66
|
|
|
60
|
-
|
|
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 不可用时仍显示静态艺术和内容;业务表单、请求与路由不依赖动画。
|
|
67
|
+
CSS zoom 不能替代浏览器原生缩放;`document.hidden` 事件模拟不能代表真实后台切换;未据此宣称原生端、任意 SSR 或宿主业务已验收。历史 0.6.0 轨道和甜甜圈单主题测试只对应当时源码,不代替本次三物品验收。
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+

|
|
68
70
|
|
|
69
|
-
|
|
71
|
+

|
|
70
72
|
|
|
71
|
-
|
|
73
|
+

|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
## 入口文字与全页背景(0.9.0 新增)
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
| 输入 | 契约 |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `controls?: boolean`(0.9.0) | 默认 `true`;`false` 不渲染局部暂停按钮,并移除为控件预留的额外底部内边距。它不解除已经暂停的状态,也不覆盖系统、prop 或祖先减少动态 |
|
|
80
|
+
| `composition?: 'full' \| 'corner'`(0.9.0) | 默认 `full` 保留现有构图;`corner` 将实际绘制区域和静态降级艺术一起放至右下角,宽度最多 480px、高度为容器的 44% 且最多 360px |
|
|
81
|
+
| `framed?: boolean`(0.9.0) | 默认 `true` 保留主题面板边框、圆角与阴影;`false` 移除这三项装饰,用于宿主定位的全页背景,背景艺术与内容内边距仍保留 |
|
|
82
|
+
|
|
83
|
+
[Issue #17](https://github.com/linhay/rhine-design/issues/17) 补充入口页面的公共构图能力。需要保留上方文字阅读空间时使用 `composition="corner"`,让完整艺术在右下方较小的绘制区域内重绘;Canvas2D、可选 WebGL 和 Canvas 不可用时的静态 SVG 使用相同几何,不裁去艺术的一部分来伪造位置变化。宿主将标题和正文组合在上方;`corner` 不替宿主判断任意内容高度,长文案仍需真实布局验证。
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<OrbitBackdrop composition="corner" controls={false} style={{ minHeight: 640 }}>
|
|
87
|
+
<Stack gap={4}>
|
|
88
|
+
<Text as="h1" variant="title">继续创造</Text>
|
|
89
|
+
<Text>从这里进入工作空间。</Text>
|
|
90
|
+
</Stack>
|
|
91
|
+
</OrbitBackdrop>
|
|
92
|
+
```
|
|
76
93
|
|
|
77
|
-
|
|
94
|
+
宽度不足以保留独立艺术栏时,宿主可将同一组件定位为整个页面的背景,使用 `framed={false}` 去掉面板装饰;前景表单使用 `Surface` 保障可读性。响应式断点、业务内容和页面高度归宿主布局负责,禁止通过业务选择器覆盖内部 Canvas 或控件。
|
|
78
95
|
|
|
79
|
-
|
|
96
|
+
`controls={false}` 只移除局部控制界面,不关闭动画、不禁用系统或手动减少动态,也不创建额外帧循环。默认用法继续提供暂停入口;用户明确采用无局部按钮的装饰背景时,可由宿主统一动效偏好控制。重新显示控件保留其原暂停状态;改变构图和面板边框不重挂载画布或重置子元素状态。该组合在 0.9.0 新增,旧版本不提供这些选项。
|
|
80
97
|
|
|
81
|
-
|
|
98
|
+
展台中的 `OrbitEntryExample` 按实际容器 760px 重排:宽容器独立艺术栏,窄容器扩展为整页背景,表单保留实色 Surface。布局变更不重挂载画布或输入;`?fixture=orbit-entry` 提供独立验收入口。
|
|
@@ -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
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linhey/rhine-design",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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": {
|
|
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,28 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Button, Checkbox, OrbitBackdrop, Select, Stack, Surface, Text, TextField } from '../components';
|
|
3
|
+
import { ThemeProvider, themes, type ThemeName } from '../components/theme';
|
|
4
|
+
import { ListFilterBarGallery } from './ListFilterBarGallery';
|
|
5
|
+
|
|
6
|
+
export function OrbitEntryExample({ reduced = false, appearance = 'theme' }: { reduced?: boolean; appearance?: 'theme' | 'original' }) {
|
|
7
|
+
return <div className="ds-orbit-entry-container"><div className="ds-orbit-entry">
|
|
8
|
+
<OrbitBackdrop className="ds-orbit-entry-background" appearance={appearance} composition="corner" controls={false} framed={false} reducedMotion={reduced} aria-hidden="true" />
|
|
9
|
+
<Stack gap={4} className="ds-orbit-entry-intro"><Text variant="overline">RHINE STUDIO</Text><Text as="h3" variant="title">从这里,继续创造。</Text><Text>让内容拥有清晰的阅读空间。</Text></Stack>
|
|
10
|
+
<Surface className="ds-orbit-entry-form"><Stack gap={4}><Text as="h3" variant="heading">欢迎回来</Text><TextField label="示例昵称" placeholder="输入你的昵称" /><Button>进入工作空间</Button></Stack></Surface>
|
|
11
|
+
</div></div>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function BackgroundFiltersFixture() {
|
|
15
|
+
const params = new URLSearchParams(location.search);
|
|
16
|
+
const zoom = Number(params.get('zoom')) || 1;
|
|
17
|
+
const [theme, setTheme] = useState<ThemeName>((params.get('theme') || 'rhine') as ThemeName);
|
|
18
|
+
const [reduced, setReduced] = useState(false), [mounted, setMounted] = useState(true);
|
|
19
|
+
useEffect(() => { document.documentElement.style.zoom = String(zoom); return () => { document.documentElement.style.zoom = ''; }; }, [zoom]);
|
|
20
|
+
return <ThemeProvider theme={theme} onThemeChange={setTheme}><div style={{ padding: 12 }}>
|
|
21
|
+
{params.get('fixture') === 'list-filters' ? <ListFilterBarGallery /> : <Stack gap={4}>
|
|
22
|
+
<Select label="验收主题" value={theme} onChange={event => setTheme(event.target.value as ThemeName)}>{themes.map(item => <option key={item.name} value={item.name}>{item.label}</option>)}</Select>
|
|
23
|
+
<Checkbox label="减少入口动态" checked={reduced} onChange={event => setReduced(event.target.checked)} />
|
|
24
|
+
<Checkbox label="挂载入口" checked={mounted} onChange={event => setMounted(event.target.checked)} />
|
|
25
|
+
{mounted && <OrbitEntryExample reduced={reduced} />}
|
|
26
|
+
</Stack>}
|
|
27
|
+
</div></ThemeProvider>;
|
|
28
|
+
}
|
|
@@ -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
|
+
}
|