@linhey/rhine-design 0.6.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 (98) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +19 -8
  3. package/components/Actions.tsx +18 -1
  4. package/components/AppShell.tsx +46 -17
  5. package/components/Media.tsx +7 -7
  6. package/components/NeutronStarBackdrop.tsx +43 -0
  7. package/components/OrbitBackdrop.tsx +4 -2
  8. package/components/Overlays.tsx +37 -8
  9. package/components/README.md +27 -3
  10. package/components/StatusTimeline.tsx +72 -6
  11. package/components/StatusTimelineList.tsx +38 -0
  12. package/components/ThemePicker.tsx +49 -0
  13. package/components/assets/orbit-bird.svg +578 -0
  14. package/components/assets/orbit-crystal.svg +11 -0
  15. package/components/assets/orbit-donut.svg +16 -0
  16. package/components/assets/theme-previews/arknights.png +0 -0
  17. package/components/assets/theme-previews/manifest.json +24 -0
  18. package/components/assets/theme-previews/neo-brutalism.png +0 -0
  19. package/components/assets/theme-previews/rhine.png +0 -0
  20. package/components/common.css +3 -3
  21. package/components/components.tsx +3 -3
  22. package/components/dialog-settings.css +9 -0
  23. package/components/extensions.css +8 -32
  24. package/components/index.ts +11 -5
  25. package/components/media-parity.css +55 -2
  26. package/components/neutron.css +53 -0
  27. package/components/neutronRenderer.ts +327 -0
  28. package/components/orbit.css +79 -1
  29. package/components/orbitBirdFlight.ts +34 -0
  30. package/components/orbitBirdGeometry.ts +158 -0
  31. package/components/orbitDonut.ts +134 -0
  32. package/components/orbitDonutThree.ts +208 -0
  33. package/components/orbitObjectMaterials.ts +76 -0
  34. package/components/orbitObjects.ts +71 -0
  35. package/components/orbitObjectsThree.ts +215 -0
  36. package/components/orbitProjection.ts +21 -0
  37. package/components/orbitRenderer.ts +163 -46
  38. package/components/styles.css +3 -1
  39. package/components/theme-neo-brutalism.css +3 -2
  40. package/components/theme-picker.css +17 -0
  41. package/components/theme.tsx +2 -8
  42. package/components/themeMetadata.ts +7 -0
  43. package/components/timeline.css +17 -6
  44. package/components/useFloatingScale.ts +96 -0
  45. package/docs/APPLICATION_COMPONENTS.md +52 -3
  46. package/docs/COMPACT_ACCOUNT_PATTERNS.md +70 -0
  47. package/docs/DATE_RANGE_FILTER.md +60 -0
  48. package/docs/DESIGN_SYSTEM.md +7 -0
  49. package/docs/FLOATING_OVERLAYS.md +15 -0
  50. package/docs/INTERACTION_PATTERNS.md +5 -1
  51. package/docs/NEUTRON_STAR_BACKDROP.md +122 -0
  52. package/docs/ORBIT_BACKDROP.md +38 -46
  53. package/docs/SETTINGS_DIALOG.md +21 -0
  54. package/docs/THEME_PICKER.md +26 -0
  55. package/docs/THEMING.md +2 -0
  56. package/docs/assets/compact-account-neo.png +0 -0
  57. package/docs/assets/neutron-star-original.png +0 -0
  58. package/docs/assets/neutron-star-theme.png +0 -0
  59. package/docs/assets/orbit-bird.png +0 -0
  60. package/docs/assets/orbit-crystal.png +0 -0
  61. package/docs/assets/orbit-strawberry.png +0 -0
  62. package/package.json +8 -3
  63. package/preview/CompactAccountGallery.tsx +178 -0
  64. package/preview/DateRangeFilterGallery.tsx +109 -0
  65. package/preview/ExtensionsGallery.tsx +27 -18
  66. package/preview/GalleryShell.tsx +3 -3
  67. package/preview/MonitoringGallery.tsx +2 -0
  68. package/preview/NeutronStarGallery.tsx +56 -0
  69. package/preview/OrbitGallery.tsx +35 -32
  70. package/preview/OverlayBoundsGallery.tsx +49 -0
  71. package/preview/SettingsDialogGallery.tsx +33 -0
  72. package/preview/ThemePickerGallery.tsx +24 -0
  73. package/preview/ThemeSample.tsx +14 -0
  74. package/preview/TimelineListGallery.tsx +36 -0
  75. package/preview/extensions.css +9 -0
  76. package/preview/main.tsx +16 -3
  77. package/preview/preview.css +8 -0
  78. package/preview/theme-sample.html +1 -0
  79. package/scripts/check-overlay-bounds.js +76 -0
  80. package/scripts/check-settings-theme.js +88 -0
  81. package/skills/rhine-design/SKILL.md +3 -2
  82. package/skills/rhine-design/references/API_MAP.md +13 -2
  83. package/skills/rhine-design/references/TASK_MAP.md +8 -2
  84. package/skills/rhine-design/references/integration.md +1 -1
  85. package/tests/AppShellControls.test.tsx +82 -0
  86. package/tests/ButtonLink.test.tsx +94 -0
  87. package/tests/CompactAccountGallery.test.tsx +116 -0
  88. package/tests/DateRangeFilterGallery.test.tsx +85 -0
  89. package/tests/NeutronStarBackdrop.test.tsx +288 -0
  90. package/tests/NeutronStarGallery.test.tsx +62 -0
  91. package/tests/NeutronStarImport.test.tsx +15 -0
  92. package/tests/OrbitBackdrop.test.tsx +246 -9
  93. package/tests/OrbitDonutThree.test.tsx +91 -0
  94. package/tests/OrbitObjectsThree.test.tsx +164 -0
  95. package/tests/SettingsDialog.test.tsx +66 -0
  96. package/tests/StatusTimelineList.test.tsx +63 -0
  97. package/tests/ThemePicker.test.tsx +30 -0
  98. package/tests/TimelineLegend.test.tsx +60 -0
