@linhey/rhine-design 0.0.0-stage → 0.2.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 (55) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CONTRIBUTING.md +44 -0
  3. package/LICENSE +202 -0
  4. package/NOTICE +4 -0
  5. package/README.md +144 -2
  6. package/components/Actions.tsx +40 -0
  7. package/components/COMMON_COMPONENTS.md +70 -0
  8. package/components/Data.tsx +42 -0
  9. package/components/Forms.tsx +64 -0
  10. package/components/Layout.tsx +13 -0
  11. package/components/Logo.tsx +34 -0
  12. package/components/MOTION.md +72 -0
  13. package/components/Overlays.tsx +53 -0
  14. package/components/README.md +137 -0
  15. package/components/STARTUP.md +34 -0
  16. package/components/Select.tsx +22 -0
  17. package/components/TYPOGRAPHY.md +60 -0
  18. package/components/Toast.tsx +25 -0
  19. package/components/Toolbar.tsx +14 -0
  20. package/components/Typography.tsx +13 -0
  21. package/components/assets/logo.svg +5 -0
  22. package/components/common.css +69 -0
  23. package/components/components.tsx +151 -0
  24. package/components/index.ts +24 -0
  25. package/components/styles.css +109 -0
  26. package/components/tokens.css +58 -0
  27. package/components/typography.css +31 -0
  28. package/design/README.md +14 -0
  29. package/docs/DESIGN_SYSTEM.md +83 -0
  30. package/docs/DESIGN_SYSTEM_AUDIT.md +63 -0
  31. package/docs/RELEASING.md +27 -0
  32. package/index.html +3 -0
  33. package/package.json +76 -4
  34. package/preview/CommonGallery.tsx +35 -0
  35. package/preview/MotionGallery.tsx +89 -0
  36. package/preview/StartupGallery.tsx +73 -0
  37. package/preview/brand-assembly.css +18 -0
  38. package/preview/main.tsx +100 -0
  39. package/preview/motion.css +25 -0
  40. package/preview/preview.css +27 -0
  41. package/preview/startup.css +33 -0
  42. package/scripts/check-build.mjs +15 -0
  43. package/skills/rhine-design/SKILL.md +37 -0
  44. package/skills/rhine-design/agents/openai.yaml +4 -0
  45. package/skills/rhine-design/references/API_MAP.md +34 -0
  46. package/skills/rhine-design/references/TASK_MAP.md +20 -0
  47. package/skills/rhine-design/references/integration.md +40 -0
  48. package/tests/CommonComponents.test.tsx +71 -0
  49. package/tests/DesignSystem.test.tsx +100 -0
  50. package/tests/MotionGallery.test.tsx +32 -0
  51. package/tests/StartupGallery.test.tsx +55 -0
  52. package/tests/setup.ts +4 -0
  53. package/tsconfig.json +32 -0
  54. package/vite.config.ts +3 -0
  55. package/vitest.config.ts +3 -0
