@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +19 -8
  3. package/components/Actions.tsx +18 -1
  4. package/components/AdvancedForms.tsx +38 -8
  5. package/components/AppShell.tsx +46 -17
  6. package/components/ListFilterBar.tsx +104 -0
  7. package/components/Media.tsx +7 -7
  8. package/components/NeutronStarBackdrop.tsx +43 -0
  9. package/components/OrbitBackdrop.tsx +11 -3
  10. package/components/Overlays.tsx +37 -8
  11. package/components/README.md +28 -3
  12. package/components/StatusTimeline.tsx +72 -6
  13. package/components/StatusTimelineList.tsx +38 -0
  14. package/components/ThemePicker.tsx +49 -0
  15. package/components/assets/orbit-bird.svg +578 -0
  16. package/components/assets/orbit-crystal.svg +11 -0
  17. package/components/assets/orbit-donut.svg +16 -0
  18. package/components/assets/theme-previews/arknights.png +0 -0
  19. package/components/assets/theme-previews/manifest.json +24 -0
  20. package/components/assets/theme-previews/neo-brutalism.png +0 -0
  21. package/components/assets/theme-previews/rhine.png +0 -0
  22. package/components/common.css +3 -3
  23. package/components/components.tsx +3 -3
  24. package/components/dialog-settings.css +9 -0
  25. package/components/extensions.css +8 -32
  26. package/components/index.ts +12 -5
  27. package/components/list-filter-bar.css +33 -0
  28. package/components/media-parity.css +55 -2
  29. package/components/neutron.css +53 -0
  30. package/components/neutronRenderer.ts +327 -0
  31. package/components/orbit.css +100 -1
  32. package/components/orbitBirdFlight.ts +34 -0
  33. package/components/orbitBirdGeometry.ts +158 -0
  34. package/components/orbitDonut.ts +134 -0
  35. package/components/orbitDonutThree.ts +208 -0
  36. package/components/orbitObjectMaterials.ts +76 -0
  37. package/components/orbitObjects.ts +71 -0
  38. package/components/orbitObjectsThree.ts +215 -0
  39. package/components/orbitProjection.ts +21 -0
  40. package/components/orbitRenderer.ts +163 -46
  41. package/components/styles.css +3 -1
  42. package/components/theme-neo-brutalism.css +3 -2
  43. package/components/theme-picker.css +17 -0
  44. package/components/theme.tsx +2 -8
  45. package/components/themeMetadata.ts +7 -0
  46. package/components/timeline.css +17 -6
  47. package/components/useFloatingScale.ts +96 -0
  48. package/docs/APPLICATION_COMPONENTS.md +52 -3
  49. package/docs/BACKGROUND_FILTER_VERIFICATION.md +12 -0
  50. package/docs/COMPACT_ACCOUNT_PATTERNS.md +70 -0
  51. package/docs/DATE_RANGE_FILTER.md +60 -0
  52. package/docs/DESIGN_SYSTEM.md +11 -0
  53. package/docs/FLOATING_OVERLAYS.md +15 -0
  54. package/docs/INTERACTION_PATTERNS.md +10 -2
  55. package/docs/LIST_FILTER_BAR.md +73 -0
  56. package/docs/NEUTRON_STAR_BACKDROP.md +122 -0
  57. package/docs/ORBIT_BACKDROP.md +61 -44
  58. package/docs/SETTINGS_DIALOG.md +21 -0
  59. package/docs/THEME_PICKER.md +26 -0
  60. package/docs/THEMING.md +2 -0
  61. package/docs/assets/compact-account-neo.png +0 -0
  62. package/docs/assets/neutron-star-original.png +0 -0
  63. package/docs/assets/neutron-star-theme.png +0 -0
  64. package/docs/assets/orbit-bird.png +0 -0
  65. package/docs/assets/orbit-crystal.png +0 -0
  66. package/docs/assets/orbit-strawberry.png +0 -0
  67. package/package.json +8 -3
  68. package/preview/BackgroundFiltersFixture.tsx +28 -0
  69. package/preview/CompactAccountGallery.tsx +178 -0
  70. package/preview/DateRangeFilterGallery.tsx +109 -0
  71. package/preview/ExtensionsGallery.tsx +27 -18
  72. package/preview/GalleryShell.tsx +3 -3
  73. package/preview/ListFilterBarGallery.tsx +170 -0
  74. package/preview/MonitoringGallery.tsx +2 -0
  75. package/preview/NeutronStarGallery.tsx +56 -0
  76. package/preview/OrbitGallery.tsx +49 -32
  77. package/preview/OverlayBoundsGallery.tsx +49 -0
  78. package/preview/SettingsDialogGallery.tsx +33 -0
  79. package/preview/ThemePickerGallery.tsx +24 -0
  80. package/preview/ThemeSample.tsx +14 -0
  81. package/preview/TimelineListGallery.tsx +36 -0
  82. package/preview/extensions.css +9 -0
  83. package/preview/main.tsx +19 -3
  84. package/preview/preview.css +23 -0
  85. package/preview/theme-sample.html +1 -0
  86. package/scripts/check-background-filters.js +58 -0
  87. package/scripts/check-background-motion.js +169 -0
  88. package/scripts/check-filter-native-date.js +47 -0
  89. package/scripts/check-overlay-bounds.js +76 -0
  90. package/scripts/check-settings-theme.js +88 -0
  91. package/skills/rhine-design/SKILL.md +3 -2
  92. package/skills/rhine-design/references/API_MAP.md +13 -2
  93. package/skills/rhine-design/references/TASK_MAP.md +8 -2
  94. package/skills/rhine-design/references/integration.md +1 -1
  95. package/tests/AppShellControls.test.tsx +82 -0
  96. package/tests/ButtonLink.test.tsx +94 -0
  97. package/tests/CompactAccountGallery.test.tsx +116 -0
  98. package/tests/DateNativeInput.test.tsx +147 -0
  99. package/tests/DateRangeFilterGallery.test.tsx +85 -0
  100. package/tests/ListFilterBar.test.tsx +198 -0
  101. package/tests/NeutronStarBackdrop.test.tsx +288 -0
  102. package/tests/NeutronStarGallery.test.tsx +62 -0
  103. package/tests/NeutronStarImport.test.tsx +15 -0
  104. package/tests/OrbitBackdrop.test.tsx +273 -9
  105. package/tests/OrbitDonutThree.test.tsx +91 -0
  106. package/tests/OrbitObjectsThree.test.tsx +164 -0
  107. package/tests/SettingsDialog.test.tsx +66 -0
  108. package/tests/StatusTimelineList.test.tsx +63 -0
  109. package/tests/ThemePicker.test.tsx +30 -0
  110. package/tests/TimelineLegend.test.tsx +60 -0
