@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,109 @@
1
+ import { useState } from 'react';
2
+ import { Button, DateRangePicker, Divider, Icon, Inline, Popover, SegmentedControl, Stack, Surface, Text, type DateRange } from '../components';
3
+
4
+ const quickRanges = [
5
+ { value: 'today', label: '今天', range: { start: '2026-10-24', end: '2026-10-24' } },
6
+ { value: 'yesterday', label: '昨天', range: { start: '2026-10-23', end: '2026-10-23' } },
7
+ { value: 'week', label: '本周', range: { start: '2026-10-19', end: '2026-10-24' } },
8
+ ];
9
+ const moreRanges = [
10
+ { label: '最近 7 天', range: { start: '2026-10-18', end: '2026-10-24' } },
11
+ { label: '最近 30 天', range: { start: '2026-09-25', end: '2026-10-24' } },
12
+ { label: '本月', range: { start: '2026-10-01', end: '2026-10-24' } },
13
+ { label: '上月', range: { start: '2026-09-01', end: '2026-09-30' } },
14
+ ];
15
+ const min = '2025-01-01', max = '2026-12-31';
16
+ const rangeLabel = ({ start, end }: DateRange) => `${start} — ${end}`;
17
+
18
+ // Preview composition only: the host owns presets, calendar boundaries and data queries.
19
+ export function DateRangeFilterExample({ label = '时间范围', initialValue = quickRanges[0].range, disabled = false, longLabels = false }: {
20
+ label?: string; initialValue?: DateRange; disabled?: boolean; longLabels?: boolean;
21
+ }) {
22
+ const [value, setValue] = useState(initialValue);
23
+ const [draft, setDraft] = useState(value);
24
+ const [open, setOpen] = useState(false);
25
+ const [error, setError] = useState<string | null>(null);
26
+ const [applied, setApplied] = useState(0);
27
+ const selected = quickRanges.find(item => item.range.start === value.start && item.range.end === value.end)?.value ?? '';
28
+ const canApply = !disabled && !!draft.start && !!draft.end && draft.start <= draft.end && draft.start >= min && draft.end <= max && !error;
29
+ const commit = (range: DateRange) => {
30
+ if (disabled) return;
31
+ setValue(range); setApplied(count => count + 1); setOpen(false);
32
+ };
33
+ const changeOpen = (next: boolean) => {
34
+ if (next && disabled) return;
35
+ if (next) { setDraft(value); setError(null); }
36
+ setOpen(next);
37
+ };
38
+ return <Stack gap={3}>
39
+ <Inline gap={2} role="group" aria-label={label}>
40
+ <Text variant="caption" tone="muted">{label}</Text>
41
+ <SegmentedControl label={`${label}快捷选择`} items={quickRanges.map(item => ({
42
+ value: item.value, label: longLabels ? `${item.label}的完整自然日范围` : item.label, disabled,
43
+ }))} value={selected} onValueChange={key => {
44
+ const preset = quickRanges.find(item => item.value === key);
45
+ if (preset) commit(preset.range);
46
+ }} />
47
+ <Popover title={`${label}设置`} open={open} onOpenChange={changeOpen} trigger={
48
+ <Button variant="secondary" disabled={disabled} aria-label={`${label}:${rangeLabel(value)},选择日期`}>
49
+ {rangeLabel(value)} <Icon name="chevronDown" size={16} />
50
+ </Button>
51
+ }>
52
+ <Stack gap={4}>
53
+ <Stack gap={2}>
54
+ <Text variant="caption" tone="muted">选择预设后立即应用</Text>
55
+ <Inline gap={2} aria-label="更多时间预设">
56
+ {moreRanges.map(item => <Button key={item.label} variant="secondary" onClick={() => commit(item.range)}>{item.label}</Button>)}
57
+ </Inline>
58
+ </Stack>
59
+ <Divider />
60
+ <form onSubmit={event => {
61
+ event.preventDefault();
62
+ if (canApply && event.currentTarget.checkValidity()) commit(draft);
63
+ }}>
64
+ <Stack gap={3}>
65
+ <DateRangePicker label="自定义日期" value={draft} onValueChange={setDraft} onValidationChange={setError}
66
+ min={min} max={max} required clearable={false} hint="填写完整日期后,点击应用。" />
67
+ <Inline gap={2}>
68
+ <Button variant="secondary" onClick={() => setOpen(false)}>取消</Button>
69
+ <Button type="submit" disabled={!canApply}>应用日期</Button>
70
+ </Inline>
71
+ </Stack>
72
+ </form>
73
+ </Stack>
74
+ </Popover>
75
+ </Inline>
76
+ <Text variant="caption" tone="muted" role="status">已应用:{rangeLabel(value)} · 本次操作 {applied} 次</Text>
77
+ </Stack>;
78
+ }
79
+
80
+ export function DateRangeFilterGallery() {
81
+ return <section id="date-range-filter">
82
+ <div className="ds-section-title"><span>RANGE</span><h2>日期筛选 · 快捷选择与自定义</h2></div>
83
+ <p className="ds-caption">常用日期是一组互斥选项;具体日期持续可见。更多预设即选即用,自定义日期在确认后生效。</p>
84
+ <Stack gap={4}>
85
+ <Surface>
86
+ <Stack gap={4}>
87
+ <Text as="h3" variant="heading">日常筛选</Text>
88
+ <DateRangeFilterExample />
89
+ <Text variant="caption" tone="muted">固定演示日期为 2026-10-24。选择只更新本地样例,不请求业务数据。</Text>
90
+ </Stack>
91
+ </Surface>
92
+ <Surface>
93
+ <Stack gap={4}>
94
+ <Text as="h3" variant="heading">跨年范围与长标签</Text>
95
+ <DateRangeFilterExample label="对比时间范围" initialValue={{ start: '2025-12-28', end: '2026-01-06' }} longLabels />
96
+ <Text variant="caption" tone="muted">超出快捷项的日期不冒充任何预设。窄容器允许换行,完整年份和操作保留。</Text>
97
+ </Stack>
98
+ </Surface>
99
+ <Surface>
100
+ <Stack gap={4}>
101
+ <Text as="h3" variant="heading">暂不可更改</Text>
102
+ <DateRangeFilterExample label="归档日期范围" disabled />
103
+ <Text variant="caption" tone="muted">归档结果的统计范围固定,保持日期可读。</Text>
104
+ </Stack>
105
+ </Surface>
106
+ <Text variant="caption" tone="muted">方向键切换快捷项;Escape 或取消退出草稿并归还焦点,点击外部控件按原生焦点行为继续。所有主题使用同一组公开组件。</Text>
107
+ </Stack>
108
+ </section>;
109
+ }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
- import { AppShell, Background, Button, Checkbox, DataTable, DateField, DateRangePicker, Dialog, EmptyState, MediaCard, MediaViewer, Notice, NumberField, Pagination, RemoteMultiSelect, Select, SideNav, Skeleton, Stack, Surface, TextField, TreeSelect, type AppShellProps, type DateRange, type TreeNode, type TableSort } from '../components';
2
+ import { AppShell, AppShellToggle, IconButton, Inline, Background, Button, Checkbox, DataTable, DateField, DateRangePicker, Dialog, EmptyState, MediaCard, MediaViewer, Notice, NumberField, Pagination, RemoteMultiSelect, Select, SideNav, Skeleton, Stack, Surface, TextField, TreeSelect, type AppShellProps, type DateRange, type TreeNode, type TableSort } from '../components';
3
3
  import type { Choice } from '../components/components';