@@ -0,0 +1,27 @@
1
+ # npm 发布
2
+
3
+ 包名为 `@linhey/rhine-design`,发布到 `https://registry.npmjs.org/`,access 固定 public。GitHub 仓库 `linhay/rhine-design` 已公开;HTTPS 克隆与 npm 安装均不需要专用仓库读取凭据。推送和发布仍需维护者权限。
4
+
5
+ ## 候选准备
6
+
7
+ 1. 确认 main、工作区、版本和 CHANGELOG;版本已存在时递增,不覆盖、不移动已发布 tag。
8
+ 2. 执行 `npm ci`、`npm run check`。预览构建不等于包消费可用,须额外安装实际 tarball 验证新包入口、主题 CSS、Logo SVG。
9
+ 3. 从干净的精确提交打包:`npm pack --json --pack-destination <制品目录>`。检查文件清单与体积,只包含声明的组件、通用规范、演示和测试;不得包含凭据、个人配置或产品专属资料。
10
+ 4. 保存源码 SHA、包名/版本、tarball SHA-256 与 npm integrity;候选制品与发布制品使用同一文件。
11
+
12
+ ## 发布与验证
13
+
14
+ 先执行 `npm whoami --registry=https://registry.npmjs.org/` 确认认证。失效时使用 `npm login --auth-type=web`,密码与验证码只在官方页面输入;不在聊天、脚本参数或仓库中保存 token。
15
+
16
+ ```sh
17
+ npm publish <已验证的tgz> --access public --tag latest --registry=https://registry.npmjs.org/
18
+ npm view @linhey/rhine-design@<版本> version dist.integrity dist.tarball --registry=https://registry.npmjs.org/
19
+ ```
20
+
21
+ 发布目录时 prepublishOnly 会运行检查;发布 tarball 时不能假设它重跑源码检查,必须先完成候选验证。npm 如要求浏览器或双因素确认,由有权限的维护者完成,不重复提交状态不明的发布请求。
22
+
23
+ 发布后从 registry 在仓库外安装精确版本,再验证导入、构建及 CSS/SVG;回读 integrity 与候选一致后才能记录成功。源码 main、签名版本 tag、registry 版本和制品摘要分别留证。
24
+
25
+ 失败版本优先发布修复版本。需要撤回推荐时恢复 prior dist-tag 或标记 deprecate,不能假定公开包可随时删除;相关远端修改需明确授权。首次发布没有 prior 版本,发布前验证尤其重要。
26
+
27
+ 当前包导出 TS/TSX/CSS 源码,支持范围是已验证的 React19/Vite 消费;未将 npm 发布外推为任意 SSR、Node-only 或原生平台兼容。项目采用 Apache-2.0,发布包必须包含 LICENSE 与 NOTICE,并保持 package.json 中的许可标识一致。
package/index.html ADDED
@@ -0,0 +1,3 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="./components/assets/logo.svg" type="image/svg+xml"><title>莱茵设计 · 通用组件与动效</title></head>
3
+ <body><div id="root"></div><script type="module" src="./preview/main.tsx"></script></body></html>
package/package.json CHANGED
@@ -1,6 +1,78 @@
1
1
  {
2
2
  "name": "@linhey/rhine-design",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "license": "Apache-2.0",
5
+ "publishConfig": {
6
+ "access": "public",
7
+ "registry": "https://registry.npmjs.org/"
8
+ },
9
+ "type": "module",
10
+ "description": "Rhine Design — independent design system, React components and motion patterns",
11
+ "repository": { "type": "git", "url": "git+https://github.com/linhay/rhine-design.git" },
12
+ "homepage": "https://github.com/linhay/rhine-design#readme",
13
+ "engines": {
14
+ "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
15
+ },
16
+ "exports": {
17
+ ".": "./components/index.ts",
18
+ "./tokens.css": "./components/tokens.css",
19
+ "./styles.css": "./components/styles.css",
20
+ "./logo.svg": "./components/assets/logo.svg"
21
+ },
22
+ "sideEffects": [
23
+ "./components/index.ts",
24
+ "**/*.css"
25
+ ],
26
+ "files": [
27
+ "components",
28
+ "preview",
29
+ "design",
30
+ "docs",
31
+ "tests",
32
+ "index.html",
33
+ "README.md",
34
+ "CHANGELOG.md",
35
+ "CONTRIBUTING.md",
36
+ "LICENSE",
37
+ "NOTICE",
38
+ "tsconfig.json",
39
+ "vite.config.ts",
40
+ "vitest.config.ts",
41
+ "package-lock.json",
42
+ "scripts",
43
+ "skills"
44
+ ],
45
+ "scripts": {
46
+ "dev": "vite --host 127.0.0.1 --port 18309 --strictPort",
47
+ "typecheck": "tsc --noEmit",
48
+ "test": "vitest run",
49
+ "build": "npm run typecheck && vite build && node scripts/check-build.mjs",
50
+ "check": "npm run test && npm run build",
51
+ "prepublishOnly": "npm run check"
52
+ },
53
+ "peerDependencies": {
54
+ "react": "^19.0.0",
55
+ "react-dom": "^19.0.0"
56
+ },
57
+ "dependencies": {
58
+ "@radix-ui/react-alert-dialog": "1.2.1",
59
+ "@radix-ui/react-dialog": "1.3.1",
60
+ "@radix-ui/react-dropdown-menu": "2.2.1",
61
+ "@radix-ui/react-popover": "1.3.1",
62
+ "@radix-ui/react-tooltip": "1.4.1"
63
+ },
64
+ "devDependencies": {
65
+ "@testing-library/jest-dom": "7.0.1",
66
+ "@testing-library/react": "16.3.3",
67
+ "@types/node": "26.6.5",
68
+ "@types/react": "19.3.0",
69
+ "@types/react-dom": "19.3.0",
70
+ "@vitejs/plugin-react": "6.1.2",
71
+ "jsdom": "29.1.1",
72
+ "typescript": "5.9.3",
73
+ "vite": "8.3.4",
74
+ "vitest": "5.0.5",
75
+ "react": "19.3.1",
76
+ "react-dom": "19.3.1"
77
+ }
78
+ }
@@ -0,0 +1,35 @@
1
+ import { useState } from 'react';
2
+ import { Badge, Button, Checkbox, Combobox, ConfirmDialog, CopyButton, DataTable, Dialog, Divider, Grid, Icon, IconButton, Inline, KeyValue, Link, Menu, Metric, Pagination, Popover, RadioGroup, SearchField, Select, Stack, Surface, Switch, Text, Textarea, TextField, Toast, Tooltip, type TableSort } from '../components/index';
3
+
4
+ const demoRows = [{ id:'request-a', time:'14:59:18', model:'gpt-6.1-sol', tokens:9600 }, { id:'request-b', time:'14:58:18', model:'gpt-6-sol', tokens:0 }];
5
+ const demoOptions = [{value:'a',label:'研究工作区'}, {value:'disabled',label:'归档工作区',disabled:true}, {value:'b',label:'共享工作区'}];
6
+ export function CommonGallery() {
7
+ const [tableState,setTableState]=useState('normal'), [page,setPage]=useState(1), [sort,setSort]=useState<TableSort>();
8
+ const [checked,setChecked]=useState(false), [mixed,setMixed]=useState(true), [radio,setRadio]=useState('daily'), [switchOn,setSwitchOn]=useState(false);
9
+ const [query,setQuery]=useState(''), [queryEvent,setQueryEvent]=useState('尚未提交'), [candidateQuery,setCandidateQuery]=useState(''), [candidate,setCandidate]=useState<string|null>(null), [candidateState,setCandidateState]=useState('normal');
10
+ const [toast,setToast]=useState(false), [toastTone,setToastTone]=useState<'success'|'error'>('success'), [confirmFail,setConfirmFail]=useState(false), [action,setAction]=useState('尚无操作');
11
+ return <>
12
+ <section id="data"><div className="ds-section-title"><span>06</span><h2>数据与分页 · 完整表达状态</h2></div>
13
+ <Surface><Stack><Grid minWidth={150}><Metric label="请求次数" value={128} description="失败 3 次"/><Metric label="预估费用" value="$5.44" description="3 次未计价"/><Metric label="平均耗时" value={null} unit="s" description="暂不可用"/><Metric label="总 Token" value={null} loading/></Grid><Divider/><KeyValue items={[{key:'mode',label:'本地配置',value:'OAuth'},{key:'model',label:'推荐模型',value:'gpt-6.1-sol'},{key:'state',label:'同步状态',value:<Badge tone="success">运行中</Badge>}]}/></Stack></Surface>
14
+ <Surface className="ds-gap-top"><Stack><Select label="表格状态" value={tableState} onChange={event=>setTableState(event.target.value)}><option value="normal">正常</option><option value="loading">刷新中(保留数据)</option><option value="empty">无记录</option><option value="error">读取失败(保留数据)</option></Select>
15
+ <DataTable caption="请求明细 · 合成数据" rows={tableState==='empty'?[]:demoRows} rowKey={row=>row.id} loading={tableState==='loading'} error={tableState==='error'?'连接暂时不可用,请重试。':undefined} onRetry={()=>setTableState('normal')} sort={sort} onSortChange={value=>{setSort(value);setAction(`排序事件:${value.key} / ${value.direction}`);}} columns={[{key:'time',header:'时间 · 上海',render:row=>row.time,sortable:true},{key:'model',header:'模型',render:row=>row.model},{key:'tokens',header:'Token',render:row=>row.tokens.toLocaleString('en-US'),align:'right',sortable:true}]}/>
16
+ <Pagination page={page} pageSize={10} total={23} loading={tableState==='loading'} onPageChange={setPage}/><Text variant="caption" tone="muted">分页和排序只演示事件;此处保留同一组合成记录,组件不本地切片或排序。{action}</Text>
17
+ </Stack></Surface>
18
+ </section>
19
+ <section id="forms"><div className="ds-section-title"><span>07</span><h2>表单与选择 · 原生操作习惯</h2></div><Grid>
20
+ <Surface><Stack><Checkbox label="包含失败请求" checked={checked} onChange={event=>setChecked(event.target.checked)} hint="复选项可以独立启停。"/><Checkbox label="混合状态示例" indeterminate={mixed} onChange={()=>setMixed(false)}/><Checkbox label="暂不可选择" disabled/>
21
+ <Divider/><RadioGroup label="报表周期" items={[{value:'daily',label:'每日'},{value:'weekly',label:'每周'},{value:'monthly',label:'每月',disabled:true}]} value={radio} onValueChange={setRadio}/><Switch label="显示辅助说明" checked={switchOn} onCheckedChange={setSwitchOn} hint="偏好示例,不控制实际同步。"/></Stack></Surface>
22
+ <Surface><Stack><Textarea label="备注" placeholder="输入工作区说明" hint="允许换行与垂直调整;此处不保存。"/><Textarea label="只读说明" readOnly value="由组织统一设置,可选择和复制。" rows={2}/><Textarea label="校验失败示例" error="请填写说明后继续。" rows={2}/></Stack></Surface>
23
+ </Grid>
24
+ <Grid className="ds-gap-top"><Surface><Stack><SearchField label="搜索请求" value={query} onValueChange={setQuery} onSearch={value=>setQueryEvent(value?`已提交:${value}`:'已清空查询')}/><Text variant="caption" tone="muted">{queryEvent}。只回传查询,不访问接口。</Text></Stack></Surface>
25
+ <Surface><Stack><Select label="候选状态" value={candidateState} onChange={event=>setCandidateState(event.target.value)}><option value="normal">正常</option><option value="loading">读取中</option><option value="empty">无匹配结果</option><option value="error">读取失败</option></Select><Combobox label="选择工作区" query={candidateQuery} onQueryChange={value=>{setCandidateQuery(value);setCandidate(null);}} value={candidate} onValueChange={option=>{setCandidate(option.value);setCandidateQuery(option.label);}} options={candidateState==='empty'?[]:demoOptions} loading={candidateState==='loading'} error={candidateState==='error'?'候选读取失败,请重新查询。':undefined}/><Text variant="caption" tone="muted">候选列表为固定合成返回结果,不随输入执行本地筛选。</Text></Stack></Surface></Grid></section>
26
+ <section id="utilities"><div className="ds-section-title"><span>08</span><h2>操作与布局 · 按同一规则组合</h2></div><Surface><Stack><Inline><Link href="#typography">查看文字规范</Link><IconButton icon="refresh" label="刷新示例" onClick={()=>setAction('已点击刷新')}/><IconButton icon="close" label="不可关闭" disabled/><Icon name="info"/><Text variant="caption">图标提供辅助识别;按钮始终有名称。</Text></Inline><Divider/><Inline><CopyButton value="Rhine Design · 组件复制示例" label="复制演示文字"/><CopyButton value="demo" label="演示复制失败" copy={()=>Promise.reject(new Error('demo'))}/></Inline><Text variant="caption" tone="muted">Stack / Inline / Grid / Divider 在本页直接参与布局,无额外业务状态。</Text></Stack></Surface></section>
27
+ <section id="overlays"><div className="ds-section-title"><span>09</span><h2>浮层与反馈 · 焦点有来有回</h2></div><Surface><Stack><Inline>
28
+ <Tooltip content="可用额度可能延迟更新,以最新采集结果为准。"><IconButton label="额度说明" icon="info"/></Tooltip>
29
+ <Popover trigger={<Button variant="secondary">查看详情</Button>} title="工作区详情"><Stack><Text>气泡用于补充内容,不阻断页面。</Text><Link href="#data">查看数据组件</Link></Stack></Popover>
30
+ <Menu label="更多操作" trigger={<Button variant="secondary">更多操作</Button>} items={[{id:'refresh',label:'刷新',onSelect:()=>setAction('菜单:刷新')},{id:'disabled',label:'导出(暂不可用)',disabled:true,onSelect:()=>{}},{id:'archive',label:'归档示例',danger:true,onSelect:()=>setAction('菜单:归档示例')}]}/>
31
+ <Dialog trigger={<Button variant="secondary">打开对话框</Button>} title="编辑工作区" description="此对话框仅演示输入、焦点与关闭行为。"><Stack><TextField label="工作区名称" defaultValue="研究工作区"/><Textarea label="工作区说明"/><Popover trigger={<Button variant="secondary">对话框内详情</Button>} title="嵌套详情"><Text>Escape先关闭当前气泡,再返回对话框。</Text></Popover></Stack></Dialog>
32
+ <ConfirmDialog trigger={<Button variant="danger">打开确认框</Button>} title="停止演示任务?" description="这是合成操作,不会修改实际同步。默认焦点在取消按钮。" confirmLabel="停止演示任务" danger onConfirm={async()=>{if(confirmFail)throw new Error('demo');setAction('确认已完成(演示)');}}/>
33
+ </Inline><Checkbox label="模拟确认操作失败" checked={confirmFail} onChange={event=>setConfirmFail(event.target.checked)}/><Text variant="caption" tone="muted">{action}。Tab/方向键/Escape可操作;减少动态保留相同功能。</Text><Divider/><Inline><Button variant="secondary" onClick={()=>{setToastTone('success');setToast(true);}}>显示成功通知</Button><Button variant="secondary" onClick={()=>{setToastTone('error');setToast(true);}}>显示错误通知</Button></Inline><Toast open={toast} onOpenChange={setToast} tone={toastTone} title={toastTone==='success'?'演示操作已完成':'演示操作失败'} description={toastTone==='success'?'5秒后关闭;悬停、聚焦或页面隐藏时暂停。':'错误通知保持显示,请重试或手动关闭。'}/></Stack></Surface></section>
34
+ </>;
35
+ }
@@ -0,0 +1,89 @@
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';
3
+ import './motion.css';
4
+ import './brand-assembly.css';
5
+
6
+ const recipes = [
7
+ ['loading', '加载 → 内容', '180ms', '骨架占位 → 内容就绪 → 原位淡入', '展台候选'],
8
+ ['result', '操作 → 结果', '120ms', '等待反馈 → 合成成功 → 图标与文字淡入', '展台候选'],
9
+ ['popover', '气泡 · 四向展开', '160ms / 3px', '触发点 → 按最终方向展开 → Escape关闭', '方向候选'],
10
+ ['menu', '菜单 · 从触发点展开', '160ms / 3px', '操作入口 → 菜单展开 → 选择或Escape', '方向候选'],
11
+ ['dialog', '弹窗 · 聚焦任务', '160ms', '原位按钮 → 模态淡入 → 关闭并归还焦点', '现有组件'],
12
+ ['disclosure', '折叠 · 连续的高度', '200ms', '收起 → 高度展开 → 可点击标题反向', '现有组件'],
13
+ ['selection', '切换 · 稳定的位置', '120 / 180ms', '默认选项 → 指示与滑块切换 → 保留内容', '现有组件'],
14
+ ['toast', '通知 · 轻量回应', '180ms', '静态占位 → 通知进入 → 手动关闭', '现有组件'],
15
+ ['tooltip', '提示 · 即看即走', '160ms', '播放或悬停 → 提示出现 → Escape关闭', '现有组件'],
16
+ ['brand', '品牌 · 旋转变形归位', '880ms / 三块错峰', '三块旋转压缩 → 展开闭合 → 定稿标志静止', '品牌候选'],
17
+ ] as const;
18
+ type Kind = typeof recipes[number][0];
19
+ type Side = 'top' | 'right' | 'bottom' | 'left';
20
+
21
+ // Restart only visual animations: never remount the modal or replay a business action.
22
+ function replayModal(button: HTMLButtonElement) {
23
+ const dialog = button.closest('.tod-dialog');
24
+ const overlay = dialog?.parentElement?.querySelector('.tod-overlay');
25
+ for (const element of [dialog, overlay]) {
26
+ if (!element) continue;
27
+ for (const animation of element.getAnimations()) { animation.currentTime = 0; animation.play(); }
28
+ }
29
+ }
30
+
31
+ function Scene({ kind, active, side }: { kind: Kind; active: boolean; side: Side }) {
32
+ const [started, setStarted] = useState(false), [done, setDone] = useState(false);
33
+ const [open, setOpen] = useState(false), [selected, setSelected] = useState('before'), [enabled, setEnabled] = useState(false);
34
+ const [compact, setCompact] = useState(false);
35
+ const disclosure = useRef<HTMLDivElement>(null);
36
+ useEffect(() => {
37
+ if (kind !== 'popover' && kind !== 'menu') return;
38
+ const media = window.matchMedia?.('(max-width:759px)');
39
+ if (!media) return;
40
+ const update = () => setCompact(media.matches); update();
41
+ media.addEventListener('change', update); return () => media.removeEventListener('change', update);
42
+ }, [kind]);
43
+ const preferredSide = compact && (side === 'left' || side === 'right') ? 'bottom' : side;
44
+ useEffect(() => {
45
+ if (!active) return;
46
+ let second = 0; let timer: ReturnType<typeof setTimeout> | undefined;
47
+ const first = requestAnimationFrame(() => { second = requestAnimationFrame(() => {
48
+ setStarted(true); setOpen(true); setSelected('after'); setEnabled(true);
49
+ if (kind === 'loading' || kind === 'result') timer = setTimeout(() => setDone(true), 700);
50
+ }); });
51
+ return () => { cancelAnimationFrame(first); cancelAnimationFrame(second); clearTimeout(timer); };
52
+ }, [active, kind]);
53
+ useEffect(() => { if (started && kind === 'disclosure') disclosure.current?.querySelector('summary')?.click(); }, [started, kind]);
54
+
55
+ if (kind === 'loading') return <div className="ds-motion-content" aria-busy={started && !done}>
56
+ {done ? <div className="ds-content-reveal"><Text variant="heading">研究工作区</Text><Text tone="muted">内容已准备好,阅读位置保持稳定。</Text><Badge tone="success">读取完成 · 演示</Badge></div> : <div className={started ? '' : 'ds-still'}><Skeleton lines={3} label="加载占位示例" /></div>}
57
+ </div>;
58
+ 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
+ if (kind === 'popover') return <Popover open={open} onOpenChange={setOpen} side={preferredSide} title="方向感知气泡" trigger={<Button variant="secondary">查看气泡</Button>}><Text>方向跟随实际位置。窄窗左右方向改为上下展开。</Text></Popover>;
60
+ 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:()=>{}}]} />;
61
+ 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
+ if (kind === 'disclosure') return <div ref={disclosure} className="ds-motion-fill"><Disclosure title="展开动效说明"><Text>内容沿高度展开,文字保持原尺寸。</Text><Text tone="muted" variant="caption">反复点击标题可观察中途反向。</Text></Disclosure></div>;
63
+ 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>;
64
+ if (kind === 'toast') return <div className="ds-motion-fill">{open ? <Toast open onOpenChange={setOpen} title="演示已完成" description="这里保持显示,方便比较动画。" duration={0}/> : <Text tone="muted" variant="caption">通知将在这里出现</Text>}</div>;
65
+ if (kind === 'tooltip') return <Tooltip open={open} onOpenChange={setOpen} content="说明只在需要时出现;不放必要操作。"><Button variant="secondary">悬停或聚焦这里</Button></Tooltip>;
66
+ return <Logo key={String(started)} size={104} animate={started} className="ds-brand-assembly" />;
67
+ }
68
+
69
+ function MotionCard({ recipe, slow, reduced }: { recipe: typeof recipes[number]; slow: boolean; reduced: boolean }) {
70
+ const [generation, setGeneration] = useState(0), [active, setActive] = useState(false), [still, setStill] = useState(false), [side, setSide] = useState<Side>('bottom');
71
+ const [kind, title, duration, flow, status] = recipe;
72
+ const run = () => { setGeneration(value=>value+1); setActive(true); };
73
+ return <article className="tod-theme ds-motion-card" aria-label={title} data-kind={kind} data-slow={slow} data-tod-motion={reduced || still ? 'reduced' : 'system'}>
74
+ <header><Text variant="code">{duration}</Text><Badge>{status}</Badge></header>
75
+ <div className="ds-motion-stage"><Scene key={generation} kind={kind} active={active} side={side}/></div>
76
+ <div className="ds-motion-description"><h3>{title}</h3><Text variant="caption" tone="muted">{flow}</Text></div>
77
+ {(kind === 'popover' || kind === 'menu') && <Select label={`${title}方向`} hint="窄窗优先上下展开,方向随最终位置调整。" value={side} onChange={event=>setSide(event.target.value as Side)}><option value="bottom">下方</option><option value="top">上方</option><option value="left">左侧</option><option value="right">右侧</option></Select>}
78
+ <div className="ds-motion-buttons"><Button variant="primary" aria-label={`播放${title}`} onClick={run}>播放</Button><Button variant="secondary" aria-label={`重播${title}`} disabled={!active} onClick={run}>重播</Button><Button variant="quiet" aria-label={`复位${title}`} onClick={()=>{setGeneration(value=>value+1);setActive(false);}}>复位</Button></div>
79
+ <Checkbox label="此例减少动态" checked={still} onChange={event=>setStill(event.target.checked)}/>
80
+ </article>;
81
+ }
82
+
83
+ export function MotionGallery({ reduced }: { reduced: boolean }) {
84
+ const [speed, setSpeed] = useState('normal');
85
+ 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>
87
+ <div className="ds-motion-grid">{recipes.map(recipe=><MotionCard key={recipe[0]} recipe={recipe} slow={speed==='slow'} reduced={reduced}/>)}</div>
88
+ </section>;
89
+ }
@@ -0,0 +1,73 @@
1
+ import { useEffect, useState, type CSSProperties } from 'react';
2
+ import { Badge, Button, Checkbox, Logo, Notice, Select, Text } from '../components/index';
3
+ import './startup.css';
4
+ import './brand-assembly.css';
5
+
6
+ type Scenario = 'ready' | 'waiting' | 'error';
7
+ type State = 'cover' | 'playing' | 'ready' | 'error';
8
+
9
+ export function StartupGallery({ reduced }: { reduced: boolean }) {
10
+ const [scenario, setScenario] = useState<Scenario>('ready');
11
+ const [state, setState] = useState<State>('cover');
12
+ const [generation, setGeneration] = useState(0);
13
+ const [slow, setSlow] = useState(false);
14
+ const [localReduced, setLocalReduced] = useState(false);
15
+ const [loginPreview, setLoginPreview] = useState(false);
16
+ const [systemReduced, setSystemReduced] = useState(() => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false);
17
+ const reduce = reduced || localReduced || systemReduced;
18
+
19
+ useEffect(() => {
20
+ const media = window.matchMedia?.('(prefers-reduced-motion: reduce)');
21
+ const update = () => setSystemReduced(media?.matches ?? false);
22
+ media?.addEventListener('change', update);
23
+ return () => media?.removeEventListener('change', update);
24
+ }, []);
25
+
26
+ useEffect(() => {
27
+ if (state !== 'playing' || scenario === 'waiting') return;
28
+ // This timer represents synthetic readiness, never a production startup gate.
29
+ if (reduce) { setState(scenario); return; }
30
+ const timer = setTimeout(() => setState(scenario), 1600 * (slow ? 4 : 1));
31
+ return () => clearTimeout(timer);
32
+ }, [state, scenario, generation, slow, reduce]);
33
+
34
+ const play = () => { setLoginPreview(false); setGeneration(value => value + 1); setState('playing'); };
35
+ const reset = () => { setLoginPreview(false); setGeneration(value => value + 1); setState('cover'); };
36
+
37
+ return <section id="startup" className="ds-startup" aria-labelledby="startup-title" data-tod-motion={reduce ? 'reduced' : 'system'} style={{ '--startup-speed': slow ? 4 : 1, '--ds-brand-speed': slow ? 4 : 1, '--ds-brand-offset': '180ms' } as CSSProperties}>
38
+ <div className="ds-section-title"><span>START / 01</span><h2 id="startup-title">构架归位,品牌留下。</h2></div>
39
+ <p className="ds-caption">启动动画设计候选 · 品牌分镜 1.6 秒,560ms 连续收拢到登录页。就绪即刻可操作,减少动态时直接呈现。</p>
40
+ <div className="ds-startup-layout">
41
+ <div className="ds-startup-window">
42
+ <div className="ds-startup-chrome"><span>Rhine Design</span><span>启动设计预览</span></div>
43
+ <div key={generation} className="ds-startup-stage" data-state={state}>
44
+ <div className="ds-startup-emblem" aria-hidden="true">
45
+ <svg className="ds-startup-construction" viewBox="0 0 240 240" fill="none"><path className="ds-startup-guide" pathLength="1" d="M120 20 225 202H15Z" /><path className="ds-startup-ticks" d="M120 8v8M120 218v14M0 120h12M228 120h12" /><circle className="ds-startup-signal" cx="120" cy="20" r="3" /></svg>
46
+ <div className="ds-startup-logo-anchor"><Logo size={176} decorative animate={state === 'playing' || state === 'ready'} className="ds-brand-assembly" /></div>
47
+ <div className="ds-startup-wordmark">Rhine Design</div>
48
+ </div>
49
+ {state === 'ready' ? <div className="ds-startup-destination">
50
+ <Text as="h3" variant="section">连接你的工作空间</Text>
51
+ <Text tone="muted">登录后继续你的工作。</Text>
52
+ <Button onClick={() => setLoginPreview(true)}>在浏览器中登录</Button>
53
+ <Text variant="caption" tone="muted" role="status">{loginPreview ? '仅演示登录入口,不会打开授权页面。' : '设计预览,不发起授权。'}</Text>
54
+ </div> : <div className="ds-startup-status" role="status">{state === 'cover' ? '准备播放' : state === 'error' ? '初始化未完成' : '正在准备工作空间'}</div>}
55
+ {state === 'error' && <Notice tone="error" title="暂时无法恢复会话">这是失败场景演示。可使用下方重播再次查看。</Notice>}
56
+ <div className="ds-startup-footnote" aria-hidden="true"><span>STRUCTURE / CONNECTION</span><span>Rhine Design</span></div>
57
+ </div>
58
+ </div>
59
+ <aside className="ds-startup-score" aria-label="启动分镜">
60
+ <Badge>品牌动效 / 设计候选</Badge><h3>以几何建立秩序。</h3><p>三块构架,汇合成一个标志。让用户记住形状与节奏。</p>
61
+ <ol><li><span>01 / 0–320ms</span><strong>定位</strong><p>细线先行,确定三角边界。</p></li><li><span>02 / 180–1060ms</span><strong>旋转 · 展开 · 归位</strong><p>三块先旋转压缩,再错开 80ms 展开闭合。</p></li><li><span>03 / 1060–1600ms</span><strong>识别</strong><p>橙点收束,辅助线退去,字标显现。</p></li></ol>
62
+ <p className="ds-caption">就绪后,同一个标志用 560ms 收拢,字标沿中轴上移,登录内容轻轻显现。操作立即可用;慢启动保持静态标志。</p>
63
+ </aside>
64
+ </div>
65
+ <div className="ds-startup-controls">
66
+ <Select label="启动场景" value={scenario} onChange={event => { setScenario(event.target.value as Scenario); reset(); }}><option value="ready">正常启动</option><option value="waiting">慢启动 · 等待就绪</option><option value="error">初始化失败</option></Select>
67
+ <Select label="分镜速度" value={slow ? 'slow' : 'normal'} onChange={event => { setSlow(event.target.value === 'slow'); reset(); }}><option value="normal">原速 · 1.6 秒</option><option value="slow">4 倍慢放 · 6.4 秒</option></Select>
68
+ <Checkbox label="此例减少动态" checked={localReduced} onChange={event => setLocalReduced(event.target.checked)} />
69
+ </div>
70
+ <div className="ds-row"><Button onClick={play}>{state === 'cover' ? '播放启动动画' : '重播启动动画'}</Button><Button variant="secondary" onClick={reset}>复位启动动画</Button><Button variant="quiet" disabled={state !== 'playing'} onClick={() => setState('ready')}>模拟就绪 · 立即进入</Button></div>
71
+ <p className="ds-caption" role="status">{state === 'ready' ? '已进入登录示例,未执行真实登录。' : state === 'error' ? '失败场景已呈现;重播仅重置本地演示。' : reduce ? '减少动态:静态呈现;慢启动仍等待就绪。' : '主动播放,不自动循环。模拟就绪可随时中断编排;所有状态均为合成。'}</p>
72
+ </section>;
73
+ }
@@ -0,0 +1,18 @@
1
+ /* Brand-only preview recipe; the canonical SVG and generic Logo entry stay unchanged. */
2
+ .tod-logo.ds-brand-assembly .tod-logo__symbol { overflow:visible; }
3
+ .tod-logo.ds-brand-assembly[data-animate=true] .tod-logo__plate {
4
+ transform-box:fill-box;
5
+ transform-origin:center;
6
+ animation:ds-brand-assemble calc(720ms * var(--ds-brand-speed,1)) var(--tod-ease-standard) both;
7
+ animation-delay:calc((var(--ds-brand-offset,0ms) + var(--ds-brand-stagger)) * var(--ds-brand-speed,1));
8
+ }
9
+ .ds-brand-assembly .tod-logo__plate--left { --ds-brand-stagger:0ms; --ds-brand-turn:-120deg; }
10
+ .ds-brand-assembly .tod-logo__plate--right { --ds-brand-stagger:80ms; --ds-brand-turn:120deg; }
11
+ .ds-brand-assembly .tod-logo__plate--base { --ds-brand-stagger:160ms; --ds-brand-turn:60deg; }
12
+ @keyframes ds-brand-assemble {
13
+ 0% { opacity:0; transform:translate(calc(var(--tod-logo-x) * 6),calc(var(--tod-logo-y) * 6)) rotate(var(--ds-brand-turn)) scale(.22,1.08); }
14
+ 24% { opacity:1; }
15
+ 70% { opacity:1; transform:translate(0,0) rotate(calc(var(--ds-brand-turn) * .06)) scale(.94,1); }
16
+ 100% { opacity:1; transform:translate(0,0) rotate(0deg) scale(1); }
17
+ }
18
+ @media(prefers-reduced-motion:reduce) { .tod-logo.ds-brand-assembly[data-animate=true] .tod-logo__plate { animation:none; } }
@@ -0,0 +1,100 @@
1
+ import { StrictMode, useEffect, useRef, useState } from 'react';
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';
4
+ import './preview.css';
5
+ import { CommonGallery } from './CommonGallery';
6
+ import { MotionGallery } from './MotionGallery';
7
+ import { StartupGallery } from './StartupGallery';
8
+
9
+ function Gallery() {
10
+ const [motion, setMotion] = useState('system');
11
+ const [logoReplay, setLogoReplay] = useState(0);
12
+ const [previewPage, setPreviewPage] = useState('oauth');
13
+ const [busy, setBusy] = useState(false);
14
+ const [selected, setSelected] = useState(false);
15
+ const [tab, setTab] = useState('tokens');
16
+ const [metric, setMetric] = useState('tokens');
17
+ const [progress, setProgress] = useState<number | null>(38);
18
+ const [field, setField] = useState('');
19
+ const [showError, setShowError] = useState(false);
20
+ const [notice, setNotice] = useState(false);
21
+ const [loading, setLoading] = useState(false);
22
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
23
+ useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
24
+ const simulate = () => { setBusy(true); timer.current = setTimeout(() => { setBusy(false); setNotice(true); }, 900); };
25
+ return <div className="tod-theme ds-gallery" data-tod-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
+
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>
34
+ <div className="ds-logo-pair">
35
+ <div className="ds-logo-stage"><Logo variant="icon" size={192} theme="light" /><span>浅色 / LIGHT</span></div>
36
+ <div className="ds-logo-stage ds-logo-stage--dark"><Logo variant="icon" size={192} theme="dark" /><span>深色 / DARK</span></div>
37
+ </div>
38
+ <p className="ds-caption">浅色与深色共用同一份矢量母版,图形在方形画布内水平、垂直居中。</p>
39
+ <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
+ <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>
42
+
43
+ <section id="foundation"><div className="ds-section-title"><span>01</span><h2>同一套变量,贯穿所有组件</h2></div>
44
+ <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
+ <div className="ds-notes"><span>14px 正文 / 12px 辅助信息</span><span>4px 卡片 / 3px 按钮圆角</span><span>120–240ms 交互过渡</span></div>
46
+ </section>
47
+
48
+ <section id="typography"><div className="ds-section-title"><span>TYPE</span><h2>字体与文字 · 清晰先于装饰</h2></div>
49
+ <p className="ds-caption">系统无衬线用于中英文正文;等宽字体用于路径、模型标识和代码。业务字号不低于 12px,中文不增加装饰字距。</p>
50
+ <Surface><div className="ds-type-scale">{([
51
+ ['title','30 / 38 · 600','连接你的工作空间'],
52
+ ['section','18 / 26 · 600','数据概览'],
53
+ ['heading','16 / 24 · 600','研究账号 A'],
54
+ ['body','14 / 22 · 400','选择一个项目后,继续你的工作。'],
55
+ ['label','14 / 22 · 500','在浏览器中登录'],
56
+ ['caption','12 / 18 · 400','最近检查 14:32 · 上海时区'],
57
+ ['overline','12 / 18 · 500','连接方式'],
58
+ ['metric','26 / 32 · 600','1,280.00'],
59
+ ['code','12 / 20 · 400','~/.codex/auth.json'],
60
+ ] 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>
61
+ <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
+ <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
+ <div className="ds-gap-top"><Toolbar title="Rhine Design / 设计评审" description="工具栏与静态页面样稿共用一套样式。">
64
+ <Select label="页面示例" value={previewPage} onChange={event=>setPreviewPage(event.target.value)}><option value="oauth">项目列表</option><option value="usage">数据概览</option></Select>
65
+ <Select label="场景示例" defaultValue="normal"><option value="normal">正常</option><option value="loading">读取中 / 等待授权</option></Select>
66
+ </Toolbar></div>
67
+ <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>
69
+
70
+ <section id="actions"><div className="ds-section-title"><span>02</span><h2>操作有分量,反馈不打断</h2></div>
71
+ <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
+ <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>
74
+
75
+ <section id="selection"><div className="ds-section-title"><span>03</span><h2>选择清楚,位置稳定</h2></div>
76
+ <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
+ <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
+ <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>
80
+
81
+ <section id="feedback"><div className="ds-section-title"><span>04</span><h2>告诉用户发生了什么</h2></div>
82
+ <div className="ds-row ds-badges"><Badge>未开始</Badge><Badge tone="success">已完成</Badge><Badge tone="warning">需要处理</Badge><Badge tone="error">操作失败</Badge></div>
83
+ <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
+ <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
+ <div className="ds-gap-top"><Button variant="secondary" onClick={()=>setNotice(value=>!value)}>切换提示</Button></div>
86
+ {notice&&<div className="ds-gap-top"><Notice tone="success" title="修改已保存(演示)" action={<Button variant="quiet" onClick={()=>setNotice(false)}>关闭提示</Button>}>这是组件的本地反馈,没有写入任何业务数据。</Notice></div>}
87
+ <div className="ds-gap-top"><Notice tone="warning" title="状态文字始终可见">颜色和动画只增强反馈,不承担唯一的状态含义。</Notice></div>
88
+ </section>
89
+
90
+ <section id="disclosure"><div className="ds-section-title"><span>05</span><h2>逐步展开,保持可读</h2></div>
91
+ <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
+ <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>;
98
+ }
99
+
100
+ createRoot(document.getElementById('root')!).render(<StrictMode><Gallery /></StrictMode>);
@@ -0,0 +1,25 @@
1
+ .ds-motion-toolbar { display:flex; align-items:center; justify-content:space-between; gap:24px; flex-wrap:wrap; padding:20px 0 28px; }
2
+ .ds-motion-toolbar>div:first-child { flex:1 1 320px; }
3
+ .ds-motion-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr)); gap:24px; align-items:start; }
4
+ .ds-motion-card { min-width:0; padding:24px; border:1px solid var(--tod-line); border-radius:var(--tod-radius-card); background:var(--tod-surface); }
5
+ .ds-motion-card>header { display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap; align-items:center; }
6
+ .ds-motion-stage { display:flex; align-items:center; justify-content:center; min-height:240px; margin:20px 0; padding:24px; border:1px solid var(--tod-line); background:var(--tod-bg); }
7
+ .ds-motion-description { min-height:88px; }.ds-motion-description h3 { font-size:16px; margin:0 0 8px; }
8
+ .ds-motion-buttons { display:flex; flex-wrap:wrap; gap:8px; margin-top:20px; }.ds-motion-card>.tod-check { margin-top:12px; }
9
+ .ds-motion-fill,.ds-motion-content { width:100%; min-width:0; }.ds-motion-content { min-height:112px; }
10
+ .ds-content-reveal { display:grid; gap:10px; justify-items:start; animation:tod-overlay-fade var(--tod-duration-panel) var(--tod-ease-standard) both; }
11
+ .ds-result-demo { display:grid; justify-items:center; gap:20px; }.ds-result-slot { min-height:44px; display:flex; align-items:center; }
12
+ .ds-result-reveal { display:inline-flex; gap:8px; align-items:center; color:var(--tod-green); animation:tod-overlay-fade var(--tod-duration-hover) var(--tod-ease-standard) both; }
13
+ .ds-still * { animation:none!important; }
14
+ .ds-motion-card :is(.tod-dialog,.tod-overlay)[data-state=open] { animation-fill-mode:both; }
15
+ .ds-motion-card :is(.tod-popover,.tod-menu,.tod-combobox__list)[data-state=open] { animation:ds-origin-enter var(--tod-duration-overlay) var(--tod-ease-standard) both; --ds-from-x:0px; --ds-from-y:0px; }
16
+ .ds-motion-card [data-side=bottom] { --ds-from-y:calc(-1 * var(--tod-motion-distance))!important; }
17
+ .ds-motion-card [data-side=top] { --ds-from-y:var(--tod-motion-distance)!important; }
18
+ .ds-motion-card [data-side=right] { --ds-from-x:calc(-1 * var(--tod-motion-distance))!important; }
19
+ .ds-motion-card [data-side=left] { --ds-from-x:var(--tod-motion-distance)!important; }
20
+ @keyframes ds-origin-enter { from { opacity:0; transform:translate(var(--ds-from-x),var(--ds-from-y)); } to { opacity:1; transform:translate(0,0); } }
21
+ .ds-motion-card[data-slow=true] { --tod-duration-hover:480ms; --tod-duration-panel:720ms; --tod-duration-notice:800ms; --tod-duration-overlay:640ms; --tod-duration-brand:960ms; }
22
+ .ds-motion-card[data-slow=true] { --ds-brand-speed:4; }
23
+ .tod-theme[data-tod-motion=reduced] .ds-motion-card,.ds-motion-card[data-tod-motion=reduced] { --tod-duration-notice:0ms; }
24
+ @media(prefers-reduced-motion:reduce) { .ds-motion-card { --tod-duration-notice:0ms!important; } }
25
+ @media(max-width:520px) { .ds-motion-card { padding:16px; }.ds-motion-stage { padding:16px; }.ds-motion-description { min-height:0; } }
@@ -0,0 +1,27 @@
1
+ body { margin:0; }
2
+ .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(--tod-line); }
3
+ .ds-type-row:last-child { border:0; }
4
+ .ds-type-copy { display:grid; gap:12px; margin-top:16px; max-width:60ch; }
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(--tod-line); padding:20px max(24px,calc((100vw - 1080px)/2)); }
7
+ .ds-brand { display:inline-flex; align-items:center; color:var(--tod-ink); text-decoration:none; }
8
+ .ds-brand>span:not(.tod-logo) { font:12px var(--tod-mono); letter-spacing:1px; margin-left:16px; }
9
+ .ds-logo-pair { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:24px; }
10
+ .ds-logo-stage { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:24px; padding:32px 12px; background:var(--tod-surface); border:1px solid var(--tod-line); }
11
+ .ds-logo-stage>span:last-child { font:12px var(--tod-mono); letter-spacing:2px; }
12
+ .ds-logo-stage--dark { background:var(--tod-ink); color:var(--tod-bg); border-color:var(--tod-ink); }
13
+ @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; }
16
+ .ds-kicker { font:12px var(--tod-mono); letter-spacing:1px; color:var(--tod-muted); }
17
+ .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(--tod-green); }
18
+ .ds-hero p { color:var(--tod-muted); }
19
+ .ds-motion-control { border-left:2px solid var(--tod-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(--tod-ink); }.ds-nav a { color:var(--tod-green); text-decoration:none; min-height:28px; display:flex; align-items:center; }
21
+ .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(--tod-mono); color:var(--tod-warning); }.ds-section-title h2 { font-size:22px; font-weight:600; margin:0; }
22
+ .ds-palette { display:grid; grid-template-columns:repeat(5,1fr); gap:16px; }.ds-palette>div>div { height:82px; border:1px solid var(--tod-line); }.ds-palette strong,.ds-palette code { display:block; font:12px/1.8 var(--tod-mono); }.ds-palette strong { margin-top:10px; }.ds-palette code { color:var(--tod-muted); }
23
+ .ds-notes { display:flex; flex-wrap:wrap; gap:12px 32px; margin-top:24px; padding-top:16px; border-top:1px solid var(--tod-line); font-size:12px; color:var(--tod-muted); }
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(--tod-mono); margin:0; }.ds-component-title>span { color:var(--tod-muted); font-size:12px; }
25
+ .ds-row { display:flex; flex-wrap:wrap; gap:12px; align-items:center; }.ds-caption { font-size:12px; line-height:1.8; color:var(--tod-muted); }.ds-gap-top { margin-top:20px; }.ds-badges { margin-bottom:24px; }.ds-loaded { min-height:72px; margin:0; }
26
+ .ds-footer { border-top:1px solid var(--tod-ink); display:flex; justify-content:space-between; gap:16px; flex-wrap:wrap; padding:24px 0; color:var(--tod-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(.tod-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; } }
@@ -0,0 +1,33 @@
1
+ .ds-startup-layout { display:grid; grid-template-columns:minmax(0,1.6fr) minmax(240px,1fr); gap:40px; margin:24px 0; align-items:center; }
2
+ .ds-startup-window { border:1px solid var(--tod-line); background:var(--tod-bg); min-width:0; }
3
+ .ds-startup-chrome { display:flex; justify-content:space-between; gap:12px; padding:12px 20px; border-bottom:1px solid var(--tod-line); font:12px/18px var(--tod-font); color:var(--tod-muted); }
4
+ .ds-startup-stage { min-height:540px; padding:40px 24px 24px; display:flex; flex-direction:column; align-items:center; justify-content:flex-start; position:relative; isolation:isolate; }
5
+ .ds-startup-emblem { width:240px; height:220px; position:relative; box-sizing:border-box; flex:none; }
6
+ .ds-startup-logo-anchor { position:absolute; left:32px; top:32px; width:176px; height:176px; transform-origin:center; transition:transform calc(560ms * var(--startup-speed)) cubic-bezier(.22,1,.36,1); }
7
+ .ds-startup-construction { position:absolute; inset:0; width:240px; height:240px; pointer-events:none; transition:opacity calc(180ms * var(--startup-speed)) ease-out; }
8
+ .ds-startup-guide { stroke:var(--tod-control); stroke-width:.75; opacity:0; }
9
+ .ds-startup-ticks { stroke:var(--tod-line); stroke-width:1; }
10
+ .ds-startup-signal { fill:var(--tod-signal); opacity:0; }
11
+ .ds-startup-wordmark { position:absolute; top:246px; width:100%; text-align:center; color:var(--tod-ink); font:600 30px/38px var(--tod-font); letter-spacing:.04em; transition:transform calc(560ms * var(--startup-speed)) cubic-bezier(.22,1,.36,1); }
12
+ .ds-startup-status { font:12px/18px var(--tod-font); color:var(--tod-muted); margin:100px 0 24px; }
13
+ .ds-startup-footnote { display:flex; justify-content:space-between; flex-wrap:wrap; gap:16px; width:100%; margin-top:auto; padding-top:32px; font:12px/18px var(--tod-mono); letter-spacing:.06em; color:var(--tod-muted); }
14
+ .ds-startup-score h3 { font:600 26px/1.4 var(--tod-font); margin:20px 0 12px; }.ds-startup-score>p { color:var(--tod-muted); }
15
+ .ds-startup-score ol { list-style:none; padding:0; margin:28px 0; }.ds-startup-score li { padding:16px 0; border-top:1px solid var(--tod-line); }.ds-startup-score li>span { display:block; color:var(--tod-muted); font:12px/18px var(--tod-mono); margin-bottom:8px; }.ds-startup-score li strong { font-size:16px; font-weight:600; }.ds-startup-score li p { margin:4px 0 0; font-size:12px; color:var(--tod-muted); }
16
+ .ds-startup-controls { display:flex; align-items:center; gap:24px; flex-wrap:wrap; margin-bottom:20px; }.ds-startup-controls>.tod-field { flex:1; min-width:180px; }
17
+ .ds-startup-destination { display:flex; flex-direction:column; gap:16px; align-items:center; text-align:center; width:100%; max-width:320px; animation:startup-arrival calc(420ms * var(--startup-speed)) ease-out both; }
18
+ .ds-startup-destination>.tod-button { margin-top:8px; width:100%; max-width:260px; }
19
+ .ds-startup-destination:focus-within { animation:none; }
20
+ .ds-startup-stage[data-state=ready] .ds-startup-logo-anchor { transform:translateY(-16px) scale(.5); }
21
+ .ds-startup-stage[data-state=ready] .ds-startup-wordmark { transform:translateY(-84px); }
22
+ .ds-startup-stage[data-state=ready] .ds-startup-construction { opacity:0; }
23
+ @keyframes startup-arrival { from { opacity:.12; transform:translateY(3px); } to { opacity:1; transform:translateY(0); } }
24
+ .ds-startup-stage[data-state=playing] .ds-startup-guide { animation:startup-guide calc(1340ms * var(--startup-speed)) var(--tod-ease-standard) both; }
25
+ .ds-startup-stage[data-state=playing] .ds-startup-signal { animation:startup-signal calc(280ms * var(--startup-speed)) linear calc(1060ms * var(--startup-speed)) both; }
26
+ .ds-startup-stage[data-state=playing] .ds-startup-wordmark { animation:startup-destination calc(240ms * var(--startup-speed)) var(--tod-ease-standard) calc(1080ms * var(--startup-speed)) both; }
27
+ @keyframes startup-guide { 0% { stroke-dasharray:1; stroke-dashoffset:1; opacity:0; } 24%,79% { stroke-dasharray:1; stroke-dashoffset:0; opacity:.65; } 100% { stroke-dasharray:1; stroke-dashoffset:0; opacity:0; } }
28
+ @keyframes startup-signal { 0%,100% { opacity:0; } 20%,65% { opacity:1; } }
29
+ @keyframes startup-destination { from { opacity:0; } to { opacity:1; } }
30
+ .ds-startup[data-tod-motion=reduced] * { animation:none!important; transition:none!important; }
31
+ @media(prefers-reduced-motion:reduce) { .ds-startup * { animation:none!important; transition:none!important; } }
32
+ @media(max-width:760px) { .ds-startup-layout { grid-template-columns:minmax(0,1fr); gap:24px; }.ds-startup-stage { padding-inline:16px; }.ds-startup-score ol { margin:20px 0; } }
33
+ @media(max-width:360px) { .ds-startup-chrome { padding-inline:12px; }.ds-startup-footnote { flex-wrap:wrap; } }
@@ -0,0 +1,15 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ const assets = new URL('../dist/assets/', import.meta.url);
5
+ const files = await readdir(assets);
6
+ const css = (await Promise.all(files.filter(file => file.endsWith('.css')).map(file => readFile(new URL(file, assets), 'utf8')))).join('\n');
7
+ for (const required of ['--tod-bg:', '.tod-theme', '.tod-button', '.ds-startup']) {
8
+ if (!css.includes(required)) throw new Error(`Design kit build lost required stylesheet content: ${required}`);
9
+ }
10
+ if (!files.some(file => /^logo-.*\.svg$/.test(file))) throw new Error('Design kit build lost the SVG master asset');
11
+ const html = await readFile(new URL('../dist/index.html', import.meta.url), 'utf8');
12
+ for (const match of html.matchAll(/(?:src|href)="(\.\/assets\/[^"#]+)"/g)) {
13
+ await readFile(new URL(join('../dist/', match[1]), import.meta.url));
14
+ }
15
+ console.log('Design kit build verified: theme, controls, startup styles and SVG asset retained');