@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,69 @@
1
+ .tod-icon { display:inline-block; vertical-align:middle; flex:none; fill:none; stroke:currentColor; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; }
2
+ .tod-icon-button { padding:10px; width:44px; height:44px; flex:none; }
3
+ .tod-link { color:var(--tod-green); text-decoration:underline; text-underline-offset:3px; overflow-wrap:anywhere; }
4
+ .tod-link:hover { text-decoration-thickness:2px; }
5
+ .tod-link>.tod-icon { margin-left:4px; }
6
+ .tod-copy { display:inline-flex; align-items:center; gap:12px; flex-wrap:wrap; }
7
+ .tod-copy>[role] { font-size:var(--tod-text-caption); }
8
+ .tod-stack { display:flex; flex-direction:column; min-width:0; }
9
+ .tod-inline { display:flex; align-items:center; flex-wrap:wrap; gap:var(--tod-space-3); min-width:0; }
10
+ .tod-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--tod-grid-min)),1fr)); min-width:0; }
11
+ .tod-grid>*,.tod-stack>* { min-width:0; }
12
+ .tod-divider { border:0; border-top:1px solid var(--tod-line); margin:0; width:100%; }
13
+ .tod-metric { min-width:0; display:grid; gap:8px; align-content:start; }
14
+ .tod-metric__unit { font-size:var(--tod-text-caption); font-weight:400; color:var(--tod-muted); }
15
+ .tod-key-value { margin:0; display:grid; gap:12px; }
16
+ .tod-key-value>div { display:grid; grid-template-columns:minmax(80px,1fr) minmax(0,2fr); gap:16px; }
17
+ .tod-key-value dt { color:var(--tod-muted); font-size:12px; }.tod-key-value dd { margin:0; overflow-wrap:anywhere; }
18
+ .tod-data { display:grid; gap:12px; min-width:0; }
19
+ .tod-table-scroll { overflow:auto; max-width:100%; }
20
+ .tod-table { width:100%; min-width:500px; border-collapse:collapse; text-align:left; font-size:12px; }
21
+ .tod-table caption { text-align:left; font-size:16px; font-weight:600; margin-bottom:16px; }
22
+ .tod-table th,.tod-table td { padding:12px; border-bottom:1px solid var(--tod-line); max-width:240px; overflow-wrap:anywhere; }
23
+ .tod-table th { color:var(--tod-muted); font-weight:500; }.tod-table td { font-variant-numeric:tabular-nums; }
24
+ .tod-table [data-align=right] { text-align:right; }.tod-table tbody tr:hover { background:var(--tod-selected); }
25
+ .tod-table th button { font:inherit; color:inherit; background:none; border:0; min-height:44px; cursor:pointer; padding:0; }
26
+ .tod-table th button:disabled { cursor:default; }
27
+ .tod-pagination { display:flex; flex-wrap:wrap; justify-content:flex-end; align-items:center; gap:12px; }
28
+ .tod-check>label,.tod-radio-group>label { display:flex; align-items:center; gap:10px; min-height:44px; cursor:pointer; overflow-wrap:anywhere; }
29
+ .tod-check input,.tod-radio-group input { width:18px; height:18px; accent-color:var(--tod-green); flex:none; margin:0; }
30
+ .tod-check input:disabled+span,.tod-radio-group input:disabled+span,.tod-radio-group:disabled span { color:var(--tod-muted); cursor:default; }
31
+ .tod-radio-group { border:0; padding:0; margin:0; min-width:0; }.tod-radio-group legend { font-weight:500; padding:0; }
32
+ .tod-switch label { display:flex; position:relative; align-items:center; gap:12px; min-height:44px; cursor:pointer; }
33
+ .tod-switch input { position:absolute; opacity:0; width:1px; height:1px; }
34
+ .tod-switch__track { display:inline-flex; width:36px; height:22px; border:1px solid var(--tod-control); border-radius:12px; padding:3px; flex:none; background:var(--tod-surface); transition:background-color var(--tod-duration-hover) var(--tod-ease-standard); }
35
+ .tod-switch__track>span { width:14px; height:14px; border-radius:50%; background:var(--tod-muted); transition:transform var(--tod-duration-hover) var(--tod-ease-standard); }
36
+ .tod-switch input:checked+.tod-switch__track { background:var(--tod-green); border-color:var(--tod-green); }
37
+ .tod-switch input:checked+.tod-switch__track>span { transform:translateX(14px); background:var(--tod-surface); }
38
+ .tod-switch input:focus-visible+.tod-switch__track { outline:2px solid var(--tod-green); outline-offset:3px; }
39
+ .tod-switch input:disabled~span { opacity:.6; cursor:default; }
40
+ .tod-field textarea { font:var(--tod-text-body)/var(--tod-leading-body) var(--tod-font); color:var(--tod-ink); background:var(--tod-surface); border:1px solid var(--tod-control); border-radius:var(--tod-radius-control); padding:10px 12px; min-height:88px; resize:vertical; width:100%; transition:border-color var(--tod-duration-hover) var(--tod-ease-standard); }
41
+ .tod-field textarea[aria-invalid=true] { border-color:var(--tod-error); }.tod-field textarea:disabled { color:var(--tod-muted); background:var(--tod-selected); }
42
+ .tod-search>.tod-inline>input { flex:1 1 140px; width:auto; }.tod-search input::-webkit-search-cancel-button { appearance:none; }
43
+ .tod-combobox { position:relative; }
44
+ .tod-combobox__input { position:relative; }.tod-combobox__input input { padding-right:40px; }
45
+ .tod-combobox__arrow { position:absolute; right:12px; top:50%; transform:translateY(-50%); pointer-events:none; }
46
+ .tod-combobox__list { z-index:50; width:var(--radix-popover-trigger-width); max-width:calc(100vw - 24px); max-height:min(240px,var(--radix-popover-content-available-height)); overflow:auto; background:var(--tod-surface); color:var(--tod-ink); border:1px solid var(--tod-control); border-radius:4px; box-shadow:0 8px 24px #242b2818; }
47
+ .tod-combobox__list [role=option],.tod-combobox__empty { padding:12px; min-height:44px; cursor:pointer; overflow-wrap:anywhere; }
48
+ .tod-combobox__list [role=option][data-active=true],.tod-combobox__list [role=option][aria-selected=true] { background:var(--tod-selected); }
49
+ .tod-combobox__list [role=option][aria-disabled=true],.tod-combobox__empty { color:var(--tod-muted); cursor:default; }
50
+ .tod-overlay-anchor { display:inline-block; vertical-align:middle; max-width:100%; }
51
+ .tod-overlay { position:fixed; inset:0; background:#242b2870; z-index:40; }
52
+ .tod-dialog { position:fixed; z-index:41; top:50%; left:50%; transform:translate(-50%,-50%); width:min(480px,calc(100vw - 32px)); max-height:calc(100dvh - 32px); overflow:auto; padding:24px; border:1px solid var(--tod-line); border-radius:4px; background:var(--tod-surface); color:var(--tod-ink); box-shadow:0 16px 48px #242b2826; }
53
+ .tod-dialog>p { margin-top:12px; }.tod-dialog>.tod-notice { margin-top:16px; }
54
+ .tod-dialog__head { display:flex; align-items:center; justify-content:space-between; gap:12px; }
55
+ .tod-dialog__head>:first-child { min-width:0; overflow-wrap:anywhere; }
56
+ .tod-dialog__body { margin-top:24px; }.tod-dialog__actions { display:flex; justify-content:flex-end; flex-wrap:wrap; gap:12px; margin-top:24px; }
57
+ .tod-popover,.tod-menu { z-index:50; background:var(--tod-surface); color:var(--tod-ink); border:1px solid var(--tod-control); border-radius:4px; box-shadow:0 8px 24px #242b2820; max-width:calc(100vw - 24px); overflow:auto; }
58
+ .tod-popover { width:300px; padding:16px; max-height:var(--radix-popover-content-available-height); }
59
+ .tod-menu { min-width:180px; max-height:var(--radix-dropdown-menu-content-available-height); padding:4px; }
60
+ .tod-menu__item { min-height:44px; padding:10px 12px; cursor:pointer; outline:none; overflow-wrap:anywhere; }
61
+ .tod-menu__item[data-highlighted] { background:var(--tod-selected); box-shadow:inset 0 0 0 2px var(--tod-green); }
62
+ .tod-menu__item[data-disabled] { color:var(--tod-muted); cursor:default; }.tod-menu__item[data-danger=true] { color:var(--tod-error); }
63
+ .tod-tooltip { z-index:60; background:var(--tod-ink); color:var(--tod-surface); padding:8px 12px; border-radius:3px; max-width:min(280px,calc(100vw - 24px)); font-size:12px; line-height:18px; overflow-wrap:anywhere; }
64
+ .tod-toast-region { position:relative; }.tod-toast { display:flex; align-items:start; gap:12px; justify-content:space-between; padding:16px; background:var(--tod-tone-bg); border-left:3px solid var(--tod-tone); color:var(--tod-ink); animation:tod-enter var(--tod-duration-panel) var(--tod-ease-standard); }
65
+ .tod-toast p { margin:4px 0 0; overflow-wrap:anywhere; }.tod-toast strong { overflow-wrap:anywhere; }.tod-toast>div { min-width:0; }
66
+ .tod-dialog[data-state=open],.tod-overlay[data-state=open] { animation:tod-overlay-fade var(--tod-duration-overlay) var(--tod-ease-standard); }
67
+ .tod-popover[data-state=open],.tod-menu[data-state=open],.tod-combobox__list[data-state=open],.tod-tooltip[data-state=delayed-open],.tod-tooltip[data-state=instant-open] { animation:tod-enter var(--tod-duration-overlay) var(--tod-ease-standard); }
68
+ @keyframes tod-overlay-fade { from { opacity:0; } to { opacity:1; } }
69
+ @media(forced-colors:active) { .tod-switch__track { border-color:ButtonText; }.tod-switch input:checked+.tod-switch__track { background:Highlight; }.tod-switch__track>span { background:ButtonText; }.tod-switch input:checked+.tod-switch__track>span { background:HighlightText; }.tod-dialog,.tod-tooltip,.tod-menu,.tod-popover { border:1px solid CanvasText; } }
@@ -0,0 +1,151 @@
1
+ import { useEffect, useId, useRef, useState, type ButtonHTMLAttributes, type HTMLAttributes, type InputHTMLAttributes, type ReactNode } from 'react';
2
+
3
+ export type Tone = 'neutral' | 'success' | 'warning' | 'error';
4
+ const classes = (...parts: (string | false | undefined)[]) => parts.filter(Boolean).join(' ');
5
+
6
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
7
+ variant?: 'primary' | 'secondary' | 'quiet' | 'danger';
8
+ busy?: boolean;
9
+ busyLabel?: string;
10
+ }
11
+ export function Button({ variant = 'primary', busy = false, busyLabel = '处理中', disabled, className, children, onClick, type = 'button', ...props }: ButtonProps) {
12
+ return <button {...props} type={type} disabled={disabled} aria-disabled={busy || disabled || undefined}
13
+ aria-busy={busy || undefined} className={classes('tod-button', `tod-button--${variant}`, className)}
14
+ onClick={event => { if (busy) { event.preventDefault(); return; } onClick?.(event); }}>
15
+ <span className={busy ? 'tod-button__ghost' : undefined}>{children}</span>
16
+ {busy && <span className="tod-button__busy"><span className="tod-spinner" aria-hidden="true" /><span className="tod-sr-only">{busyLabel}</span></span>}
17
+ </button>;
18
+ }
19
+ export function Badge({ tone = 'neutral', children }: { tone?: Tone; children: ReactNode }) {
20
+ return <span className={`tod-badge tod-tone-${tone}`}>{children}</span>;
21
+ }
22
+ export function Surface({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
23
+ return <div {...props} className={classes('tod-surface', className)} />;
24
+ }
25
+ export function SelectionCard({ label, selected, onSelectedChange, disabled = false, children }: {
26
+ label: string; selected: boolean; onSelectedChange: (value: boolean) => void; disabled?: boolean; children: ReactNode;
27
+ }) {
28
+ return <label className="tod-selection" data-selected={selected} data-disabled={disabled}>
29
+ <span className="tod-selection__heading"><strong>{label}</strong><input type="checkbox" aria-label={label} checked={selected} disabled={disabled} onChange={event => onSelectedChange(event.target.checked)} /></span>
30
+ <span className="tod-selection__content">{children}</span>
31
+ </label>;
32
+ }
33
+ export function TextField({ label, hint, error, id: providedId, className, ...props }: InputHTMLAttributes<HTMLInputElement> & { label: string; hint?: string; error?: string }) {
34
+ const generatedId = useId();
35
+ const id = providedId || generatedId;
36
+ const describedBy = [props['aria-describedby'], hint && `${id}-hint`, error && `${id}-error`].filter(Boolean).join(' ');
37
+ return <div className={classes('tod-field', className)}>
38
+ <label htmlFor={id}>{label}</label>
39
+ <input {...props} id={id} aria-invalid={error ? true : props['aria-invalid']} aria-describedby={describedBy || undefined} />
40
+ {hint && <span id={`${id}-hint`} className="tod-field__hint">{hint}</span>}
41
+ {error && <span id={`${id}-error`} className="tod-field__error" role="alert">{error}</span>}
42
+ </div>;
43
+ }
44
+ export interface Choice { value: string; label: string; disabled?: boolean }
45
+ export interface TabItem extends Choice { content: ReactNode }
46
+ export function Tabs({ label, items, value, onValueChange }: { label: string; items: TabItem[]; value: string; onValueChange: (value: string) => void }) {
47
+ const id = useId();
48
+ const buttons = useRef<(HTMLButtonElement | null)[]>([]);
49
+ return <div className="tod-tabs">
50
+ <div role="tablist" aria-label={label} className="tod-tabs__list" onKeyDown={event => {
51
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
52
+ const available = items.map((item, index) => ({ ...item, index })).filter(item => !item.disabled);
53
+ if (!available.length) return;
54
+ event.preventDefault();
55
+ const current = available.findIndex(item => buttons.current[item.index] === event.target);
56
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? available.length - 1 : (current + (event.key === 'ArrowRight' ? 1 : -1) + available.length) % available.length;
57
+ onValueChange(available[next].value); buttons.current[available[next].index]?.focus();
58
+ }}>
59
+ {items.map((item, index) => <button key={item.value} ref={node => { buttons.current[index] = node; }}
60
+ type="button" role="tab" disabled={item.disabled} id={`${id}-tab-${index}`} aria-controls={`${id}-panel-${index}`}
61
+ aria-selected={value === item.value} tabIndex={value === item.value ? 0 : -1} onClick={() => onValueChange(item.value)}>{item.label}</button>)}
62
+ </div>
63
+ {items.map((item, index) => <div key={item.value} role="tabpanel" id={`${id}-panel-${index}`} aria-labelledby={`${id}-tab-${index}`}
64
+ hidden={value !== item.value} tabIndex={0} className="tod-tabs__panel">{item.content}</div>)}
65
+ </div>;
66
+ }
67
+ export function SegmentedControl({ label, items, value, onValueChange }: { label: string; items: Choice[]; value: string; onValueChange: (value: string) => void }) {
68
+ const name = useId();
69
+ return <fieldset className="tod-segmented"><legend className="tod-sr-only">{label}</legend>
70
+ {items.map(item => <label key={item.value} className="tod-segmented__option">
71
+ <input type="radio" name={name} value={item.value} checked={value === item.value} disabled={item.disabled} onChange={() => onValueChange(item.value)} />
72
+ <span>{item.label}</span>
73
+ </label>)}
74
+ </fieldset>;
75
+ }
76
+ export function Notice({ tone = 'neutral', title, children, action }: { tone?: Tone; title: string; children?: ReactNode; action?: ReactNode }) {
77
+ return <div className={`tod-notice tod-tone-${tone}`} role={tone === 'error' ? 'alert' : 'status'}>
78
+ <div><strong>{title}</strong>{children && <div className="tod-notice__body">{children}</div>}</div>{action}
79
+ </div>;
80
+ }
81
+ export function Progress({ label, value, valueText }: { label: string; value: number | null; valueText?: string }) {
82
+ const determinate = value !== null && Number.isFinite(value);
83
+ const amount = determinate ? Math.max(0, Math.min(100, value)) : null;
84
+ return <div className="tod-progress">
85
+ <div className="tod-progress__label"><span>{label}</span><strong>{valueText ?? (amount === null ? '处理中' : `${amount}%`)}</strong></div>
86
+ <div className="tod-progress__track" role="progressbar" aria-label={label} aria-valuemin={0} aria-valuemax={100}
87
+ aria-valuenow={amount ?? undefined} aria-valuetext={valueText ?? (amount === null ? '进度未知' : `${amount}%`)} data-indeterminate={amount === null}>
88
+ <span style={amount === null ? undefined : { transform: `scaleX(${amount / 100})` }} />
89
+ </div>
90
+ </div>;
91
+ }
92
+ export function Skeleton({ label = '正在加载', lines = 3 }: { label?: string; lines?: number }) {
93
+ return <div className="tod-skeleton" role="status"><span className="tod-sr-only">{label}</span>
94
+ {Array.from({ length: Math.max(1, Math.min(8, Math.floor(lines) || 1)) }, (_, index) => <span aria-hidden="true" key={index} />)}
95
+ </div>;
96
+ }
97
+ export function Disclosure({ title, children, defaultOpen = false }: { title: string; children: ReactNode; defaultOpen?: boolean }) {
98
+ const node = useRef<HTMLDetailsElement>(null);
99
+ const animation = useRef<Animation | null>(null);
100
+ const intent = useRef(defaultOpen);
101
+ const [open, setOpen] = useState(defaultOpen);
102
+ useEffect(() => {
103
+ const media = window.matchMedia?.('(prefers-reduced-motion: reduce)');
104
+ const settle = () => {
105
+ if (!media?.matches || !animation.current || !node.current) return;
106
+ animation.current.cancel(); animation.current = null;
107
+ node.current.open = intent.current; setOpen(intent.current);
108
+ };
109
+ media?.addEventListener('change', settle);
110
+ return () => { animation.current?.cancel(); media?.removeEventListener('change', settle); };
111
+ }, []);
112
+ useEffect(() => {
113
+ if (!animation.current || !node.current) return;
114
+ if (parseFloat(getComputedStyle(node.current).getPropertyValue('--tod-duration-notice')) !== 0) return;
115
+ animation.current.cancel(); animation.current = null;
116
+ node.current.open = intent.current; setOpen(intent.current);
117
+ });
118
+ function toggle() {
119
+ const el = node.current;
120
+ if (!el) return;
121
+ const from = el.getBoundingClientRect().height;
122
+ animation.current?.cancel();
123
+ intent.current = !intent.current;
124
+ const expanded = intent.current;
125
+ el.dataset.expanded = String(expanded);
126
+ const summary = el.querySelector('summary')!;
127
+ // Native details removes closed content from keyboard and accessibility navigation.
128
+ el.open = true;
129
+ if (!expanded && el.contains(document.activeElement)) summary.focus();
130
+ const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
131
+ if (reduced || !el.animate) { el.open = expanded; setOpen(expanded); return; }
132
+ const style = getComputedStyle(el);
133
+ const parsedDuration = parseFloat(style.getPropertyValue('--tod-duration-notice'));
134
+ const duration = Number.isFinite(parsedDuration) ? parsedDuration : 200;
135
+ if (duration === 0) { el.open = expanded; setOpen(expanded); return; }
136
+ const to = expanded ? el.getBoundingClientRect().height : summary.getBoundingClientRect().height + parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
137
+ const next = el.animate([{ height: `${from}px` }, { height: `${to}px` }], { duration, easing: style.getPropertyValue(expanded ? '--tod-ease-standard' : '--tod-ease-exit').trim() || 'ease' });
138
+ animation.current = next;
139
+ next.onfinish = () => {
140
+ if (animation.current !== next) return;
141
+ el.open = expanded; setOpen(expanded); animation.current = null;
142
+ };
143
+ }
144
+ return <details ref={node} open={open} data-expanded={open} className="tod-disclosure">
145
+ <summary onClick={event => { event.preventDefault(); toggle(); }}>{title}<span aria-hidden="true">+</span></summary>
146
+ <div className="tod-disclosure__content">{children}</div>
147
+ </details>;
148
+ }
149
+ export function EmptyState({ title, description, action }: { title: string; description: string; action?: ReactNode }) {
150
+ return <div className="tod-empty"><strong>{title}</strong><p>{description}</p>{action}</div>;
151
+ }
@@ -0,0 +1,24 @@
1
+ import './styles.css';
2
+
3
+ export { Logo } from './Logo';
4
+ export type { LogoProps } from './Logo';
5
+ export { Text } from './Typography';
6
+ export type { TextProps, TextVariant } from './Typography';
7
+ export { Select } from './Select';
8
+ export type { SelectProps } from './Select';
9
+ export { Toolbar } from './Toolbar';
10
+ export type { ToolbarProps } from './Toolbar';
11
+ export { Icon, IconButton, Link, CopyButton } from './Actions';
12
+ export type { IconName } from './Actions';
13
+ export { Divider, Stack, Inline, Grid } from './Layout';
14
+ export type { Spacing } from './Layout';
15
+ export { Metric, KeyValue, DataTable, Pagination } from './Data';
16
+ export type { DataColumn, DataTableProps, TableSort } from './Data';
17
+ export { Checkbox, RadioGroup, Switch, Textarea, SearchField, Combobox } from './Forms';
18
+ export type { ComboboxProps } from './Forms';
19
+ export { Dialog, ConfirmDialog, Popover, Menu, Tooltip } from './Overlays';
20
+ export type { DialogProps, MenuItem } from './Overlays';
21
+ export { Toast } from './Toast';
22
+
23
+ export { Button, Badge, Surface, SelectionCard, TextField, Tabs, SegmentedControl, Notice, Progress, Skeleton, Disclosure, EmptyState } from './components';
24
+ export type { ButtonProps, Tone, Choice, TabItem } from './components';
@@ -0,0 +1,109 @@
1
+ @import './tokens.css';
2
+ @import './typography.css';
3
+ @import './common.css';
4
+
5
+ .tod-theme { color:var(--tod-ink); background:var(--tod-bg); font:var(--tod-text-body)/var(--tod-leading-body) var(--tod-font); font-synthesis:none; }
6
+ .tod-theme *, .tod-theme *::before, .tod-theme *::after { box-sizing:border-box; }
7
+ .tod-theme [hidden] { display:none!important; }
8
+ .tod-theme :is(button,input,select,textarea,summary,a,[tabindex]):focus-visible { outline:2px solid var(--tod-green); outline-offset:3px; }
9
+ .tod-sr-only { position:absolute!important; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0; }
10
+ .tod-button { position:relative; display:inline-flex; align-items:center; justify-content:center; min-height:44px; min-width:44px; padding:10px 16px; border:1px solid var(--tod-ink); border-radius:var(--tod-radius-control); font:500 14px/22px var(--tod-font); background:var(--tod-ink); color:var(--tod-surface); cursor:pointer; overflow-wrap:anywhere; transition:background-color var(--tod-duration-hover) var(--tod-ease-standard),border-color var(--tod-duration-hover) var(--tod-ease-standard),transform var(--tod-duration-press) var(--tod-ease-standard); }
11
+ .tod-button:hover:not(:disabled):not([aria-disabled=true]) { background:var(--tod-green); border-color:var(--tod-green); }
12
+ .tod-button:active:not(:disabled):not([aria-disabled=true]) { transform:translateY(1px); }
13
+ .tod-button--secondary { background:var(--tod-surface); color:var(--tod-ink); border-color:var(--tod-control); }
14
+ .tod-button--quiet { background:transparent; color:var(--tod-green); border-color:transparent; }
15
+ .tod-button--secondary:hover:not(:disabled):not([aria-disabled=true]),.tod-button--quiet:hover:not(:disabled):not([aria-disabled=true]) { background:var(--tod-selected); }
16
+ .tod-button--danger { background:var(--tod-error); border-color:var(--tod-error); }
17
+ .tod-button--danger:hover:not(:disabled):not([aria-disabled=true]) { background:#81342e; border-color:#81342e; }
18
+ .tod-button:disabled { background:var(--tod-selected); border-color:var(--tod-line); color:var(--tod-muted); cursor:default; }
19
+ .tod-button[aria-busy=true] { cursor:wait; }
20
+ .tod-button__ghost { visibility:hidden; }
21
+ .tod-button__busy { position:absolute; inset:0; display:grid; place-items:center; }
22
+ .tod-spinner { width:18px; height:18px; border:2px solid currentColor; border-right-color:transparent; border-radius:50%; animation:tod-spin var(--tod-duration-loading) linear infinite; }
23
+ .tod-tone-neutral { --tod-tone:var(--tod-muted); --tod-tone-bg:var(--tod-selected); }
24
+ .tod-tone-success { --tod-tone:var(--tod-green); --tod-tone-bg:var(--tod-selected); }
25
+ .tod-tone-warning { --tod-tone:var(--tod-warning); --tod-tone-bg:var(--tod-warning-bg); }
26
+ .tod-tone-error { --tod-tone:var(--tod-error); --tod-tone-bg:var(--tod-error-bg); }
27
+ .tod-badge { display:inline-block; padding:2px 8px; border-radius:2px; background:var(--tod-tone-bg); color:var(--tod-tone); font:500 12px/20px var(--tod-font); }
28
+ .tod-surface { padding:24px; border:1px solid var(--tod-line); border-radius:var(--tod-radius-card); background:var(--tod-surface); min-width:0; }
29
+ .tod-selection { display:block; border:1px solid var(--tod-control); background:var(--tod-surface); border-radius:var(--tod-radius-card); padding:20px; cursor:pointer; min-width:0; transition:background-color var(--tod-duration-hover) var(--tod-ease-standard),border-color var(--tod-duration-hover) var(--tod-ease-standard),box-shadow var(--tod-duration-hover) var(--tod-ease-standard); }
30
+ .tod-selection:hover:not([data-disabled=true]) { border-color:var(--tod-green); background:#f4f7ef; }
31
+ .tod-selection[data-selected=true] { border-color:var(--tod-green); background:var(--tod-selected); box-shadow:inset 0 0 0 1px var(--tod-green); }
32
+ .tod-selection[data-disabled=true] { cursor:default; color:var(--tod-muted); background:var(--tod-bg); }
33
+ .tod-selection:focus-within { outline:2px solid var(--tod-green); outline-offset:3px; }
34
+ .tod-selection__heading { display:flex; align-items:center; justify-content:space-between; gap:16px; font-size:16px; overflow-wrap:anywhere; }
35
+ .tod-selection input { width:18px; height:18px; accent-color:var(--tod-green); flex-shrink:0; }
36
+ .tod-selection__content { display:block; margin-top:16px; overflow-wrap:anywhere; }
37
+ .tod-field { display:flex; flex-direction:column; gap:8px; min-width:0; }
38
+ .tod-field>label { font-weight:500; }
39
+ .tod-field input { width:100%; min-width:0; min-height:44px; padding:10px 12px; color:var(--tod-ink); background:var(--tod-surface); border:1px solid var(--tod-control); border-radius:var(--tod-radius-control); font:14px/22px var(--tod-font); transition:border-color var(--tod-duration-hover) var(--tod-ease-standard),background-color var(--tod-duration-hover) var(--tod-ease-standard); }
40
+ .tod-field input::placeholder { color:var(--tod-muted); }
41
+ .tod-field input[aria-invalid=true] { border-color:var(--tod-error); }
42
+ .tod-field input:disabled { background:var(--tod-selected); color:var(--tod-muted); }
43
+ .tod-field__hint,.tod-field__error { font-size:12px; line-height:18px; }
44
+ .tod-field__hint { color:var(--tod-muted); }
45
+ .tod-field__error { color:var(--tod-error); }
46
+ .tod-tabs__list { display:flex; flex-wrap:wrap; gap:24px; border-bottom:1px solid var(--tod-line); }
47
+ .tod-tabs__list button { position:relative; background:none; border:0; padding:12px 0; min-height:44px; color:var(--tod-muted); font:500 14px/22px var(--tod-font); cursor:pointer; transition:color var(--tod-duration-hover) var(--tod-ease-standard); }
48
+ .tod-tabs__list button::after { content:''; position:absolute; bottom:-1px; inset-inline:0; height:2px; background:var(--tod-green); transform:scaleX(0); transform-origin:left; transition:transform var(--tod-duration-panel) var(--tod-ease-standard); }
49
+ .tod-tabs__list button[aria-selected=true] { color:var(--tod-ink); }
50
+ .tod-tabs__list button[aria-selected=true]::after { transform:scaleX(1); }
51
+ .tod-tabs__list button:disabled { color:var(--tod-muted); opacity:.6; cursor:default; }
52
+ .tod-tabs__panel { padding-top:20px; animation:tod-enter var(--tod-duration-panel) var(--tod-ease-standard); }
53
+ .tod-segmented { display:inline-flex; flex-wrap:wrap; gap:3px; padding:3px; border:1px solid var(--tod-control); border-radius:var(--tod-radius-control); margin:0; min-width:0; }
54
+ .tod-segmented__option { position:relative; display:flex; flex:1 0 auto; cursor:pointer; }
55
+ .tod-segmented input { position:absolute; opacity:0; width:1px; height:1px; }
56
+ .tod-segmented__option span { display:flex; justify-content:center; align-items:center; min-height:40px; padding:8px 12px; color:var(--tod-muted); font-size:14px; transition:background-color var(--tod-duration-hover) var(--tod-ease-standard),color var(--tod-duration-hover) var(--tod-ease-standard); }
57
+ .tod-segmented input:checked+span { background:var(--tod-ink); color:var(--tod-surface); }
58
+ .tod-segmented input:focus-visible+span { outline:2px solid var(--tod-green); outline-offset:3px; }
59
+ .tod-segmented input:disabled+span { opacity:.6; cursor:default; }
60
+ .tod-notice { padding:16px; border-left:3px solid var(--tod-tone); background:var(--tod-tone-bg); color:var(--tod-tone); display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; animation:tod-enter var(--tod-duration-notice) var(--tod-ease-standard); }
61
+ .tod-notice>div { flex:1 1 200px; min-width:0; overflow-wrap:anywhere; }
62
+ .tod-notice strong { font-size:14px; font-weight:600; }
63
+ .tod-notice__body { margin-top:4px; font-size:12px; line-height:20px; }
64
+ .tod-progress__label { display:flex; justify-content:space-between; gap:12px; font-size:12px; margin-bottom:8px; }
65
+ .tod-progress__label strong { font-variant-numeric:tabular-nums; }
66
+ .tod-progress__track { height:6px; background:var(--tod-line); overflow:hidden; }
67
+ .tod-progress__track>span { display:block; height:100%; background:var(--tod-green); transform-origin:left; transition:transform var(--tod-duration-progress) var(--tod-ease-standard); }
68
+ .tod-progress__track[data-indeterminate=true]>span { width:30%; animation:tod-progress var(--tod-duration-loading) var(--tod-ease-standard) infinite; }
69
+ .tod-skeleton { position:relative; display:flex; flex-direction:column; gap:12px; }
70
+ .tod-skeleton>span[aria-hidden] { display:block; height:16px; background:var(--tod-line); animation:tod-pulse var(--tod-duration-skeleton) ease-in-out infinite; }
71
+ .tod-skeleton>span[aria-hidden]:last-child { width:65%; }
72
+ .tod-disclosure { border-top:1px solid var(--tod-line); border-bottom:1px solid var(--tod-line); overflow:hidden; }
73
+ .tod-disclosure summary { min-height:48px; padding:12px 0; list-style:none; cursor:pointer; display:flex; justify-content:space-between; align-items:center; gap:16px; font-weight:500; }
74
+ .tod-disclosure summary::-webkit-details-marker { display:none; }
75
+ .tod-theme .tod-disclosure summary:focus-visible { outline-offset:-3px; }
76
+ .tod-disclosure summary>span { font:20px var(--tod-mono); transition:transform var(--tod-duration-notice) var(--tod-ease-standard); }
77
+ .tod-disclosure[data-expanded=true] summary>span { transform:rotate(45deg); }
78
+ .tod-disclosure__content { padding:4px 0 20px; }
79
+ .tod-empty { padding:32px 24px; text-align:center; border:1px solid var(--tod-line); background:var(--tod-surface); }
80
+ .tod-empty strong { font-size:16px; }.tod-empty p { max-width:400px; margin:8px auto 20px; color:var(--tod-muted); }
81
+ @keyframes tod-spin { to { transform:rotate(360deg); } }
82
+ .tod-logo { display:inline-flex; flex:none; align-items:center; vertical-align:middle; color:var(--tod-green); line-height:1; }
83
+ .tod-logo__symbol { display:block; width:var(--tod-logo-size); height:var(--tod-logo-size); flex:none; fill:currentColor; }
84
+ .tod-logo--icon { width:var(--tod-logo-size); height:var(--tod-logo-size); border-radius:22%; background:var(--tod-bg); }
85
+ .tod-logo[data-theme=dark] { color:var(--tod-bg); }
86
+ .tod-logo--icon[data-theme=dark] { background:var(--tod-green); }
87
+ .tod-logo--lockup { gap:12px; }
88
+ .tod-logo__wordmark { font:600 20px/1 var(--tod-font); letter-spacing:.08em; white-space:nowrap; }
89
+ .tod-logo__plate--left { --tod-logo-x:calc(var(--tod-logo-travel) * -.5); --tod-logo-y:calc(var(--tod-logo-travel) * -.866); }
90
+ .tod-logo__plate--right { --tod-logo-x:calc(var(--tod-logo-travel) * .5); --tod-logo-y:calc(var(--tod-logo-travel) * -.866); }
91
+ .tod-logo__plate--base { --tod-logo-x:0px; --tod-logo-y:var(--tod-logo-travel); }
92
+ .tod-logo[data-animate=true] .tod-logo__plate { animation:tod-logo-enter var(--tod-duration-brand) var(--tod-ease-standard) both; }
93
+ @keyframes tod-logo-enter { from { opacity:0; transform:translate(var(--tod-logo-x),var(--tod-logo-y)); } to { opacity:1; transform:translate(0,0); } }
94
+ @media(prefers-color-scheme:dark) {
95
+ .tod-logo[data-theme=auto] { color:var(--tod-bg); }
96
+ .tod-logo--icon[data-theme=auto] { background:var(--tod-green); }
97
+ }
98
+ @media(prefers-reduced-motion:reduce) { .tod-logo[data-animate=true] .tod-logo__plate { animation:none; } }
99
+ @keyframes tod-enter { from { opacity:0; transform:translateY(var(--tod-motion-distance)); } to { opacity:1; transform:translateY(0); } }
100
+ @keyframes tod-progress { from { transform:translateX(-100%); } to { transform:translateX(340%); } }
101
+ @keyframes tod-pulse { 50% { opacity:.55; } }
102
+ .tod-theme[data-tod-motion=reduced] { --tod-duration-hover:0ms; --tod-duration-press:0ms; --tod-duration-panel:0ms; --tod-duration-notice:0ms; --tod-duration-progress:0ms; }
103
+ .tod-theme[data-tod-motion=reduced] *, .tod-theme[data-tod-motion=reduced] *::before, .tod-theme[data-tod-motion=reduced] *::after { animation:none!important; transition:none!important; }
104
+ .tod-theme[data-tod-motion=reduced] .tod-button:active { transform:none; }
105
+ @media(prefers-reduced-motion:reduce) {
106
+ .tod-theme { --tod-duration-hover:0ms; --tod-duration-press:0ms; --tod-duration-panel:0ms; --tod-duration-notice:0ms; --tod-duration-progress:0ms; }
107
+ .tod-theme *, .tod-theme *::before, .tod-theme *::after { animation:none!important; transition:none!important; }
108
+ .tod-theme .tod-button:active { transform:none; }
109
+ }
@@ -0,0 +1,58 @@
1
+ /* Canonical tokens for the component library and design previews. */
2
+ :root {
3
+ --tod-bg: #f2f1e9;
4
+ --tod-surface: #fcfcf8;
5
+ --tod-ink: #242b28;
6
+ --tod-muted: #59645d;
7
+ --tod-line: #c5ccc1;
8
+ --tod-control: #78857b;
9
+ --tod-selected: #e5ece3;
10
+ --tod-green: #425c4b;
11
+ --tod-signal: #c65c34;
12
+ --tod-warning: #8a472c;
13
+ --tod-error: #a13e35;
14
+ --tod-warning-bg: #f4e8dc;
15
+ --tod-error-bg: #f6e9e4;
16
+ --tod-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', sans-serif;
17
+ --tod-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
18
+ --tod-text-title: 30px;
19
+ --tod-leading-title: 38px;
20
+ --tod-text-section: 18px;
21
+ --tod-leading-section: 26px;
22
+ --tod-text-heading: 16px;
23
+ --tod-leading-heading: 24px;
24
+ --tod-text-body: 14px;
25
+ --tod-leading-body: 22px;
26
+ --tod-text-caption: 12px;
27
+ --tod-leading-caption: 18px;
28
+ --tod-text-metric: 26px;
29
+ --tod-leading-metric: 32px;
30
+ --tod-text-metric-compact: 20px;
31
+ --tod-leading-metric-compact: 26px;
32
+ --tod-text-code: 12px;
33
+ --tod-leading-code: 20px;
34
+ --tod-weight-regular: 400;
35
+ --tod-weight-label: 500;
36
+ --tod-weight-heading: 600;
37
+ --tod-space-1: 4px;
38
+ --tod-space-2: 8px;
39
+ --tod-space-3: 12px;
40
+ --tod-space-4: 16px;
41
+ --tod-space-6: 24px;
42
+ --tod-space-8: 32px;
43
+ --tod-space-12: 48px;
44
+ --tod-radius-card: 4px;
45
+ --tod-radius-control: 3px;
46
+ --tod-duration-hover: 120ms;
47
+ --tod-duration-press: 80ms;
48
+ --tod-duration-panel: 180ms;
49
+ --tod-duration-notice: 200ms;
50
+ --tod-duration-progress: 240ms;
51
+ --tod-duration-brand: 240ms;
52
+ --tod-duration-overlay: 160ms;
53
+ --tod-duration-loading: 1000ms;
54
+ --tod-duration-skeleton: 1400ms;
55
+ --tod-ease-standard: cubic-bezier(.2, 0, 0, 1);
56
+ --tod-ease-exit: cubic-bezier(.4, 0, 1, 1);
57
+ --tod-motion-distance: 3px;
58
+ }
@@ -0,0 +1,31 @@
1
+ .tod-text { margin:0; font-family:var(--tod-font); font-weight:var(--tod-weight-regular); letter-spacing:0; overflow-wrap:anywhere; }
2
+ .tod-text--title { font-size:var(--tod-text-title); line-height:var(--tod-leading-title); font-weight:var(--tod-weight-heading); }
3
+ .tod-text--section { font-size:var(--tod-text-section); line-height:var(--tod-leading-section); font-weight:var(--tod-weight-heading); }
4
+ .tod-text--heading { font-size:var(--tod-text-heading); line-height:var(--tod-leading-heading); font-weight:var(--tod-weight-heading); }
5
+ .tod-text--body,.tod-text--label { font-size:var(--tod-text-body); line-height:var(--tod-leading-body); }
6
+ .tod-text--label { font-weight:var(--tod-weight-label); }
7
+ .tod-text--caption,.tod-text--overline { font-size:var(--tod-text-caption); line-height:var(--tod-leading-caption); }
8
+ .tod-text--overline { font-weight:var(--tod-weight-label); }
9
+ .tod-text--overline:lang(en) { letter-spacing:.08em; text-transform:uppercase; }
10
+ .tod-text--metric { font-size:var(--tod-text-metric); line-height:var(--tod-leading-metric); font-weight:var(--tod-weight-heading); font-variant-numeric:tabular-nums lining-nums; }
11
+ .tod-text--code { font:var(--tod-text-code)/var(--tod-leading-code) var(--tod-mono); }
12
+ .tod-text--numeric { font-variant-numeric:tabular-nums lining-nums; }
13
+ .tod-text--default { color:inherit; }
14
+ .tod-text--muted { color:var(--tod-muted); }
15
+ .tod-text--success { color:var(--tod-green); }
16
+ .tod-text--warning { color:var(--tod-warning); }
17
+ .tod-text--error { color:var(--tod-error); }
18
+ .tod-select { position:relative; min-width:0; color:var(--tod-ink); }
19
+ .tod-select select { appearance:none; -webkit-appearance:none; display:block; width:100%; min-width:0; min-height:44px; padding:10px 40px 10px 12px; border:1px solid var(--tod-control); border-radius:var(--tod-radius-control); background:var(--tod-surface); color:var(--tod-ink); font:var(--tod-text-body)/var(--tod-leading-body) var(--tod-font); cursor:pointer; transition:border-color var(--tod-duration-hover) var(--tod-ease-standard),background-color var(--tod-duration-hover) var(--tod-ease-standard); }
20
+ .tod-select select:hover:not(:disabled) { border-color:var(--tod-green); }
21
+ .tod-select select[aria-invalid=true] { border-color:var(--tod-error); }
22
+ .tod-select select:disabled { color:var(--tod-muted); background:var(--tod-selected); cursor:default; }
23
+ .tod-select__arrow { position:absolute; width:16px; height:16px; right:12px; top:50%; transform:translateY(-50%); pointer-events:none; fill:none; stroke:currentColor; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; }
24
+ .tod-select select:disabled+.tod-select__arrow { color:var(--tod-muted); }
25
+ .tod-toolbar { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:20px 32px; padding:20px 24px; color:var(--tod-ink); background:var(--tod-surface); border-bottom:1px solid var(--tod-line); }
26
+ .tod-toolbar__intro { min-width:0; flex:1 1 220px; }
27
+ .tod-toolbar__intro>p { margin-top:4px; }
28
+ .tod-toolbar__controls { display:flex; flex-wrap:wrap; align-items:flex-start; gap:12px; flex:2 1 440px; min-width:0; }
29
+ .tod-toolbar__controls>.tod-field { flex:1 1 150px; min-width:0; }
30
+ .tod-toolbar .tod-field>label { font-size:var(--tod-text-caption); line-height:var(--tod-leading-caption); }
31
+ @media(forced-colors:active) { .tod-select select { appearance:auto; -webkit-appearance:auto; } .tod-select__arrow { display:none; } }
@@ -0,0 +1,14 @@
1
+ # 莱茵设计 · 设计模式索引
2
+
3
+ 本库维护跨产品视觉规范、组件状态和动效组合,具体产品页面稿由消费项目维护。
4
+
5
+ | 模式 | 规范与真实演示 |
6
+ | --- | --- |
7
+ | 视觉与文字 | [通用规范](../docs/DESIGN_SYSTEM.md)、[文字规范](../components/TYPOGRAPHY.md),/#foundation、/#typography |
8
+ | 选择与表单 | [组件契约](../components/COMMON_COMPONENTS.md),/#selection、/#forms |
9
+ | 数据与分页 | 同一组件契约,/#data |
10
+ | 反馈与浮层 | 同一组件契约,/#feedback、/#overlays |
11
+ | 动效过渡 | [动效规范](../components/MOTION.md),/#motion |
12
+ | 三角构架与启动衔接 | [启动编排](../components/STARTUP.md)、[SVG母版](../components/assets/logo.svg),/#brand、/#startup |
13
+
14
+ 在仓库根运行npm run dev,上述路径相对服务根(默认18309)。播放、重播、慢放与减少动态由真实展台提供,不执行授权或业务写入。新模式先组合已有组件,缺少通用能力时再扩展组件和契约。
@@ -0,0 +1,83 @@
1
+ # 莱茵设计 · 通用设计规范
2
+
3
+ 本规范是 Rhine Design 的设计基线,覆盖色彩、文字、组件状态、布局和动画,供不同产品复用。配套 React 实现为 @linhey/rhine-design;版本演进见[CHANGELOG](../CHANGELOG.md),组件契约见[组件入口](../components/README.md)。
4
+
5
+ ## 1. 设计语言
6
+
7
+ 暖白背景、苔绿结构、墨色文字、细线分隔和少量橙色定位形成安静、精密的秩序。三角几何用于品牌结构,装饰不穿过正文、不遮挡操作,不制造虚假仪表状态。参考作品仅提供风格方向;标志、参数和实现是独立设计,不构成官方VI。
8
+
9
+ ## 2. 通用原则
10
+
11
+ - 状态真实:未知不显示为零,操作完成前不宣称成功,刷新失败可保留旧数据并标明时效。
12
+ - 可读优先:系统字体、清晰层级、自然换行;200%缩放不丢失文字或操作。
13
+ - 交互连续:忙碌保留焦点,浮层有明确退出与焦点归还;快速重播和打断不留下透明遮挡。
14
+ - 组件优先:先更新规范、组件与真实展台,再由消费项目选择版本;业务流程不进入通用组件。
15
+ - 动效克制:反馈服务于状态理解,就绪不等待装饰,减少动态不削弱功能。
16
+
17
+ ## 3. 视觉变量
18
+
19
+
20
+ ### 3.1 色彩
21
+
22
+ | Token | 值 | 用途 |
23
+ | --- | --- | --- |
24
+ | `--tod-bg` | `#F2F1E9` | 窗口底色 |
25
+ | `--tod-surface` | `#FCFCF8` | 卡片、输入、浮层 |
26
+ | `--tod-ink` | `#242B28` | 正文、主按钮 |
27
+ | `--tod-muted` | `#59645D` | 次级文字 |
28
+ | `--tod-line` | `#C5CCC1` | 装饰分隔线,不独立承担控件识别 |
29
+ | `--tod-control` | `#78857B` | 必须可识别的控件边界 |
30
+ | `--tod-selected` | `#E5ECE3` | 选中背景 |
31
+ | `--tod-green` | `#425C4B` | 选中标记、成功文字、焦点环 |
32
+ | `--tod-signal` | `#C65C34` | 少量品牌定位色,不用于小字 |
33
+ | `--tod-warning` | `#8A472C` | 待处理、冲突、覆盖不完整 |
34
+ | `--tod-error` | `#A13E35` | 操作失败、需要恢复 |
35
+
36
+ 本稿测算:正文 / 底色 12.77:1,次级文字 / 底色 5.44:1;品牌橙 / 底色仅 3.72:1,因此只用于非文字标记或符合大字要求的展示文字。重要状态同时有文字。数据未知显示「— / 暂不可用」,不显示 0%,未知进度不画百分比。
37
+
38
+ ### 3.2 字体与密度
39
+
40
+ 完整规则见 [字体与文字规范](../components/TYPOGRAPHY.md);涵盖9档文字样式、中文混排、数字单位、换行截断、内容语义和动效边界。主流系统对照与待补能力见 [补齐顺序](./DESIGN_SYSTEM_AUDIT.md)。
41
+
42
+ 正文使用系统栈 `-apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Microsoft YaHei, sans-serif`;数字使用 `font-variant-numeric: tabular-nums`;路径与编号使用系统等宽栈。首期不下载 Web Font。
43
+
44
+ | 用途 | 字号 / 行高 | 字重 |
45
+ | --- | --- | --- |
46
+ | 登录标题 | 30 / 38px | 600 |
47
+ | 工作区分组标题 | 18 / 26px | 600 |
48
+ | 卡片身份 | 16 / 24px | 600 |
49
+ | 正文、主要操作 | 14 / 22px | 400–600 |
50
+ | 标签、时间、辅助说明 | 12 / 18px | 400–500 |
51
+ | 用量关键数字 | 26 / 32px;窄窗 20 / 26px | 600 |
52
+
53
+ 正常视图不出现小于 12px 的业务信息。英文短标签可使用 0.08em 字距;中文不刻意拉大字距。大写英文只作为少量层级辅助。
54
+
55
+ ### 3.3 几何与布局
56
+
57
+ - 间距序列 4/8/12/16/24/32/48px。窗口内容边距 24px;宽度低于 480 时 20px。
58
+ - 展台内容按容器重排;卡片最小宽度按信息密度设定,不能由某个产品窗口宽度推导所有宿主布局。状态区预留文字换行空间。
59
+ - 卡片圆角 4px,按钮 3px;保留原生窗口圆角,不在 WebView 内再画一层假窗口。
60
+ - 控件最小高度 40px,主要操作 44px;移动预览/触屏按至少 44px 目标校验。
61
+ - 1px 分隔线;选中卡以 2px 深绿边界与「已选择」文字共同表达。焦点环 2px,偏移 3px。
62
+ - 400/520/880px作为展台验收宽度;消费项目按其最小容器尺寸追加验证。
63
+ - 指标与表格按容器重排;数字和说明可换行,必要时表格局部滚动,不以超小字号塞入全部列。
64
+ - 100% 与 200% 字体缩放下允许纵向增长、重排;不固定卡片高度、不裁掉报错和主要操作。
65
+
66
+
67
+ ## 4. 组件与内容契约
68
+
69
+ 使用[40项组件契约](../components/README.md)及[常用组合](../components/COMMON_COMPONENTS.md)。表单标签持续可见,提示与错误关联;交互元素使用原生语义或可访问基础。选择、筛选、排序和分页向宿主回传意图,宿主决定数据来源及操作结果。
70
+
71
+ 错误说明发生了什么及可执行的下一步,避免内部堆栈和秘密;成功、警告、未知与空态有文字区分。通知不得依赖颜色或短暂动画才能理解。品牌、实体名、时区和业务单位由宿主定义,具体文案规则见[字体与文字](../components/TYPOGRAPHY.md)。
72
+
73
+ ## 5. 动效与品牌
74
+
75
+ 三角构架以同一SVG母版呈现;默认字标Rhine Design,可配置。几何、颜色和安全间距见[Logo契约](../components/README.md#品牌标志)。完整深色主题仍待补齐,Logo变色不代表全套主题完成。
76
+
77
+ 通用状态过渡以120–240ms为基线,按下80ms;循环只表示实际等待。具体配方、减少动态及候选边界见[动效规范](../components/MOTION.md)。[启动编排](../components/STARTUP.md)展示三块旋转压缩、展开归位以及同一标志收拢的连续性,不是业务初始化门槛。
78
+
79
+ ## 6. 独立迭代与验收
80
+
81
+ 在本仓运行npm run check,新增组件同步契约和默认/禁用/加载/错误/长文本/键盘/动效示例。展台浏览器验收400/520/880宽度、200%缩放、焦点、快速中断、重播和减少动态;独立消费构建验证CSS及SVG资源。
82
+
83
+ 变化记录在CHANGELOG并按语义版本提供迁移说明。消费项目显式选择版本,不随主线自动升级。库验收不依赖某个App迁移完成;真实业务、原生平台与辅助技术由宿主按适用范围补验。剩余能力见[系统对照](DESIGN_SYSTEM_AUDIT.md)。
@@ -0,0 +1,63 @@
1
+ # 莱茵设计 · 主流系统对照与补齐顺序
2
+
3
+ 调研日期:2026-10-22。对照[莱茵设计](../README.md)的`components/`与通用表单、数据展示、反馈和导航场景。以下优先级是本项目判断;引用用于说明参考系统的原则,不代表它们要求采用我们的尺寸、配色或组件清单。独立展台使用18309的`/#…`入口。
4
+
5
+ ## 参考与取舍
6
+
7
+ | 官方系统 | 关注点 | Rhine Design采用方式 |
8
+ | --- | --- | --- |
9
+ | [Material Foundations](https://m3.material.io/foundations/) | 把token、交互状态、布局、内容和无障碍作为基础层 | 补齐状态与文字规范;保持现有几何、苔绿和暖白,不引入整套Material外观 |
10
+ | [Fluent Typography](https://fluent2.microsoft.design/typography) | 平台字体与语义文字层级 | 系统字体、中文混排、字号行高token;不照搬其最小字号 |
11
+ | [Fluent Select](https://fluent2.microsoft.design/components/web/react/core/select/usage) | 区分原生选择、富选项Dropdown和可输入Combobox | 当前短列表使用原生Select,保留系统菜单和键盘能力 |
12
+ | [Carbon Overview](https://www.carbondesignsystem.com/getting-started/designing/overview) | 区分基础、组件、任务模式和数据可视化 | 用量统计、账号身份、同步状态属于业务组合,避免全部塞进基础组件 |
13
+ | [Carbon Data table](https://www.carbondesignsystem.com/building-blocks/core/components/data-table/guidelines) | 明确表头、工具栏、数据行和分页的职责 | 后续优先提取请求明细与分页;不预建电子表格、批量编辑或表格引擎 |
14
+ | [Radix Accessibility](https://www.radix-ui.com/primitives/docs/overview/accessibility) | 标签、键盘和焦点管理是组件契约 | 每个控件写清语义和焦点行为;复杂浮层出现时评估成熟基础实现,不手写只在鼠标下有效的弹层 |
15
+
16
+ ## 当前已经落地
17
+
18
+ 2026-10-22追加用户指定的[Transitions.dev](https://transitions.dev/)参考,具体UX、动效映射和已实现/候选边界见[MOTION.md](../components/MOTION.md)。随后按用户授权在`/#motion`实现10组可播放/重播、4倍慢放及减少动态对照;新动画限定在展台,组件数量仍为40,未迁移App。
19
+
20
+ V1.4共40个React组件。原16项为Button、Badge、Surface、SelectionCard、TextField、Tabs、SegmentedControl、Notice、Progress、Skeleton、Disclosure、EmptyState、Logo、Text、Select、Toolbar;用户进一步授权补齐常用组件,新增24项见[组件契约](../components/COMMON_COMPONENTS.md)。
21
+
22
+ V1.3新增Text/Select/Toolbar、9档文字样式和[字体/文案规范](../components/TYPOGRAPHY.md)。静态样稿工具栏复用同源CSS,修正箭头并移除App内重复品牌顶栏。已有Logo深浅版本不等于完整深色主题。
23
+
24
+ ## 还需要补什么
25
+
26
+ | 优先级 | 补充项 | 具体范围与验收 | 状态 |
27
+ | --- | --- | --- | --- |
28
+ | P0 | 字体、表单标签、选择器、工具栏 | 文字尺度、混排、数字、长文本、箭头留白;标签关联、原生键盘 | V1.3已实现 |
29
+ | P0 | 统一状态矩阵 | 默认/悬停/按下/焦点/禁用/选中/只读/加载/错误,按适用范围验收;信息不可只靠颜色 | 规则已补;完整逐组件辅助技术审计待做 |
30
+ | P1 | 数据展示组合 | Metric、KeyValue、DataTable、Pagination;未知/零、刷新加载/失败、窄窗局部滚动;请求明细业务组合由页面负责 | V1.4已实现,待业务接入 |
31
+ | P1 | 链接、功能图标、图标按钮 | 16/20px统一图标网格、明确名称、44px操作目标、禁用与focus;链接跳转和按钮动作区分 | V1.4已实现Link/Icon/IconButton/CopyButton |
32
+ | P1 | 表单Field公共结构 | 输入/Select统一label/hint/error,必填和只读;多字段失败时焦点与错误汇总 | 现有TextField/Select满足单字段;多字段流程出现后提取 |
33
+ | P1 | 完整主题语义 | 表面、文字、边界、选中、焦点、错误与图表在深色下成对校验 | 未实现完整深色模式;不能只反转颜色 |
34
+ | P1 | 焦点与原生平台验收 | WebView、VoiceOver/Narrator、纯键盘、纯文本缩放、系统高对比与字体差异 | 浏览器证据不能替代原生验证 |
35
+ | P2 | Dialog/ConfirmDialog/Popover/Tooltip/Menu | Escape、焦点进入/归还、遮罩、滚动、层级和减少动态一起处理 | V1.4采用Radix无样式基础并提供真实展台 |
36
+ | P2 | Toast与通知组合 | 短暂成功可自动消失;警告错误持久;hover/focus/隐藏暂停 | V1.4单条Toast已实现,不建全局队列 |
37
+ | P2 | 搜索、Checkbox/Radio/Switch、Textarea、Combobox | 原生表单习惯、受控值、IME保护;Switch不混同选择账号 | V1.4已实现;查询与过期响应仍由业务层处理 |
38
+ | P2 | Divider/Stack/Inline/Grid | 同源间距、自适应网格、换行与缩放 | V1.4已用于展台布局 |
39
+
40
+ 常用组件补齐后,下一步按用户授权先改页面设计稿,再将同一组件接入正式App。剩余明确缺口是完整深色主题、多字段错误汇总及原生辅助技术验收;不因Logo支持深色而标记整套主题完成。40个组件存在不等于正式业务页已迁移。
41
+
42
+ ## 状态与交互补充规范
43
+
44
+ | 状态/场景 | 设计与行为规则 |
45
+ | --- | --- |
46
+ | 默认、hover、press | 布局稳定;只有可操作项有交互反馈;hover不承载必要信息 |
47
+ | focus-visible | 即时可见,不能被overflow裁掉;顺序与阅读顺序一致,不用正tabindex |
48
+ | disabled与busy | 禁用不可操作并给出原因;异步busy保留焦点、防止重复操作,内容仍可识别 |
49
+ | selected | 文字/边界与ARIA状态同步;选择、正在浏览与已生效配置区分 |
50
+ | readonly | 值可选择/复制,解释不可编辑原因;不要伪装成disabled;未提供对应能力的控件不杜撰readonly |
51
+ | invalid | 与字段关联的错误说明,不抖动、不仅变红;提交失败时保留输入 |
52
+ | loading/empty/error | 首次加载、刷新旧数据、无记录、无权限、错误分开;刷新失败不把旧值变成0 |
53
+ | 长文本/缩放 | 允许换行,关键状态不截断,200%缩放不丢信息;400%重排作为进一步无障碍目标 |
54
+ | 浮层 | modal需限制焦点并归还触发点;非modal不要阻断页面;Escape只关闭最上层可关闭浮层 |
55
+ | 业务列表 | 搜索/筛选/排序/分页以服务端结果为准;不为UI便利拉取全量本地过滤 |
56
+
57
+ 关于逻辑标题、焦点和缩放的参考为 [Fluent Accessibility](https://fluent2.microsoft.design/accessibility)。本项目仍需独立验证,不能继承参考库的合规结论。
58
+
59
+ ## 消融与范围
60
+
61
+ - 不增加侧栏、面包屑、命令面板、日期选择器、拖拽、富文本编辑器:现有两个主功能无对应任务入口。
62
+ - 不引入动画框架、主题生成器或整体UI库。V1.4用户已授权完整浮层能力,采用Radix的5个无样式primitive包处理焦点/键盘/边缘避让;视觉和动效仍使用Rhine Design CSS。
63
+ - 不把每种视觉模式封装成组件:先出现稳定复用,再抽取;业务数据契约与服务端状态继续保留在调用方。