@@ -0,0 +1,17 @@
1
+ .rhine-theme-picker { min-width:0; margin:0; padding:0; border:0; }
2
+ .rhine-theme-picker>legend { padding:0; margin-bottom:var(--rhine-space-3); font-weight:var(--rhine-weight-heading); }
3
+ .rhine-theme-picker__options { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr)); gap:var(--rhine-space-4); }
4
+ .rhine-theme-option { position:relative; display:grid; align-content:start; gap:var(--rhine-space-3); min-width:0; min-height:44px; padding:var(--rhine-space-3); border:1px solid var(--rhine-control); border-radius:var(--rhine-radius-card); background:var(--rhine-surface); color:var(--rhine-ink); cursor:pointer; }
5
+ .rhine-theme-option:has(input:checked) { border-color:var(--rhine-green); box-shadow:inset 0 0 0 2px var(--rhine-green); }
6
+ .rhine-theme-option:has(input:focus-visible) { outline:3px solid var(--rhine-green); outline-offset:3px; }
7
+ .rhine-theme-option:has(input:disabled) { opacity:.55; cursor:not-allowed; }
8
+ .rhine-theme-option:hover:not(:has(input:disabled)) { background:var(--rhine-selected); }
9
+ .rhine-theme-option__copy { display:grid; gap:var(--rhine-space-2); min-width:0; overflow-wrap:anywhere; }
10
+ .rhine-theme-option__description { font-size:var(--rhine-text-caption); line-height:var(--rhine-leading-caption); color:var(--rhine-muted); }
11
+ .rhine-theme-option__status { font-size:var(--rhine-text-caption); min-height:24px; }
12
+ .rhine-theme-preview { position:relative; display:grid; place-items:center; aspect-ratio:8/5; min-width:0; overflow:hidden; border:1px solid var(--rhine-line); border-radius:var(--rhine-radius-control); background:var(--rhine-bg); color:var(--rhine-muted); font-size:var(--rhine-text-caption); }
13
+ .rhine-theme-preview img { display:block; width:100%; height:100%; object-fit:contain; }
14
+ .rhine-theme-preview__loading { position:absolute; inset:0; display:grid; place-items:center; background:var(--rhine-bg); }
15
+ .rhine-theme[data-rhine-theme=neo-brutalism] .rhine-theme-option { border:2px solid var(--rhine-ink); box-shadow:var(--rhine-hard-shadow); }
16
+ .rhine-theme[data-rhine-theme=neo-brutalism] .rhine-theme-option:has(input:checked) { box-shadow:inset 0 0 0 2px var(--rhine-green),var(--rhine-hard-shadow); }
17
+ @media(forced-colors:active) { .rhine-theme-option:has(input:checked) { outline:3px solid Highlight; } }
@@ -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,12 @@
1
+ # 0.9.0 背景与筛选验收
2
+
3
+ 基线:v0.8.0;目标:#17、#19。2026-10-24 Asia/Shanghai,macOS Chromium,固定 Vite production preview。展示记录均为 synthetic,无业务 API;不宣称 Gateway 生产权限、查询或部署已验证。
4
+
5
+ - `npm run check`:31 文件、245 测试、TypeScript 与构建通过。
6
+ - `scripts/check-background-filters.js`:三主题 × 400/520/880px × 1/2 CSS zoom × 三类列表,共54组;主筛选上限、摘要清除、焦点归还、加载/错误/空态/禁用;背景三主题 ×400/520/880/1280px ×1/2 CSS zoom,共24组,文字避让、不透明表单、无控件/边框、容器断点和输入/Canvas节点保留。
7
+ - `scripts/check-filter-native-date.js`:真实日期分段键盘八项,删除片段不扩大已接受范围、补全年份、初始空值的 keyup-only 片段、清空及同值预设恢复、三种展台整体重置清理同值日期草稿。
8
+ - `scripts/check-background-motion.js`:Three.js实际像素变化,手动/系统减少动态、恢复、零尺寸、离屏、模拟 document.hidden、卸载停帧/观察器清理,Canvas不可用时三主题静态几何与实际画布一致。自动化 Chromium 的标签切换不产生真实 document.hidden,保留该限制;不以事件模拟冒充真实后台切换。GPU内部共享资源由renderer.dispose/forceContextLoss收口,不能仅以独立delete调用数量判泄漏。
9
+
10
+ 审查修复:搜索 label 保持可见,整体重置同步清理日期内部草稿;独立复核修复:有效日期被宿主拒绝时保持受控值;展台窗口断点改为容器查询;提供宽艺术栏/窄全页的同节点入口组合;仅剩非聚焦字段摘要时,清除后焦点落在筛选区域。首轮浏览器脚本在Drawer动画未结束/焦点恢复前断言,稳定状态复核已修正;原始失败记录与修正后证据保留在根仓库 `.local/artifacts/rhine-design-v0.9.0/`。
11
+
12
+ 运行脚本前用 `npm run build`,将 dist 固定服务到127.0.0.1:18484。动效脚本须新浏览器会话,防止旧 addInitScript instrumentation 重复注册。公开脚本不包含认证或业务数据。
@@ -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,20 @@
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
+
26
+ ### 列表顶部的复杂度约束
27
+
28
+ 列表搜索与筛选优先使用共享 `ListFilterBar`:可选搜索、最多两个常显条件、统一的更多筛选 Drawer、活跃条件摘要、结果与刷新,再加至多一个业务操作。少条件页面不补空入口,多条件页面不靠新增控件行扩张。筛选业务状态与服务端查询仍归宿主,Drawer 内同步显示加载/错误;批量动作留在结果区。该 API 在 0.9.0 新增,消费时核对安装版本;契约、消融依据与真实展台见[列表筛选栏](LIST_FILTER_BAR.md)。
29
+
19
30
  ## 3. 视觉变量