4
4
  import './extensions.css';
5
5
 
@@ -16,16 +16,24 @@ export function ShellGallery() {
16
16
  const [navDrawerOpen, setNavDrawerOpen] = useState(false);
17
17
  const [current, setCurrent] = useState('inbox'), [draft, setDraft] = useState('主题切换和侧栏折叠会保留这段内容。');
18
18
  const [plain, setPlain] = useState(true), [state, setState] = useState('ready');
19
+ const [inlineControls, setInlineControls] = useState(true);
19
20
  return <section><div className="ds-section-title"><span>LAYOUT</span><h2>画布与应用外壳</h2></div>
20
21
  <p className="ds-caption">单侧栏适合简单工具;双左栏适合目录与列表;左右栏适合编辑器与检查器。可用宽度不足 1120px 时次栏转抽屉,不足 760px 时全局导航也转抽屉。正文始终保留。</p>
21
- <div className="ds-extension-controls"><Select label="布局方案" value={layout} onChange={event => setLayout(event.target.value as AppShellProps['layout'])}><option value="single">单侧栏</option><option value="double-left">双左侧栏</option><option value="left-right">左右侧栏</option></Select><Checkbox label="纯色背景" checked={plain} onChange={event => setPlain(event.target.checked)} /><StateControl value={state} onChange={setState} /></div>
22
- <div className="ds-shell-stage"><AppShell layout={layout} decoration={plain ? 'none' : 'theme'}
23
- navigation={{ label: '全局导航', open: navOpen, onOpenChange: setNavOpen, drawerOpen: navDrawerOpen, onDrawerOpenChange: setNavDrawerOpen, content: <SideNav aria-label="应用导航" currentId={current} groups={[{ label: '工作空间', items: [{ id: 'inbox', label: '收件箱', href: '#app-shell' }, { id: 'archive', label: longName, href: '#app-shell' }] }]} onNavigate={(item, event) => { event.preventDefault(); setCurrent(item.id); setNavDrawerOpen(false); }} /> }}
24
- secondary={{ label: layout === 'left-right' ? '辅助详情' : '上下文目录', open: secondaryOpen, onOpenChange: setSecondaryOpen, content: <Stack gap={4}><strong>{current === 'inbox' ? '收件箱' : longName}</strong>{state === 'loading' ? <Skeleton label="目录加载中" /> : state === 'error' ? <Notice title="目录读取失败" tone="error">可以继续编辑正文。</Notice> : state === 'empty' ? <EmptyState title="目录暂无内容" description="选择其他项目或继续编辑正文。" /> : <><p>当前条目、所属目录和属性。</p><Button variant="secondary" disabled={state === 'disabled'}>上下文操作</Button></>}</Stack> }}
25
- header={<Dialog title="背景与浮层" description="浮层仍然挂载在唯一主题根;不受局部背景裁剪。" trigger={<Button>打开对话框</Button>}><TextField label="对话框输入" placeholder="试试 Tab 和 Escape" /></Dialog>}>
26
- <Stack gap={6}><div><p className="ds-kicker">CURRENT TASK / {current}</p><h3>同一主题,独立正文背景</h3><p>正文默认使用纯色背景。关闭“纯色背景”可显式启用主题纹理;纸面主题保持纯净。</p></div>
22
+ <div className="ds-extension-controls"><Select label="布局方案" value={layout} onChange={event => setLayout(event.target.value as AppShellProps['layout'])}><option value="single">单侧栏</option><option value="double-left">双左侧栏</option><option value="left-right">左右侧栏</option></Select><Checkbox label="使用正文导航栏" checked={inlineControls} onChange={event => setInlineControls(event.target.checked)} /><Checkbox label="纯色背景" checked={plain} onChange={event => setPlain(event.target.checked)} /><StateControl value={state} onChange={setState} /></div>
23
+ <div className="ds-shell-stage"><AppShell controls={inlineControls ? 'inline' : 'header'} layout={layout} decoration={plain ? 'none' : 'theme'}
24
+ navigation={{ label: '全局导航', open: navOpen, onOpenChange: setNavOpen, drawerOpen: navDrawerOpen, onDrawerOpenChange: setNavDrawerOpen, content: <Stack gap={4}>{inlineControls && <Inline><strong>Rhine Workspace</strong><AppShellToggle placement="panel" iconOnly /></Inline>}<SideNav aria-label="应用导航" currentId={current} groups={[{ label: '工作空间', items: [{ id: 'inbox', label: '收件箱', href: '#app-shell' }, { id: 'archive', label: longName, href: '#app-shell' }] }]} onNavigate={(item, event) => { event.preventDefault(); setCurrent(item.id); setNavDrawerOpen(false); }} /></Stack> }}
25
+ secondary={{ label: layout === 'left-right' ? '辅助详情' : '上下文目录', open: secondaryOpen, onOpenChange: setSecondaryOpen, content: <Stack gap={4}>{inlineControls && <AppShellToggle panel="secondary" placement="panel" />}<strong>{current === 'inbox' ? '收件箱' : longName}</strong>{state === 'loading' ? <Skeleton label="目录加载中" /> : state === 'error' ? <Notice title="目录读取失败" tone="error">可以继续编辑正文。</Notice> : state === 'empty' ? <EmptyState title="目录暂无内容" description="选择其他项目或继续编辑正文。" /> : <><p>当前条目、所属目录和属性。</p><Button variant="secondary" disabled={state === 'disabled'}>上下文操作</Button></>}</Stack> }}
26
+ contentHeader={inlineControls ? <nav aria-label="工作区导航"><Inline>
27
+ <AppShellToggle placement="content" iconOnly />
28
+ <h1>{current === 'inbox' ? '收件箱' : longName}</h1>
29
+ <div className="rhine-shell__navigation-actions"><AppShellToggle panel="secondary" placement="content" />
30
+ <Dialog title="工作区信息" description="查看当前工作区的信息。关闭后返回导航入口。" trigger={<IconButton icon="info" label="工作区信息" title="工作区信息" />}><TextField label="对话框输入" placeholder="试试 Tab 和 Escape" /></Dialog>
31
+ </div>
32
+ </Inline></nav> : undefined}
33
+ header={inlineControls ? undefined : <Dialog title="背景与浮层" description="浮层仍然挂载在唯一主题根;不受局部背景裁剪。" trigger={<Button>打开对话框</Button>}><TextField label="对话框输入" placeholder="试试 Tab 和 Escape" /></Dialog>}>
34
+ <Stack gap={6}>{!inlineControls && <h1 className="rhine-sr-only">{current === 'inbox' ? '收件箱' : longName}</h1>}<p>本地草稿 · 整理工作区,继续你的记录。</p>
27
35
  <Surface><Stack gap={4}><TextField label="正文草稿" value={draft} onChange={event => setDraft(event.target.value)} /><Button disabled={state === 'disabled'}>继续当前任务</Button></Stack></Surface>
28
- {Array.from({ length: 5 }, (_, i) => <Surface key={i}><h3>内容段落 {i + 1}</h3><p>滚动正文时顶部操作栏保持可达;每栏仅有一个滚动容器。Tab 可依次访问导航、正文与辅助详情。</p></Surface>)}
36
+ {Array.from({ length: 5 }, (_, i) => <Surface key={i}><h3>内容段落 {i + 1}</h3><p>滚动正文时导航栏保持可达;每栏仅有一个滚动容器。Tab 可依次访问导航、正文与辅助详情。</p></Surface>)}
29
37
  </Stack>
30
38
  </AppShell></div>
31
39
  <div className="ds-extension-grid ds-gap-top">{(['canvas','content','sidebar'] as const).map(role => <Background key={role} roleTone={role} decoration={plain ? 'none' : 'theme'} className="ds-background-swatch"><strong>{role}</strong><Surface>Surface 始终提供不透明阅读表面。</Surface></Background>)}</div>
@@ -76,20 +84,21 @@ export function AdvancedDataGallery() {
76
84
 
77
85
  export function MediaGallery() {
78
86
  const [selected, setSelected] = useState(false), [open, setOpen] = useState(false), [index, setIndex] = useState(0), [metadata, setMetadata] = useState(true), [version, setVersion] = useState('original');
79
- const titles = ['山野 · 午后的光', longName, '视频预览示例'];
87
+ const titles = ['山野 · 午后的光', '远山手记 · 原始影像与拍摄记录', '暮色之间 · 动态影像'];
80
88
  const [retried, setRetried] = useState(false);
81
- return <section><div className="ds-section-title"><span>MEDIA</span><h2>媒体保持比例,画布保持中性</h2></div><p className="ds-caption">照片、RAW、Live 和视频共享卡片契约。中性暗色查看器独立于三套界面主题;媒体请求、视频播放、下载与版本资源均由宿主提供。示例只使用本地内嵌矢量预览。</p>
82
- <MediaViewer open={open} onOpenChange={setOpen} trigger={<Button>打开全屏查看器</Button>} title={titles[index]} description={`${index + 1} / 3 · 左右方向键切换;输入和播放器保留自身按键`} onPrevious={() => setIndex(current => Math.max(0, current - 1))} onNext={() => setIndex(current => Math.min(2, current + 1))} previousDisabled={index === 0} nextDisabled={index === 2} metadataOpen={metadata} onMetadataOpenChange={setMetadata}
89
+ return <section className="ds-media-gallery"><div className="ds-section-title"><span>MEDIA</span><h2>影像与收藏</h2></div><p className="ds-media-intro">浏览、选择,或打开一张影像。保留画面的原始比例,让每一份作品有自己的位置。</p><div className="ds-media-toolbar"><span role="status">3 份资源 · {selected ? '已选 1 项' : '未选择'}</span>
90
+ <MediaViewer open={open} onOpenChange={setOpen} trigger={<Button>打开全屏查看器</Button>} title={titles[index]} description={`${index + 1} / 3 · 左右方向键切换影像`} onPrevious={() => setIndex(current => Math.max(0, current - 1))} onNext={() => setIndex(current => Math.min(2, current + 1))} previousDisabled={index === 0} nextDisabled={index === 2} metadataOpen={metadata} onMetadataOpenChange={setMetadata}
83
91
  actions={<><label>资源版本 <select aria-label="资源版本" value={version} onChange={event => setVersion(event.target.value)}><option value="original">原始资源</option><option value="preview">预览资源</option></select></label><a href={sampleImage} download="rhine-demo.svg">下载演示资源</a></>}
84
92
  metadata={<><h3>媒体信息</h3><p>{titles[index]}</p><p>类型:{index === 2 ? '视频' : '照片'}</p><p>版本:{version}</p><p>日期:未知</p><Dialog title="资源说明" description="Escape 先关闭此层,再关闭媒体查看器。" trigger={<button type="button">打开资源说明</button>}><p>业务元数据由宿主管理。</p></Dialog></>}
85
93
  >{index === 2 ? <video controls poster={sampleImage} aria-label="视频播放器示例" /> : <img src={sampleImage} alt="山峰与太阳构成的演示插画" />}</MediaViewer>
86
- <div className="ds-extension-grid ds-gap-top">
87
- <MediaCard title={titles[0]} kind="photo" date="2026-10-23" preview={<img src={sampleImage} alt="山野插画" />} onOpen={() => { setIndex(0); setOpen(true); }} selected={selected} onSelectedChange={setSelected} />
94
+ </div><div className="ds-media-grid">
95
+ <MediaCard title={titles[0]} kind="photo" date="2026-10-23" preview={<img src={sampleImage} alt="山野插画" />} onOpen={() => { setIndex(0); setOpen(true); }} selected={selected} onSelectedChange={setSelected} aspectRatio="3 / 2" />
88
96
  <MediaCard title={titles[1]} kind="RAW" preview={<img src={sampleImage} alt="原始预览" />} aspectRatio="3 / 2" onOpen={() => { setIndex(1); setOpen(true); }} />
89
- <MediaCard title="视频,保持原始比例" kind="video" preview={<img src={sampleImage} alt="视频封面" />} onOpen={() => { setIndex(2); setOpen(true); }} />
90
- <MediaCard title="加载中的 Live 资源" kind="Live" state="loading" onOpen={() => setOpen(true)} />
91
- <MediaCard title="不可解码的文件" kind="RAW" state={retried ? 'ready' : 'error'} preview={retried ? <img src={sampleImage} alt="重试后恢复的示例预览" /> : undefined} onOpen={() => { setIndex(1); setOpen(true); }} actions={<Button variant="secondary" onClick={() => setRetried(true)} disabled={retried}>{retried ? '预览已恢复' : '重试预览'}</Button>} />
92
- <MediaCard title="尚无预览的资源" kind="photo" state="empty" onOpen={() => setOpen(true)} />
93
- </div>
97
+ <MediaCard title={titles[2]} date="2026-10-21" aspectRatio="3 / 2" kind="video" preview={<img src={sampleImage} alt="视频封面" />} onOpen={() => { setIndex(2); setOpen(true); }} />
98
+ </div><div className="ds-media-section-heading"><h3>状态与反馈</h3><p>加载、读取失败和缺少预览时,资源信息仍然保留。</p></div><div className="ds-media-grid ds-media-states">
99
+ <MediaCard aspectRatio="3 / 2" title="加载中的 Live 资源" kind="Live" state="loading" onOpen={() => setOpen(true)} />
100
+ <MediaCard aspectRatio="3 / 2" title="不可解码的文件" kind="RAW" state={retried ? 'ready' : 'error'} preview={retried ? <img src={sampleImage} alt="重试后恢复的示例预览" /> : undefined} onOpen={() => { setIndex(1); setOpen(true); }} actions={<Button variant="secondary" onClick={() => setRetried(true)} disabled={retried}>{retried ? '预览已恢复' : '重试预览'}</Button>} />
101
+ <MediaCard aspectRatio="3 / 2" title="尚无预览的资源" kind="photo" state="empty" onOpen={() => setOpen(true)} />
102
+ </div><p className="ds-caption ds-media-note">展台使用本地矢量插画;视频项展示封面与控件,不包含可播放文件。</p>
94
103
  </section>;
95
104
  }
@@ -9,8 +9,8 @@ export const galleryGroups = [
9
9
  { label: '开始', items: [['top', '设计总览'], ['subscriptions', '订阅管理示例']] },
10
10
  { label: '应用能力', items: [['app-shell', '背景与多侧栏'], ['advanced-forms', '复杂表单'], ['advanced-data', '受控数据表'], ['media', '媒体卡片与查看器'], ['monitoring', '健康详情与图表']] },
11
11
  { label: '设计语言', items: [['foundation', '视觉基础'], ['backgrounds', '场景背景'], ['spacing', '间距与组合'], ['typography', '字体与文字'], ['brand', '品牌标志']] },
12
- { label: '通用组件', items: [['actions', '按钮与输入'], ['selection', '选择与切换'], ['feedback', '状态与加载'], ['disclosure', '展开与组合'], ['data', '数据与分页'], ['forms', '表单与选择'], ['utilities', '操作与布局'], ['overlays', '浮层与反馈'], ['navigation', '导航与步骤'], ['form-workflow', '表单流程']] },
13
- { label: '交互与动效', items: [['patterns', '场景与交互指南'], ['motion', '动效实验室'], ['startup', '启动动画'], ['orbit', '轨道星空']] },
12
+ { label: '通用组件', items: [['actions', '按钮与输入'], ['selection', '选择与切换'], ['feedback', '状态与加载'], ['disclosure', '展开与组合'], ['data', '数据与分页'], ['forms', '表单与选择'], ['utilities', '操作与布局'], ['overlays', '浮层与反馈'], ['overlay-bounds', '浮层缩放与边界'], ['settings-dialog', '设置导航弹窗'], ['theme-picker', '主题预览与选择'], ['navigation', '导航与步骤'], ['form-workflow', '表单流程']] },
13
+ { label: '交互与动效', items: [['patterns', '场景与交互指南'], ['compact-account', '紧凑账号与凭证'], ['list-filter-bar', '列表筛选栏'], ['date-range-filter', '日期筛选组合'], ['motion', '动效实验室'], ['startup', '启动动画'], ['orbit', '轨道星空'], ['neutron-star', '中子星']] },
14
14
  ] as const;
15
15
  export type GallerySection = typeof galleryGroups[number]['items'][number][0];
16
16
  const entries = galleryGroups.flatMap(group => [...group.items]);
@@ -94,7 +94,7 @@ function GalleryContent({ motion, onMotionChange, children }: GalleryShellProps)
94
94
  <SideNav id="gallery-navigation" className="ds-sidebar-nav" aria-label="组件目录" currentId={section} groups={galleryGroups.map(group=>({label:group.label,items:group.items.map(([id,label])=>({id,label,href:`#${id}`}))}))} onNavigate={item=>{
95
95
  setMenuOpen(false); if(section===item.id) main.current?.focus({preventScroll:true});
96
96
  }}/>
97
- <div className="ds-sidebar-foot"><span>COMPONENT LIBRARY</span><span>60 个组件 · 持续演进</span><span className="ak-readout">{readout}</span></div>
97
+ <div className="ds-sidebar-foot"><span>COMPONENT LIBRARY</span><span>66 个组件 · 0.8.0</span><span className="ak-readout">{readout}</span></div>
98
98
  </aside>
99
99
  <main className="ds-workspace" id="gallery-content" ref={main} tabIndex={-1} aria-label={label}>
100
100
  <header className="ds-workspace-head"><Breadcrumbs label="展台位置" items={[{label:"组件展台",href:"#top"},{label}]} /><div className="ds-head-controls"><SegmentedControl label="动效偏好" items={[{value:'system',label:'跟随系统'},{value:'reduced',label:'减少动态'}]} value={motion} onValueChange={onMotionChange} /><Select label="界面主题" value={theme} onChange={event => {
@@ -0,0 +1,170 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Badge, Button, Checkbox, DateRangePicker, EmptyState, Inline, ListFilterBar,
4
+ Select, Skeleton, Stack, Surface, Text, type DateRange,
5
+ } from '../components';
6
+
7
+ type PreviewState = 'normal' | 'loading' | 'empty' | 'error' | 'disabled';
8
+ const records = [
9
+ { id: 'design-review', title: '核对页面设计与窄屏布局', person: '示例成员', state: 'done', reviewed: true, date: '2026-10-24' },
10
+ { id: 'component-review', title: '整理共享组件与交互规则', person: '组件维护者', state: 'running', reviewed: false, date: '2026-10-23' },
11
+ ];
12
+ const dateLabel = ({ start, end }: DateRange) => start || end ? `${start || '不限开始'} — ${end || '不限结束'}` : undefined;
13
+
14
+ function usePreviewRefresh() {
15
+ const [refreshing, setRefreshing] = useState(false);
16
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
17
+ useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
18
+ return {
19
+ refreshing,
20
+ refresh: () => {
21
+ if (timer.current) clearTimeout(timer.current);
22
+ setRefreshing(true);
23
+ timer.current = setTimeout(() => setRefreshing(false), 600);
24
+ },
25
+ };
26
+ }
27
+
28
+ function SessionListExample({ mode, embedded = false }: { mode: PreviewState; embedded?: boolean }) {
29
+ const [query, setQuery] = useState('');
30
+ const [state, setState] = useState('all');
31
+ const [sort, setSort] = useState('newest');
32
+ const [person, setPerson] = useState('all');
33
+ const [reviewed, setReviewed] = useState(false);
34
+ const [range, setRange] = useState<DateRange>({ start: '', end: '' });
35
+ const [dateResetKey, setDateResetKey] = useState(0);
36
+ const [exportCount, setExportCount] = useState(0);
37
+ const { refreshing, refresh } = usePreviewRefresh();
38
+ const disabled = mode === 'disabled';
39
+ const pending = mode === 'loading' || refreshing;
40
+ const label = embedded ? '人员会话筛选' : '录制会话筛选';
41
+ const reset = () => { setQuery(''); setState('all'); setSort('newest'); setPerson('all'); setReviewed(false); setRange({ start: '', end: '' }); setDateResetKey(key => key + 1); };
42
+ // Gallery records are synthetic; production hosts send these values to their own query service.
43
+ const rows = mode === 'empty' ? [] : records.filter(row =>
44
+ (state === 'all' || row.state === state) &&
45
+ (person === 'all' || row.person === person) &&
46
+ (!reviewed || row.reviewed) &&
47
+ (!range.start || row.date >= range.start) && (!range.end || row.date <= range.end) &&
48
+ `${row.title} ${row.person} ${row.id}`.includes(query.trim()),
49
+ ).slice().sort((a, b) => sort === 'oldest' ? a.id.localeCompare(b.id) : b.id.localeCompare(a.id));
50
+ const common = [
51
+ {
52
+ id: 'status', label: '会话状态', activeLabel: state === 'all' ? undefined : state === 'done' ? '已完成' : '进行中',
53
+ onClear: disabled ? undefined : () => setState('all'),
54
+ control: <Select label="会话状态" value={state} disabled={disabled} onChange={event => setState(event.target.value)}>
55
+ <option value="all">全部状态</option><option value="running">进行中</option><option value="done">已完成</option>
56
+ </Select>,
57
+ },
58
+ {
59
+ id: 'sort', label: '排序', activeLabel: sort === 'newest' ? undefined : '最早事件优先',
60
+ onClear: disabled ? undefined : () => setSort('newest'),
61
+ control: <Select label="排序" value={sort} disabled={disabled} onChange={event => setSort(event.target.value)}>
62
+ <option value="newest">最后事件最新</option><option value="oldest">最早事件优先</option>
63
+ </Select>,
64
+ },
65
+ ];
66
+ const reviewField = {
67
+ id: 'reviewed', label: '审查', activeLabel: reviewed ? '只看已审查' : undefined,
68
+ onClear: disabled ? undefined : () => setReviewed(false),
69
+ control: <Checkbox label="只看已审查" checked={reviewed} disabled={disabled} onChange={event => setReviewed(event.target.checked)} />,
70
+ };
71
+ return <Stack gap={4} style={embedded ? { maxWidth: 420, width: '100%' } : undefined}>
72
+ <ListFilterBar label={label}
73
+ search={{ label: '搜索会话', placeholder: '搜索消息或会话 ID', value: query, disabled, onChange: event => setQuery(event.target.value) }}
74
+ primary={embedded ? common : [...common, reviewField]}
75
+ filters={[
76
+ ...(embedded ? [reviewField] : []),
77
+ {
78
+ id: 'person', label: '人员', activeLabel: person === 'all' ? undefined : person,
79
+ onClear: disabled ? undefined : () => setPerson('all'),
80
+ control: <Select label="人员" value={person} disabled={disabled} onChange={event => setPerson(event.target.value)}>
81
+ <option value="all">全部人员</option><option>示例成员</option><option>组件维护者</option>
82
+ </Select>,
83
+ },
84
+ {
85
+ id: 'date', label: '最后事件日期', activeLabel: dateLabel(range),
86
+ onClear: disabled ? undefined : () => setRange({ start: '', end: '' }),
87
+ control: <DateRangePicker key={dateResetKey} label="最后事件日期" value={range} onValueChange={setRange} disabled={disabled}
88
+ presets={[{ label: '今天', value: { start: '2026-10-24', end: '2026-10-24' } }]} />,
89
+ },
90
+ ]}
91
+ onReset={disabled ? undefined : reset}
92
+ refresh={{ onClick: refresh, busy: pending, disabled }}
93
+ action={embedded ? undefined : <Button variant="secondary" disabled={disabled || pending} onClick={() => setExportCount(count => count + 1)}>导出示例</Button>}
94
+ result={pending ? undefined : `${rows.length} 条结果`}
95
+ pending={pending}
96
+ error={mode === 'error' ? '读取失败,当前保留上次结果。请刷新重试。' : undefined}
97
+ />
98
+ {pending ? <Skeleton label="正在读取会话" lines={3} /> : rows.length ? rows.map(row => <Surface key={row.id}>
99
+ <Stack gap={2}>
100
+ <Inline gap={2}><Text as="h4" variant="label">{row.person}</Text><Badge>{row.state === 'done' ? '已完成' : '进行中'}</Badge></Inline>
101
+ <Text>{row.title}</Text><Text variant="caption" tone="muted">{row.id}</Text>
102
+ </Stack>
103
+ </Surface>) : <EmptyState title="没有匹配的会话" description="试试其他条件。" action={!disabled ? <Button variant="secondary" onClick={reset}>清除筛选</Button> : undefined} />}
104
+ {!!exportCount && <Text role="status" variant="caption" tone="muted">已触发 {exportCount} 次示例操作;展台不生成或下载文件。</Text>}
105
+ </Stack>;
106
+ }
107
+
108
+ function StatisticsExample({ mode }: { mode: PreviewState }) {
109
+ const [scope, setScope] = useState('all');
110
+ const [platform, setPlatform] = useState('all');
111
+ const [metric, setMetric] = useState('cost');
112
+ const [range, setRange] = useState<DateRange>({ start: '2026-10-24', end: '2026-10-24' });
113
+ const [dateResetKey, setDateResetKey] = useState(0);
114
+ const { refreshing, refresh } = usePreviewRefresh();
115
+ const disabled = mode === 'disabled';
116
+ const pending = mode === 'loading' || refreshing;
117
+ const reset = () => { setScope('all'); setPlatform('all'); setMetric('cost'); setRange({ start: '2026-10-24', end: '2026-10-24' }); setDateResetKey(key => key + 1); };
118
+ return <Stack gap={4}>
119
+ <ListFilterBar label="部门用量筛选"
120
+ primary={[
121
+ { id: 'platform', label: '平台', activeLabel: platform === 'all' ? undefined : platform, onClear: disabled ? undefined : () => setPlatform('all'),
122
+ control: <Select label="平台" value={platform} disabled={disabled} onChange={event => setPlatform(event.target.value)}><option value="all">全部平台</option><option>OpenAI</option></Select> },
123
+ { id: 'metric', label: '排行依据', activeLabel: metric === 'cost' ? undefined : 'Token 排行', onClear: disabled ? undefined : () => setMetric('cost'),
124
+ control: <Select label="排行依据" value={metric} disabled={disabled} onChange={event => setMetric(event.target.value)}><option value="cost">消费排行</option><option value="tokens">Token 排行</option></Select> },
125
+ ]}
126
+ filters={[
127
+ { id: 'scope', label: '统计范围', activeLabel: scope === 'all' ? undefined : scope, onClear: disabled ? undefined : () => setScope('all'),
128
+ control: <Select label="统计范围" value={scope} disabled={disabled} onChange={event => setScope(event.target.value)}><option value="all">所有可见部门</option><option>示例团队 / 跨部门体验与可访问性协作小组</option></Select> },
129
+ { id: 'range', label: '统计日期', activeLabel: range.start === '2026-10-24' && range.end === '2026-10-24' ? undefined : dateLabel(range),
130
+ onClear: disabled ? undefined : () => setRange({ start: '2026-10-24', end: '2026-10-24' }),
131
+ control: <DateRangePicker key={dateResetKey} label="统计日期" value={range} onValueChange={setRange} disabled={disabled} clearable={false} /> },
132
+ ]}
133
+ onReset={disabled ? undefined : reset} refresh={{ onClick: refresh, busy: pending, disabled }} pending={pending}
134
+ result={pending ? undefined : mode === 'empty' ? '当前范围无用量' : `${dateLabel(range)} · ${scope === 'all' ? '所有可见部门' : scope}`}
135
+ error={mode === 'error' ? '统计读取失败,暂不能确认当前范围结果。' : undefined}
136
+ />
137
+ {pending ? <Skeleton label="正在读取用量" lines={2} /> : mode === 'empty' ? <EmptyState title="当前范围没有用量" description="可更改范围或日期后重试。" /> : <Surface><Stack gap={2}>
138
+ <Text as="h4" variant="label">示例统计结果</Text><Text variant="caption" tone="muted">统计范围、日期与排行方式由宿主控制;此处仅展示合成数据。</Text>
139
+ <Text numeric>{metric === 'cost' ? '$ 8.42' : '1,400,000 Token'}</Text>
140
+ </Stack></Surface>}
141
+ </Stack>;
142
+ }
143
+
144
+ export function ListFilterBarGallery() {
145
+ const [mode, setMode] = useState<PreviewState>('normal');
146
+ return <section id="list-filter-bar">
147
+ <div className="ds-section-title"><span>FILTERS</span><h2>列表筛选 · 限制常显复杂度</h2></div>
148
+ <p className="ds-caption">搜索、最多两个常显条件和一个业务操作;其他条件按需展开。对应 Rhine #19,查询与权限由宿主管理。</p>
149
+ <Stack gap={6}>
150
+ <Select label="示例状态" value={mode} onChange={event => setMode(event.target.value as PreviewState)}>
151
+ <option value="normal">正常 / 有数据</option><option value="loading">加载中</option><option value="empty">无结果</option><option value="error">读取失败</option><option value="disabled">条件不可修改</option>
152
+ </Select>
153
+ <Stack gap={3}>
154
+ <Text as="h3" variant="heading">独立列表</Text>
155
+ <Text variant="caption" tone="muted">传入的第三个常显条件“审查”自动归入更多筛选;不会继续增加顶部行数。</Text>
156
+ <SessionListExample mode={mode} />
157
+ </Stack>
158
+ <Stack gap={3}>
159
+ <Text as="h3" variant="heading">嵌入人员详情 · 420px 容器</Text>
160
+ <Text variant="caption" tone="muted">同一组件随实际容器重排,窄区域仍保留结果、刷新和退出筛选的入口。</Text>
161
+ <SessionListExample mode={mode} embedded />
162
+ </Stack>
163
+ <Stack gap={3}>
164
+ <Text as="h3" variant="heading">统计范围 · 无搜索框</Text>
165
+ <StatisticsExample mode={mode} />
166
+ </Stack>
167
+ <Text variant="caption" tone="muted">筛选即时改变宿主状态,关闭抽屉不撤销条件。加载、错误和结果在抽屉内同样可见;用 Escape 关闭并返回入口。</Text>
168
+ </Stack>
169
+ </section>;
170
+ }
@@ -1,4 +1,5 @@
1
1
  import { useState } from 'react';
2
+ import { TimelineListGallery } from './TimelineListGallery';
2
3
  import { Select, Stack, StackedBar, StatusTimeline, Surface, TimelineLegend, TrendChart, type ChartPoint, type TimelineSegment } from '../components';
3
4
 
4
5
  export function MonitoringGallery() {
@@ -15,6 +16,7 @@ export function MonitoringGallery() {
15
16
  }));
16
17
  const feedback = { stale: state === 'stale' ? '最后采集于 20 分钟前,请核对来源。' : undefined, error: state === 'error' ? '本次读取失败,保留上次采样。' : undefined };
17
18
  return <section><div className="ds-section-title"><span>MONITORING</span><h2>看见证据,也看见缺口</h2></div>
19
+ <TimelineListGallery />
18
20
  <p className="ds-caption">计数与口径由宿主提供。未知保持未知,折线在缺失处断开;图例与数据表提供文字替代,不生成健康评分。时段详情支持鼠标、键盘、触屏和 Escape,并自动避让视口。</p>
19
21
  <Select label="监控演示状态" value={state} onChange={event => setState(event.target.value)}><option value="ready">默认 / 含缺失采样</option><option value="empty">空数据</option><option value="stale">陈旧数据</option><option value="error">错误 / 保留旧数据</option><option value="unknown">占比总量未知</option></Select>
20
22
  <Surface className="ds-gap-top"><Stack gap={4}><h3>结构化时段详情</h3><StatusTimeline label="结构化健康历史" startLabel="10:00 UTC+8" endLabel="10:40 UTC+8" segments={segments} /><TimelineLegend /><p>方向键与 Home/End 切换时段;轻触健康条查看同一份完整详情。</p><StatusTimeline compact label="紧凑结构化历史" startLabel="10:00" endLabel="10:40" segments={segments} /></Stack></Surface>
@@ -0,0 +1,56 @@
1
+ import { useState } from 'react';
2
+ import { Badge, Button, Disclosure, Inline, NeutronStarBackdrop, Select, Stack, Surface, Switch, Text, TextField } from '../components';
3
+
4
+ export function NeutronStarGallery() {
5
+ const [replay, setReplay] = useState(0);
6
+ const [reduced, setReduced] = useState(false);
7
+ const [appearance, setAppearance] = useState<'theme' | 'original'>('theme');
8
+
9
+ return <section id="neutron-star">
10
+ <div className="ds-section-title"><span>NEUTRON STAR</span><h2>中子星 · 磁场与星尘</h2></div>
11
+ <p className="ds-caption">磁场线、流动粒子与极向喷流围绕星核运行。先看完整的艺术画面,再展开控制与内容组合;装饰不表示加载或业务进度。</p>
12
+ <Stack gap={4}>
13
+ <NeutronStarBackdrop key={replay} data-neutron-example="artwork" aria-label="中子星艺术" role="group" appearance={appearance} reducedMotion={reduced} style={{ minHeight: 560 }} />
14
+ <Disclosure title="演示控制">
15
+ <Stack gap={4}>
16
+ <Inline gap={3}>
17
+ <Badge>源码候选</Badge>
18
+ <Button variant="secondary" onClick={() => setReplay(value => value + 1)}>重播中子星</Button>
19
+ <Switch label="减少中子星动态" checked={reduced} onCheckedChange={setReduced} />
20
+ </Inline>
21
+ <Select label="艺术配色" value={appearance} onChange={event => setAppearance(event.target.value as 'theme' | 'original')}>
22
+ <option value="theme">跟随界面主题</option>
23
+ <option value="original">原始深空</option>
24
+ </Select>
25
+ <Text variant="caption" tone="muted">顶部切换界面主题;艺术配色改变时继续当前时间。暂停后原地继续,重播只重新创建上方艺术,不清空下方输入。</Text>
26
+ </Stack>
27
+ </Disclosure>
28
+ <Disclosure title="查看内容组合">
29
+ <NeutronContentExample appearance={appearance} reducedMotion={reduced} />
30
+ </Disclosure>
31
+ </Stack>
32
+ <p className="ds-caption">28 条磁场线、560 个流动粒子、700 颗星尘、极向喷流与星核;星系以 28 秒有效播放时间完成一轮深度公转。系统、顶部或本节减少动态均保留静态画面;页面隐藏、离屏或尺寸为零时停止逐帧绘制。</p>
33
+ </section>;
34
+ }
35
+
36
+ function NeutronContentExample({ appearance, reducedMotion }: { appearance: 'theme' | 'original'; reducedMotion: boolean }) {
37
+ const [name, setName] = useState('Rhine Observatory');
38
+ return <NeutronStarBackdrop data-neutron-example="content" aria-label="中子星内容组合" role="group" appearance={appearance} reducedMotion={reducedMotion} style={{ minHeight: 460 }}>
39
+ <Stack style={{ padding: 'var(--rhine-space-6)' }}>
40
+ <Surface style={{ maxWidth: 400 }}>
41
+ <Stack gap={4}>
42
+ <Stack gap={2}>
43
+ <Text as="h3" variant="heading">保持内容清晰</Text>
44
+ <Text tone="muted">卡片、文字与操作使用当前界面主题,艺术层独立绘制。</Text>
45
+ </Stack>
46
+ <TextField label="观测名称" value={name} onChange={event => setName(event.target.value)} hint="本地演示草稿;切换配色或重播不会清空。" />
47
+ <Inline gap={2}>
48
+ <Button>开始观测</Button>
49
+ <Button variant="secondary">查看介绍</Button>
50
+ <Button disabled>暂不可用</Button>
51
+ </Inline>
52
+ </Stack>
53
+ </Surface>
54
+ </Stack>
55
+ </NeutronStarBackdrop>;
56
+ }
@@ -1,47 +1,64 @@
1
1
  import { useState } from 'react';
2
- import { Badge, Button, Inline, Logo, OrbitBackdrop, Stack, Surface, Switch, Text, TextField } from '../components';
2
+ import { OrbitEntryExample } from './BackgroundFiltersFixture';
3
+ import { Badge, Button, Disclosure, Inline, OrbitBackdrop, SegmentedControl, Stack, Surface, Switch, Text, TextField } from '../components';
3
4
 
4
5
  export function OrbitGallery() {
5
6
  const [replay, setReplay] = useState(0);
6
7
  const [reduced, setReduced] = useState(false);
8
+ const [appearance, setAppearance] = useState<'theme' | 'original'>('theme');
7
9
  const [artworkName, setArtworkName] = useState('Orbit workspace');
8
10
  const [workspaceName, setWorkspaceName] = useState('Rhine Studio');
9
11
  return <section id="orbit">
10
12
  <div className="ds-section-title"><span>ORBIT</span><h2>轨道星空 · 品牌艺术背景</h2></div>
11
- <p className="ds-caption">420 个环节点、185 条空间曲线与发射箭头组成同一幅画面。用于入口和品牌叙事,内容可立即阅读,不表示加载或业务进度。</p>
13
+ <p className="ds-caption">莱茵的折纸鸟、战术的悬浮源石晶簇、橙趣的草莓甜甜圈。每个主题都有自己的物件,也可以切换到原始深色星空。</p>
12
14
  <Inline gap={3}>
13
- <Badge>0.6.0</Badge>
15
+ <Badge>0.7.0</Badge>
16
+ <SegmentedControl label="画面风格" value={appearance} onValueChange={value => setAppearance(value as 'theme' | 'original')} items={[{ value: 'theme', label: '跟随主题' }, { value: 'original', label: '原始深色' }]} />
14
17
  <Button variant="secondary" onClick={() => setReplay(value => value + 1)}>重播轨道</Button>
15
18
  <Switch label="减少轨道动态" checked={reduced} onCheckedChange={setReduced} />
16
19
  </Inline>
17
- <OrbitBackdrop key={replay} className="ds-gap-top" reducedMotion={reduced} style={{ minHeight: 560 }}>
18
- <Logo variant="lockup" size={40} />
19
- <Stack gap={3}>
20
- <Text as="h3" variant="title">连接彼此,探索更多。</Text>
21
- <Text tone="muted">在有序的轨迹中,留下新的可能。<br />艺术背景不阻挡内容与操作。</Text>
22
- <TextField label="直接叠加的输入框" value={artworkName} onChange={event => setArtworkName(event.target.value)} hint="提示文字随当前内容前景保持可读。" />
23
- <Inline gap={2}>
24
- <Button>开始探索</Button>
25
- <Button variant="secondary">查看轨道</Button>
26
- <Button variant="quiet">了解更多</Button>
27
- <Button variant="quiet" busy busyLabel="示例处理中">示例处理中</Button>
28
- <Button variant="quiet" disabled>暂不可用</Button>
29
- </Inline>
30
- </Stack>
31
- <Surface>
32
- <Stack gap={3}>
33
- <Text as="h3" variant="heading">带底色的内容保持宿主主题</Text>
34
- <Text tone="muted">也可组合到 Surface,保留完整的前景、背景与状态配色。</Text>
35
- <TextField label="工作空间名称" value={workspaceName} onChange={event => setWorkspaceName(event.target.value)} hint="这是可编辑的本地展台示例。" />
36
- <Inline gap={2}>
37
- <Button>继续探索</Button>
38
- <Button variant="secondary">查看介绍</Button>
39
- <Button variant="quiet">更多介绍</Button>
40
- </Inline>
41
- </Stack>
42
- </Surface>
43
- <Text variant="caption" tone="muted">空间、连接与持续探索。</Text>
44
- </OrbitBackdrop>
45
- <p className="ds-caption">暂停后在原画面继续;系统、顶部或本节减少动态均保留静态画面。视口不超过 760px、页面隐藏或画面离屏时停帧。重播只重建这幅艺术,不执行任何业务操作。</p>
20
+ <OrbitBackdrop key={`art-${replay}`} className="ds-gap-top ds-orbit-artwork" appearance={appearance} reducedMotion={reduced} role="region" aria-label="轨道背景预览" />
21
+ <p className="ds-caption">纸鸟在飞行中连续折叠成纸蝴蝶,再舒展还原;晶簇与甜甜圈缓缓旋转。切换主题保留播放位置,窄屏也能播放;暂停或开启减少动态后保持当前形态。</p>
22
+ <Disclosure title="查看组合控件示例">
23
+ <Text tone="muted">输入框与按钮的叠加效果单独展示,不遮挡上方轨道。</Text>
24
+ <OrbitBackdrop key={`controls-${replay}`} className="ds-gap-top" appearance={appearance} reducedMotion={reduced} role="region" aria-label="轨道上的控件组合">
25
+ <div className="ds-orbit-example-content"><Stack gap={6}>
26
+ <Stack gap={3}>
27
+ <Text as="h3" variant="heading">直接叠加</Text>
28
+ <TextField label="直接叠加的输入框" value={artworkName} onChange={event => setArtworkName(event.target.value)} hint="提示文字随当前内容前景保持可读。" />
29
+ <Inline gap={2}>
30
+ <Button>开始探索</Button>
31
+ <Button variant="secondary">查看轨道</Button>
32
+ <Button variant="quiet">了解更多</Button>
33
+ <Button variant="quiet" busy busyLabel="示例处理中">示例处理中</Button>
34
+ <Button variant="quiet" disabled>暂不可用</Button>
35
+ </Inline>
36
+ </Stack>
37
+ <Surface><Stack gap={3}>
38
+ <Text as="h3" variant="heading">带底色的内容</Text>
39
+ <Text tone="muted">Surface 与控件保留当前主题的完整配色。</Text>
40
+ <TextField label="工作空间名称" value={workspaceName} onChange={event => setWorkspaceName(event.target.value)} hint="这是可编辑的本地展台示例。" />
41
+ <Inline gap={2}>
42
+ <Button>继续探索</Button>
43
+ <Button variant="secondary">查看介绍</Button>
44
+ <Button variant="quiet">更多介绍</Button>
45
+ </Inline>
46
+ </Stack></Surface>
47
+ </Stack></div>
48
+ </OrbitBackdrop>
49
+ </Disclosure>
50
+ <Disclosure title="查看入口与全页背景组合">
51
+ <Text tone="muted">文字保留在上方,艺术位于右下角;无框背景下的表单使用 Surface 保持清晰。</Text>
52
+ <div className="ds-orbit-layout-container"><div className="ds-orbit-layout-examples ds-gap-top">
53
+ <OrbitBackdrop className="ds-orbit-corner-example" appearance={appearance} reducedMotion={reduced} composition="corner" controls={false} role="region" aria-label="右下角艺术与文字">
54
+ <Stack gap={4}>
55
+ <Text variant="overline">RHINE STUDIO</Text>
56
+ <Text as="h3" variant="title">从这里,<br />继续创造。</Text>
57
+ <Text tone="muted">让内容拥有清晰的阅读空间。</Text>
58
+ </Stack>
59
+ </OrbitBackdrop>
60
+ <OrbitEntryExample appearance={appearance} reduced={reduced} />
61
+ </div></div>
62
+ </Disclosure>
46
63
  </section>;
47
64
  }