@@ -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', '场景与交互指南'], ['date-range-filter', '日期筛选组合'], ['compact-account', '紧凑账号与凭证'], ['motion', '动效实验室'], ['startup', '启动动画'], ['orbit', '轨道星空'], ['neutron-star', '中子星']] },
14
14
  ] as const;
15
15
  export type GallerySection = typeof galleryGroups[number]['items'][number][0];
16
16
  const entries = galleryGroups.flatMap(group => [...group.items]);
@@ -94,7 +94,7 @@ function GalleryContent({ motion, onMotionChange, children }: GalleryShellProps)
94
94
  <SideNav id="gallery-navigation" className="ds-sidebar-nav" aria-label="组件目录" currentId={section} groups={galleryGroups.map(group=>({label:group.label,items:group.items.map(([id,label])=>({id,label,href:`#${id}`}))}))} onNavigate={item=>{
95
95
  setMenuOpen(false); if(section===item.id) main.current?.focus({preventScroll:true});
96
96
  }}/>
97
- <div className="ds-sidebar-foot"><span>COMPONENT LIBRARY</span><span>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 => {
@@ -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,50 @@
1
1
  import { useState } from 'react';
2
- import { Badge, Button, Inline, Logo, OrbitBackdrop, Stack, Surface, Switch, Text, TextField } from '../components';
2
+ import { Badge, Button, Disclosure, Inline, OrbitBackdrop, SegmentedControl, Stack, Surface, Switch, Text, TextField } from '../components';
3
3
 
4
4
  export function OrbitGallery() {
5
5
  const [replay, setReplay] = useState(0);
6
6
  const [reduced, setReduced] = useState(false);
7
+ const [appearance, setAppearance] = useState<'theme' | 'original'>('theme');
7
8
  const [artworkName, setArtworkName] = useState('Orbit workspace');
8
9
  const [workspaceName, setWorkspaceName] = useState('Rhine Studio');
9
10
  return <section id="orbit">
10
11
  <div className="ds-section-title"><span>ORBIT</span><h2>轨道星空 · 品牌艺术背景</h2></div>
11
- <p className="ds-caption">420 个环节点、185 条空间曲线与发射箭头组成同一幅画面。用于入口和品牌叙事,内容可立即阅读,不表示加载或业务进度。</p>
12
+ <p className="ds-caption">莱茵的折纸鸟、战术的悬浮源石晶簇、橙趣的草莓甜甜圈。每个主题都有自己的物件,也可以切换到原始深色星空。</p>
12
13
  <Inline gap={3}>
13
- <Badge>0.6.0</Badge>
14
+ <Badge>0.7.0</Badge>
15
+ <SegmentedControl label="画面风格" value={appearance} onValueChange={value => setAppearance(value as 'theme' | 'original')} items={[{ value: 'theme', label: '跟随主题' }, { value: 'original', label: '原始深色' }]} />
14
16
  <Button variant="secondary" onClick={() => setReplay(value => value + 1)}>重播轨道</Button>
15
17
  <Switch label="减少轨道动态" checked={reduced} onCheckedChange={setReduced} />
16
18
  </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>
19
+ <OrbitBackdrop key={`art-${replay}`} className="ds-gap-top ds-orbit-artwork" appearance={appearance} reducedMotion={reduced} role="region" aria-label="轨道背景预览" />
20
+ <p className="ds-caption">纸鸟在飞行中连续折叠成纸蝴蝶,再舒展还原;晶簇与甜甜圈缓缓旋转。切换主题保留播放位置,窄屏也能播放;暂停或开启减少动态后保持当前形态。</p>
21
+ <Disclosure title="查看组合控件示例">
22
+ <Text tone="muted">输入框与按钮的叠加效果单独展示,不遮挡上方轨道。</Text>
23
+ <OrbitBackdrop key={`controls-${replay}`} className="ds-gap-top" appearance={appearance} reducedMotion={reduced} role="region" aria-label="轨道上的控件组合">
24
+ <div className="ds-orbit-example-content"><Stack gap={6}>
25
+ <Stack gap={3}>
26
+ <Text as="h3" variant="heading">直接叠加</Text>
27
+ <TextField label="直接叠加的输入框" value={artworkName} onChange={event => setArtworkName(event.target.value)} hint="提示文字随当前内容前景保持可读。" />
28
+ <Inline gap={2}>
29
+ <Button>开始探索</Button>
30
+ <Button variant="secondary">查看轨道</Button>
31
+ <Button variant="quiet">了解更多</Button>
32
+ <Button variant="quiet" busy busyLabel="示例处理中">示例处理中</Button>
33
+ <Button variant="quiet" disabled>暂不可用</Button>
34
+ </Inline>
35
+ </Stack>
36
+ <Surface><Stack gap={3}>
37
+ <Text as="h3" variant="heading">带底色的内容</Text>
38
+ <Text tone="muted">Surface 与控件保留当前主题的完整配色。</Text>
39
+ <TextField label="工作空间名称" value={workspaceName} onChange={event => setWorkspaceName(event.target.value)} hint="这是可编辑的本地展台示例。" />
40
+ <Inline gap={2}>
41
+ <Button>继续探索</Button>
42
+ <Button variant="secondary">查看介绍</Button>
43
+ <Button variant="quiet">更多介绍</Button>
44
+ </Inline>
45
+ </Stack></Surface>
46
+ </Stack></div>
47
+ </OrbitBackdrop>
48
+ </Disclosure>
46
49
  </section>;
47
50
  }
@@ -0,0 +1,49 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Button, DateRangePicker, Inline, Menu, Popover, Select, Stack, Text, TextField, Tooltip } from '../components';
3
+ import { ThemeProvider, themes, type ThemeName } from '../components/theme';
4
+
5
+ export function OverlayBoundsFixture() {
6
+ const query = new URLSearchParams(location.search);
7
+ const theme = query.get('theme') as ThemeName || 'rhine';
8
+ const zoom = Number(query.get('zoom')) || 1;
9
+ const scope = query.get('scope') || 'document';
10
+ const edge = query.get('edge') || 'bottom-right';
11
+ const side = (query.get('side') || 'top') as 'top' | 'bottom' | 'left' | 'right';
12
+ const [draft, setDraft] = useState('研究工作区');
13
+ const [range, setRange] = useState({ start: '', end: '' });
14
+ const [action, setAction] = useState('尚未操作');
15
+ useEffect(() => {
16
+ const previous = document.documentElement.style.zoom;
17
+ document.documentElement.style.zoom = scope === 'document' ? String(zoom) : '1';
18
+ return () => { document.documentElement.style.zoom = previous; };
19
+ }, [scope, zoom]);
20
+ const scale = scope === 'content' ? 1 : zoom;
21
+ return <ThemeProvider theme={theme} data-rhine-motion="reduced" style={{ zoom: scope === 'theme' ? zoom : 1 }}>
22
+ <div data-testid="overlay-bounds" style={{ display: 'grid', alignItems: edge.startsWith('top') ? 'start' : 'end', justifyItems: edge.endsWith('left') ? 'start' : 'end', padding: 12, width: `calc(100vw / ${scale})`, height: `calc(100dvh / ${scale})`, boxSizing: 'border-box' }}>
23
+ <Stack gap={2} style={{ maxWidth: '100%', zoom: scope === 'content' ? zoom : 1 }}>
24
+ <Popover title="工作区与日期详情" side={side} trigger={<Button variant="secondary">账号与日期</Button>}>
25
+ <Stack gap={3}><Text>研究工作区的长名称与说明,测试换行和完整阅读。</Text><Text>long-unbroken-account-address@example.invalid</Text>
26
+ <TextField label="草稿名称" value={draft} onChange={event => setDraft(event.target.value)} />
27
+ <DateRangePicker label="日期范围" value={range} onValueChange={setRange} />
28
+ <Text>内容超出可用高度时在浮层内滚动;缩放不清空草稿。</Text>
29
+ <Inline><Button variant="secondary" onClick={() => setAction('已取消')}>取消日期</Button><Button onClick={() => setAction('已应用')}>应用日期</Button></Inline>
30
+ </Stack>
31
+ </Popover>
32
+ <Menu label="对象操作" side={side} trigger={<Button variant="secondary">对象操作</Button>} items={['查看完整信息', '复制演示地址', '导出演示内容', '暂不可用', '最后一项操作'].map((label, i) => ({ id: String(i), label, disabled: i === 3, onSelect: () => setAction(label) }))} />
33
+ <Tooltip side={side} content="很长的辅助说明也应在可用范围内换行,保留用户需要的字体大小。"><Button variant="quiet">辅助说明</Button></Tooltip>
34
+ <Text role="status" variant="caption">{action}</Text>
35
+ </Stack>
36
+ </div>
37
+ </ThemeProvider>;
38
+ }
39
+
40
+ export function OverlayBoundsGallery() {
41
+ const [theme, setTheme] = useState('neo-brutalism'), [width, setWidth] = useState('400'), [zoom, setZoom] = useState('2');
42
+ const [scope, setScope] = useState('document'), [edge, setEdge] = useState('bottom-right'), [side, setSide] = useState('top');
43
+ const choices = (label: string, value: string, set: (value: string) => void, values: string[]) => <Select label={label} value={value} onChange={event => set(event.target.value)}>{values.map(item => <option key={item}>{item}</option>)}</Select>;
44
+ const params = new URLSearchParams({ fixture: 'overlay-bounds', theme, zoom, scope, edge, side });
45
+ return <Stack gap={4}><Text as="h2" variant="section">浮层缩放与边界</Text><Text tone="muted">独立文档验证 CSS zoom;它不等同于浏览器原生缩放。示例不访问业务数据。</Text>
46
+ <Inline>{choices('样机主题', theme, setTheme, themes.map(item => item.name))}{choices('样机宽度', width, setWidth, ['400', '520', '880'])}{choices('CSS zoom', zoom, setZoom, ['1', '2'])}{choices('缩放范围', scope, setScope, ['document', 'theme', 'content'])}{choices('锚点位置', edge, setEdge, ['top-left', 'top-right', 'bottom-left', 'bottom-right'])}{choices('优先方向', side, setSide, ['top', 'right', 'bottom', 'left'])}</Inline>
47
+ <iframe title="浮层边界样机" src={`?${params}`} style={{ width: Number(width), maxWidth: '100%', height: 758, border: '1px solid var(--rhine-line)', background: 'var(--rhine-bg)' }} />
48
+ </Stack>;
49
+ }
@@ -0,0 +1,33 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { Button, Dialog, EmptyState, Notice, Select, SideNav, Skeleton, Stack, Text, TextField, Tooltip } from '../components';
3
+ import { ThemeProvider, type ThemeName } from '../components/theme';
4
+
5
+ export function SettingsDialogGallery() {
6
+ const [section, setSection] = useState('account'), [state, setState] = useState('normal');
7
+ const [draft, setDraft] = useState('研究工作区'), [saved, setSaved] = useState('研究工作区');
8
+ const [busy, setBusy] = useState(false), [message, setMessage] = useState('');
9
+ const [open, setOpen] = useState(false);
10
+ const draftEdited = useRef(false);
11
+ const save = async () => { const submitted = draft; draftEdited.current = false; setBusy(true); await new Promise(resolve => setTimeout(resolve, 300)); setSaved(submitted); if (!draftEdited.current) setDraft(submitted); setBusy(false); setMessage('已保存演示草稿'); };
12
+ return <Stack gap={4}><Text as="h2" variant="section">设置导航对话框</Text><Text>窄导航与宽任务内容。这里只操作合成数据;切换栏目不会改变宿主地址。</Text>
13
+ <TextField label="宿主查询" defaultValue="保留这条查询" />
14
+ <Dialog title="设置" description="管理演示账号与公开项目" trigger={<Button>打开设置</Button>} open={open} onOpenChange={next => { setOpen(next); if (next) { draftEdited.current = false; setDraft(saved); setMessage(''); } }}
15
+ navigation={<SideNav aria-label="设置导航" currentId={section} groups={[{ label: '偏好与管理', items: [{ id: 'account', label: '账号', href: '#account' }, { id: 'users', label: '用户管理与权限', href: '#users' }, { id: 'public', label: '公开项目', href: '#public' }] }]} onNavigate={(item, event) => { event.preventDefault(); setSection(item.id); }} />}>
16
+ <Stack gap={4}>
17
+ <Tooltip content="选择栏目以管理设置;保存后生效,取消仅丢弃未保存的修改。"><Button variant="quiet">设置帮助</Button></Tooltip>
18
+ {section === 'account' && <><Text as="h3" variant="heading">账号</Text><TextField label="工作区名称" value={draft} onChange={event => { draftEdited.current = true; setDraft(event.target.value); }} /><Button busy={busy} onClick={save}>保存设置</Button><Button variant="secondary" onClick={() => { draftEdited.current = false; setDraft(saved); setMessage('已撤销未保存修改'); }}>取消草稿</Button></>}
19
+ {section === 'users' && <><Text as="h3" variant="heading">用户管理</Text>{Array.from({length: 12}, (_, i) => <Stack key={i} gap={2}><Text>演示成员 {i + 1} · 长名称研究工作组</Text><Dialog trigger={<Button variant="secondary">编辑成员 {i + 1}</Button>} title={`编辑成员 ${i + 1}`} description="内层 Escape 只关闭当前编辑"><TextField label="成员备注" defaultValue="合成备注" /></Dialog></Stack>)}</>}
20
+ {section === 'public' && <><Text as="h3" variant="heading">公开项目</Text><Select label="项目状态" value={state} onChange={event => setState(event.target.value)}><option value="normal">正常</option><option value="loading">加载</option><option value="error">失败</option><option value="empty">空态</option></Select>{state === 'loading' ? <Skeleton label="正在读取项目" /> : state === 'error' ? <Notice tone="error" title="项目读取失败" action={<Button onClick={() => setState('normal')}>重试</Button>} /> : state === 'empty' ? <EmptyState title="暂无公开项目" description="添加项目后会显示在这里。" /> : <TextField label="项目标题" defaultValue="公开研究计划" />}</>}
21
+ {message && <Text role="status">{message}</Text>}
22
+ </Stack>
23
+ </Dialog>
24
+ <Text>已保存名称:{saved}</Text>
25
+ </Stack>;
26
+ }
27
+
28
+ export function SettingsDialogFixture() {
29
+ const params = new URLSearchParams(location.search);
30
+ const zoom = Number(params.get('zoom')) || 1;
31
+ useEffect(() => { document.documentElement.style.zoom = String(zoom); return () => { document.documentElement.style.zoom = ''; }; }, [zoom]);
32
+ return <ThemeProvider theme={(params.get('theme') || 'rhine') as ThemeName} data-rhine-motion="reduced"><div style={{padding:16}}><SettingsDialogGallery /></div></ThemeProvider>;
33
+ }
@@ -0,0 +1,24 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Button, Checkbox, Dialog, Stack, Text, TextField } from '../components';
3
+ import { ThemePicker } from '../components/ThemePicker';
4
+ import { ThemeProvider, themes, useTheme, type ThemeName } from '../components/theme';
5
+
6
+ export function ThemePickerGallery() {
7
+ const { theme, setTheme } = useTheme();
8
+ const [disabled, setDisabled] = useState(false), [longNames, setLongNames] = useState(false), [failed, setFailed] = useState(false);
9
+ const [draft, setDraft] = useState('切换主题时保留');
10
+ const options = themes.map(item => ({ value: item.name, label: longNames ? `${item.label} · 适合长时间阅读与资源管理的工作区外观` : item.label, previewSrc: failed ? '/missing-theme-preview.png' : undefined }));
11
+ return <Stack gap={4}><Text as="h2" variant="section">先比较,再应用</Text><Text tone="muted">预览来自同一组官方组件,保持各自的配色。选择即应用;偏好是否保存由宿主管理。</Text>
12
+ <ThemePicker value={theme} onValueChange={setTheme} options={options} disabled={disabled} />
13
+ <Stack gap={2}><Checkbox label="长主题名称" checked={longNames} onChange={event => setLongNames(event.target.checked)} /><Checkbox label="禁用主题选择" checked={disabled} onChange={event => setDisabled(event.target.checked)} /><Checkbox label="模拟预览失败" checked={failed} onChange={event => setFailed(event.target.checked)} /></Stack>
14
+ <TextField label="未保存草稿" value={draft} onChange={event => setDraft(event.target.value)} />
15
+ <Dialog trigger={<Button variant="secondary">弹窗内切换主题</Button>} title="外观设置" description="切换后当前弹窗与草稿保持打开"><Stack gap={4}><TextField label="弹窗草稿" defaultValue="继续编辑" /><ThemePicker value={theme} onValueChange={setTheme} /></Stack></Dialog>
16
+ </Stack>;
17
+ }
18
+
19
+ export function ThemePickerFixture() {
20
+ const params = new URLSearchParams(location.search);
21
+ const zoom = Number(params.get('zoom')) || 1;
22
+ useEffect(() => { document.documentElement.style.zoom = String(zoom); return () => { document.documentElement.style.zoom = ''; }; }, [zoom]);
23
+ return <ThemeProvider defaultTheme={(params.get('theme') || 'rhine') as ThemeName} data-rhine-motion="reduced"><div style={{padding:16}}><ThemePickerGallery /></div></ThemeProvider>;
24
+ }
@@ -0,0 +1,14 @@
1
+ import { Badge, Button, Inline, SideNav, Stack, Surface, Text } from '../components';
2
+ import { ThemeProvider, type ThemeName } from '../components/theme';
3
+
4
+ /** Synthetic, deterministic source for the shipped 960 × 600 theme previews. */
5
+ export function ThemeSample() {
6
+ const theme = (new URLSearchParams(location.search).get('theme') || 'rhine') as ThemeName;
7
+ return <ThemeProvider theme={theme} data-rhine-motion="reduced" style={{ boxSizing: 'border-box', width: 960, height: 600, padding: 32, display: 'grid', gridTemplateColumns: '200px minmax(0,1fr)', gap: 28 }}>
8
+ <SideNav aria-label="样本导航" currentId="library" groups={[{ label: '研究工作区', items: [{ id: 'library', label: '资源库', href: '#library' }, { id: 'collections', label: '收藏集', href: '#collections' }, { id: 'activity', label: '最近活动', href: '#activity' }] }]} />
9
+ <Stack gap={4}><Inline><Text as="h1" variant="title">让内容保持清晰</Text><Badge tone="success">已同步</Badge></Inline><Text tone="muted">相同的页面,比较每种主题的表面、文字与操作。</Text>
10
+ <Surface><Stack gap={4}><Text as="h2" variant="heading">本周研究计划</Text><Text>整理资料与发现,在需要时继续工作。</Text><Inline><Button>新建项目</Button><Button variant="secondary">查看详情</Button></Inline></Stack></Surface>
11
+ <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:16}}>{['设计与观察', '记录与归档'].map((title, i) => <Surface key={title}><Stack gap={3}><div aria-hidden="true" style={{height:80,background:'var(--rhine-selected)',border:'1px solid var(--rhine-line)',display:'grid',placeItems:'center'}}><span style={{fontSize:40,color:'var(--rhine-green)'}}>{i ? '◷' : '△'}</span></div><Text variant="heading">{title}</Text><Text variant="caption" tone="muted">12 项合成资料 · 随时继续</Text></Stack></Surface>)}</div>
12
+ </Stack>
13
+ </ThemeProvider>;
14
+ }
@@ -0,0 +1,36 @@
1
+ import { useState } from 'react';
2
+ import { Button, Dialog, Inline, Notice, Select, Skeleton, Stack, StatusTimelineList, Surface, Text, TimelineLegend, type StatusTimelineItem, type TimelineSegment } from '../components';
3
+
4
+ export function TimelineListGallery() {
5
+ const [state, setState] = useState('ready');
6
+ const segments: TimelineSegment[] = Array.from({ length: 96 }, (_, i) => ({
7
+ id: String(i), label: `10/23 ${String(Math.floor(i / 4)).padStart(2, '0')}:${String(i % 4 * 15).padStart(2, '0')} · UTC+8`,
8
+ status: i % 29 === 0 ? 'failed' : i % 17 === 0 ? 'degraded' : i % 13 === 0 ? 'recovered' : i % 7 === 0 ? 'unknown' : 'healthy',
9
+ }));
10
+ const items: StatusTimelineItem[] = state === 'empty' ? [] : ['资源 A', '资源 B'].map((title, i) => ({
11
+ id: String(i), title: state === 'long' ? `${title} · 跨区域协作工作空间 / very-long-resource-name-without-a-break-2026` : title,
12
+ summary: <>7 日剩余 <strong>{i ? '—' : '78%'}</strong></>,
13
+ note: state === 'partial' ? '记录不完整,仅展示已确认的时段。' : undefined,
14
+ timeline: state === 'unavailable' ? null : { startLabel: '10/23 00:00 · UTC+8', endLabel: '10/24 00:00 · UTC+8', segments, disabled: state === 'disabled' },
15
+ }));
16
+ const content = <Stack gap={3}>
17
+ {state === 'loading' ? <Skeleton label="正在读取历史记录" lines={3} /> : <>
18
+ {state === 'error' && <Notice tone="error" title="读取失败,保留上次记录">请重试或检查数据来源;旧记录不代表当前状态。</Notice>}
19
+ <StatusTimelineList label="资源历史记录" items={items} />
20
+ </>}
21
+ </Stack>;
22
+ return <Surface className="ds-gap-top"><Stack gap={4}>
23
+ <Text as="h3" variant="section">多条历史记录</Text>
24
+ <Text tone="muted">标题、摘要、分隔、紧凑时间条与详情统一组合;同一列表只展开一条详情。示例数据不连接任何服务。</Text>
25
+ <Select label="历史列表状态" value={state} onChange={event => setState(event.target.value)}>
26
+ <option value="ready">默认</option><option value="loading">首次加载</option><option value="error">读取失败 / 保留旧记录</option>
27
+ <option value="empty">空列表</option><option value="unavailable">历史未知</option><option value="partial">不完整</option>
28
+ <option value="disabled">禁用</option><option value="long">长名称</option>
29
+ </Select>
30
+ {content}<TimelineLegend />
31
+ <Dialog title="资源范围" description="左侧操作,右侧查看当前范围的记录;纯演示,不保存设置。" trigger={<Button variant="secondary">在分栏弹窗中查看</Button>}
32
+ sidePanel={closeButton => <Stack gap={4}><Inline style={{ justifyContent: 'space-between' }}><Text as="h3">资源 · {items.length}</Text>{closeButton}</Inline>{content}</Stack>}>
33
+ <Text>同一组件也可嵌入侧栏;在两条时间轴间悬停或按 Tab,检查详情互斥与焦点。</Text>
34
+ </Dialog>
35
+ </Stack></Surface>;
36
+ }
@@ -4,3 +4,12 @@
4
4
  .ds-extension-controls .rhine-select { width:auto; max-width:100%; }
