@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
@@ -2,14 +2,8 @@ import { createContext, useCallback, useContext, useMemo, useState, type HTMLAtt
2
2
  import './styles.css';
3
3
  import './theme-arknights.css';
4
4
  import './theme-neo-brutalism.css';
5
-
6
- export const themes = [
7
- { name: 'rhine', label: '莱茵 · 纸面' },
8
- { name: 'arknights', label: '明日方舟 · 战术终端' },
9
- { name: 'neo-brutalism', label: '橙趣 · 粗描边' },
10
- ] as const;
11
-
12
- export type ThemeName = typeof themes[number]['name'];
5
+ import { themes, type ThemeName } from './themeMetadata';
6
+ export { themes, type ThemeName } from './themeMetadata';
13
7
 
14
8
  export interface ThemeProviderProps extends HTMLAttributes<HTMLDivElement> {
15
9
  defaultTheme?: ThemeName;
@@ -0,0 +1,7 @@
1
+ export const themes = [
2
+ { name: 'rhine', label: '莱茵 · 纸面' },
3
+ { name: 'arknights', label: '明日方舟 · 战术终端' },
4
+ { name: 'neo-brutalism', label: '橙趣 · 粗描边' },
5
+ ] as const;
6
+
7
+ export type ThemeName = typeof themes[number]['name'];
@@ -1,23 +1,34 @@
1
1
  .rhine-timeline { display:grid; gap:var(--rhine-space-1); min-width:0; }
2
2
  .rhine-timeline__track { position:relative; height:44px; }
3
3
  .rhine-timeline__bars { height:100%; display:flex; gap:1px; align-items:center; }
4
- .rhine-timeline__bars span { flex:1; min-width:0; height:20px; background:var(--rhine-line); }
4
+ .rhine-timeline__bars span { flex:1; min-width:0; height:28px; background:var(--rhine-line); }
5
5
  .rhine-timeline [data-status=healthy],.rhine-timeline__legend [data-status=healthy] { background:var(--rhine-health-healthy); }
6
6
  .rhine-timeline [data-status=recovered],.rhine-timeline__legend [data-status=recovered] { background:repeating-linear-gradient(135deg,var(--rhine-health-recovered) 0 2px,var(--rhine-surface) 2px 4px); }
7
7
  .rhine-timeline [data-status=degraded],.rhine-timeline__legend [data-status=degraded] { background:var(--rhine-health-degraded); }
8
8
  .rhine-timeline [data-status=failed],.rhine-timeline__legend [data-status=failed] { background:var(--rhine-health-failed); }
9
9
  .rhine-timeline [data-status=unknown],.rhine-timeline__legend [data-status=unknown] { background:var(--rhine-health-unknown); }
10
- .rhine-timeline__bars [data-selected=true] { height:28px; outline:1px solid var(--rhine-ink); outline-offset:1px; }
10
+ .rhine-timeline__bars [data-selected=true] { height:36px; outline:1px solid var(--rhine-ink); outline-offset:1px; }
11
11
  .rhine-timeline__track input { appearance:none; position:absolute; inset:0; margin:0; width:100%; height:44px; background:transparent; cursor:crosshair; color:transparent; }
12
12
  .rhine-timeline__track input::-webkit-slider-thumb { appearance:none; width:1px; height:44px; background:transparent; }
13
13
  .rhine-timeline__track input::-moz-range-thumb { border:0; width:1px; height:44px; background:transparent; }
14
14
  .rhine-timeline__range { display:flex; justify-content:space-between; gap:var(--rhine-space-2); }
15
15
  .rhine-timeline__legend { display:flex; flex-wrap:wrap; gap:var(--rhine-space-2) var(--rhine-space-3); margin:0; padding:0; list-style:none; font-size:var(--rhine-text-caption); line-height:var(--rhine-leading-caption); color:var(--rhine-muted); }
16
16
  .rhine-timeline__legend li { display:flex; align-items:center; gap:var(--rhine-space-1); }
17
- .rhine-timeline__legend span { width:8px; height:12px; background:var(--rhine-line); }
17
+ .rhine-timeline__legend [data-status] { flex:none; width:8px; height:12px; }
18
+ .rhine-timeline__legend--compact { flex-wrap:nowrap; gap:var(--rhine-space-1); min-width:0; max-width:100%; overflow-x:auto; padding:var(--rhine-space-1); }
19
+ .rhine-timeline__legend--compact li { flex:none; white-space:nowrap; }
20
+ .rhine-timeline__legend-trigger { display:inline-flex; align-items:center; justify-content:center; gap:var(--rhine-space-1); min-width:44px; min-height:44px; padding:var(--rhine-space-1); border:0; border-radius:var(--rhine-radius-button); background:transparent; color:inherit; font:inherit; cursor:help; }
21
+ .rhine-timeline__legend-trigger:hover,.rhine-timeline__legend-trigger[aria-expanded=true] { background:var(--rhine-selected); color:var(--rhine-ink); }
22
+ .rhine-theme .rhine-timeline__legend--compact button.rhine-timeline__legend-trigger:focus-visible { outline-offset:-3px; }
18
23
  .rhine-timeline--compact { position:relative; gap:0; }
19
- .rhine-timeline--compact .rhine-timeline__track { height:20px; }
20
- .rhine-timeline--compact .rhine-timeline__track input { top:-12px; bottom:auto; }
24
+ .rhine-timeline--compact .rhine-timeline__track { height:28px; }
25
+ .rhine-timeline--compact .rhine-timeline__track input { top:-8px; bottom:auto; }
21
26
  .rhine-timeline-popover { z-index:50; width:min(360px,calc(100vw - 24px)); max-height:var(--radix-popover-content-available-height); overflow:auto; padding:16px; border:1px solid var(--rhine-control); border-radius:var(--rhine-radius-card); background:var(--rhine-overlay-bg); color:var(--rhine-overlay-text); box-shadow:0 4px 12px #242b2814; overflow-wrap:anywhere; }
22
27
  .rhine-timeline-popover .rhine-key-value { margin-block:16px; }
23
- .rhine-timeline--compact:not(:hover):not(:focus-within) .rhine-timeline__bars [data-selected=true] { height:20px; outline:none; }
28
+ .rhine-timeline--compact:not(:hover):not(:focus-within) .rhine-timeline__bars [data-selected=true] { height:28px; outline:none; }
29
+ .rhine-timeline-list { list-style:none; margin:0; padding:4px 4px var(--rhine-space-4); min-width:0; }
30
+ .rhine-timeline-list__item { display:grid; gap:var(--rhine-space-2); min-width:0; padding-block:var(--rhine-space-4); }
31
+ .rhine-timeline-list__item:first-child { padding-top:0; }
32
+ .rhine-timeline-list__item+.rhine-timeline-list__item { border-top:1px solid var(--rhine-line); }
33
+ .rhine-timeline-list__heading { display:flex; flex-wrap:wrap; justify-content:space-between; align-items:baseline; gap:var(--rhine-space-2); min-width:0; overflow-wrap:anywhere; }
34
+ .rhine-timeline-list__heading>* { min-width:0; }
@@ -0,0 +1,96 @@
1
+ import { useCallback, useLayoutEffect, useState, type RefObject } from 'react';
2
+
3
+ type Side = 'top' | 'right' | 'bottom' | 'left';
4
+
5
+ // Fall back before pointerdown opens a menu, so its first item cannot appear
6
+ // beneath the still-pressed trigger and accidentally receive the pointerup.
7
+ export function useFloatingSide(anchor: RefObject<HTMLSpanElement | null>, side: Side | undefined, width: number) {
8
+ const [vertical, setVertical] = useState(false);
9
+ useLayoutEffect(() => {
10
+ if (side !== 'left' && side !== 'right') return;
11
+ const root = anchor.current?.closest('.rhine-theme') ?? document.body;
12
+ const ancestors: Element[] = [];
13
+ for (let element: Element | null = root; element; element = element.parentElement) ancestors.push(element);
14
+ const measure = () => {
15
+ const trigger = anchor.current?.querySelector<HTMLElement>('[aria-haspopup], button, a');
16
+ if (!trigger) return;
17
+ const scale = ancestors.reduce((total, element) => total * (parseFloat(getComputedStyle(element).zoom) || 1), 1);
18
+ const rect = trigger.getBoundingClientRect();
19
+ setVertical(Math.max(rect.left, window.innerWidth - rect.right) < Math.min(width * scale, window.innerWidth - 24) + 20);
20
+ };
21
+ measure();
22
+ const observer = new MutationObserver(measure);
23
+ ancestors.forEach(element => observer.observe(element, { attributes: true, attributeFilter: ['style', 'class', 'data-rhine-theme'] }));
24
+ window.addEventListener('resize', measure);
25
+ window.addEventListener('scroll', measure, true);
26
+ return () => { observer.disconnect(); window.removeEventListener('resize', measure); window.removeEventListener('scroll', measure, true); };
27
+ }, [anchor, side, width]);
28
+ return vertical && (side === 'left' || side === 'right') ? 'bottom' : side;
29
+ }
30
+
31
+ /** Keep Radix's viewport coordinates unscaled while preserving the user's content zoom. */
32
+ export function useFloatingScale(ref?: RefObject<HTMLDivElement | null>, kind = 'popover') {
33
+ const [content, setContent] = useState<HTMLDivElement | null>(null);
34
+ const attach = useCallback((element: HTMLDivElement | null) => {
35
+ if (ref) ref.current = element;
36
+ setContent(element);
37
+ }, [ref]);
38
+ useLayoutEffect(() => {
39
+ const wrapper = content?.parentElement;
40
+ if (!content || !wrapper?.hasAttribute('data-radix-popper-content-wrapper')) return;
41
+ const ancestors: HTMLElement[] = [];
42
+ for (let element = wrapper.parentElement; element; element = element.parentElement) ancestors.push(element);
43
+ const original = { wrapperZoom: wrapper.style.zoom, translate: wrapper.style.translate, contentZoom: content.style.zoom, maxWidth: content.style.maxWidth, maxHeight: content.style.maxHeight, minWidth: content.style.minWidth };
44
+ let offsetX = 0, offsetY = 0;
45
+ const constrain = () => {
46
+ const rect = wrapper.getBoundingClientRect();
47
+ const x = rect.x - offsetX, y = rect.y - offsetY;
48
+ // Radix shifts along the alignment axis only. A side-facing panel may need
49
+ // to overlap its trigger when neither horizontal side has enough room.
50
+ offsetX = Math.max(12, Math.min(x, window.innerWidth - rect.width - 12)) - x;
51
+ offsetY = Math.max(12, Math.min(y, window.innerHeight - rect.height - 12)) - y;
52
+ const translate = `${offsetX}px ${offsetY}px`;
53
+ if (wrapper.style.translate !== translate) wrapper.style.translate = translate;
54
+ };
55
+ const measure = () => {
56
+ const scale = ancestors.reduce((value, element) => {
57
+ const raw = getComputedStyle(element).zoom;
58
+ const zoom = raw.endsWith('%') ? parseFloat(raw) / 100 : parseFloat(raw);
59
+ return value * (Number.isFinite(zoom) && zoom > 0 ? zoom : 1);
60
+ }, 1);
61
+ wrapper.style.zoom = String(1 / scale);
62
+ content.style.zoom = String(scale);
63
+ const width = `calc((100vw - 24px) / ${scale})`;
64
+ content.style.maxWidth = kind === 'tooltip' ? `min(280px, ${width})` : width;
65
+ content.style.maxHeight = `calc(var(--radix-${kind}-content-available-height) / ${scale})`;
66
+ if (kind === 'dropdown-menu') content.style.minWidth = `min(180px, ${width})`;
67
+ constrain();
68
+ };
69
+ measure();
70
+ const observer = new MutationObserver(measure);
71
+ ancestors.forEach(element => observer.observe(element, { attributes: true, attributeFilter: ['style', 'class', 'data-rhine-theme'] }));
72
+ let lastTransform = wrapper.style.transform;
73
+ const placement = new MutationObserver(() => {
74
+ if (lastTransform === wrapper.style.transform) return;
75
+ lastTransform = wrapper.style.transform;
76
+ constrain();
77
+ });
78
+ placement.observe(wrapper, { attributes: true, attributeFilter: ['style'] });
79
+ const size = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(constrain);
80
+ size?.observe(wrapper);
81
+ window.addEventListener('resize', measure);
82
+ return () => {
83
+ observer.disconnect();
84
+ placement.disconnect();
85
+ size?.disconnect();
86
+ window.removeEventListener('resize', measure);
87
+ wrapper.style.zoom = original.wrapperZoom;
88
+ wrapper.style.translate = original.translate;
89
+ content.style.zoom = original.contentZoom;
90
+ content.style.maxWidth = original.maxWidth;
91
+ content.style.maxHeight = original.maxHeight;
92
+ content.style.minWidth = original.minWidth;
93
+ };
94
+ }, [content, kind]);
95
+ return attach;
96
+ }
@@ -39,7 +39,36 @@ import { ThemeProvider } from '@linhey/rhine-design/theme';
39
39
 
40
40
  ## 三种外壳布局
41
41
 
42
- `AppShell` 接收 `layout="single" | "double-left" | "left-right"`;`navigation` 必填、`secondary` 在后两种布局可选。面板形状为 `{ label, content, open, onOpenChange }`;`header` 放当前任务操作、`children` 为正文,`decoration` 控制正文背景。
42
+ ### 内联控制按钮(0.8.0)
43
+
44
+ 默认 `controls="header"` 保持既有顶部操作栏。宿主已有页面导航栏时可用 `controls="inline"`:没有 `header` 内容就不生成顶部横栏。将 `AppShellToggle placement="panel"` 放入侧栏品牌区,将对应的 `placement="content"` 放入正文导航栏;每个面板各放一对。`panel` 默认 navigation,可选 secondary;`iconOnly` 使用已有 IconButton。不要另写折叠状态、复制 Drawer 或用 CSS 隐藏默认横栏。
45
+
46
+ 展开桌面面板时只显示品牌区的收起按钮;收起后正文显示展开按钮;窄窗正文按钮打开 Drawer,抽屉使用已有关闭按钮。`content` 控制保留挂载以归还焦点,`panel` 控制随内联面板隐藏。正文入口必须放在始终可达的位置(推荐使用 `contentHeader` 导航插槽),即使切换页面也要保留。折叠/展开与断点变化迁移键盘焦点,Drawer 继续负责 Escape 与焦点约束。控件没有异步、加载或错误状态,不把业务 busy 绑定到导航。
47
+
48
+ ```tsx
49
+ <AppShell controls="inline" layout="single"
50
+ navigation={{ label: '导航', open, onOpenChange: setOpen,
51
+ content: <><div>Workspace <AppShellToggle placement="panel" iconOnly /></div><Navigation /></> }}
52
+ contentHeader={<nav aria-label="工作区导航"><Inline>
53
+ <AppShellToggle placement="content" iconOnly /><h1>{pageTitle}</h1>{actions}
54
+ </Inline></nav>}>
55
+ <Content />
56
+ </AppShell>
57
+ ```
58
+
59
+ ### 工作区导航栏(0.8.0)
60
+
61
+ 参考 [Ant Design Layout](https://ant.design/components/layout/) 的 Header / Sider / Content 分工与 [MUI App Bar](https://mui.com/material-ui/react-app-bar/) 的标题、导航和操作组合,工作区顶部承载当前位置与导航入口。用可选 `contentHeader` 将导航栏放在正文滚动区外,与侧栏并列;它不会随正文滚走,不需要宿主另写 sticky。既有 `header` 仍位于整个外壳顶部,不传新插槽时保持原有行为。
62
+
63
+ 展台左侧显示全局导航入口与当前页标题,右侧显示上下文目录与工作区信息;桌面侧栏展开时入口由品牌区承担,收起后恢复到导航栏,窄窗使用菜单图标打开 Drawer。标题允许换行,操作保留命中尺寸;正文的草稿状态和业务操作留在正文。底色与分隔线随三主题变化,橙趣使用墨色粗线,战术使用信号色边界。
64
+
65
+ 平级工作区不强制 Breadcrumbs;当前页只保留一个可访问的 h1,正文不再重复标题。此处依据用户“这是导航区”的纠正,替代旧展台仅有状态/操作行的组合;没有导航栏需求的宿主仍可省略 `contentHeader`。真实父子层级可按需组合 Breadcrumbs,展台自身的章节路径不属于产品平级工作区。
66
+
67
+ 验收包括三主题 × 400/520/880px 长标题无横向溢出、200% CSS zoom 重排、正文滚动时导航栏位置不变、侧栏展开/收起及焦点归还、窄窗 Drawer 切页与未保存草稿保留、信息弹窗 Escape 返回入口。原生浏览器缩放与屏幕阅读器组合需另验。
68
+
69
+ 真实展台 `#app-shell` 可切换默认顶部和内联控制模式;沿用三布局、长文案、正文草稿、禁用/加载/错误状态与全局主题/减少动态设置。
70
+
71
+ `AppShell` 接收 `layout="single" | "double-left" | "left-right"`;`navigation` 必填、`secondary` 在后两种布局可选。面板形状为 `{ label, content, open, onOpenChange }`;`header` 为整壳顶部区域、`contentHeader` 为正文列的常驻导航区域(0.8.0)、`children` 为正文,`decoration` 控制正文背景。
43
72
 
44
73
  | 方案 | 用途 | 顺序 |
45
74
  | --- | --- | --- |
@@ -132,9 +161,11 @@ Pagination 新增 `disabled/label/showFirstButton/showLastButton`;首末页按
132
161
 
133
162
  MediaCard 支持原生 article 属性、ref 与独立 actions(重试等操作不能嵌套在打开按钮里)。MediaViewer 支持 defaultOpen、defaultMetadataOpen 及 className/style;不传 metadataOpen 时,信息栏按钮即可管理内部状态。`previousDisabled/nextDisabled` 显式表达边界,左右键默认触发上/下一项,可用 `keyboardNavigation=false` 关闭;输入框、选择器、媒体播放器和嵌套浮层保留自身按键。导航按钮到达边界变禁用时焦点留在查看器内,可继续反向浏览。
134
163
 
135
- 查看器采用同一主题根中的中性媒体 tokens:canvas `#101214`、overlay `#202428`、text `#f6f7f8`、muted `#bcc3ca`、border `#68727c`、focus `#9bceff`。不把战术主题当暗色媒体模式,也不嵌套 Provider。顶部操作可换行,触控目标至少 44px;桌面 metadata 位于右侧,760px 以下移到底部。短窗口内容可滚动,关闭按钮始终在顶部可达。
164
+ 0.8.0 将中性媒体区域限定为查看器的画布(canvas `#101214`、text `#f6f7f8`、focus `#9bceff`)。卡片、类型标签、工具栏和信息栏使用宿主主题的 surface/ink/control/radius tokens:莱茵纸面细边、战术直角与信号色、橙趣墨色粗边与硬阴影。卡片选中态不改变边框宽度,不引起排列跳动;选择与重试位于独立底部操作区。媒体像素保持原色和 contain 比例,不跟随主题变色。
165
+
166
+ 顶部操作可换行,触控目标至少 44px;桌面 metadata 位于右侧,760px 以下移到底部。查看器按固定定位视口的百分比尺寸取景,避免 CSS zoom 将 dvh 高度重复放大;操作栏可达,正文和信息可以滚动。不嵌套 Provider,沿用宿主的动效与主题偏好。此视觉调整自 0.8.0 提供,0.6.0 不包含。
136
167
 
137
- 媒体 children、下载链接和资源版本选择通过 slots 提供;库不持有下载路径、播放状态或路由。actions 中使用原生 button/select/a 可自动使用中性叠加样式。宿主可将 scroll-snap 列表放入画布,并保留原有 Masonic 或其他虚拟列表。仅复用模态的背景滚动管理,没有增加 wheel 事件锁或自定义手势拦截。内部 Dialog/Popover 优先响应 Escape,下一次 Escape 再关闭查看器;媒体自身不播放进场动画,减少动态遵循同一主题根。
168
+ 媒体 children、下载链接和资源版本选择通过 slots 提供;库不持有下载路径、播放状态或路由。actions 中使用原生 button/select/a 可自动使用宿主主题的控件样式。宿主可将 scroll-snap 列表放入画布,并保留原有 Masonic 或其他虚拟列表。仅复用模态的背景滚动管理,没有增加 wheel 事件锁或自定义手势拦截。内部 Dialog/Popover 优先响应 Escape,下一次 Escape 再关闭查看器;媒体自身不播放进场动画,减少动态遵循同一主题根。
138
169
 
139
170
  ## 健康详情与运维图表(issue #6)
140
171
 
@@ -144,6 +175,24 @@ MediaCard 支持原生 article 属性、ref 与独立 actions(重试等操作
144
175
 
145
176
  StatusTimeline 通过 `activeId/defaultActiveId/onActiveChange` 按稳定时段 ID 控制选中;未指定或当前 ID 消失时显示最后一个时段。`open/defaultOpen/onOpenChange` 控制详情,`disabled` 禁止操作及显示详情。Enter/Space 将键盘焦点送入详情关闭按钮,退出后返回健康条且不会立即重开。
146
177
 
178
+ ### 多条历史记录(0.8.0)
179
+
180
+ 时间条默认色块高 28px,悬停或聚焦选中块高 36px;compact 轨道高 28px,44px 命中区垂直居中。完整时间条沿用 44px 轨道;图例尺寸独立,不随色块加高。
181
+
182
+ `StatusTimelineList({ label, items, emptyLabel? })` 统一标题、摘要、备注、分隔与 compact 时间条。同一列表一次只展开一条详情;鼠标切换记录不抢焦点,键盘进入详情后退出返回对应时间条,嵌套 Dialog 的下一次 Escape 才关闭外层。行删除、禁用或历史变为空时关闭旧详情。
183
+
184
+ 每项为 `StatusTimelineItem`:稳定的 `id`、文字 `title`、可选 `summary: ReactNode`、`note`、`unavailableLabel`,以及 `timeline: { segments, startLabel, endLabel, disabled? } | null`。null 表示历史不可用,空 segments 表示没有时段;两者均不冒充健康。`emptyLabel` 默认“暂无历史记录”,`unavailableLabel` 默认“历史记录暂不可用 · 未知”。摘要由宿主提供,0 与未知应明确区分。行 ID 和时段 ID 不使用数组索引,确保排序或刷新后保留正确身份。
185
+
186
+ 组件不请求、排序、分页或计算健康/额度。宿主保留数据权限、服务端分页与加载/错误反馈;列表只协调呈现与详情开关。真实展台 `/#monitoring` 覆盖默认、加载、错误保留旧记录、空、未知、不完整、禁用、长名称及分栏弹窗。
187
+
188
+ ```tsx
189
+ <StatusTimelineList label="资源历史" items={[
190
+ { id: 'resource-a', title: '资源 A', summary: '剩余 78%',
191
+ timeline: { segments, startLabel: '10/23 00:00 UTC+8', endLabel: '10/24 00:00 UTC+8' } },
192
+ { id: 'resource-b', title: '资源 B', summary: '剩余未知', timeline: null },
193
+ ]} />
194
+ ```
195
+
147
196
  `TrendChart({ label, unit, rangeLabel, description, points, kind, stale?, error?, loading? })` 提供 `kind="bar" | "line"`,每个 point 是 `{id,label,value:number|null}`。宿主负责时间桶、排序、聚合、单位与统计说明;组件只映射坐标。零是有效采样,未知值不补零、不插值,折线路径在缺失点处断开;图内 ×、SVG 可访问说明和可展开的原始数据表共同表示缺失。单位、范围与图例均为 HTML 文字,避免窄屏 SVG 缩放导致文字小于 12px。多条资源指标使用多个命名图表组合,不内置数据请求或图表引擎。
148
197
 
149
198
  `StackedBar` 使用相同的 label/unit/rangeLabel/description/反馈字段,接收 `parts=[{id,label,value:number|null,tone?:1|2|3|4}]`。只在每项计数均已知、非负、有限且总和大于零时按传入计数绘制占比;任意未知值会停止归一化并显示“总量未知”,仍列出全部已知/未知计数。全零和负数各有说明。百分比是图例中对这组计数的数学占比,保留一位小数,不是健康分数或成功率。宿主必须提供互斥分类,不能把重叠统计相加当作总量。
@@ -0,0 +1,70 @@
1
+ # 紧凑摘要与凭证操作
2
+
3
+ 对应 [issue #14](https://github.com/linhay/rhine-design/issues/14)。本页的 `ButtonLink`、`StatusTimelineList` 与 `TimelineLegend` 扩展自 npm 0.8.0 提供,0.6.0 不包含;使用前核对实际导出并固定消费版本。真实展台:`/#compact-account`。
4
+
5
+ ## 从用户任务开始消融
6
+
7
+ 先确定用户现在要识别什么、判断什么、执行什么,再选择常显信息。接口返回多少字段不决定界面有多少行。优先保留对象身份、当前有效读数、可执行动作、未知/失败与影响当前决策的后果;机制说明、重复状态和低频记录退出首屏。
8
+
9
+ 每次删减都说明为何不影响任务,并验证反例:链接框与复制/下载已表达“有链接”,无需再堆叠“当前链接”和“已生成”;读取失败、无链接与操作结果未知仍须分别呈现。历史记录有内容时按需展开,空历史不占一块常驻区域。生成时间可进入记录,不能把时间本身删除或改成其他语义。
10
+
11
+ 短风险提示放在相关动作附近。例如“链接可下载凭证,请勿分享。”持续可见;“重置或撤销后,旧链接失效。”紧邻管理入口。客户端如何轮询、接口如何返回,不是用户取得链接所需的首屏信息。若机制影响当前动作或恢复路径,则保留其具体后果,不能用消融隐藏失败或撤销边界。不展示与当前选择无关的技术徽标来代替用户可行动的提示。
12
+
13
+ ## 只读摘要使用无框组合
14
+
15
+ 外层 `Surface` 已定义对象边界时,内部额度摘要不再包细线圆角框。进度、剩余额度、重置时间与预测状态通过 `Progress`、`Text`、`Inline` 和 `Stack` 组合,必要分区使用 `Divider`;不能移除未知或预测失败的文字。
16
+
17
+ 事实按共享维度配对:优先级与密钥数量一行;账号到期与订阅到期分别呈现;今日与本周期读数配对。复用 `KeyValue`、`Inline`、`Stack`,不把每个字段装成胶囊或小卡片。压缩来自移除装饰、重复标签和无意义空白,不减小业务字号、不裁切数字、不固定高度。保留双成本的顺序和含义、未知 `—` 与真实 `0`,不相加或相互替换。长内容和 200% 缩放允许自然增高。
18
+
19
+ ## ButtonLink:具有按钮外观的原生链接
20
+
21
+ ```tsx
22
+ <ButtonLink href="/files/example.json" download="example.json"
23
+ variant="secondary" target="_blank" rel="noreferrer">
24
+ 下载文件
25
+ </ButtonLink>
26
+ ```
27
+
28
+ `ButtonLinkProps` 包含必填 `href: string`、原生 anchor 属性、`variant: primary | secondary | quiet | danger`、`disabled`、`busy`、`busyLabel`,支持 anchor ref。与 `Button` 复用同一视觉体系。默认 variant 为 primary;同一组动作通过任务优先级选择 variant,不因 HTML 元素不同而降低视觉权重。
29
+
30
+ - 可用时是真实 `<a href>`:Enter 激活,修饰键、右键、`download`、`target` 与 `rel` 由浏览器处理;不设置 `role="button"`,不模拟 Space 按钮行为,不命令式导航。
31
+ - `disabled` 移除 href、设置 `aria-disabled` 与 `tabIndex=-1`,阻止主/辅助点击;原生 anchor 没有 `disabled` 属性。
32
+ - `busy` 移除 href 并阻止重复激活,但保留焦点和 Tab 位置,设置 `aria-busy` / `aria-disabled`;原文字占位、spinner 与 busyLabel 遵循 Button。
33
+ - 原生 capture 事件仍先于组件拦截运行。业务动作使用 `onClick` / `onAuxClick`,capture 仅用于观测;不要在 capture 内绕过禁用状态。
34
+ - 组件只管理可交互语义。宿主负责同源/目标路径校验、权限、请求生命周期与结果,不把历史失效链接传成可用下载。
35
+
36
+ ## 一行图例,按需查看规则
37
+
38
+ ```tsx
39
+ <TimelineLegend label="历史状态图例" compact items={[
40
+ { status: 'healthy', label: '有输出', description: '有已确认输出;当前证据未发现异常,不代表完整可用性。' },
41
+ { status: 'recovered', label: '已恢复', description: '本时段曾异常,后续重试已有成功证据。' },
42
+ { status: 'degraded', label: '待确认', description: '存在异常,但最终结果证据不足。' },
43
+ { status: 'failed', label: '失败', description: '存在已确认的最终失败。' },
44
+ { status: 'unknown', label: '未知', description: '缺少记录或证据不完整,不能解释为健康。' },
45
+ ]} />
46
+ ```
47
+
48
+ `TimelineLegendProps` 为 `label?: string`、`compact?: boolean`、`items?: TimelineLegendItem[]`;item 为 `status: TimelineStatus`、`label: string`、`description?: string`。每种 status 在一组中唯一。以上规则仅为合成示例,宿主必须传入自己的实际证据定义,组件不推断健康评分。
49
+
50
+ 默认无参调用仍显示原五类完整标签。`compact` 把项目保留在一行;容器过窄时仅图例局部横向滚动,不使页面溢出。有 description 的项目可鼠标悬停、键盘聚焦或触屏点按查看,操作区域至少 44px;一次只开一项规则。Enter/Space 从触发器进入详情关闭按钮,Escape 关闭并按需要归还焦点。没有 description 的项目保持只读文本,不制造空交互。
51
+
52
+ `StatusTimelineList` 负责稳定行身份与详情互斥,公共契约见[应用组件](APPLICATION_COMPONENTS.md)。时间窗口、统计范围、unknown/partial/error 与实际业务判定仍由宿主保留;短标签不改变详情证据。
53
+
54
+ 时间轴与图例的浮层内部归一化定位包装层的祖先 CSS zoom,正文保留等效缩放与视口内滚动。此修复仅覆盖这两类详情,不宣称其他浮层已支持主题根 CSS zoom;浏览器原生网页缩放与 CSS zoom 是不同的证据。跨卡鼠标移走后延迟关闭,并允许移入详情继续阅读;键盘焦点和触屏操作不被鼠标离开逻辑打断。
55
+
56
+ ## 验收边界
57
+
58
+ 展台覆盖三主题、400/520/880 宽度、200% 缩放、长值/双成本/零值/未知、可选到期、loading/error/no-link/历史展开、原生下载语义、图例鼠标/键盘/触屏入口。界面仅使用合成数据;库测试和浏览器样机不证明消费方鉴权、真实下载或生产接口通过。
59
+
60
+ ### 2026-10-24 源码候选验证
61
+
62
+ - Node 24:164 项测试、类型检查、构建与资源门禁通过。
63
+ - Chromium:30 个内容布局组合(18 个三主题/三宽度/100–200%组合,加12个400px/200%订阅状态组合)、79 条行为断言通过。有效零值、未知、长双成本、两种到期、历史展开、触屏、键盘及复制失败保留链接均得到检查。
64
+ - 9 组局部内容 200% 图例定位通过;全页 `document.documentElement.style.zoom=2` 的三主题图例及时间轴详情在视口内、可实际点击关闭,并恢复正确焦点。主题根缩放、打开时动态2→1恢复也通过;正文物理尺寸保留2倍,不以抵消字体缩放换取位置通过。
65
+ - 跨两个独立 `StatusTimelineList` 鼠标悬停和鼠标点击后切卡只保留当前详情;移入详情保持,移走关闭,键盘 Enter/Escape 返回正确时间条。
66
+ - 最终运行无 pageerror、console error、外部请求或下载事件。下载仅验证原生 anchor 语义并阻止真实导航;生产下载、npm 发布和浏览器原生工具栏缩放不属于本次证据。
67
+
68
+ 正常880px橙趣样机的事实区高104px;这是当前样机测量,不把不同宿主截图作为严格前后对照。
69
+
70
+ ![紧凑账号摘要与凭证操作,橙趣主题合成样机](assets/compact-account-neo.png)
@@ -0,0 +1,60 @@
1
+ # 日期筛选组合
2
+
3
+ 本模式来自 [#10](https://github.com/linhay/rhine-design/issues/10),适用于记录、报表等单视图的日期过滤。真实可交互样例为 `preview/DateRangeFilterGallery.tsx`,展台章节 `#date-range-filter`。它组合既有公共组件,不新增 `DateRangeFilter` 公共 API,不改变 npm 版本;支持这些能力的消费端可继续固定使用 0.6.0。
4
+
5
+ ## 结构与信息
6
+
7
+ 1. 简短的范围标签之后,使用一个 `SegmentedControl` 放置 2–3 个最常用、互斥的日期预设,例如今天、昨天、本周。一组只有一个外框;不使用三个完整 Button 拼接,不逐项重复粗边与硬阴影。超出这些预设的范围不选中任何快捷项。
8
+ 2. 一个独立的 `Button variant="secondary"` 作为 `Popover` 触发器,持续展示已应用的完整起止日期及展开图标。不要仅显示「更多」或只在悬停时显示真实范围。跨年仍保留两端年份;同一天也保留明确的起止语义。
9
+ 3. Popover 中放置低频预设与 `DateRangePicker`。低频预设即选即用;自定义字段是草稿,由「取消」和「应用日期」结束。用简短文字区分两种提交语义。
10
+ 4. 使用 `Inline gap={2}`、`Stack` 和现有 `Divider` 组织布局。窄容器换行、浮层在视口内避让并可滚动,文字与控件自然增高;不缩小字号、不裁切日期、不在宿主复制组件 CSS。
11
+
12
+ 按钮、分段单选、日期字段和浮层跟随所在主题。组件组合没有额外入场动画;减少动态由已发布组件与主题规则处理。
13
+
14
+ 页面整体放大使用浏览器的网页缩放。0.6.0 的浮层挂载在主题根内;直接给 `.rhine-theme` 设置 CSS `zoom` 会影响浮层定位,不属于本组合支持的布局方式。正文局部 CSS zoom 与浏览器原生缩放是不同的验收项,不能互相代替。
15
+
16
+ ## 状态与提交
17
+
18
+ | 操作 | 已应用范围 | 草稿与浮层 |
19
+ | --- | --- | --- |
20
+ | 选择顶部快捷项 | 立即提交宿主传入的预设 | 关闭浮层 |
21
+ | 打开日期触发器 | 不变、不请求数据 | 从当前已应用值复制草稿 |
22
+ | 修改日期输入 | 不变、不请求数据 | 只更新草稿并显示字段错误 |
23
+ | 选择浮层内预设 | 立即提交完整范围 | 关闭浮层 |
24
+ | 应用完整且有效的日期 | 一次提交完整范围 | 关闭浮层 |
25
+ | 取消、Escape、关闭或点击外部 | 不变、不请求数据 | 放弃草稿;下次打开重新复制当前值 |
26
+ | 禁用 | 保留可读范围 | 所有快捷项和触发按钮禁用 |
27
+
28
+ 应用必须同时满足两端非空、格式有效、起止顺序正确、宿主上下界约束与字段无错误。`required` 保留原生表单验证,`onValidationChange` 用于错误回显和按钮状态;它的回调异步发生,且空字段不自动返回错误,不能单独充当提交门禁。示例使用完整性和范围的同步条件,加上提交时 `form.checkValidity()`。
29
+
30
+ 只有用户触发预设或有效应用时才通知宿主;初始回显、打开、输入、取消、关闭不触发查询。请求、加载、错误、分页复位及过期响应保护由宿主处理。请求失败时不得把草稿当作已成功取得结果;本模式不规定业务结果的缓存策略。
31
+
32
+ ## 键盘与焦点
33
+
34
+ - `SegmentedControl` 使用原生单选。Tab 进入组,方向键切换;禁用项由浏览器跳过。无快捷项命中时仍可用 Tab 进入并选择。
35
+ - 日期触发器有完整可访问名称和 Popover 提供的展开状态;Enter/Space 打开,焦点进入浮层。不要把 Popover 伪装成 Menu:里面含表单,不能使用菜单项键盘模型。
36
+ - Escape、取消、应用和浮层内关闭按钮结束任务后归还触发器焦点。外部点击允许焦点留在用户刚点的控件,不强制抢回;它仍然不得提交草稿。
37
+ - 原生日期输入保留平台日期选择与键盘方式;错误由 `DateRangePicker` 关联。浮层过长时允许自身滚动,底部取消和应用必须可达。
38
+
39
+ ## 宿主边界
40
+
41
+ `DateRange` 是 ISO 日期字符串 `{ start, end }`。日期预设、周起点、时区、首尾是否包含、自然日到 API 时间戳的转换和查询约束都属于宿主。不要在设计库固化「本周」的业务定义,也不要将单个产品的模型、角色、接口或内网地址带入组件。
42
+
43
+ 展台固定基准为 2026-10-24,仅用合成日期。`DateRangeFilterExample` 只在 `preview/` 中作为真实组件组合与验证样例导出,不从包入口导出,不应当作新组件依赖。
44
+
45
+ ## 验收
46
+
47
+ - 预设即选即用、自定义仅显式有效提交;空端、反向和越界不提交。
48
+ - 取消、Escape、关闭与外部点击不改变范围,重新打开草稿恢复;焦点遵循上述规则。
49
+ - 默认、自定义、跨年、长标签、禁用状态均可操作或正确禁用。
50
+ - Rhine、战术终端、橙趣三主题;400/520/880 宽度与 200% 缩放;无页面横向溢出、日期截断、浮层或操作不可达。
51
+ - 真实浏览器核对原生 radio 方向键、焦点归还、外点与浮层避让;JSDOM 不能替代布局和原生键盘证据。
52
+
53
+ ### 本次验证(2026-10-24)
54
+
55
+ - `npm run check`:143 项测试通过(其中日期组合 6 项),类型检查、生产构建及资源检查通过。
56
+ - Chromium 真实交互:原生方向键、Enter 打开、取消/Escape/外点不提交、重新打开恢复、缺端/反向/越界阻止提交、跨年应用、更多预设、禁用与减少动态检查通过。
57
+ - 三主题 × 400/520/880 视口 × 100%/200% 内容缩放,共 18 个布局;快捷项/长标签/日期按钮无水平越界,浮层保持视口内、底部应用操作可达。200% 指样稿内容 CSS zoom;浮层复用主题根中的原生尺寸,并非浏览器工具栏缩放或系统辅助技术认证。
58
+ - 76 项浏览器断言通过,无未捕获异常、无外部请求。此证据针对本地真实组件展台;不包含任何消费产品或服务端查询验收。
59
+ - 合并前复验:Node 24 下 143 项测试、类型检查、构建及上述 76 项浏览器断言再次通过。另在独立 Chrome 配置中将「网页缩放」设为 200%,不设置页面 CSS zoom;三主题 × 400/520/880px 共 9 组检查通过。实际 CSS 视口分别为 200/260/440px,浮层位于视口内,应用按钮经滚动可点击,跨年日期应用与焦点归还均正常。
60
+ - 单独给整个主题根设置 CSS `zoom: 2` 的 9 组实验出现浮层偏移及按钮不可达,因此没有计入通过项;保留上面的使用边界。该限制属于现有浮层,组合样例不通过新增组件样式或第二套日期实现绕过它。
@@ -13,9 +13,16 @@
13
13
  - 交互连续:忙碌保留焦点,浮层有明确退出与焦点归还;快速重播和打断不留下透明遮挡。
14
14
  - 组件优先:先更新规范、组件与真实展台,再由消费项目选择版本;业务流程不进入通用组件。
15
15
  - 动效克制:反馈服务于状态理解,就绪不等待装饰,减少动态不削弱功能。
16
+ - 以用户任务为中心消融:先保留身份、当前动作、结果与必要后果;去掉重复状态和不影响决策的机制长文,历史按需展开。已有外层 Surface 时,只读额度与事实优先无框紧凑组合;压缩不靠缩小字号、裁数字或隐藏未知/失败。具体取舍与反例见[紧凑摘要与凭证操作](COMPACT_ACCOUNT_PATTERNS.md)。
16
17
 
17
18
  密集任务浮层的落地规则:先按用户决策划分常显/按需信息;区域标题、数量和操作集中到导航行;短内容自然撑高、默认单层滚动;可见间距与44px操作范围分别验证。详见[场景与交互规范](INTERACTION_PATTERNS.md#弹窗高度与滚动归属)和[间距组合](../components/SPACING.md#视觉间距与操作范围),选择即保存仅按其适用边界使用。
18
19
 
20
+ ### 工作区顶部与页面身份
21
+
22
+ 侧栏已经明确当前页面的平级工作区,顶部默认不再重复“工作空间 / 当前页”面包屑或页面大标题。顶部用于导航展开、当前对象/连接状态与实际操作;正文直接呈现任务内容。保留主内容的可访问名称、一级标题(可用 `rhine-sr-only`)以及导航 `aria-current`,路由变化后仍将焦点移入正文。
23
+
24
+ 移除标题不等于移除操作行:窄屏抽屉、收起侧栏后的恢复入口、刷新和必要状态必须持续可达。没有可见内容的操作容器无需为标题预留额外高度;若承载恢复入口,应按实际可见控件保留。只有存在真实父子资源层级且用户需要返回上级时才使用 Breadcrumbs,不把它作为每个页面必填的装饰。组件本身继续提供该能力。组合与验收见[应用布局契约](APPLICATION_COMPONENTS.md#平级工作区顶部操作区)。
25
+
19
26
  ## 3. 视觉变量
20
27
 
21
28
  公开样式接口统一使用 `rhine-`:主题根 `.rhine-theme`、变量 `--rhine-*`、组件类与关键帧 `rhine-*`、减少动态属性 `data-rhine-motion="reduced"`。消费项目的品牌名称只通过内容与组件参数传入,不进入共享命名空间。
@@ -0,0 +1,15 @@
1
+ # 浮层的缩放与可达性
2
+
3
+ 自 0.8.0 提供,对应 #16;旧版本不因此获得此能力。
4
+
5
+ Popover、Menu、Tooltip、StatusTimeline 与 TimelineLegend 的 Portal 保留在最近的 `.rhine-theme` 内,继承主题与减少动态偏好。所有这些浮层共用定位缩放修正,不需要消费方覆盖 Radix 包装层或库选择器。
6
+
7
+ - 文档或主题根的祖先 CSS `zoom`:定位坐标归一化,正文与 44px 点击目标仍按用户设置放大。浮层宽高受视口限制,长内容在内部滚动;左右空间不足时优先改为上下展开,避免菜单首次松开指针时误选;最后以视口边界限制定位,优先保证操作可达。
8
+ - 正文局部 CSS `zoom`:锚点按实际像素定位,Portal 内容沿主题根继承缩放,不继承已经离开的正文节点的缩放。
9
+ - 浏览器原生缩放:由浏览器改变 CSS 视口处理,不能用 CSS zoom 或截图缩小代替其验收。
10
+ - 打开期间变更祖先样式、主题、窗口尺寸会重新测量,不重新挂载内容或清空草稿。Escape、外点关闭、菜单键盘与焦点归还仍由 Radix 管理。
11
+ - 不支持把主题根置于 `transform`、`filter` 或 `contain` 创建的固定定位包含块中;这与 CSS zoom 是不同约束。
12
+
13
+ 真实展台 `#overlay-bounds` 使用独立 iframe,提供三主题、400/520/880px、100%/200%、文档/主题根/正文、四角锚点和四个优先方向。账号长文本、日期字段、取消/应用、菜单末项均为合成数据。
14
+
15
+ 浏览器回归入口为 `scripts/check-overlay-bounds.js`,使用 Playwright CLI 的 `run-code --filename`。先运行 `npm run build && npx vite preview --host 127.0.0.1 --port 18480 --strictPort`;脚本固定使用 18480,并非 `npm run dev` 的默认端口。检查实际浮层和操作边界、内部滚动后的真实点击、焦点归还,以及打开期间动态缩放的输入节点与草稿保持;仅 `document.scrollWidth` 无法证明浮层没有越界。
@@ -2,6 +2,8 @@
2
2
 
3
3
  本规范面向通用工具、管理界面和数据应用。先确定用户任务、信息是否需要持续可见、是否允许中断,再选组件。视觉沿用莱茵设计;行为参考成熟模式。`preview/PatternGallery.tsx` 提供可操作的选型示例。
4
4
 
5
+ 密集账号摘要、可下载凭证的操作组及内容消融见[紧凑摘要与凭证操作](COMPACT_ACCOUNT_PATTERNS.md):先依据用户任务保留身份、动作、结果和后果,合并重复提示,将历史渐进展示;使用真实 ButtonLink 与 compact 图例,不在业务层另造按钮外观。
6
+
5
7
  **版本边界:**本页新增的 SideNav、Breadcrumbs、Steps、Drawer、Fieldset、ErrorSummary、StatusTimeline、TimelineLegend 及 Dialog.sidePanel 自 0.4.0 提供。npm 0.3.0 不包含它们;接入前核对安装版本及 `components/index.ts`。
6
8
 
7
9
  ## 选择与关联证据并排
@@ -47,9 +49,11 @@ SideNav采用44px最小操作行、14/22文字、4px行间距,组间16px;图
47
49
 
48
50
  ## 快速决策
49
51
 
52
+ 日期过滤优先采用[日期筛选组合](DATE_RANGE_FILTER.md):少量常用预设使用一个 SegmentedControl,具体范围由独立日期按钮持续显示,Popover 内组合低频预设与 DateRangePicker 草稿。预设即时生效,自定义显式应用;取消与关闭不提交。真实展台为 `#date-range-filter`,组合使用现有 0.6.0 公开组件,没有新增公共 API。
53
+
50
54
  | 场景 | 组件组合 | 交互与状态 | 不宜使用 |
51
55
  | --- | --- | --- | --- |
52
- | 跨工作区、跨模块 | SideNav + Breadcrumbs | 当前页有文字、边界与 aria-current;路由变化后由宿主聚焦主内容 | Tabs 不代替全局导航;面包屑不显示浏览历史 |
56
+ | 跨工作区、跨模块 | SideNav;仅真实父子层级按需加 Breadcrumbs | 当前页有文字、边界与 aria-current;平级工作区顶部保留状态与操作,不重复页名;路由变化后由宿主聚焦主内容 | Tabs 不代替全局导航;面包屑不显示浏览历史、不作必填标题 |
53
57
  | 同一对象的内容页签 | Tabs | 方向键选项、隐藏面板退出可访问树;焦点留在选项 | 不用一串按钮伪装页签 |
54
58
  | 单视图指标或显示模式 | SegmentedControl | 原生单选,状态即时更新 | 不用于独立页面或执行命令 |
55
59
  | 多字段配置 | Fieldset + TextField/Select + ErrorSummary | 提交后校验,摘要聚焦,链接回字段,保留已填值 | 不只弹 Toast;不在每次输入时抢焦点 |
@@ -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
+ ![橙趣主题中的中子星艺术](assets/neutron-star-theme.png)
121
+
122
+ ![原始深空配色](assets/neutron-star-original.png)