20
31
 
21
32
  公开样式接口统一使用 `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,15 @@ SideNav采用44px最小操作行、14/22文字、4px行间距,组间16px;图
47
49
 
48
50
  ## 快速决策
49
51
 
52
+ 列表顶部的搜索、排序与条件使用 [ListFilterBar](LIST_FILTER_BAR.md) 约束,最多两个常显条件,其余在同一 Drawer 内即时修改;结果反馈在抽屉内也可见。无搜索场景不增加空搜索,批量操作不混入筛选。该组件在 0.9.0 新增,`Toolbar` 的任意 children 组合不能代替这些约束。
53
+
54
+ 日期过滤优先采用[日期筛选组合](DATE_RANGE_FILTER.md):少量常用预设使用一个 SegmentedControl,具体范围由独立日期按钮持续显示,Popover 内组合低频预设与 DateRangePicker 草稿。预设即时生效,自定义显式应用;取消与关闭不提交。真实展台为 `#date-range-filter`,组合使用现有 0.6.0 公开组件,没有新增公共 API。
55
+
56
+ 原生日期框的未完成片段可能同时满足 `value === ''` 与 `validity.badInput`,不能当作用户清空范围。DateField 保留未完成草稿并报告校验,DateRangePicker 汇总字段错误;补全原生片段后才回传日期值,范围限制继续由校验与宿主提交边界处理,明确清空与快捷项可丢弃草稿。整体重置必须通知局部日期状态,即使已接受日期字符串没有变化。验收使用真实键盘编辑日期片段,并证明期间没有扩大查询范围;只设置完整 ISO 值的测试不覆盖此行为。此修复随本地列表筛选候选验证,不倒推为既有已发布版本能力。
57
+
50
58
  | 场景 | 组件组合 | 交互与状态 | 不宜使用 |