5
5
  .ds-shell-stage { height:680px; max-height:85dvh; min-height:380px; min-width:0; }
6
6
  .ds-background-swatch { display:grid; gap:24px; padding:24px; border:1px solid var(--rhine-line); }
7
+
8
+ .ds-media-intro { max-width:58ch; color:var(--rhine-muted); margin:0 0 24px; line-height:var(--rhine-leading-body); }
9
+ .ds-media-toolbar { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:16px; padding:16px 0; margin-bottom:24px; border-block:1px solid var(--rhine-control); }
10
+ .ds-media-toolbar>span { font:var(--rhine-text-caption)/var(--rhine-leading-caption) var(--rhine-mono); }
11
+ .ds-media-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr)); gap:24px; align-items:start; }
12
+ .ds-media-section-heading { margin-top:48px; margin-bottom:24px; }
13
+ .ds-media-section-heading h3 { font-size:var(--rhine-text-section); margin:0 0 8px; }
14
+ .ds-media-section-heading p { color:var(--rhine-muted); margin:0; }
15
+ .ds-media-note { margin-top:24px; }
package/preview/main.tsx CHANGED
@@ -1,7 +1,10 @@
1
1
  import { StrictMode, useEffect, useRef, useState } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
- import { Badge, Button, Disclosure, EmptyState, Logo, Notice, Progress, SegmentedControl, Select, SelectionCard, Skeleton, Surface, Tabs, Text, TextField, Toolbar, type TextVariant } from '../components/index';
3
+ import { Badge, Button, Checkbox, Disclosure, EmptyState, Logo, Notice, Progress, SegmentedControl, Select, SelectionCard, Skeleton, Surface, Tabs, Text, TextField, Toolbar, type TextVariant } from '../components/index';
4
4
  import './preview.css';
