@linhey/rhine-design 0.3.0 → 0.4.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 (52) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +53 -14
  3. package/components/Actions.tsx +2 -0
  4. package/components/COMMON_COMPONENTS.md +5 -1
  5. package/components/FormWorkflow.tsx +25 -0
  6. package/components/MOTION.md +6 -2
  7. package/components/Navigation.tsx +42 -0
  8. package/components/Overlays.tsx +9 -3
  9. package/components/README.md +13 -5
  10. package/components/SPACING.md +82 -0
  11. package/components/StatusTimeline.tsx +29 -0
  12. package/components/TYPOGRAPHY.md +34 -2
  13. package/components/common.css +12 -3
  14. package/components/index.ts +7 -1
  15. package/components/patterns.css +39 -0
  16. package/components/styles.css +12 -8
  17. package/components/theme-arknights.css +301 -0
  18. package/components/theme-neo-brutalism.css +84 -0
  19. package/components/theme.tsx +58 -0
  20. package/components/timeline.css +23 -0
  21. package/components/tokens.css +6 -1
  22. package/docs/DESIGN_SYSTEM.md +12 -2
  23. package/docs/DESIGN_SYSTEM_AUDIT.md +4 -4
  24. package/docs/INTERACTION_PATTERNS.md +129 -0
  25. package/docs/REFERENCE_RESEARCH.md +36 -0
  26. package/docs/THEME_NEO_BRUTALISM.md +37 -0
  27. package/docs/THEMING.md +104 -0
  28. package/package.json +5 -1
  29. package/preview/CommonGallery.tsx +8 -8
  30. package/preview/GalleryShell.tsx +106 -0
  31. package/preview/LineHeightGallery.tsx +27 -0
  32. package/preview/MotionGallery.tsx +6 -2
  33. package/preview/PatternGallery.tsx +52 -0
  34. package/preview/SpacingGallery.tsx +32 -0
  35. package/preview/SubscriptionGallery.tsx +84 -0
  36. package/preview/line-height.css +2 -0
  37. package/preview/main.tsx +39 -25
  38. package/preview/patterns.css +7 -0
  39. package/preview/preview.css +2 -8
  40. package/preview/shell.css +35 -0
  41. package/preview/spacing.css +2 -0
  42. package/preview/subscriptions.css +27 -0
  43. package/preview/theme-arknights.css +234 -0
  44. package/preview/theme-neo-brutalism.css +11 -0
  45. package/skills/rhine-design/SKILL.md +5 -3
  46. package/skills/rhine-design/references/API_MAP.md +7 -3
  47. package/skills/rhine-design/references/TASK_MAP.md +4 -1
  48. package/skills/rhine-design/references/integration.md +4 -2
  49. package/tests/GalleryShell.test.tsx +98 -0
  50. package/tests/Patterns.test.tsx +75 -0
  51. package/tests/StatusTimeline.test.tsx +15 -0
  52. package/tests/Theme.test.tsx +112 -0
@@ -0,0 +1,27 @@
1
+ import { useState } from 'react';
2
+ import { Checkbox, Grid, Stack, Surface, Text, type TextVariant } from '../components';
3
+ import type { CSSProperties } from 'react';
4
+ import './line-height.css';
5
+
6
+ const samples: {variant: TextVariant; label: string; size: string; leading: string; copy: string}[] = [
7
+ {variant:'heading',label:'卡片标题',size:'16 / 24px',leading:'heading',copy:'让每一行标题保持清晰,让多行内容自然延续。'},
8
+ {variant:'body',label:'正文',size:'14 / 22px',leading:'body',copy:'使用统一的文字节奏组织内容。说明自然换行,保留中英文 API 和数字 128 的阅读空间,不通过压缩行高限制卡片高度。'},
9
+ {variant:'caption',label:'辅助说明',size:'12 / 18px',leading:'caption',copy:'最近检查 14:32 · 示例数据。长说明允许换行,时间与来源仍保持可读,重要信息不使用省略号隐藏。'},
10
+ {variant:'code',label:'代码与路径',size:'12 / 20px',leading:'code',copy:'workspace/example-project/configuration/very-long-resource-identifier-for-line-wrapping.json'},
11
+ ];
12
+
13
+ export function LineHeightGallery() {
14
+ const [guides,setGuides]=useState(true);
15
+ return <div className="ds-gap-top"><Stack gap={6}>
16
+ <Stack gap={2}><Text as="h3" variant="section">多行行高与段落</Text><Text tone="muted">行高控制每一行的高度,gap控制内容组之间的距离。辅助线标出行框,不是字体基线。</Text></Stack>
17
+ <Checkbox label="显示行框辅助线" checked={guides} onChange={event=>setGuides(event.target.checked)}/>
18
+ <Grid gap={6} minWidth={240}>{samples.map(sample=><Surface key={sample.variant}><Stack gap={4}>
19
+ <Stack gap={2}><Text as="h4" variant="heading">{sample.label}</Text><Text variant="code" tone="muted">{sample.size} · --rhine-leading-{sample.leading}</Text></Stack>
20
+ <Text variant={sample.variant} className="ds-leading-sample" data-guides={guides} style={{'--ds-leading':`var(--rhine-leading-${sample.leading})`} as CSSProperties}>{sample.copy}</Text>
21
+ </Stack></Surface>)}</Grid>
22
+ <Surface><Stack gap={4}><Text as="h4" variant="heading">段内22px,段间12px</Text><Stack gap={3} className="ds-leading-paragraphs">
23
+ <Text className="ds-leading-sample" data-guides={guides} style={{'--ds-leading':'var(--rhine-leading-body)'} as CSSProperties}>第一段:文本自然换行,每行高度为22px。文字越长,段落越高,不添加额外的行间gap。</Text>
24
+ <Text className="ds-leading-sample" data-guides={guides} style={{'--ds-leading':'var(--rhine-leading-body)'} as CSSProperties}>第二段:这是独立的信息段,由Stack gap=3提供12px段落间距,不使用空行或br撑开。</Text>
25
+ </Stack></Stack></Surface>
26
+ </Stack></div>;
27
+ }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
- import { Badge, Button, Checkbox, Dialog, Disclosure, Icon, Logo, Menu, Popover, SegmentedControl, Select, Skeleton, Stack, Switch, Tabs, Text, Toast, Tooltip } from '../components/index';
2
+ import { Badge, Button, Checkbox, Dialog, Drawer, Disclosure, Icon, Logo, Menu, Popover, SegmentedControl, Select, Skeleton, Stack, Steps, Switch, Tabs, Text, TextField, Toast, Tooltip } from '../components/index';
3
3
  import './motion.css';
4
4
  import './brand-assembly.css';
5
5
 
@@ -13,6 +13,8 @@ const recipes = [
13
13
  ['selection', '切换 · 稳定的位置', '120 / 180ms', '默认选项 → 指示与滑块切换 → 保留内容', '现有组件'],
14
14
  ['toast', '通知 · 轻量回应', '180ms', '静态占位 → 通知进入 → 手动关闭', '现有组件'],
15
15
  ['tooltip', '提示 · 即看即走', '160ms', '播放或悬停 → 提示出现 → Escape关闭', '现有组件'],
16
+ ['drawer', '抽屉 · 保留上下文', '160ms / 3px', '入口 → 右侧详情 → Escape 返回入口', '现有组件'],
17
+ ['steps', '步骤 · 验证后推进', '180ms / 3px', '模拟本步完成 → 下一步进入 → 保持静止', '现有组件'],
16
18
  ['brand', '品牌 · 旋转变形归位', '880ms / 三块错峰', '三块旋转压缩 → 展开闭合 → 定稿标志静止', '品牌候选'],
17
19
  ] as const;
18
20
  type Kind = typeof recipes[number][0];