51
59
  | --- | --- | --- | --- |
52
- | 跨工作区、跨模块 | SideNav + Breadcrumbs | 当前页有文字、边界与 aria-current;路由变化后由宿主聚焦主内容 | Tabs 不代替全局导航;面包屑不显示浏览历史 |
60
+ | 跨工作区、跨模块 | SideNav;仅真实父子层级按需加 Breadcrumbs | 当前页有文字、边界与 aria-current;平级工作区顶部保留状态与操作,不重复页名;路由变化后由宿主聚焦主内容 | Tabs 不代替全局导航;面包屑不显示浏览历史、不作必填标题 |
53
61
  | 同一对象的内容页签 | Tabs | 方向键选项、隐藏面板退出可访问树;焦点留在选项 | 不用一串按钮伪装页签 |
54
62
  | 单视图指标或显示模式 | SegmentedControl | 原生单选,状态即时更新 | 不用于独立页面或执行命令 |
55
63
  | 多字段配置 | Fieldset + TextField/Select + ErrorSummary | 提交后校验,摘要聚焦,链接回字段,保留已填值 | 不只弹 Toast;不在每次输入时抢焦点 |
@@ -61,7 +69,7 @@ SideNav采用44px最小操作行、14/22文字、4px行间距,组间16px;图
61
69
  | 持续错误、离线或权限状态 | Notice | 就地说明原因、影响、下一步;需行动的信息持续显示 | 不用自动消失通知掩盖失败 |
62
70
  | 首次加载 | Skeleton / Progress | 已知结构用骨架,未知进度不写百分比 | 不伪造进度,不循环品牌标志 |
63
71
  | 保存、刷新 | Button busy + 保留旧数据 | 防重复提交;刷新明确旧值和更新时间;真实响应决定成败 | 不清空阅读上下文,不提前显示成功 |