5
+ import { OverlayBoundsFixture, OverlayBoundsGallery } from './OverlayBoundsGallery';
6
+ import { SettingsDialogGallery, SettingsDialogFixture } from './SettingsDialogGallery';
7
+ import { ThemePickerGallery, ThemePickerFixture } from './ThemePickerGallery';
5
8
  import { CommonGallery } from './CommonGallery';
6
9
  import { MotionGallery } from './MotionGallery';
7
10
  import { StartupGallery } from './StartupGallery';
@@ -12,8 +15,11 @@ import { LineHeightGallery } from './LineHeightGallery';
12
15
  import { SubscriptionGallery } from './SubscriptionGallery';
13
16
  import { BackgroundGallery } from './BackgroundGallery';
14
17
  import { OrbitGallery } from './OrbitGallery';
18
+ import { NeutronStarGallery } from './NeutronStarGallery';
15
19
  import { AdvancedDataGallery, AdvancedFormsGallery, MediaGallery, ShellGallery } from './ExtensionsGallery';
16
20
  import { MonitoringGallery } from './MonitoringGallery';
21
+ import { DateRangeFilterGallery } from './DateRangeFilterGallery';
22
+ import { CompactAccountGallery } from './CompactAccountGallery';
17
23
 
18
24
  function Gallery() {
19
25
  const [motion, setMotion] = useState('system');
@@ -21,6 +27,7 @@ function Gallery() {
21
27
  const [previewPage, setPreviewPage] = useState('oauth');
22
28
  const [busy, setBusy] = useState(false);
23
29
  const [selected, setSelected] = useState(false);
30
+ const [showSelectionIndicator, setShowSelectionIndicator] = useState(false);
24
31
  const [tab, setTab] = useState('tokens');
25
32
  const [metric, setMetric] = useState('tokens');
26
33
  const [progress, setProgress] = useState<number | null>(38);
@@ -32,10 +39,16 @@ function Gallery() {
32
39
  useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
33
40
  const simulate = () => { setBusy(true); timer.current = setTimeout(() => { setBusy(false); setNotice(true); }, 900); };
34
41
  return <GalleryShell motion={motion} onMotionChange={setMotion}>{section => <>
42
+ {section === 'theme-picker' && <ThemePickerGallery />}
43
+ {section === 'settings-dialog' && <SettingsDialogGallery />}
44
+ {section === 'overlay-bounds' && <OverlayBoundsGallery />}
35
45
  {section === 'monitoring' && <MonitoringGallery />}
46
+ {section === 'date-range-filter' && <DateRangeFilterGallery />}
47
+ {section === 'compact-account' && <CompactAccountGallery />}
36
48
  {section === 'backgrounds' && <BackgroundGallery />}
37
49
  {section === 'app-shell' && <ShellGallery />}
38
50
  {section === 'orbit' && <OrbitGallery />}
51
+ {section === 'neutron-star' && <NeutronStarGallery />}
39
52
  {section === 'advanced-forms' && <AdvancedFormsGallery />}
40
53
  {section === 'advanced-data' && <AdvancedDataGallery />}
41
54
  {section === 'media' && <MediaGallery />}
@@ -99,7 +112,7 @@ function Gallery() {
99
112
  </section>}
100
113
 
101
114
  {section === 'selection' && <section id="selection"><div className="ds-section-title"><span>03</span><h2>选择清楚,位置稳定</h2></div>
102
- <div className="ds-grid"><div><SelectionCard label="可选择容器" selected={selected} onSelectedChange={setSelected}><span className="ds-caption">适用于账号、模板或可取消选择项。<br />同一位置切换底色与边框,不抬升卡片。</span><div className="ds-gap-top"><Badge tone={selected?'success':'neutral'}>{selected?'已选择':'未选择'}</Badge></div></SelectionCard><p className="ds-caption">SelectionCard · 背景与边界 120ms</p></div>
115
+ <div className="ds-grid"><div><Checkbox label="显示卡片勾选框" checked={showSelectionIndicator} onChange={event => setShowSelectionIndicator(event.target.checked)} /><SelectionCard showIndicator={showSelectionIndicator} label="可选择容器" selected={selected} onSelectedChange={setSelected}><span className="ds-caption">适用于账号、模板或可取消选择项。<br />同一位置切换底色与边框,不抬升卡片。</span><div className="ds-gap-top"><Badge tone={selected?'success':'neutral'}>{selected?'已选择':'未选择'}</Badge></div></SelectionCard><p className="ds-caption">SelectionCard · 背景与边界 120ms</p></div>
103
116
  <Surface><div className="ds-component-title"><h3>SegmentedControl</h3><span>原生单选 / 120ms</span></div><SegmentedControl label="展示指标" items={[{value:'tokens',label:'Token'},{value:'requests',label:'请求数'},{value:'cost',label:'费用',disabled:true}]} value={metric} onValueChange={setMetric} /><p className="ds-caption">当前指标:{metric==='tokens'?'Token':'请求数'}。方向键切换,自动跳过禁用项。</p></Surface></div>
104
117
  <Surface className="ds-gap-top"><div className="ds-component-title"><h3>Tabs</h3><span>下划线与内容进入 180ms</span></div><Tabs label="组件说明" value={tab} onValueChange={setTab} items={[{value:'tokens',label:'视觉变量',content:<p>颜色、间距、字体与动效集中定义。业务页面只消费语义变量。</p>},{value:'disabled',label:'预留示例',disabled:true,content:null},{value:'motion',label:'动效规则',content:<p>内容进入最多移动 3px。保留隐藏面板状态,切换不触发重复业务操作。</p>}]} /></Surface>
105
118
  </section>}
@@ -122,4 +135,4 @@ function Gallery() {
122
135
 
123
136
  }
124
137
 
125
- createRoot(document.getElementById('root')!).render(<StrictMode><Gallery /></StrictMode>);
138
+ createRoot(document.getElementById('root')!).render(<StrictMode>{new URLSearchParams(location.search).get('fixture') === 'overlay-bounds' ? <OverlayBoundsFixture /> : new URLSearchParams(location.search).get('fixture') === 'settings-dialog' ? <SettingsDialogFixture /> : new URLSearchParams(location.search).get('fixture') === 'theme-picker' ? <ThemePickerFixture /> : <Gallery />}</StrictMode>);
@@ -1,4 +1,10 @@
1
1
  body { margin:0; }
2
+ .compact-gallery { padding-block:var(--rhine-space-6); }
3
+ .compact-gallery-stage { max-width:100%; min-width:0; }
4
+ .compact-gallery-examples { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,320px),1fr)); gap:var(--rhine-space-6); align-items:start; }
5
+ .compact-gallery-examples>* { min-width:0; }
6
+ .compact-gallery-fact-pair { align-items:baseline; }
7
+ .compact-gallery-fact-pair>* { min-width:0; overflow-wrap:anywhere; }
2
8
  .ds-type-row { display:grid; grid-template-columns:180px minmax(0,1fr); align-items:baseline; gap:12px 24px; padding:16px 0; border-bottom:1px solid var(--rhine-line); }
3
9
  .ds-type-row:last-child { border:0; }
4
10
  .ds-type-copy { display:grid; gap:12px; margin-top:16px; max-width:60ch; }
@@ -18,4 +24,6 @@ body { margin:0; }
18
24
  .ds-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:24px; align-items:start; }.ds-grid>* { min-width:0; }.ds-component-title { display:flex; align-items:baseline; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-bottom:20px; }.ds-component-title h3 { font:600 16px var(--rhine-mono); margin:0; }.ds-component-title>span { color:var(--rhine-muted); font-size:12px; }