@@ -58,6 +60,8 @@ function Scene({ kind, active, side }: { kind: Kind; active: boolean; side: Side
58
60
  if (kind === 'result') return <div className="ds-result-demo"><Button disabled={!started || done} busy={started && !done} busyLabel="演示处理中">保存示例</Button><div className="ds-result-slot" role="status">{done ? <span className="ds-result-reveal"><Icon name="check" /> 已完成 · 演示</span> : <Text variant="caption" tone="muted">{started ? '等待合成结果…' : '等待播放'}</Text>}</div></div>;
59
61
  if (kind === 'popover') return <Popover open={open} onOpenChange={setOpen} side={preferredSide} title="方向感知气泡" trigger={<Button variant="secondary">查看气泡</Button>}><Text>方向跟随实际位置。窄窗左右方向改为上下展开。</Text></Popover>;
60
62
  if (kind === 'menu') return <Menu open={open} onOpenChange={setOpen} side={preferredSide} label="动效菜单" trigger={<Button variant="secondary">打开动效菜单</Button>} items={[{id:'view',label:'查看示例',onSelect:()=>{}},{id:'disabled',label:'暂不可用',disabled:true,onSelect:()=>{}},{id:'close',label:'结束演示',onSelect:()=>{}}]} />;
63
+ if (kind === 'steps') return <Steps current={started ? 1 : 0} items={[{id:'input',label:'输入信息'},{id:'verify',label:'核对配置'},{id:'done',label:'完成任务'}]}/>;
64
+ if (kind === 'drawer') return <Drawer open={open} onOpenChange={setOpen} title="抽屉动效示例" description="详情由右侧轻入;重播只重启动画,输入和焦点不变。" trigger={<Button variant="secondary">打开动效抽屉</Button>}><Stack><Text>按 Escape 关闭后回到入口。</Text><TextField label="重播保留输入" placeholder="输入演示文字后重播"/><Button variant="secondary" onClick={event=>replayModal(event.currentTarget)}>重播抽屉入场</Button></Stack></Drawer>;
61
65
  if (kind === 'dialog') return <Dialog open={open} onOpenChange={setOpen} title="弹窗动效示例" description="文字保持原尺寸;重播仅重新播放淡入,不重建弹窗或移动焦点。" trigger={<Button variant="secondary">打开动效弹窗</Button>}><Stack><Text>按 Escape 关闭,焦点回到入口。</Text><Button variant="secondary" onClick={event=>replayModal(event.currentTarget)}>重播弹窗入场</Button></Stack></Dialog>;
62
66
  if (kind === 'disclosure') return <div ref={disclosure} className="ds-motion-fill"><Disclosure title="展开动效说明"><Text>内容沿高度展开,文字保持原尺寸。</Text><Text tone="muted" variant="caption">反复点击标题可观察中途反向。</Text></Disclosure></div>;
63
67
  if (kind === 'selection') return <Stack className="ds-motion-fill"><Switch label="显示辅助信息" checked={enabled} onCheckedChange={setEnabled}/><Tabs label="动效切换示例" value={selected} onValueChange={setSelected} items={[{value:'before',label:'概览',content:<Text>默认阅读内容。</Text>},{value:'after',label:'详情',content:<Text>选中状态即时更新,内容轻轻进入。</Text>}]} /></Stack>;
@@ -83,7 +87,7 @@ function MotionCard({ recipe, slow, reduced }: { recipe: typeof recipes[number];
83
87
  export function MotionGallery({ reduced }: { reduced: boolean }) {
84
88
  const [speed, setSpeed] = useState('normal');
85
89
  return <section id="motion" className="ds-motion-lab"><div className="ds-section-title"><span>MOTION LAB</span><h2>按下播放,感受变化。</h2></div>
86
- <div className="ds-motion-toolbar"><div><Text>10 组可重播示例 · 现有组件与新动效候选</Text><Text variant="caption" tone="muted">播放只改变本地合成状态。浮层按 Escape 关闭;慢放只延长视觉过渡,减少动态始终优先。</Text></div><SegmentedControl label="动效播放速度" value={speed} onValueChange={setSpeed} items={[{value:'normal',label:'原速'},{value:'slow',label:'4 倍慢放'}]}/></div>
90
+ <div className="ds-motion-toolbar"><div><Text>12 组可重播示例 · 现有组件与新动效候选</Text><Text variant="caption" tone="muted">播放只改变本地合成状态。浮层按 Escape 关闭;慢放只延长视觉过渡,减少动态始终优先。</Text></div><SegmentedControl label="动效播放速度" value={speed} onValueChange={setSpeed} items={[{value:'normal',label:'原速'},{value:'slow',label:'4 倍慢放'}]}/></div>
87
91
  <div className="ds-motion-grid">{recipes.map(recipe=><MotionCard key={recipe[0]} recipe={recipe} slow={speed==='slow'} reduced={reduced}/>)}</div>
88
92
  </section>;
89
93
  }
@@ -0,0 +1,52 @@
1
+ import { useId, useState } from 'react';
2
+ import { Dialog, Inline, StatusTimeline, TimelineLegend, Badge, Breadcrumbs, Button, Drawer, ErrorSummary, Fieldset, KeyValue, Metric, Notice, Select, SideNav, Stack, Steps, Surface, Text, TextField, type FieldError } from '../components';
3
+ import './patterns.css';
4
+
5
+ export const scenarios = [
6
+ ['navigation', '跨模块工作', 'SideNav + Breadcrumbs', '侧边栏切换工作区;面包屑表达父子层级。当前位置用 aria-current,切换后聚焦主内容。', '导航状态即时变化,内容 180ms / 3px 进入。', '同页少量内容用 Tabs;面包屑不表示操作历史。'],
7
+ ['tabs', '同一对象的不同视图', 'Tabs / SegmentedControl', 'Tabs 切换独立内容区域;SegmentedControl 改变一个视图的指标或模式。方向键操作,焦点留在选项。', '边界 120ms,面板进入 180ms。', '不要把筛选指标做成全局导航。'],
8
+ ['form', '复杂表单与纠错', 'Fieldset + TextField + ErrorSummary', '按任务分组,提交后同时显示字段错误与顶部摘要。摘要获得焦点,点问题回到字段;保留已填值。', '立即显示错误,不抖动、不滚动循环。', '单字段就地错误即可;不能只给 Toast。'],
9
+ ['steps', '分阶段配置', 'Steps + Fieldset', '当前步骤由业务校验推进;允许返回已完成步骤。步骤完成与真实结果一致。', '当前段 180ms 淡入 / 3px;不自动走步骤。', '未知耗时用 Progress 不确定态;步骤条不是百分比。'],
10
+ ['detail', '列表中查看记录', 'Drawer + KeyValue', '保持列表上下文,临时查看详情。焦点进入抽屉,Escape 关闭后返回入口。', '160ms / 3px 由右侧进入,关闭即时归还焦点。', '需要长期链接或大量编辑时使用独立详情页。'],
11
+ ['confirm', '不可逆操作', 'ConfirmDialog', '说明对象、影响和明确动词;默认焦点在取消。等待真实结果,失败保留上下文。', '160ms 淡入;等待期间保持尺寸,不延迟结果。', '普通保存不弹二次确认;可撤销操作优先撤销。'],
12
+ ['feedback', '任务结果与系统异常', 'Toast / Notice', '短暂成功用 Toast;字段或区域失败用就地 Notice;需要行动的错误持续显示。', '通知 180ms / 3px;悬停或聚焦暂停倒计时。', '不能让关键错误自动消失,也不抢占焦点。'],
13
+ ['load', '首次读取与重新请求', 'Skeleton / Progress / Button busy', '首次已知结构用骨架;提交用按钮 busy;刷新保留旧数据并标记更新状态。', '内容就绪 180ms 原位淡入;减少动态时静态占位。', '不伪造百分比,不清空用户正在看的内容。'],
14
+ ['search', '大列表搜索与选择', 'SearchField / Combobox + Pagination', '输入法完成后查询,防抖、取消过期请求;服务端返回结果和排序。加载时不可选旧结果。', '状态即时更新;结果区域 180ms,不逐条错峰。', '不能拉取全量数据后假装服务端搜索。'],
15
+ ['choice', '选项数量与选择规则', 'RadioGroup / Select / Checkbox / Switch', '少量互斥项用 Radio;较多互斥项用 Select;多选用 Checkbox;即时生效的二态偏好用 Switch。', '选择边界 120ms,开关保持固定位置。', '需要保存才生效的选项应说明保存规则;不伪装即时生效。'],
16
+ ['overlay', '局部说明与次级操作', 'Tooltip / Popover / Menu', 'Tooltip 是非必要补充;Popover 放可交互详情;Menu 提供命令。都应有键盘入口和关闭路径。', '160ms / 3px,按实际放置方向进入。', '必要说明常驻;不要把表单放进 Tooltip。'],
17
+ ['empty', '空白、无结果、无权限', 'EmptyState / Notice', '区分尚未创建、筛选无结果、读取失败与无权限;提供对应的创建、清除筛选或重试动作。', '静态为主,状态替换 180ms。', '失败不是空列表;无权限不能提供无效创建按钮。'],
18
+ ['monitor', '监控数据与告警', 'Metric + Badge + DataTable + Notice', '值、单位、时间范围和最后更新时间一起展示。告警须有文字、级别和处理入口,过期数据标明陈旧。', '更新值原位替换,不滚动数字、不闪烁告警。', '未知值显示 —;不要用 0 掩盖采集失败或只靠红绿色。'],
19
+ ['motion', '品牌与日常交互', 'Logo / 启动编排 / 基础过渡', '品牌动画只在启动或明确重播时出现;业务界面用几何边界和稳定位置建立识别。', '日常 120–240ms;品牌独立编排,减少动态直接到最终态。', '不把 Logo 当加载器,不因动画阻塞可用页面。'],
20
+ ] as const;
21
+
22
+ export function PatternGallery({ section }: { section: string }) {
23
+ const [step, setStep] = useState(0), [chapter, setChapter] = useState('overview');
24
+ const [choice, setChoice] = useState('navigation');
25
+ const [monitor, setMonitor] = useState('normal');
26
+ const [previewScope, setPreviewScope] = useState('primary');
27
+ const [name, setName] = useState(''), [endpoint, setEndpoint] = useState('');
28
+ const [attempt, setAttempt] = useState(0), [errors, setErrors] = useState<FieldError[]>([]), [saved, setSaved] = useState(false);
29
+ const id = useId(), nameId = `${id}-name`, endpointId = `${id}-endpoint`;
30
+ const scenario = scenarios.find(item => item[0] === choice)!;
31
+ if (section === 'navigation') return <section id="navigation"><div className="ds-section-title"><span>NAV</span><h2>去哪里,现在到哪一步。</h2></div><p className="ds-caption">导航表达空间层级,步骤表达任务顺序。当前位置始终有文字与边界提示。</p>
32
+ <Surface><Breadcrumbs items={[{label:'设计总览',href:'#top'},{label:'导航与步骤'}]} /><div className="ds-navigation-demo"><SideNav aria-label="工作区演示导航" currentId={chapter} groups={[{label:'工作区',items:[{id:'overview',label:'概览',href:'#navigation',icon:'chart'},{id:'records',label:'记录',href:'#navigation',icon:'connection'},{id:'settings',label:'设置',href:'#navigation',icon:'info'}]}]} onNavigate={(item,event) => {event.preventDefault(); setChapter(item.id);}}/><div className="ds-navigation-content"><Badge>SideNav</Badge><h3>{({overview:'概览',records:'记录',settings:'设置'})[chapter]}</h3><Text>20px线性图标辅助识别,文字始终可见。44px操作行、4px行间距,选中项以苔绿底和细边界标识,不使用外链箭头。图标可选;长名称自然换行。页面路由、数据和权限由业务提供。</Text></div></div></Surface>
33
+ <Surface className="ds-gap-top"><h3>Steps · 验证后推进</h3><Steps current={step} onStepChange={setStep} items={[{id:'scope',label:'选择范围',description:'确认任务边界'},{id:'setup',label:'完成配置',description:'校验必填信息'},{id:'review',label:'核对结果',description:'完成后才显示成功'}]}/><div className="ds-row ds-gap-top"><Button onClick={()=>setStep(value=>Math.min(3,value+1))} disabled={step===3}>模拟本步完成</Button><Button variant="secondary" onClick={()=>setStep(0)}>重置步骤</Button></div><p role="status" className="ds-caption">{step===3?'全部完成 · 合成演示':'未完成的步骤不能点击跳过;已完成步骤可返回。'}</p></Surface>
34
+ </section>;
35
+ if (section === 'form-workflow') return <section id="form-workflow"><div className="ds-section-title"><span>FORM</span><h2>让问题可定位,让输入被保留。</h2></div><p className="ds-caption">试着直接提交,再点击错误摘要回到字段。这里只做本地验证,不发送或保存数据。</p>
36
+ <Surface><form noValidate onSubmit={event => {
37
+ event.preventDefault(); const next: FieldError[]=[];
38
+ if (!name.trim()) next.push({fieldId:nameId,message:'请输入工作区名称'});
39
+ let validAddress=false;
40
+ try { const url=new URL(endpoint); validAddress=url.protocol==='https:' && !!url.hostname && !url.username && !url.password; } catch { /* Invalid address stays in the form for correction. */ }
41
+ if (!validAddress) next.push({fieldId:endpointId,message:'请输入有效的 HTTPS 服务地址'});
42
+ setErrors(next);setAttempt(value=>value+1);setSaved(next.length===0);
43
+ }}><Stack><ErrorSummary errors={errors} focusKey={attempt}/><Fieldset legend="工作区信息" hint="两项均为必填;失败后保留已输入内容。"><TextField id={nameId} label="工作区名称" required value={name} onChange={event=>{setName(event.target.value);setSaved(false);}} error={errors.find(error=>error.fieldId===nameId)?.message}/><TextField id={endpointId} label="服务地址" required type="url" value={endpoint} placeholder="https://example.com" onChange={event=>{setEndpoint(event.target.value);setSaved(false);}} error={errors.find(error=>error.fieldId===endpointId)?.message}/></Fieldset><div className="ds-row"><Button type="submit">验证配置</Button><Drawer trigger={<Button variant="secondary">查看填写说明</Button>} title="填写说明" description="临时帮助保留当前表单。关闭后回到入口,内容不会丢失。"><Stack><Text>名称用于识别工作区。服务地址必须以 HTTPS 开头。</Text><Notice title="仅为演示">不要填入密码、Token 或真实凭证。这里不发起网络请求。</Notice></Stack></Drawer></div>{saved && <Notice tone="success" title="配置格式通过">演示已完成,没有保存到服务端。</Notice>}</Stack></form></Surface>
44
+ <p className="ds-caption">Fieldset 保留原生分组语义;ErrorSummary 在失败提交后获得焦点;Drawer 继承弹窗的焦点约束与 Escape 关闭。</p>
45
+ </section>;
46
+ if (section !== 'patterns') return null;
47
+ return <section id="patterns"><div className="ds-section-title"><span>PLAYBOOK</span><h2>先选交互,再选组件。</h2></div><p className="ds-caption">14 个常见场景。按任务、信息持续时间和是否需要打断来选择,动效只解释状态变化。</p><Surface><Select label="我正在设计什么场景" value={choice} onChange={event=>setChoice(event.target.value)}>{scenarios.map(item=><option key={item[0]} value={item[0]}>{item[1]}</option>)}</Select><div className="ds-pattern-detail" key={choice}><Text variant="overline">推荐组合</Text><h3>{scenario[2]}</h3><dl><dt>交互规则</dt><dd>{scenario[3]}</dd><dt>动效规则</dt><dd>{scenario[4]}</dd><dt>避免误用</dt><dd>{scenario[5]}</dd></dl></div></Surface>
48
+ {choice==='monitor' && <Surface className="ds-gap-top"><Stack><Text variant="section">健康记录时间轴</Text><Text variant="caption">合成数据;键盘方向键、Home / End 或指针查看时段。更新不播放装饰动画。</Text><StatusTimeline label="示例历史时段" startLabel="昨天 15:30" endLabel="今天 15:30" segments={Array.from({length:96},(_,i)=>({id:String(i),label:`时段 ${i+1}`,status:monitor==='unknown'?'unknown':i%23===0?'failed':i%17===0?'degraded':i%11===0?'recovered':i%7===0?'unknown':'healthy'}))}/><TimelineLegend/><Dialog trigger={<Button variant="secondary">左右分栏预览</Button>} title="选择范围" description="左侧操作,右侧查看选择对应的证据。此演示不保存更改。" sidePanel={closeButton => <Stack><Inline role="group" aria-label="预览导航" style={{justifyContent:'space-between'}}><Text as="h3" variant="body">{previewScope==='primary'?'主要资源':'备用资源'} · 健康记录</Text>{closeButton}</Inline><StatusTimeline compact label="范围历史时段" startLabel="昨天" endLabel="今天" segments={Array.from({length:96},(_,i)=>({id:String(i),label:`时段 ${i+1}`,status:previewScope==='primary'?'healthy':'unknown'}))}/><TimelineLegend/></Stack>}><Select label="范围" value={previewScope} onChange={event=>setPreviewScope(event.target.value)}><option value="primary">主要资源</option><option value="backup">备用资源</option></Select></Dialog></Stack></Surface>}
49
+ {choice==='monitor' && <Surface className="ds-gap-top"><Stack><Select label="监控状态演示" value={monitor} onChange={event=>setMonitor(event.target.value)}><option value="normal">正常</option><option value="stale">数据陈旧</option><option value="unknown">首次采集失败</option></Select><div className="ds-row"><Metric label="请求成功率" value={monitor==='unknown'?null:'99.2'} unit="%" description="最近 5 分钟 · 合成数据"/><Badge tone={monitor==='normal'?'success':monitor==='stale'?'warning':'error'}>{monitor==='normal'?'采集正常':monitor==='stale'?'数据已过期':'暂无有效数据'}</Badge></div><KeyValue items={[{key:'time',label:'最后成功采集',value:monitor==='unknown'?null:'14:32:00 · 上海时区(示例)'},{key:'rule',label:'新鲜度规则',value:'超过 60 秒未成功采集即标为陈旧'}]}/>{monitor!=='normal' && <Notice tone={monitor==='stale'?'warning':'error'} title={monitor==='stale'?'显示上次结果,不代表当前状态':'未能获取指标'} action={<Drawer trigger={<Button variant="secondary">查看处理建议</Button>} title="采集恢复建议" description="这是一段监控交互示例,不连接真实系统。"><Text>检查采集器状态与数据源连接,恢复后重新读取。不要把未知值重置为零。</Text></Drawer>}>{monitor==='stale'?'本例模拟超过 60 秒未更新,请检查采集状态。':'首次读取失败,因此数值显示 —。'}</Notice>}</Stack></Surface>}
50
+ <div className="ds-overview-links ds-gap-top"><a href="#navigation"><strong>导航与步骤</strong><span>↗</span><p>侧边栏、层级位置、阶段进度。</p></a><a href="#form-workflow"><strong>表单纠错与抽屉</strong><span>↗</span><p>试一次失败提交,观察焦点与输入保留。</p></a><a href="#motion"><strong>动效实验室</strong><span>↗</span><p>播放、重播和减少动态对照。</p></a><a href="https://github.com/linhay/rhine-design/blob/main/docs/INTERACTION_PATTERNS.md"><strong>完整使用规范</strong><span>↗</span><p>组件选择、代码契约与参考来源。</p></a></div>
51
+ </section>;
52
+ }
@@ -0,0 +1,32 @@
1
+ import { useState } from 'react';
2
+ import { Button, Grid, Inline, Notice, Select, Stack, Surface, Text, type Spacing } from '../components';
3
+ import './spacing.css';
4
+
5
+ const scale: Spacing[] = [1, 2, 3, 4, 6, 8, 12];
6
+ export function SpacingGallery() {
7
+ const [state, setState] = useState('normal');
8
+ return <section id="spacing">
9
+ <div className="ds-section-title"><span>SPACE</span><h2>间距与组合 · 让关系可见</h2></div>
10
+ <Stack gap={6}>
11
+ <Text tone="muted">同组紧、组间松。间距使用已有 token;gap 的数字是后缀,例如 gap=4 对应16px。</Text>
12
+ <Surface><Stack gap={4}>{scale.map(space => <div className="ds-spacing-scale" key={space}><Text variant="code">space-{space}</Text><span className="ds-spacing-bar" style={{width:`var(--rhine-space-${space})`}} aria-hidden="true"/><Text numeric variant="caption">{space * 4}px</Text></div>)}</Stack></Surface>
13
+ <Select label="卡片内容状态" value={state} onChange={event=>setState(event.target.value)}><option value="normal">默认</option><option value="loading">检查中</option><option value="error">读取失败</option><option value="long">长文案</option></Select>
14
+ <Grid gap={6} minWidth={280}>
15
+ <Stack gap={3}>
16
+ <Text variant="caption" tone="muted">24px 内边距 · 8px 标题/说明 · 16px 内容/操作</Text>
17
+ <Surface><Stack gap={4}>
18
+ <Stack gap={2}><Text as="h3" variant="heading">应用更新</Text><Text tone="muted">{state==='long'?'当前版本已是最新。应用会在后台检查新版本,准备好后通知你;未完成的工作会保留,请在方便时安装。':state==='loading'?'正在检查新版本…':state==='error'?'暂时无法确认版本状态。':'当前版本已是最新。'}</Text></Stack>
19
+ {state==='error'&&<Notice tone="error" title="检查更新失败">请确认网络连接后重试;已安装的版本仍可继续使用。</Notice>}
20
+ <Inline gap={2}><Button variant="secondary" busy={state==='loading'} busyLabel="正在检查" onClick={()=>setState('loading')}>{state==='error'?'重试检查':'检查更新'}</Button>{state==='loading'&&<Button variant="quiet" onClick={()=>setState('normal')}>结束演示</Button>}</Inline>
21
+ </Stack></Surface>
22
+ <Text variant="caption" tone="muted">按钮仅改变合成状态;选择上方状态或结束演示复位,不联网检查更新。</Text>
23
+ </Stack>
24
+ <Surface><Stack gap={4}>
25
+ <Stack gap={2}><Text as="h3" variant="heading">同一套组合,适用于不同内容</Text><Text tone="muted">换行只增加内容高度,不压缩间距。</Text></Stack>
26
+ <Stack gap={3}><Text>标题 + 说明:Stack gap=2</Text><Text>内容 + 操作:Stack gap=4</Text><Text>按钮组:Inline gap=2</Text><Text>卡片之间:Grid / Stack gap=6</Text></Stack>
27
+ <Text variant="caption" tone="muted">Surface只负责边界与内边距。避免父gap与子margin重复累加;不固定卡高,不为空白增加动效。</Text>
28
+ </Stack></Surface>
29
+ </Grid>
30
+ </Stack>
31
+ </section>;
32
+ }
@@ -0,0 +1,84 @@
1
+ import { useState } from 'react';
2
+ import { Badge, Button, DataTable, Dialog, Grid, Inline, KeyValue, Menu, Metric, Select, Stack, Surface, Text, TextField } from '../components';
3
+ import './subscriptions.css';
4
+
5
+ const initialPlans = [
6
+ { id: 'gpt100', name: 'GPT100', color: 'green', count: 12, price: 100, description: '日常创作与轻量任务', status: '运行中' },
7
+ { id: 'gpt200', name: 'GPT200', color: 'orange', count: 8, price: 200, description: '复杂推理与高频工作', status: '运行中' },
8
+ { id: 'grok', name: 'Grok', color: 'violet', count: 4, price: 80, description: '探索灵感与多样表达', status: '运行中' },
9
+ ];
10
+ const money = (value: number) => `¥${value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
11
+ type Plan = typeof initialPlans[number];
12
+ function TypeTag({ plan }: { plan: Plan }) {
13
+ return <span className="rhine-type-tag" data-color={plan.color}>{plan.name}</span>;
14
+ }
15
+
16
+ export function SubscriptionGallery() {
17
+ const [plans, setPlans] = useState(initialPlans);
18
+ const [createOpen, setCreateOpen] = useState(false);
19
+ const [planId, setPlanId] = useState('gpt100');
20
+ const [quantity, setQuantity] = useState('1');
21
+ const [error, setError] = useState('');
22
+ const [message, setMessage] = useState('');
23
+ const [showPrices, setShowPrices] = useState(true);
24
+ const total = plans.reduce((sum, plan) => sum + plan.count, 0);
25
+ const cost = plans.reduce((sum, plan) => sum + plan.count * plan.price, 0);
26
+ const displayMoney = (value: number) => showPrices ? money(value) : '••••';
27
+ const selectedPlan = plans.find(plan => plan.id === planId)!;
28
+
29
+ return <section id="subscriptions" className="ds-subscriptions">
30
+ <div className="ds-subscription-heading">
31
+ <div><Text variant="overline" tone="muted" lang="en">YOUR SUBSCRIPTIONS, AT A GLANCE</Text><h1>订阅,一目了然<span aria-hidden="true">.</span></h1><Text tone="muted">数量、支出与状态,在这里轻松掌握。</Text></div>
32
+ <Inline gap={3}>
33
+ <Dialog title="新增订阅" description="选择类型与数量,预览更新后的订阅概览。仅使用演示数据,不产生实际扣费。" open={createOpen} onOpenChange={open => { setCreateOpen(open); setError(''); }} trigger={<Button>+ 新增订阅</Button>}>
34
+ <form onSubmit={event => {
35
+ event.preventDefault();
36
+ const count = Number(quantity);
37
+ if (!Number.isInteger(count) || count < 1 || count > 99) { setError('请输入 1–99 之间的整数。'); return; }
38
+ setPlans(current => current.map(plan => plan.id === planId ? { ...plan, count: plan.count + count } : plan));
39
+ setMessage(`已添加 ${count} 个 ${selectedPlan.name} 演示订阅。`);
40
+ setCreateOpen(false); setQuantity('1');
41
+ }} noValidate>
42
+ <Stack gap={4}>
43
+ <Select label="订阅类型" value={planId} onChange={event => setPlanId(event.target.value)}>{plans.map(plan => <option value={plan.id} key={plan.id}>{plan.name}</option>)}</Select>
44
+ <TextField label="订阅数量" type="number" inputMode="numeric" min={1} max={99} value={quantity} onChange={event => setQuantity(event.target.value)} error={error} hint="每次可添加 1–99 个演示订阅。" />
45
+ <KeyValue items={[{ key: 'price', label: '单价 / 月', value: displayMoney(selectedPlan.price) }]} />
46
+ <Inline gap={3}><Button type="submit">添加到概览</Button><Button variant="secondary" onClick={() => setCreateOpen(false)}>取消</Button></Inline>
47
+ </Stack>
48
+ </form>
49
+ </Dialog>
50
+ <Menu label="订阅管理" trigger={<Button variant="secondary" aria-label="管理订阅">管理 ⋯</Button>} items={[
51
+ { id: 'amounts', label: showPrices ? '隐藏金额' : '显示金额', onSelect: () => setShowPrices(value => !value) },
52
+ { id: 'reset', label: '重置演示数据', onSelect: () => { setPlans(initialPlans); setMessage('演示数据已重置。'); } },
53
+ ]} />
54
+ </Inline>
55
+ </div>
56
+ <div className="ds-subscription-notice"><span>主题场景 / 订阅管理</span><span>示例数量与价格 · 无实际交易</span></div>
57
+ <div role="status" className="ds-subscription-status">{message}</div>
58
+ <Surface className="ds-subscription-summary"><Grid minWidth={180} gap={6}>
59
+ <Metric label="订阅总数" value={total} unit="个" description="所有订阅均在正常运行" />
60
+ <Metric label="每月支出" value={displayMoney(cost)} description="按当前演示订阅计算" />
61
+ <Metric label="订阅类型" value="03" unit="类" description="GPT100 / GPT200 / Grok" />
62
+ </Grid></Surface>
63
+ <div className="ds-subscription-section-head"><Text as="h2" variant="section">我的订阅</Text><Text tone="muted" variant="caption">点击卡片,查看明细 ↗</Text></div>
64
+ <div className="ds-subscription-cards">{plans.map(plan => <Dialog key={plan.id} title={`${plan.name} 订阅明细`} description={plan.description} trigger={
65
+ <button type="button" className="rhine-surface ds-subscription-card" aria-label={`查看 ${plan.name} 订阅明细`}>
66
+ <span className="ds-subscription-card-top"><TypeTag plan={plan} /><span className="ds-subscription-arrow" aria-hidden="true">↗</span></span>
67
+ <span className="ds-subscription-count">{plan.count}<small>个订阅</small></span>
68
+ <span className="ds-subscription-description">{plan.description}</span>
69
+ <span className="ds-subscription-card-bottom"><span className="ds-subscription-running">● {plan.status}</span><strong>{displayMoney(plan.count * plan.price)}<small> / 月</small></strong></span>
70
+ </button>
71
+ }><Stack gap={6}><Inline><TypeTag plan={plan} /><Badge tone="success">{plan.status}</Badge></Inline><Metric label="当前订阅数量" value={plan.count} unit="个" /><KeyValue items={[
72
+ { key: 'price', label: '单价 / 月', value: displayMoney(plan.price) },
73
+ { key: 'total', label: '每月合计', value: displayMoney(plan.count * plan.price) },
74
+ { key: 'cycle', label: '计费周期', value: '按月 · 演示数据' },
75
+ ]} /><Text variant="caption" tone="muted">这是交互示例,价格不代表真实产品报价。</Text></Stack></Dialog>)}</div>
76
+ <Surface className="ds-subscription-table"><DataTable caption="每月费用明细" rows={plans} rowKey={plan => plan.id} columns={[
77
+ { key: 'type', header: '订阅类型', render: plan => <TypeTag plan={plan} /> },
78
+ { key: 'count', header: '数量', align: 'right', render: plan => `${plan.count} 个` },
79
+ { key: 'price', header: '单价 / 月', align: 'right', render: plan => displayMoney(plan.price) },
80
+ { key: 'total', header: '合计 / 月', align: 'right', render: plan => <strong>{displayMoney(plan.count * plan.price)}</strong> },
81
+ { key: 'status', header: '状态', render: plan => <Badge tone="success">{plan.status}</Badge> },
82
+ ]} /><div className="ds-subscription-total"><Text tone="muted">每月合计</Text><Text variant="heading" numeric>{displayMoney(cost)}</Text></div></Surface>
83
+ </section>;
84
+ }
@@ -0,0 +1,2 @@
1
+ .ds-leading-sample[data-guides=true] { background-image:repeating-linear-gradient(to bottom,var(--rhine-selected) 0,var(--rhine-selected) calc(var(--ds-leading) - 1px),var(--rhine-line) calc(var(--ds-leading) - 1px),var(--rhine-line) var(--ds-leading)); }
2
+ .ds-leading-paragraphs { max-width:44ch; }
package/preview/main.tsx CHANGED
@@ -5,6 +5,11 @@ import './preview.css';
5
5
  import { CommonGallery } from './CommonGallery';
6
6
  import { MotionGallery } from './MotionGallery';
7
7
  import { StartupGallery } from './StartupGallery';
8
+ import { PatternGallery } from './PatternGallery';
9
+ import { GalleryShell } from './GalleryShell';
10
+ import { SpacingGallery } from './SpacingGallery';
11
+ import { LineHeightGallery } from './LineHeightGallery';
12
+ import { SubscriptionGallery } from './SubscriptionGallery';
8
13
 
9
14
  function Gallery() {
10
15
  const [motion, setMotion] = useState('system');
@@ -22,15 +27,24 @@ function Gallery() {
22
27
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
23
28
  useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
24
29
  const simulate = () => { setBusy(true); timer.current = setTimeout(() => { setBusy(false); setNotice(true); }, 900); };
25
- return <div className="rhine-theme ds-gallery" data-rhine-motion={motion}>
26
- <header className="ds-header"><a href="#top" className="ds-brand"><Logo variant="lockup" size={36} /><span>DESIGN SYSTEM</span></a><Badge>V1.4 / COMPONENTS</Badge></header>
27
- <main id="top">
28
- <div className="ds-hero"><div><p className="ds-kicker">FOUNDATIONS / COMPONENTS / MOTION</p><h1>精密,有序。<br /><span>每一次交互都有回应。</span></h1><p>40 个通用组件,共用一套视觉变量和动效规则。<br />这里运行的就是后续业务页面可直接导入的 React 组件。</p></div><div className="ds-motion-control"><span className="ds-caption">动效偏好 · 仅预览</span><SegmentedControl label="动效偏好" items={[{ value:'system', label:'跟随系统' },{ value:'reduced', label:'减少动态' }]} value={motion} onValueChange={setMotion} /><p>系统减少动态始终优先。<br />动画不延迟操作,也不代替业务结果。</p></div></div>
29
- <nav className="ds-nav" aria-label="组件目录"><a href="#startup">启动动画</a><a href="#motion">动效实验室</a><a href="#brand">品牌标志</a><a href="#foundation">视觉基础</a><a href="#typography">字体与文字</a><a href="#actions">按钮与输入</a><a href="#selection">选择与切换</a><a href="#feedback">状态与加载</a><a href="#disclosure">展开与组合</a><a href="#data">数据与分页</a><a href="#forms">表单与选择</a><a href="#utilities">操作与布局</a><a href="#overlays">浮层与反馈</a></nav>
30
+ return <GalleryShell motion={motion} onMotionChange={setMotion}>{section => <>
31
+ {section === 'subscriptions' && <SubscriptionGallery />}
32
+ {section === 'top' && <>
33
+ <div className="ds-hero"><div><p className="ds-kicker">FOUNDATIONS / COMPONENTS / MOTION</p><h1>精密,有序。<br /><span>每一次交互都有回应。</span></h1><p>48 个通用组件,共用一套视觉变量和动效规则。<br />这里运行的就是后续业务页面可直接导入的 React 组件。</p></div></div>
34
+ <div className="ds-overview-links">{[
35
+ ['foundation','视觉基础','从色彩、间距与材质开始。'],
36
+ ['typography','字体与文字','清晰的层级,稳定的阅读节奏。'],
37
+ ['patterns','场景与交互指南','先明确任务,再选择组件和动效。'],
38
+ ['motion','动效实验室','播放、重播,观察交互的连续性。'],
39
+ ].map(([id,title,description]) => <a key={id} href={`#${id}`}><strong>{title}</strong><span aria-hidden="true">↗</span><p>{description}</p></a>)}</div>
40
+ </>}
30
41
 
31
- <StartupGallery reduced={motion==='reduced'} />
32
- <MotionGallery reduced={motion==='reduced'} />
33
- <section id="brand"><div className="ds-section-title"><span>00</span><h2>三角构架 · Rhine Design</h2></div>
42
+
43
+ {['navigation','form-workflow','patterns'].includes(section) && <PatternGallery section={section} />}
44
+ {section === 'spacing' && <SpacingGallery />}
45
+ {section === 'startup' && <StartupGallery reduced={motion==='reduced'} />}
46
+ {section === 'motion' && <MotionGallery reduced={motion==='reduced'} />}
47
+ {section === 'brand' && <section id="brand"><div className="ds-section-title"><span>00</span><h2>三角构架 · Rhine Design</h2></div>
34
48
  <div className="ds-logo-pair">
35
49
  <div className="ds-logo-stage"><Logo variant="icon" size={192} theme="light" /><span>浅色 / LIGHT</span></div>
36
50
  <div className="ds-logo-stage ds-logo-stage--dark"><Logo variant="icon" size={192} theme="dark" /><span>深色 / DARK</span></div>
@@ -38,14 +52,14 @@ function Gallery() {
38
52
  <p className="ds-caption">浅色与深色共用同一份矢量母版,图形在方形画布内水平、垂直居中。</p>
39
53
  <div className="ds-grid"><Surface><div className="ds-component-title"><h3>Logo · 尺寸与组合</h3><span>16 / 24 / 32 / 48 / 64px</span></div><div className="ds-row">{[16,24,32,48,64].map(size=><Logo key={size} size={size} label={`Rhine Design ${size}px`} />)}</div><div className="ds-gap-top"><Logo variant="lockup" size={48} /></div></Surface>
40
54
  <Surface><div className="ds-component-title"><h3>Logo · 三块归位</h3><span>240ms / 单次入场</span></div><div className="ds-row"><Logo key={logoReplay} size={80} animate /><Button variant="secondary" onClick={()=>setLogoReplay(value=>value+1)}>重播标志动画</Button></div><p className="ds-caption">默认静态;减少动态时直接呈现。标志不表示同步进度。</p></Surface></div>
41
- </section>
55
+ </section>}
42
56
 
43
- <section id="foundation"><div className="ds-section-title"><span>01</span><h2>同一套变量,贯穿所有组件</h2></div>
57
+ {section === 'foundation' && <section id="foundation"><div className="ds-section-title"><span>01</span><h2>同一套变量,贯穿所有组件</h2></div>
44
58
  <div className="ds-palette">{[['PAPER','#F2F1E9'],['INK','#242B28'],['SAGE','#E5ECE3'],['GREEN','#425C4B'],['SIGNAL','#C65C34']].map(([name,color])=><div key={name}><div style={{background:color}} /><strong>{name}</strong><code>{color}</code></div>)}</div>
45
59
  <div className="ds-notes"><span>14px 正文 / 12px 辅助信息</span><span>4px 卡片 / 3px 按钮圆角</span><span>120–240ms 交互过渡</span></div>
46
- </section>
60
+ </section>}
47
61
 
48
- <section id="typography"><div className="ds-section-title"><span>TYPE</span><h2>字体与文字 · 清晰先于装饰</h2></div>
62
+ {section === 'typography' && <section id="typography"><div className="ds-section-title"><span>TYPE</span><h2>字体与文字 · 清晰先于装饰</h2></div>
49
63
  <p className="ds-caption">系统无衬线用于中英文正文;等宽字体用于路径、模型标识和代码。业务字号不低于 12px,中文不增加装饰字距。</p>
50
64
  <Surface><div className="ds-type-scale">{([
51
65
  ['title','30 / 38 · 600','连接你的工作空间'],
@@ -58,6 +72,7 @@ function Gallery() {
58
72
  ['metric','26 / 32 · 600','1,280.00'],
59
73
  ['code','12 / 20 · 400','~/.codex/auth.json'],
60
74
  ] satisfies [TextVariant,string,string][]).map(([variant,spec,sample])=><div className="ds-type-row" key={variant}><div><Text variant="code">{variant}</Text><Text variant="caption" tone="muted">{spec}</Text></div><Text variant={variant}>{sample}</Text></div>)}</div></Surface>
75
+ <LineHeightGallery />
61
76
  <div className="ds-grid ds-gap-top"><Surface><Text as="h3" variant="heading">中英文与数字</Text><div className="ds-type-copy"><Text>已完成 128 次请求,共使用 1.4M Token。</Text><Text numeric>预估费用 $5.44 · 平均耗时 2.4 s</Text><Text tone="muted" variant="caption">2026-10-22 14:32 · 上海(UTC+8)</Text><Text variant="overline" lang="en">Usage / Today</Text><Text>未采集的数据显示「—」,不替换为 0。小数位按指标语义固定,原始值保持可查。</Text></div></Surface>
62
77
  <Surface><Text as="h3" variant="heading">换行与状态文案</Text><div className="ds-type-copy"><Text>长标题、错误原因和下一步操作允许自然换行。</Text><Text variant="code">example-very-long-model-identifier-for-wrapping-preview-2026-10-22</Text><Text tone="error">暂时无法读取列表。请检查连接后重试。</Text><Text variant="caption" tone="muted">文字不缩放、不跑马灯、不逐字播放;复制或展开应能获取完整内容。</Text></div></Surface></div>
63
78
  <div className="ds-gap-top"><Toolbar title="Rhine Design / 设计评审" description="工具栏与静态页面样稿共用一套样式。">
@@ -65,36 +80,35 @@ function Gallery() {
65
80
  <Select label="场景示例" defaultValue="normal"><option value="normal">正常</option><option value="loading">读取中 / 等待授权</option></Select>
66
81
  </Toolbar></div>
67
82
  <div className="ds-grid ds-gap-top"><Select label="错误状态" defaultValue="" error="请选择一个页面后继续。"><option value="" disabled>请选择</option><option value="usage">数据概览</option></Select><Select label="禁用状态" disabled defaultValue="fixed" hint="原生菜单保留键盘操作,箭头不接收点击。"><option value="fixed">暂不可更改</option></Select></div>
68
- </section>
83
+ </section>}
69
84
 
70
- <section id="actions"><div className="ds-section-title"><span>02</span><h2>操作有分量,反馈不打断</h2></div>
85
+ {section === 'actions' && <section id="actions"><div className="ds-section-title"><span>02</span><h2>操作有分量,反馈不打断</h2></div>
71
86
  <div className="ds-grid"><Surface><div className="ds-component-title"><h3>Button</h3><span>悬停 120ms / 按下 80ms</span></div><div className="ds-row"><Button busy={busy} busyLabel="正在保存" onClick={simulate}>保存修改</Button><Button variant="secondary" onClick={()=>setNotice(false)}>取消提示</Button><Button variant="quiet" onClick={()=>setNotice(true)}>查看反馈</Button><Button variant="danger" disabled>删除记录</Button></div><p className="ds-caption">处理中保持尺寸和键盘焦点,阻止重复提交。</p><Button disabled>暂不可用</Button></Surface>
72
87
  <Surface><div className="ds-component-title"><h3>TextField</h3><span>边框反馈 / 不抖动报错</span></div><TextField label="显示名称" value={field} onChange={event=>setField(event.target.value)} hint="此字段仅演示输入状态,不保存数据。" error={showError&&!field.trim()?'请输入显示名称':undefined} placeholder="例如:研究工作区" /><div className="ds-row ds-gap-top"><Button variant="secondary" onClick={()=>setShowError(true)}>验证输入</Button></div></Surface></div>
73
- </section>
88
+ </section>}
74
89
 
75
- <section id="selection"><div className="ds-section-title"><span>03</span><h2>选择清楚,位置稳定</h2></div>
90
+ {section === 'selection' && <section id="selection"><div className="ds-section-title"><span>03</span><h2>选择清楚,位置稳定</h2></div>
76
91
  <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>
77
92
  <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>
78
93
  <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>
79
- </section>
94
+ </section>}
80
95
 
81
- <section id="feedback"><div className="ds-section-title"><span>04</span><h2>告诉用户发生了什么</h2></div>
96
+ {section === 'feedback' && <section id="feedback"><div className="ds-section-title"><span>04</span><h2>告诉用户发生了什么</h2></div>
82
97
  <div className="ds-row ds-badges"><Badge>未开始</Badge><Badge tone="success">已完成</Badge><Badge tone="warning">需要处理</Badge><Badge tone="error">操作失败</Badge></div>
83
98
  <div className="ds-grid"><Surface><div className="ds-component-title"><h3>Progress</h3><span>数值更新 240ms</span></div><Progress label="进度示例" value={progress} /><div className="ds-row ds-gap-top"><Button variant="secondary" onClick={()=>setProgress(value=>value===null?38:Math.min(100,value+20))}>推进进度</Button><Button variant="quiet" onClick={()=>setProgress(value=>value===null?38:null)}>切换未知进度</Button></div><p className="ds-caption">只有真实进度才显示百分比;未知进度单独表达。</p></Surface>
84
99
  <Surface><div className="ds-component-title"><h3>Skeleton</h3><span>1400ms 低幅明暗</span></div>{loading?<Skeleton label="正在读取组件示例" />:<p className="ds-loaded">内容已准备好。加载占位与内容共用容器。</p>}<div className="ds-gap-top"><Button variant="secondary" onClick={()=>setLoading(value=>!value)}>切换加载状态</Button></div></Surface></div>
85
100
  <div className="ds-gap-top"><Button variant="secondary" onClick={()=>setNotice(value=>!value)}>切换提示</Button></div>
86
101
  {notice&&<div className="ds-gap-top"><Notice tone="success" title="修改已保存(演示)" action={<Button variant="quiet" onClick={()=>setNotice(false)}>关闭提示</Button>}>这是组件的本地反馈,没有写入任何业务数据。</Notice></div>}
87
102
  <div className="ds-gap-top"><Notice tone="warning" title="状态文字始终可见">颜色和动画只增强反馈,不承担唯一的状态含义。</Notice></div>
88
- </section>
103
+ </section>}
89
104
 
90
- <section id="disclosure"><div className="ds-section-title"><span>05</span><h2>逐步展开,保持可读</h2></div>
105
+ {section === 'disclosure' && <section id="disclosure"><div className="ds-section-title"><span>05</span><h2>逐步展开,保持可读</h2></div>
91
106
  <div className="ds-grid"><Surface><div className="ds-component-title"><h3>Disclosure</h3><span>实测高度 / 200ms</span></div><Disclosure title="查看组件动画规则"><p>展开和收起都支持中途反向。关闭后内容不参与 Tab 导航。</p><Button variant="secondary" onClick={()=>setNotice(true)}>内容内操作</Button><p>减少动态模式直接到达最终状态,信息不会丢失。</p></Disclosure></Surface>
92
107
  <EmptyState title="暂无记录" description="用一句话说明当前状态,按业务需要提供一个下一步操作。" action={<Button variant="secondary" onClick={()=>setNotice(true)}>演示下一步</Button>} /></div>
93
- </section>
94
- <CommonGallery />
95
- <footer className="ds-footer"><span>Rhine Design · 通用设计组件 V1.4</span><span>合成数据;复制示例仅在点击时写入演示文字。</span></footer>
96
- </main>
97
- </div>;
108
+ </section>}
109
+ {['data','forms','utilities','overlays'].includes(section) && <CommonGallery key={section} section={section} />}
110
+ </>}</GalleryShell>;
111
+
98
112
  }
99
113
 
100
114
  createRoot(document.getElementById('root')!).render(<StrictMode><Gallery /></StrictMode>);
@@ -0,0 +1,7 @@
1
+ .ds-navigation-demo { display:grid; grid-template-columns:180px minmax(0,1fr); gap:24px; margin-top:20px; }
2
+ .ds-navigation-content { border-inline-start:1px solid var(--rhine-line); padding:24px; }
3
+ .ds-pattern-detail { margin-top:28px; padding-top:24px; border-top:1px solid var(--rhine-line); animation:rhine-pattern-enter var(--rhine-duration-panel) var(--rhine-ease-standard); }
4
+ .ds-pattern-detail h3 { font-size:22px; line-height:1.4; margin:8px 0 24px; overflow-wrap:anywhere; }
5
+ .ds-pattern-detail dl { display:grid; grid-template-columns:80px minmax(0,1fr); gap:20px; margin:0; }
6
+ .ds-pattern-detail dt { font-size:12px; color:var(--rhine-muted); }.ds-pattern-detail dd { margin:0; }
7
+ @media(max-width:600px) { .ds-navigation-demo { grid-template-columns:1fr; gap:16px; }.ds-navigation-content { border-inline-start:0; border-top:1px solid var(--rhine-line); padding:20px 0; }.ds-pattern-detail dl { grid-template-columns:1fr; gap:8px; }.ds-pattern-detail dd { margin-bottom:12px; } }
@@ -3,25 +3,19 @@ body { margin:0; }
3
3
  .ds-type-row:last-child { border:0; }
4
4
  .ds-type-copy { display:grid; gap:12px; margin-top:16px; max-width:60ch; }
5
5
  @media(max-width:600px) { .ds-type-row { grid-template-columns:1fr; } }
6
- .ds-header { display:flex; justify-content:space-between; align-items:center; gap:16px; border-bottom:1px solid var(--rhine-line); padding:20px max(24px,calc((100vw - 1080px)/2)); }
7
- .ds-brand { display:inline-flex; align-items:center; color:var(--rhine-ink); text-decoration:none; }
8
- .ds-brand>span:not(.rhine-logo) { font:12px var(--rhine-mono); letter-spacing:1px; margin-left:16px; }
9
6
  .ds-logo-pair { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:24px; }
10
7
  .ds-logo-stage { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:24px; padding:32px 12px; background:var(--rhine-surface); border:1px solid var(--rhine-line); }
11
8
  .ds-logo-stage>span:last-child { font:12px var(--rhine-mono); letter-spacing:2px; }
12
9
  .ds-logo-stage--dark { background:var(--rhine-ink); color:var(--rhine-bg); border-color:var(--rhine-ink); }
13
10
  @media(max-width:600px) { .ds-logo-pair { grid-template-columns:1fr; } }
14
- .ds-gallery main { max-width:1128px; padding:0 24px; margin:auto; }
15
- .ds-hero { display:grid; grid-template-columns:1.5fr 1fr; gap:48px; padding:56px 0 40px; align-items:end; }
11
+ .ds-hero { display:grid; grid-template-columns:1fr; gap:48px; padding:56px 0 40px; align-items:end; }
16
12
  .ds-kicker { font:12px var(--rhine-mono); letter-spacing:1px; color:var(--rhine-muted); }
17
13
  .ds-hero h1 { font-size:44px; line-height:1.35; letter-spacing:-2px; margin:24px 0; font-weight:600; }.ds-hero h1>span { color:var(--rhine-green); }
18
14
  .ds-hero p { color:var(--rhine-muted); }
19
- .ds-motion-control { border-left:2px solid var(--rhine-signal); padding-left:24px; }.ds-motion-control>.ds-caption { display:block; margin-bottom:12px; }.ds-motion-control p { font-size:12px; }
20
- .ds-nav { display:flex; flex-wrap:wrap; gap:16px 28px; padding:16px 0; border-block:1px solid var(--rhine-ink); }.ds-nav a { color:var(--rhine-green); text-decoration:none; min-height:28px; display:flex; align-items:center; }
21
15
  .ds-gallery section { padding:32px 0; scroll-margin-top:24px; }.ds-section-title { display:flex; align-items:baseline; gap:16px; margin-bottom:24px; }.ds-section-title>span { font:12px var(--rhine-mono); color:var(--rhine-warning); }.ds-section-title h2 { font-size:22px; font-weight:600; margin:0; }
22
16
  .ds-palette { display:grid; grid-template-columns:repeat(5,1fr); gap:16px; }.ds-palette>div>div { height:82px; border:1px solid var(--rhine-line); }.ds-palette strong,.ds-palette code { display:block; font:12px/1.8 var(--rhine-mono); }.ds-palette strong { margin-top:10px; }.ds-palette code { color:var(--rhine-muted); }
23
17
  .ds-notes { display:flex; flex-wrap:wrap; gap:12px 32px; margin-top:24px; padding-top:16px; border-top:1px solid var(--rhine-line); font-size:12px; color:var(--rhine-muted); }
24
18
  .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; }
25
19
  .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; }
26
20
  .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
- @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-brand>span:not(.rhine-logo) { display:none; }.ds-palette { grid-template-columns:repeat(auto-fit,minmax(min(100%,90px),1fr)); }.ds-palette>div>div { height:64px; }.ds-gallery main { padding:0 20px; }.ds-header { padding:16px 20px; flex-wrap:wrap; }.ds-section-title h2 { font-size:19px; } }
21
+ @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,35 @@
1
+ .ds-shell { display:grid; grid-template-columns:232px minmax(0,1fr); height:100dvh; background:var(--rhine-bg); }
2
+ .ds-skip { position:fixed; top:8px; left:16px; z-index:100; padding:12px; background:var(--rhine-ink); color:var(--rhine-surface); transform:translateY(-160%); }
3
+ .ds-skip:focus { transform:none; }
4
+ .ds-sidebar { display:flex; flex-direction:column; min-height:0; border-right:1px solid var(--rhine-line); background:var(--rhine-surface); }
5
+ .ds-sidebar-brand { padding:28px 24px 24px; }
6
+ .ds-sidebar-brand>a { display:flex; gap:12px; align-items:center; color:var(--rhine-ink); text-decoration:none; font-size:18px; font-weight:600; }
7
+ .ds-sidebar-brand small { display:block; margin-top:2px; font:10px/16px var(--rhine-mono); letter-spacing:.08em; color:var(--rhine-muted); }
8
+ .ds-sidebar-nav { flex:1; min-height:0; overflow:auto; padding:0 16px 20px; scrollbar-width:thin; }
9
+ .ds-sidebar-foot { display:grid; gap:6px; padding:20px 28px; border-top:1px solid var(--rhine-line); color:var(--rhine-muted); font:12px/18px var(--rhine-font); }
10
+ .ds-sidebar-foot>span:first-child { font:10px/16px var(--rhine-mono); letter-spacing:.06em; }
11
+ .ds-menu-toggle { display:none; }
12
+ .ds-workspace { min-width:0; min-height:0; overflow:auto; scrollbar-gutter:stable; }
13
+ .ds-workspace:focus { outline:none; }
14
+ .ds-workspace-head { min-height:80px; display:flex; align-items:center; justify-content:space-between; gap:20px; padding:16px 32px; border-bottom:1px solid var(--rhine-line); }
15
+ .ds-head-controls { display:flex; align-items:flex-end; gap:16px; flex-wrap:wrap; }
16
+ .ds-workspace-head>div:first-child { font-size:12px; }.ds-workspace-head>div:first-child>span { color:var(--rhine-muted); }
17
+ .ds-page { max-width:1200px; margin:0 auto; padding:0 32px; animation:rhine-enter var(--rhine-duration-panel) var(--rhine-ease-standard); }
18
+ .ds-overview-links { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:16px; padding-bottom:40px; }
19
+ .ds-overview-links>a { display:grid; grid-template-columns:1fr auto; gap:12px; padding:24px; color:var(--rhine-ink); text-decoration:none; background:var(--rhine-surface); border:1px solid var(--rhine-line); transition:border-color var(--rhine-duration-hover) var(--rhine-ease-standard); }
20
+ .ds-overview-links>a:hover { border-color:var(--rhine-green); }.ds-overview-links strong { font-size:18px; }.ds-overview-links p { grid-column:1/-1; margin:0; color:var(--rhine-muted); }
21
+ @media(min-width:761px) and (max-width:1100px) {
22
+ .ds-shell { grid-template-columns:200px minmax(0,1fr); }.ds-page { padding:0 24px; }
23
+ .ds-page .ds-grid,.ds-page .ds-startup-layout,.ds-page .ds-motion-grid { grid-template-columns:1fr; }
24
+ .ds-page .ds-hero { grid-template-columns:1fr; gap:24px; }.ds-page .ds-hero h1 { font-size:36px; }
25
+ .ds-page .ds-type-row { grid-template-columns:130px minmax(0,1fr); }
26
+ }
27
+ @media(max-width:760px) {
28
+ .ds-shell { grid-template-columns:minmax(0,1fr); grid-template-rows:auto minmax(0,1fr); }
29
+ .ds-sidebar { border-right:0; border-bottom:1px solid var(--rhine-line); }
30
+ .ds-sidebar-brand { display:flex; align-items:center; justify-content:space-between; gap:16px; padding:12px 20px; }
31
+ .ds-menu-toggle { display:inline-flex; }.ds-sidebar-foot { display:none; }
32
+ .ds-sidebar-nav { display:none; flex:none; max-height:45dvh; }.ds-sidebar[data-open=true] .ds-sidebar-nav { display:block; }
33
+ .ds-workspace-head { min-height:0; padding:16px 20px; flex-wrap:wrap; gap:12px; }
34
+ .ds-page { padding:0 20px; }.ds-overview-links { grid-template-columns:1fr; }
35
+ }
@@ -0,0 +1,2 @@
1
+ .ds-spacing-scale { display:grid; grid-template-columns:88px 48px minmax(0,1fr); align-items:center; gap:var(--rhine-space-4); }
2
+ .ds-spacing-bar { display:block; height:var(--rhine-space-3); background:var(--rhine-green); }
@@ -0,0 +1,27 @@
1
+ .ds-subscription-heading { display: flex; justify-content: space-between; align-items: center; gap: 24px; flex-wrap: wrap; }
2
+ .ds-subscription-heading h1 { font-size: clamp(28px, 3vw, 40px); line-height: 1.3; font-weight: 800; letter-spacing: -1px; margin: 12px 0; }
3
+ .ds-subscription-heading h1>span { color: var(--rhine-signal); }
4
+ .ds-subscription-notice { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin-top: 24px; font-size: 12px; color: var(--rhine-muted); }
5
+ .ds-subscription-status { color: var(--rhine-green); font-size: 12px; }
6
+ .ds-subscription-status:not(:empty) { margin-top: 16px; }
7
+ .ds-subscription-summary { margin-top: 24px; }
8
+ .ds-subscription-section-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 12px; margin: 32px 0 20px; }
9
+ .ds-subscription-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr)); gap: 24px; }
10
+ .ds-subscription-cards>.rhine-overlay-anchor { display: block; min-width: 0; }
11
+ .ds-subscription-card { display: flex; flex-direction: column; width: 100%; height: 100%; text-align: left; font: inherit; color: var(--rhine-ink); cursor: pointer; transition: background-color 120ms; }
12
+ .ds-subscription-card:hover { background: var(--rhine-selected); }
13
+ .ds-subscription-card-top { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
14
+ .ds-subscription-arrow { font-size: 24px; }
15
+ .ds-subscription-count { display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px; font-size: 52px; font-weight: 800; line-height: 1.2; margin: 24px 0 8px; font-variant-numeric: tabular-nums; }
16
+ .ds-subscription-count small, .ds-subscription-card-bottom small { font-size: 12px; color: var(--rhine-muted); font-weight: 400; }
17
+ .ds-subscription-description { font-size: 12px; color: var(--rhine-muted); margin-bottom: 24px; }
18
+ .ds-subscription-card-bottom { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 12px; margin-top: auto; padding-top: 16px; border-top: 1px solid var(--rhine-line); }
19
+ .ds-subscription-running { color: var(--rhine-green); font-size: 12px; }
20
+ .ds-subscription-card-bottom strong { font-size: 16px; font-variant-numeric: tabular-nums; }
21
+ .ds-subscription-table { margin-top: 32px; }
22
+ .ds-subscription-total { display: flex; justify-content: space-between; gap: 16px; padding-top: 20px; }
23
+ .ds-subscriptions .rhine-type-tag { font-size: 12px; font-weight: 700; }
24
+ @media(max-width:480px) {
25
+ .ds-subscription-heading h1 { font-size: 28px; }
26
+ .ds-subscription-summary .rhine-grid { grid-template-columns: 1fr; }
27
+ }