64
- | 大列表查询 | SearchField / Combobox + DataTable + Pagination | 宿主处理防抖、IME、取消与过期响应;服务端返回筛选排序分页 | 不为 UI 便利拉取全量本地筛选 |
72
+ | 大列表查询 | ListFilterBar + DataTable + Pagination;候选 API 先核对安装版本 | 最多两个常显条件;宿主处理防抖、IME、取消与过期响应;服务端返回筛选排序分页 | 不为 UI 便利拉取全量本地筛选,不把低频条件继续堆在顶部 |
65
73
  | 少量/较多互斥选项 | RadioGroup / Select | Radio 展开选项便于比较,Select 压缩较长清单 | Menu 是命令,不能代替选择器 |
66
74
  | 多选 / 即时二态设置 | Checkbox / Switch | Checkbox 选择项;Switch 即时偏好,若需保存明确说明 | 不混同“选中”与“已生效” |
67
75
  | 补充说明 / 局部交互 | Tooltip / Popover | Tooltip 不放操作;Popover 可交互;鼠标、键盘均可触发 | 必要说明必须常驻,不能只在 hover 下出现 |
@@ -0,0 +1,73 @@
1
+ # 列表筛选栏
2
+
3
+ `ListFilterBar` 把列表顶部限制为搜索、最多两个常显条件、更多筛选、结果反馈、刷新与一个业务操作。它统一信息层级与布局,不承接查询、权限、分页或业务筛选状态。需求来源为 [#19](https://github.com/linhay/rhine-design/issues/19),真实展台为 `/#list-filter-bar`。
4
+
5
+ **版本边界:本组件在 0.9.0 新增。** 0.8.0 及更早版本不提供该组件;消费时必须核对实际导出及固定源码/安装快照。图库跟随源码,不能代替 npm 发布证据。
6
+
7
+ ## 先决定常显内容
8
+
9
+ - 搜索在用户确实需要查找记录时提供;统计范围等场景无需填充一个无用搜索框。
10
+ - `primary` 只常显前两项,超出的项目自动进入“更多筛选”。选择高频且互相独立的条件,例如状态和排序;不要为了展开所有控件增加第二个筛选栏。
11
+ - 其他条件进入同一个 Drawer。字段自己的 label 持续可见,组件不重复加可见标题,也不提供任意 `children` 让页面插回一排控件。
12
+ - 非默认条件用短摘要说明当前限制;单项清除与整体重置都由宿主定义。默认排序、默认统计日期等是否需要摘要由宿主的 `activeLabel` 决定,不把所有非空值都视为筛选。
13
+ - 结果数量、更新中与错误反馈在栏内呈现;Drawer 打开时同样显示反馈。失败不能藏在遮罩后的页面上,也不能把旧数量当作新条件已经成功。
14
+ - 刷新之外最多一个业务操作。多记录的选择、批量操作属于结果区,录制启停等对象设置也不塞进筛选栏。
15
+ - 平级工作区已有侧栏身份时不重复大标题。组件的 `label` 提供可访问分组名称,宿主仍负责页面标题、主内容名称及路由焦点。
16
+
17
+ 布局按组件实际容器宽度重排,窄嵌入区域和 200% 内容缩放保持完整文字、可达操作和 44px 目标;不以省略日期、缩小文字或横向撑破页面换取一行。三套主题使用同一组件结构与 tokens。
18
+
19
+ ## 公共契约
20
+
21
+ | 属性 | 输入与责任 |
22
+ | --- | --- |
23
+ | `label` | 必填分组名称,也是更多筛选 Drawer 的标题 |
24
+ | `search` | 可选原生 input 属性加必填 `label`;支持受控 value、change、composition、disabled;内部为 TextField |
25
+ | `primary` | `ListFilterField[]`;最多前两项常显,其他顺序移入 Drawer |
26
+ | `filters` | `ListFilterField[]`;按顺序追加到 Drawer |
27
+ | `onReset` | 宿主清除搜索与恢复各条件默认值;有搜索或 activeLabel 才显示重置入口 |
28
+ | `refresh` | `{onClick,busy?,disabled?}`;原生共享 Button,不自动执行请求 |
29
+ | `action` | 可选单个 ReactElement,代表一个业务操作;消费方不能用 Fragment 或自建按钮组绕过数量限制 |
30
+ | `result` | 宿主提供的结果摘要;数量未知时明确显示未知,不用已加载条数冒充总数 |
31
+ | `pending` / `error` | 正在更新 / 失败说明;在栏与已展开 Drawer 中保持一致 |
32
+ | `className` | 只用于外层组合布局,不覆盖组件内部视觉或状态 |
33
+
34
+ 字段包含 `{id,label,control,activeLabel?,onClear?}`。`id` 在当前栏内唯一,用于稳定标识;它不是输入框 DOM id。`control` 提供真实受控组件及自己的可见/可访问标签;字段包装只添加分组语义。`activeLabel` 是宿主判断后的非默认条件摘要;`onClear` 缺省时摘要只读。不同实例的输入 DOM id 仍须唯一,优先使用组件自动生成的 id。
35
+
36
+ ```tsx
37
+ <ListFilterBar
38
+ label="会话筛选"
39
+ search={{ label: '搜索会话', value: query, onChange: event => setQuery(event.target.value) }}
40
+ primary={[
41
+ {
42
+ id: 'state', label: '会话状态',
43
+ control: <Select label="会话状态" value={state} onChange={event => setState(event.target.value)}>
44
+ <option value="all">全部状态</option><option value="done">已完成</option>
45
+ </Select>,
46
+ activeLabel: state === 'all' ? undefined : '已完成',
47
+ onClear: () => setState('all'),
48
+ },
49
+ ]}
50
+ filters={additionalFields}
51
+ onReset={resetFilters}
52
+ refresh={{ onClick: reload, busy: loading }}
53
+ result={total === null ? '总数待确认' : `${total} 条结果`}
54
+ pending={loading}
55
+ error={error}
56
+ />
57
+ ```
58
+
59
+ 示例中的状态、函数与额外字段由宿主提供。使用受控 `search.value` 才能准确决定搜索是否需要重置;组件不解析其他控件的值。
60
+
61
+ ## 状态与查询边界
62
+
63
+ 更多筛选是同一组状态的展开视图,不建立另一套草稿/应用流程。普通选择即时回传宿主;关闭、点击外部与 Escape 只收起抽屉,不撤销已经接受的条件。业务本来要求完整范围显式应用的日期控件可以保留其局部草稿,但应在 Drawer 内内联展示,不再嵌套日期 Drawer。
64
+
65
+ 服务端搜索、权限裁剪、默认值、URL 与偏好、请求防抖、IME、取消和过期响应保护都留在宿主。输入中的值不等于最后接受的结果;失败保留还是回滚条件按接口契约处理,并把结果/错误传入组件。不要为了接入组件改成拉取全量数据在浏览器过滤。
66
+
67
+ Drawer 复用已有模态、Escape 与焦点归还;移除当前聚焦摘要时焦点回到有效控件。忙碌与禁用不同:busy 保留焦点,禁用字段由宿主明确传入;不因一次后台刷新禁用所有条件。只读身份应从传入字段中移除其无权使用的条件,不能只改变颜色或 CSS 隐藏。
68
+
69
+ ## 真实展台与验收
70
+
71
+ `preview/ListFilterBarGallery.tsx` 提供独立会话列表、420px 嵌入列表、无搜索统计范围三种组合。独立例子刻意传入第三项 primary,以验证运行时移入 Drawer;全局状态选择使用真实控件演示正常、加载、无结果、错误与禁用。记录与操作均为合成示例,无业务请求或文件下载。
72
+
73
+ 验收至少覆盖:三主题与真实窄容器;400/520/880px、200% CSS zoom;主条件上限;输入法、键盘和焦点;多个活跃条件的完整摘要、单项/全量重置;Drawer 内加载与失败;筛选后总数、分页与迟到响应。图库证明共享组件组合,产品的服务端权限、导出和查询契约仍分别验收。