19
25
  .ds-row { display:flex; flex-wrap:wrap; gap:12px; align-items:center; }.ds-caption { font-size:12px; line-height:1.8; color:var(--rhine-muted); }.ds-gap-top { margin-top:20px; }.ds-badges { margin-bottom:24px; }.ds-loaded { min-height:72px; margin:0; }
20
26
  .ds-footer { border-top:1px solid var(--rhine-ink); display:flex; justify-content:space-between; gap:16px; flex-wrap:wrap; padding:24px 0; color:var(--rhine-muted); font-size:12px; }
27
+ .ds-orbit-artwork { min-height:clamp(360px, 52vw, 560px); }
28
+ .ds-orbit-example-content { width:min(100%, 560px); }
21
29
  @media(max-width:760px) { .ds-hero,.ds-grid { grid-template-columns:1fr; gap:24px; }.ds-hero { padding-top:32px; }.ds-hero h1 { font-size:32px; letter-spacing:-1px; }.ds-palette { grid-template-columns:repeat(auto-fit,minmax(min(100%,90px),1fr)); }.ds-palette>div>div { height:64px; }.ds-section-title h2 { font-size:19px; } }
@@ -0,0 +1 @@
1
+ <!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Theme preview source</title><style>body{margin:0}</style></head><body><div id="root"></div><script type="module">import React from 'react'; import {createRoot} from 'react-dom/client'; import {ThemeSample} from './ThemeSample'; createRoot(document.getElementById('root')).render(React.createElement(ThemeSample));</script></body></html>