@linhey/rhine-design 0.0.0-stage → 0.3.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.
- package/CHANGELOG.md +22 -0
- package/CONTRIBUTING.md +50 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +149 -2
- package/components/Actions.tsx +40 -0
- package/components/COMMON_COMPONENTS.md +70 -0
- package/components/Data.tsx +42 -0
- package/components/Forms.tsx +64 -0
- package/components/Layout.tsx +13 -0
- package/components/Logo.tsx +34 -0
- package/components/MOTION.md +72 -0
- package/components/Overlays.tsx +53 -0
- package/components/README.md +137 -0
- package/components/STARTUP.md +34 -0
- package/components/Select.tsx +22 -0
- package/components/TYPOGRAPHY.md +60 -0
- package/components/Toast.tsx +25 -0
- package/components/Toolbar.tsx +14 -0
- package/components/Typography.tsx +13 -0
- package/components/assets/logo.svg +5 -0
- package/components/common.css +69 -0
- package/components/components.tsx +151 -0
- package/components/index.ts +24 -0
- package/components/styles.css +109 -0
- package/components/tokens.css +58 -0
- package/components/typography.css +31 -0
- package/design/README.md +14 -0
- package/docs/DESIGN_SYSTEM.md +84 -0
- package/docs/DESIGN_SYSTEM_AUDIT.md +63 -0
- package/docs/RELEASING.md +27 -0
- package/index.html +3 -0
- package/package.json +76 -4
- package/preview/CommonGallery.tsx +35 -0
- package/preview/MotionGallery.tsx +89 -0
- package/preview/StartupGallery.tsx +73 -0
- package/preview/brand-assembly.css +18 -0
- package/preview/main.tsx +100 -0
- package/preview/motion.css +25 -0
- package/preview/preview.css +27 -0
- package/preview/startup.css +33 -0
- package/scripts/check-build.mjs +15 -0
- package/skills/rhine-design/SKILL.md +37 -0
- package/skills/rhine-design/agents/openai.yaml +4 -0
- package/skills/rhine-design/references/API_MAP.md +34 -0
- package/skills/rhine-design/references/TASK_MAP.md +20 -0
- package/skills/rhine-design/references/integration.md +40 -0
- package/tests/CommonComponents.test.tsx +71 -0
- package/tests/DesignSystem.test.tsx +100 -0
- package/tests/MotionGallery.test.tsx +32 -0
- package/tests/StartupGallery.test.tsx +55 -0
- package/tests/setup.ts +4 -0
- package/tsconfig.json +32 -0
- package/vite.config.ts +3 -0
- package/vitest.config.ts +3 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
.rhine-icon { display:inline-block; vertical-align:middle; flex:none; fill:none; stroke:currentColor; stroke-width:1.5; stroke-linecap:round; stroke-linejoin:round; }
|
|
2
|
+
.rhine-icon-button { padding:10px; width:44px; height:44px; flex:none; }
|
|
3
|
+
.rhine-link { color:var(--rhine-green); text-decoration:underline; text-underline-offset:3px; overflow-wrap:anywhere; }
|
|
4
|
+
.rhine-link:hover { text-decoration-thickness:2px; }
|
|
5
|
+
.rhine-link>.rhine-icon { margin-left:4px; }
|
|
6
|
+
.rhine-copy { display:inline-flex; align-items:center; gap:12px; flex-wrap:wrap; }
|
|
7
|
+
.rhine-copy>[role] { font-size:var(--rhine-text-caption); }
|
|
8
|
+
.rhine-stack { display:flex; flex-direction:column; min-width:0; }
|
|
9
|
+
.rhine-inline { display:flex; align-items:center; flex-wrap:wrap; gap:var(--rhine-space-3); min-width:0; }
|
|
10
|
+
.rhine-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--rhine-grid-min)),1fr)); min-width:0; }
|
|
11
|
+
.rhine-grid>*,.rhine-stack>* { min-width:0; }
|
|
12
|
+
.rhine-divider { border:0; border-top:1px solid var(--rhine-line); margin:0; width:100%; }
|
|
13
|
+
.rhine-metric { min-width:0; display:grid; gap:8px; align-content:start; }
|
|
14
|
+
.rhine-metric__unit { font-size:var(--rhine-text-caption); font-weight:400; color:var(--rhine-muted); }
|
|
15
|
+
.rhine-key-value { margin:0; display:grid; gap:12px; }
|
|
16
|
+
.rhine-key-value>div { display:grid; grid-template-columns:minmax(80px,1fr) minmax(0,2fr); gap:16px; }
|
|
17
|
+
.rhine-key-value dt { color:var(--rhine-muted); font-size:12px; }.rhine-key-value dd { margin:0; overflow-wrap:anywhere; }
|
|
18
|
+
.rhine-data { display:grid; gap:12px; min-width:0; }
|
|
19
|
+
.rhine-table-scroll { overflow:auto; max-width:100%; }
|
|
20
|
+
.rhine-table { width:100%; min-width:500px; border-collapse:collapse; text-align:left; font-size:12px; }
|
|
21
|
+
.rhine-table caption { text-align:left; font-size:16px; font-weight:600; margin-bottom:16px; }
|
|
22
|
+
.rhine-table th,.rhine-table td { padding:12px; border-bottom:1px solid var(--rhine-line); max-width:240px; overflow-wrap:anywhere; }
|
|
23
|
+
.rhine-table th { color:var(--rhine-muted); font-weight:500; }.rhine-table td { font-variant-numeric:tabular-nums; }
|
|
24
|
+
.rhine-table [data-align=right] { text-align:right; }.rhine-table tbody tr:hover { background:var(--rhine-selected); }
|
|
25
|
+
.rhine-table th button { font:inherit; color:inherit; background:none; border:0; min-height:44px; cursor:pointer; padding:0; }
|
|
26
|
+
.rhine-table th button:disabled { cursor:default; }
|
|
27
|
+
.rhine-pagination { display:flex; flex-wrap:wrap; justify-content:flex-end; align-items:center; gap:12px; }
|
|
28
|
+
.rhine-check>label,.rhine-radio-group>label { display:flex; align-items:center; gap:10px; min-height:44px; cursor:pointer; overflow-wrap:anywhere; }
|
|
29
|
+
.rhine-check input,.rhine-radio-group input { width:18px; height:18px; accent-color:var(--rhine-green); flex:none; margin:0; }
|
|
30
|
+
.rhine-check input:disabled+span,.rhine-radio-group input:disabled+span,.rhine-radio-group:disabled span { color:var(--rhine-muted); cursor:default; }
|
|
31
|
+
.rhine-radio-group { border:0; padding:0; margin:0; min-width:0; }.rhine-radio-group legend { font-weight:500; padding:0; }
|
|
32
|
+
.rhine-switch label { display:flex; position:relative; align-items:center; gap:12px; min-height:44px; cursor:pointer; }
|
|
33
|
+
.rhine-switch input { position:absolute; opacity:0; width:1px; height:1px; }
|
|
34
|
+
.rhine-switch__track { display:inline-flex; width:36px; height:22px; border:1px solid var(--rhine-control); border-radius:12px; padding:3px; flex:none; background:var(--rhine-surface); transition:background-color var(--rhine-duration-hover) var(--rhine-ease-standard); }
|
|
35
|
+
.rhine-switch__track>span { width:14px; height:14px; border-radius:50%; background:var(--rhine-muted); transition:transform var(--rhine-duration-hover) var(--rhine-ease-standard); }
|
|
36
|
+
.rhine-switch input:checked+.rhine-switch__track { background:var(--rhine-green); border-color:var(--rhine-green); }
|
|
37
|
+
.rhine-switch input:checked+.rhine-switch__track>span { transform:translateX(14px); background:var(--rhine-surface); }
|
|
38
|
+
.rhine-switch input:focus-visible+.rhine-switch__track { outline:2px solid var(--rhine-green); outline-offset:3px; }
|
|
39
|
+
.rhine-switch input:disabled~span { opacity:.6; cursor:default; }
|
|
40
|
+
.rhine-field textarea { font:var(--rhine-text-body)/var(--rhine-leading-body) var(--rhine-font); color:var(--rhine-ink); background:var(--rhine-surface); border:1px solid var(--rhine-control); border-radius:var(--rhine-radius-control); padding:10px 12px; min-height:88px; resize:vertical; width:100%; transition:border-color var(--rhine-duration-hover) var(--rhine-ease-standard); }
|
|
41
|
+
.rhine-field textarea[aria-invalid=true] { border-color:var(--rhine-error); }.rhine-field textarea:disabled { color:var(--rhine-muted); background:var(--rhine-selected); }
|
|
42
|
+
.rhine-search>.rhine-inline>input { flex:1 1 140px; width:auto; }.rhine-search input::-webkit-search-cancel-button { appearance:none; }
|
|
43
|
+
.rhine-combobox { position:relative; }
|
|
44
|
+
.rhine-combobox__input { position:relative; }.rhine-combobox__input input { padding-right:40px; }
|
|
45
|
+
.rhine-combobox__arrow { position:absolute; right:12px; top:50%; transform:translateY(-50%); pointer-events:none; }
|
|
46
|
+
.rhine-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(--rhine-surface); color:var(--rhine-ink); border:1px solid var(--rhine-control); border-radius:4px; box-shadow:0 8px 24px #242b2818; }
|
|
47
|
+
.rhine-combobox__list [role=option],.rhine-combobox__empty { padding:12px; min-height:44px; cursor:pointer; overflow-wrap:anywhere; }
|
|
48
|
+
.rhine-combobox__list [role=option][data-active=true],.rhine-combobox__list [role=option][aria-selected=true] { background:var(--rhine-selected); }
|
|
49
|
+
.rhine-combobox__list [role=option][aria-disabled=true],.rhine-combobox__empty { color:var(--rhine-muted); cursor:default; }
|
|
50
|
+
.rhine-overlay-anchor { display:inline-block; vertical-align:middle; max-width:100%; }
|
|
51
|
+
.rhine-overlay { position:fixed; inset:0; background:#242b2870; z-index:40; }
|
|
52
|
+
.rhine-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(--rhine-line); border-radius:4px; background:var(--rhine-surface); color:var(--rhine-ink); box-shadow:0 16px 48px #242b2826; }
|
|
53
|
+
.rhine-dialog>p { margin-top:12px; }.rhine-dialog>.rhine-notice { margin-top:16px; }
|
|
54
|
+
.rhine-dialog__head { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
|
55
|
+
.rhine-dialog__head>:first-child { min-width:0; overflow-wrap:anywhere; }
|
|
56
|
+
.rhine-dialog__body { margin-top:24px; }.rhine-dialog__actions { display:flex; justify-content:flex-end; flex-wrap:wrap; gap:12px; margin-top:24px; }
|
|
57
|
+
.rhine-popover,.rhine-menu { z-index:50; background:var(--rhine-surface); color:var(--rhine-ink); border:1px solid var(--rhine-control); border-radius:4px; box-shadow:0 8px 24px #242b2820; max-width:calc(100vw - 24px); overflow:auto; }
|
|
58
|
+
.rhine-popover { width:300px; padding:16px; max-height:var(--radix-popover-content-available-height); }
|
|
59
|
+
.rhine-menu { min-width:180px; max-height:var(--radix-dropdown-menu-content-available-height); padding:4px; }
|
|
60
|
+
.rhine-menu__item { min-height:44px; padding:10px 12px; cursor:pointer; outline:none; overflow-wrap:anywhere; }
|
|
61
|
+
.rhine-menu__item[data-highlighted] { background:var(--rhine-selected); box-shadow:inset 0 0 0 2px var(--rhine-green); }
|
|
62
|
+
.rhine-menu__item[data-disabled] { color:var(--rhine-muted); cursor:default; }.rhine-menu__item[data-danger=true] { color:var(--rhine-error); }
|
|
63
|
+
.rhine-tooltip { z-index:60; background:var(--rhine-ink); color:var(--rhine-surface); padding:8px 12px; border-radius:3px; max-width:min(280px,calc(100vw - 24px)); font-size:12px; line-height:18px; overflow-wrap:anywhere; }
|
|
64
|
+
.rhine-toast-region { position:relative; }.rhine-toast { display:flex; align-items:start; gap:12px; justify-content:space-between; padding:16px; background:var(--rhine-tone-bg); border-left:3px solid var(--rhine-tone); color:var(--rhine-ink); animation:rhine-enter var(--rhine-duration-panel) var(--rhine-ease-standard); }
|
|
65
|
+
.rhine-toast p { margin:4px 0 0; overflow-wrap:anywhere; }.rhine-toast strong { overflow-wrap:anywhere; }.rhine-toast>div { min-width:0; }
|
|
66
|
+
.rhine-dialog[data-state=open],.rhine-overlay[data-state=open] { animation:rhine-overlay-fade var(--rhine-duration-overlay) var(--rhine-ease-standard); }
|
|
67
|
+
.rhine-popover[data-state=open],.rhine-menu[data-state=open],.rhine-combobox__list[data-state=open],.rhine-tooltip[data-state=delayed-open],.rhine-tooltip[data-state=instant-open] { animation:rhine-enter var(--rhine-duration-overlay) var(--rhine-ease-standard); }
|
|
68
|
+
@keyframes rhine-overlay-fade { from { opacity:0; } to { opacity:1; } }
|
|
69
|
+
@media(forced-colors:active) { .rhine-switch__track { border-color:ButtonText; }.rhine-switch input:checked+.rhine-switch__track { background:Highlight; }.rhine-switch__track>span { background:ButtonText; }.rhine-switch input:checked+.rhine-switch__track>span { background:HighlightText; }.rhine-dialog,.rhine-tooltip,.rhine-menu,.rhine-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('rhine-button', `rhine-button--${variant}`, className)}
|
|
14
|
+
onClick={event => { if (busy) { event.preventDefault(); return; } onClick?.(event); }}>
|
|
15
|
+
<span className={busy ? 'rhine-button__ghost' : undefined}>{children}</span>
|
|
16
|
+
{busy && <span className="rhine-button__busy"><span className="rhine-spinner" aria-hidden="true" /><span className="rhine-sr-only">{busyLabel}</span></span>}
|
|
17
|
+
</button>;
|
|
18
|
+
}
|
|
19
|
+
export function Badge({ tone = 'neutral', children }: { tone?: Tone; children: ReactNode }) {
|
|
20
|
+
return <span className={`rhine-badge rhine-tone-${tone}`}>{children}</span>;
|
|
21
|
+
}
|
|
22
|
+
export function Surface({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
23
|
+
return <div {...props} className={classes('rhine-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="rhine-selection" data-selected={selected} data-disabled={disabled}>
|
|
29
|
+
<span className="rhine-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="rhine-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('rhine-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="rhine-field__hint">{hint}</span>}
|
|
41
|
+
{error && <span id={`${id}-error`} className="rhine-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="rhine-tabs">
|
|
50
|
+
<div role="tablist" aria-label={label} className="rhine-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="rhine-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="rhine-segmented"><legend className="rhine-sr-only">{label}</legend>
|
|
70
|
+
{items.map(item => <label key={item.value} className="rhine-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={`rhine-notice rhine-tone-${tone}`} role={tone === 'error' ? 'alert' : 'status'}>
|
|
78
|
+
<div><strong>{title}</strong>{children && <div className="rhine-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="rhine-progress">
|
|
85
|
+
<div className="rhine-progress__label"><span>{label}</span><strong>{valueText ?? (amount === null ? '处理中' : `${amount}%`)}</strong></div>
|
|
86
|
+
<div className="rhine-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="rhine-skeleton" role="status"><span className="rhine-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('--rhine-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('--rhine-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 ? '--rhine-ease-standard' : '--rhine-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="rhine-disclosure">
|
|
145
|
+
<summary onClick={event => { event.preventDefault(); toggle(); }}>{title}<span aria-hidden="true">+</span></summary>
|
|
146
|
+
<div className="rhine-disclosure__content">{children}</div>
|
|
147
|
+
</details>;
|
|
148
|
+
}
|
|
149
|
+
export function EmptyState({ title, description, action }: { title: string; description: string; action?: ReactNode }) {
|
|
150
|
+
return <div className="rhine-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
|
+
.rhine-theme { color:var(--rhine-ink); background:var(--rhine-bg); font:var(--rhine-text-body)/var(--rhine-leading-body) var(--rhine-font); font-synthesis:none; }
|
|
6
|
+
.rhine-theme *, .rhine-theme *::before, .rhine-theme *::after { box-sizing:border-box; }
|
|
7
|
+
.rhine-theme [hidden] { display:none!important; }
|
|
8
|
+
.rhine-theme :is(button,input,select,textarea,summary,a,[tabindex]):focus-visible { outline:2px solid var(--rhine-green); outline-offset:3px; }
|
|
9
|
+
.rhine-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
|
+
.rhine-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(--rhine-ink); border-radius:var(--rhine-radius-control); font:500 14px/22px var(--rhine-font); background:var(--rhine-ink); color:var(--rhine-surface); cursor:pointer; overflow-wrap:anywhere; transition:background-color var(--rhine-duration-hover) var(--rhine-ease-standard),border-color var(--rhine-duration-hover) var(--rhine-ease-standard),transform var(--rhine-duration-press) var(--rhine-ease-standard); }
|
|
11
|
+
.rhine-button:hover:not(:disabled):not([aria-disabled=true]) { background:var(--rhine-green); border-color:var(--rhine-green); }
|
|
12
|
+
.rhine-button:active:not(:disabled):not([aria-disabled=true]) { transform:translateY(1px); }
|
|
13
|
+
.rhine-button--secondary { background:var(--rhine-surface); color:var(--rhine-ink); border-color:var(--rhine-control); }
|
|
14
|
+
.rhine-button--quiet { background:transparent; color:var(--rhine-green); border-color:transparent; }
|
|
15
|
+
.rhine-button--secondary:hover:not(:disabled):not([aria-disabled=true]),.rhine-button--quiet:hover:not(:disabled):not([aria-disabled=true]) { background:var(--rhine-selected); }
|
|
16
|
+
.rhine-button--danger { background:var(--rhine-error); border-color:var(--rhine-error); }
|
|
17
|
+
.rhine-button--danger:hover:not(:disabled):not([aria-disabled=true]) { background:#81342e; border-color:#81342e; }
|
|
18
|
+
.rhine-button:disabled { background:var(--rhine-selected); border-color:var(--rhine-line); color:var(--rhine-muted); cursor:default; }
|
|
19
|
+
.rhine-button[aria-busy=true] { cursor:wait; }
|
|
20
|
+
.rhine-button__ghost { visibility:hidden; }
|
|
21
|
+
.rhine-button__busy { position:absolute; inset:0; display:grid; place-items:center; }
|
|
22
|
+
.rhine-spinner { width:18px; height:18px; border:2px solid currentColor; border-right-color:transparent; border-radius:50%; animation:rhine-spin var(--rhine-duration-loading) linear infinite; }
|
|
23
|
+
.rhine-tone-neutral { --rhine-tone:var(--rhine-muted); --rhine-tone-bg:var(--rhine-selected); }
|
|
24
|
+
.rhine-tone-success { --rhine-tone:var(--rhine-green); --rhine-tone-bg:var(--rhine-selected); }
|
|
25
|
+
.rhine-tone-warning { --rhine-tone:var(--rhine-warning); --rhine-tone-bg:var(--rhine-warning-bg); }
|
|
26
|
+
.rhine-tone-error { --rhine-tone:var(--rhine-error); --rhine-tone-bg:var(--rhine-error-bg); }
|
|
27
|
+
.rhine-badge { display:inline-block; padding:2px 8px; border-radius:2px; background:var(--rhine-tone-bg); color:var(--rhine-tone); font:500 12px/20px var(--rhine-font); }
|
|
28
|
+
.rhine-surface { padding:24px; border:1px solid var(--rhine-line); border-radius:var(--rhine-radius-card); background:var(--rhine-surface); min-width:0; }
|
|
29
|
+
.rhine-selection { display:block; border:1px solid var(--rhine-control); background:var(--rhine-surface); border-radius:var(--rhine-radius-card); padding:20px; cursor:pointer; min-width:0; transition:background-color var(--rhine-duration-hover) var(--rhine-ease-standard),border-color var(--rhine-duration-hover) var(--rhine-ease-standard),box-shadow var(--rhine-duration-hover) var(--rhine-ease-standard); }
|
|
30
|
+
.rhine-selection:hover:not([data-disabled=true]) { border-color:var(--rhine-green); background:#f4f7ef; }
|
|
31
|
+
.rhine-selection[data-selected=true] { border-color:var(--rhine-green); background:var(--rhine-selected); box-shadow:inset 0 0 0 1px var(--rhine-green); }
|
|
32
|
+
.rhine-selection[data-disabled=true] { cursor:default; color:var(--rhine-muted); background:var(--rhine-bg); }
|
|
33
|
+
.rhine-selection:focus-within { outline:2px solid var(--rhine-green); outline-offset:3px; }
|
|
34
|
+
.rhine-selection__heading { display:flex; align-items:center; justify-content:space-between; gap:16px; font-size:16px; overflow-wrap:anywhere; }
|
|
35
|
+
.rhine-selection input { width:18px; height:18px; accent-color:var(--rhine-green); flex-shrink:0; }
|
|
36
|
+
.rhine-selection__content { display:block; margin-top:16px; overflow-wrap:anywhere; }
|
|
37
|
+
.rhine-field { display:flex; flex-direction:column; gap:8px; min-width:0; }
|
|
38
|
+
.rhine-field>label { font-weight:500; }
|
|
39
|
+
.rhine-field input { width:100%; min-width:0; min-height:44px; padding:10px 12px; color:var(--rhine-ink); background:var(--rhine-surface); border:1px solid var(--rhine-control); border-radius:var(--rhine-radius-control); font:14px/22px var(--rhine-font); transition:border-color var(--rhine-duration-hover) var(--rhine-ease-standard),background-color var(--rhine-duration-hover) var(--rhine-ease-standard); }
|
|
40
|
+
.rhine-field input::placeholder { color:var(--rhine-muted); }
|
|
41
|
+
.rhine-field input[aria-invalid=true] { border-color:var(--rhine-error); }
|
|
42
|
+
.rhine-field input:disabled { background:var(--rhine-selected); color:var(--rhine-muted); }
|
|
43
|
+
.rhine-field__hint,.rhine-field__error { font-size:12px; line-height:18px; }
|
|
44
|
+
.rhine-field__hint { color:var(--rhine-muted); }
|
|
45
|
+
.rhine-field__error { color:var(--rhine-error); }
|
|
46
|
+
.rhine-tabs__list { display:flex; flex-wrap:wrap; gap:24px; border-bottom:1px solid var(--rhine-line); }
|
|
47
|
+
.rhine-tabs__list button { position:relative; background:none; border:0; padding:12px 0; min-height:44px; color:var(--rhine-muted); font:500 14px/22px var(--rhine-font); cursor:pointer; transition:color var(--rhine-duration-hover) var(--rhine-ease-standard); }
|
|
48
|
+
.rhine-tabs__list button::after { content:''; position:absolute; bottom:-1px; inset-inline:0; height:2px; background:var(--rhine-green); transform:scaleX(0); transform-origin:left; transition:transform var(--rhine-duration-panel) var(--rhine-ease-standard); }
|
|
49
|
+
.rhine-tabs__list button[aria-selected=true] { color:var(--rhine-ink); }
|
|
50
|
+
.rhine-tabs__list button[aria-selected=true]::after { transform:scaleX(1); }
|
|
51
|
+
.rhine-tabs__list button:disabled { color:var(--rhine-muted); opacity:.6; cursor:default; }
|
|
52
|
+
.rhine-tabs__panel { padding-top:20px; animation:rhine-enter var(--rhine-duration-panel) var(--rhine-ease-standard); }
|
|
53
|
+
.rhine-segmented { display:inline-flex; flex-wrap:wrap; gap:3px; padding:3px; border:1px solid var(--rhine-control); border-radius:var(--rhine-radius-control); margin:0; min-width:0; }
|
|
54
|
+
.rhine-segmented__option { position:relative; display:flex; flex:1 0 auto; cursor:pointer; }
|
|
55
|
+
.rhine-segmented input { position:absolute; opacity:0; width:1px; height:1px; }
|
|
56
|
+
.rhine-segmented__option span { display:flex; justify-content:center; align-items:center; min-height:40px; padding:8px 12px; color:var(--rhine-muted); font-size:14px; transition:background-color var(--rhine-duration-hover) var(--rhine-ease-standard),color var(--rhine-duration-hover) var(--rhine-ease-standard); }
|
|
57
|
+
.rhine-segmented input:checked+span { background:var(--rhine-ink); color:var(--rhine-surface); }
|
|
58
|
+
.rhine-segmented input:focus-visible+span { outline:2px solid var(--rhine-green); outline-offset:3px; }
|
|
59
|
+
.rhine-segmented input:disabled+span { opacity:.6; cursor:default; }
|
|
60
|
+
.rhine-notice { padding:16px; border-left:3px solid var(--rhine-tone); background:var(--rhine-tone-bg); color:var(--rhine-tone); display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; animation:rhine-enter var(--rhine-duration-notice) var(--rhine-ease-standard); }
|
|
61
|
+
.rhine-notice>div { flex:1 1 200px; min-width:0; overflow-wrap:anywhere; }
|
|
62
|
+
.rhine-notice strong { font-size:14px; font-weight:600; }
|
|
63
|
+
.rhine-notice__body { margin-top:4px; font-size:12px; line-height:20px; }
|
|
64
|
+
.rhine-progress__label { display:flex; justify-content:space-between; gap:12px; font-size:12px; margin-bottom:8px; }
|
|
65
|
+
.rhine-progress__label strong { font-variant-numeric:tabular-nums; }
|
|
66
|
+
.rhine-progress__track { height:6px; background:var(--rhine-line); overflow:hidden; }
|
|
67
|
+
.rhine-progress__track>span { display:block; height:100%; background:var(--rhine-green); transform-origin:left; transition:transform var(--rhine-duration-progress) var(--rhine-ease-standard); }
|
|
68
|
+
.rhine-progress__track[data-indeterminate=true]>span { width:30%; animation:rhine-progress var(--rhine-duration-loading) var(--rhine-ease-standard) infinite; }
|
|
69
|
+
.rhine-skeleton { position:relative; display:flex; flex-direction:column; gap:12px; }
|
|
70
|
+
.rhine-skeleton>span[aria-hidden] { display:block; height:16px; background:var(--rhine-line); animation:rhine-pulse var(--rhine-duration-skeleton) ease-in-out infinite; }
|
|
71
|
+
.rhine-skeleton>span[aria-hidden]:last-child { width:65%; }
|
|
72
|
+
.rhine-disclosure { border-top:1px solid var(--rhine-line); border-bottom:1px solid var(--rhine-line); overflow:hidden; }
|
|
73
|
+
.rhine-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
|
+
.rhine-disclosure summary::-webkit-details-marker { display:none; }
|
|
75
|
+
.rhine-theme .rhine-disclosure summary:focus-visible { outline-offset:-3px; }
|
|
76
|
+
.rhine-disclosure summary>span { font:20px var(--rhine-mono); transition:transform var(--rhine-duration-notice) var(--rhine-ease-standard); }
|
|
77
|
+
.rhine-disclosure[data-expanded=true] summary>span { transform:rotate(45deg); }
|
|
78
|
+
.rhine-disclosure__content { padding:4px 0 20px; }
|
|
79
|
+
.rhine-empty { padding:32px 24px; text-align:center; border:1px solid var(--rhine-line); background:var(--rhine-surface); }
|
|
80
|
+
.rhine-empty strong { font-size:16px; }.rhine-empty p { max-width:400px; margin:8px auto 20px; color:var(--rhine-muted); }
|
|
81
|
+
@keyframes rhine-spin { to { transform:rotate(360deg); } }
|
|
82
|
+
.rhine-logo { display:inline-flex; flex:none; align-items:center; vertical-align:middle; color:var(--rhine-green); line-height:1; }
|
|
83
|
+
.rhine-logo__symbol { display:block; width:var(--rhine-logo-size); height:var(--rhine-logo-size); flex:none; fill:currentColor; }
|
|
84
|
+
.rhine-logo--icon { width:var(--rhine-logo-size); height:var(--rhine-logo-size); border-radius:22%; background:var(--rhine-bg); }
|
|
85
|
+
.rhine-logo[data-theme=dark] { color:var(--rhine-bg); }
|
|
86
|
+
.rhine-logo--icon[data-theme=dark] { background:var(--rhine-green); }
|
|
87
|
+
.rhine-logo--lockup { gap:12px; }
|
|
88
|
+
.rhine-logo__wordmark { font:600 20px/1 var(--rhine-font); letter-spacing:.08em; white-space:nowrap; }
|
|
89
|
+
.rhine-logo__plate--left { --rhine-logo-x:calc(var(--rhine-logo-travel) * -.5); --rhine-logo-y:calc(var(--rhine-logo-travel) * -.866); }
|
|
90
|
+
.rhine-logo__plate--right { --rhine-logo-x:calc(var(--rhine-logo-travel) * .5); --rhine-logo-y:calc(var(--rhine-logo-travel) * -.866); }
|
|
91
|
+
.rhine-logo__plate--base { --rhine-logo-x:0px; --rhine-logo-y:var(--rhine-logo-travel); }
|
|
92
|
+
.rhine-logo[data-animate=true] .rhine-logo__plate { animation:rhine-logo-enter var(--rhine-duration-brand) var(--rhine-ease-standard) both; }
|
|
93
|
+
@keyframes rhine-logo-enter { from { opacity:0; transform:translate(var(--rhine-logo-x),var(--rhine-logo-y)); } to { opacity:1; transform:translate(0,0); } }
|
|
94
|
+
@media(prefers-color-scheme:dark) {
|
|
95
|
+
.rhine-logo[data-theme=auto] { color:var(--rhine-bg); }
|
|
96
|
+
.rhine-logo--icon[data-theme=auto] { background:var(--rhine-green); }
|
|
97
|
+
}
|
|
98
|
+
@media(prefers-reduced-motion:reduce) { .rhine-logo[data-animate=true] .rhine-logo__plate { animation:none; } }
|
|
99
|
+
@keyframes rhine-enter { from { opacity:0; transform:translateY(var(--rhine-motion-distance)); } to { opacity:1; transform:translateY(0); } }
|
|
100
|
+
@keyframes rhine-progress { from { transform:translateX(-100%); } to { transform:translateX(340%); } }
|
|
101
|
+
@keyframes rhine-pulse { 50% { opacity:.55; } }
|
|
102
|
+
.rhine-theme[data-rhine-motion=reduced] { --rhine-duration-hover:0ms; --rhine-duration-press:0ms; --rhine-duration-panel:0ms; --rhine-duration-notice:0ms; --rhine-duration-progress:0ms; }
|
|
103
|
+
.rhine-theme[data-rhine-motion=reduced] *, .rhine-theme[data-rhine-motion=reduced] *::before, .rhine-theme[data-rhine-motion=reduced] *::after { animation:none!important; transition:none!important; }
|
|
104
|
+
.rhine-theme[data-rhine-motion=reduced] .rhine-button:active { transform:none; }
|
|
105
|
+
@media(prefers-reduced-motion:reduce) {
|
|
106
|
+
.rhine-theme { --rhine-duration-hover:0ms; --rhine-duration-press:0ms; --rhine-duration-panel:0ms; --rhine-duration-notice:0ms; --rhine-duration-progress:0ms; }
|
|
107
|
+
.rhine-theme *, .rhine-theme *::before, .rhine-theme *::after { animation:none!important; transition:none!important; }
|
|
108
|
+
.rhine-theme .rhine-button:active { transform:none; }
|
|
109
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* Canonical tokens for the component library and design previews. */
|
|
2
|
+
:root {
|
|
3
|
+
--rhine-bg: #f2f1e9;
|
|
4
|
+
--rhine-surface: #fcfcf8;
|
|
5
|
+
--rhine-ink: #242b28;
|
|
6
|
+
--rhine-muted: #59645d;
|
|
7
|
+
--rhine-line: #c5ccc1;
|
|
8
|
+
--rhine-control: #78857b;
|
|
9
|
+
--rhine-selected: #e5ece3;
|
|
10
|
+
--rhine-green: #425c4b;
|
|
11
|
+
--rhine-signal: #c65c34;
|
|
12
|
+
--rhine-warning: #8a472c;
|
|
13
|
+
--rhine-error: #a13e35;
|
|
14
|
+
--rhine-warning-bg: #f4e8dc;
|
|
15
|
+
--rhine-error-bg: #f6e9e4;
|
|
16
|
+
--rhine-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', sans-serif;
|
|
17
|
+
--rhine-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
|
18
|
+
--rhine-text-title: 30px;
|
|
19
|
+
--rhine-leading-title: 38px;
|
|
20
|
+
--rhine-text-section: 18px;
|
|
21
|
+
--rhine-leading-section: 26px;
|
|
22
|
+
--rhine-text-heading: 16px;
|
|
23
|
+
--rhine-leading-heading: 24px;
|
|
24
|
+
--rhine-text-body: 14px;
|
|
25
|
+
--rhine-leading-body: 22px;
|
|
26
|
+
--rhine-text-caption: 12px;
|
|
27
|
+
--rhine-leading-caption: 18px;
|
|
28
|
+
--rhine-text-metric: 26px;
|
|
29
|
+
--rhine-leading-metric: 32px;
|
|
30
|
+
--rhine-text-metric-compact: 20px;
|
|
31
|
+
--rhine-leading-metric-compact: 26px;
|
|
32
|
+
--rhine-text-code: 12px;
|
|
33
|
+
--rhine-leading-code: 20px;
|
|
34
|
+
--rhine-weight-regular: 400;
|
|
35
|
+
--rhine-weight-label: 500;
|
|
36
|
+
--rhine-weight-heading: 600;
|
|
37
|
+
--rhine-space-1: 4px;
|
|
38
|
+
--rhine-space-2: 8px;
|
|
39
|
+
--rhine-space-3: 12px;
|
|
40
|
+
--rhine-space-4: 16px;
|
|
41
|
+
--rhine-space-6: 24px;
|
|
42
|
+
--rhine-space-8: 32px;
|
|
43
|
+
--rhine-space-12: 48px;
|
|
44
|
+
--rhine-radius-card: 4px;
|
|
45
|
+
--rhine-radius-control: 3px;
|
|
46
|
+
--rhine-duration-hover: 120ms;
|
|
47
|
+
--rhine-duration-press: 80ms;
|
|
48
|
+
--rhine-duration-panel: 180ms;
|
|
49
|
+
--rhine-duration-notice: 200ms;
|
|
50
|
+
--rhine-duration-progress: 240ms;
|
|
51
|
+
--rhine-duration-brand: 240ms;
|
|
52
|
+
--rhine-duration-overlay: 160ms;
|
|
53
|
+
--rhine-duration-loading: 1000ms;
|
|
54
|
+
--rhine-duration-skeleton: 1400ms;
|
|
55
|
+
--rhine-ease-standard: cubic-bezier(.2, 0, 0, 1);
|
|
56
|
+
--rhine-ease-exit: cubic-bezier(.4, 0, 1, 1);
|
|
57
|
+
--rhine-motion-distance: 3px;
|
|
58
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.rhine-text { margin:0; font-family:var(--rhine-font); font-weight:var(--rhine-weight-regular); letter-spacing:0; overflow-wrap:anywhere; }
|
|
2
|
+
.rhine-text--title { font-size:var(--rhine-text-title); line-height:var(--rhine-leading-title); font-weight:var(--rhine-weight-heading); }
|
|
3
|
+
.rhine-text--section { font-size:var(--rhine-text-section); line-height:var(--rhine-leading-section); font-weight:var(--rhine-weight-heading); }
|
|
4
|
+
.rhine-text--heading { font-size:var(--rhine-text-heading); line-height:var(--rhine-leading-heading); font-weight:var(--rhine-weight-heading); }
|
|
5
|
+
.rhine-text--body,.rhine-text--label { font-size:var(--rhine-text-body); line-height:var(--rhine-leading-body); }
|
|
6
|
+
.rhine-text--label { font-weight:var(--rhine-weight-label); }
|
|
7
|
+
.rhine-text--caption,.rhine-text--overline { font-size:var(--rhine-text-caption); line-height:var(--rhine-leading-caption); }
|
|
8
|
+
.rhine-text--overline { font-weight:var(--rhine-weight-label); }
|
|
9
|
+
.rhine-text--overline:lang(en) { letter-spacing:.08em; text-transform:uppercase; }
|
|
10
|
+
.rhine-text--metric { font-size:var(--rhine-text-metric); line-height:var(--rhine-leading-metric); font-weight:var(--rhine-weight-heading); font-variant-numeric:tabular-nums lining-nums; }
|
|
11
|
+
.rhine-text--code { font:var(--rhine-text-code)/var(--rhine-leading-code) var(--rhine-mono); }
|
|
12
|
+
.rhine-text--numeric { font-variant-numeric:tabular-nums lining-nums; }
|
|
13
|
+
.rhine-text--default { color:inherit; }
|
|
14
|
+
.rhine-text--muted { color:var(--rhine-muted); }
|
|
15
|
+
.rhine-text--success { color:var(--rhine-green); }
|
|
16
|
+
.rhine-text--warning { color:var(--rhine-warning); }
|
|
17
|
+
.rhine-text--error { color:var(--rhine-error); }
|
|
18
|
+
.rhine-select { position:relative; min-width:0; color:var(--rhine-ink); }
|
|
19
|
+
.rhine-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(--rhine-control); border-radius:var(--rhine-radius-control); background:var(--rhine-surface); color:var(--rhine-ink); font:var(--rhine-text-body)/var(--rhine-leading-body) var(--rhine-font); cursor:pointer; transition:border-color var(--rhine-duration-hover) var(--rhine-ease-standard),background-color var(--rhine-duration-hover) var(--rhine-ease-standard); }
|
|
20
|
+
.rhine-select select:hover:not(:disabled) { border-color:var(--rhine-green); }
|
|
21
|
+
.rhine-select select[aria-invalid=true] { border-color:var(--rhine-error); }
|
|
22
|
+
.rhine-select select:disabled { color:var(--rhine-muted); background:var(--rhine-selected); cursor:default; }
|
|
23
|
+
.rhine-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
|
+
.rhine-select select:disabled+.rhine-select__arrow { color:var(--rhine-muted); }
|
|
25
|
+
.rhine-toolbar { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:20px 32px; padding:20px 24px; color:var(--rhine-ink); background:var(--rhine-surface); border-bottom:1px solid var(--rhine-line); }
|
|
26
|
+
.rhine-toolbar__intro { min-width:0; flex:1 1 220px; }
|
|
27
|
+
.rhine-toolbar__intro>p { margin-top:4px; }
|
|
28
|
+
.rhine-toolbar__controls { display:flex; flex-wrap:wrap; align-items:flex-start; gap:12px; flex:2 1 440px; min-width:0; }
|
|
29
|
+
.rhine-toolbar__controls>.rhine-field { flex:1 1 150px; min-width:0; }
|
|
30
|
+
.rhine-toolbar .rhine-field>label { font-size:var(--rhine-text-caption); line-height:var(--rhine-leading-caption); }
|
|
31
|
+
@media(forced-colors:active) { .rhine-select select { appearance:auto; -webkit-appearance:auto; } .rhine-select__arrow { display:none; } }
|
package/design/README.md
ADDED
|
@@ -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,84 @@
|
|
|
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
|
+
公开样式接口统一使用 `rhine-`:主题根 `.rhine-theme`、变量 `--rhine-*`、组件类与关键帧 `rhine-*`、减少动态属性 `data-rhine-motion="reduced"`。消费项目的品牌名称只通过内容与组件参数传入,不进入共享命名空间。
|
|
20
|
+
|
|
21
|
+
### 3.1 色彩
|
|
22
|
+
|
|
23
|
+
| Token | 值 | 用途 |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `--rhine-bg` | `#F2F1E9` | 窗口底色 |
|
|
26
|
+
| `--rhine-surface` | `#FCFCF8` | 卡片、输入、浮层 |
|
|
27
|
+
| `--rhine-ink` | `#242B28` | 正文、主按钮 |
|
|
28
|
+
| `--rhine-muted` | `#59645D` | 次级文字 |
|
|
29
|
+
| `--rhine-line` | `#C5CCC1` | 装饰分隔线,不独立承担控件识别 |
|
|
30
|
+
| `--rhine-control` | `#78857B` | 必须可识别的控件边界 |
|
|
31
|
+
| `--rhine-selected` | `#E5ECE3` | 选中背景 |
|
|
32
|
+
| `--rhine-green` | `#425C4B` | 选中标记、成功文字、焦点环 |
|
|
33
|
+
| `--rhine-signal` | `#C65C34` | 少量品牌定位色,不用于小字 |
|
|
34
|
+
| `--rhine-warning` | `#8A472C` | 待处理、冲突、覆盖不完整 |
|
|
35
|
+
| `--rhine-error` | `#A13E35` | 操作失败、需要恢复 |
|
|
36
|
+
|
|
37
|
+
本稿测算:正文 / 底色 12.77:1,次级文字 / 底色 5.44:1;品牌橙 / 底色仅 3.72:1,因此只用于非文字标记或符合大字要求的展示文字。重要状态同时有文字。数据未知显示「— / 暂不可用」,不显示 0%,未知进度不画百分比。
|
|
38
|
+
|
|
39
|
+
### 3.2 字体与密度
|
|
40
|
+
|
|
41
|
+
完整规则见 [字体与文字规范](../components/TYPOGRAPHY.md);涵盖9档文字样式、中文混排、数字单位、换行截断、内容语义和动效边界。主流系统对照与待补能力见 [补齐顺序](./DESIGN_SYSTEM_AUDIT.md)。
|
|
42
|
+
|
|
43
|
+
正文使用系统栈 `-apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Microsoft YaHei, sans-serif`;数字使用 `font-variant-numeric: tabular-nums`;路径与编号使用系统等宽栈。首期不下载 Web Font。
|
|
44
|
+
|
|
45
|
+
| 用途 | 字号 / 行高 | 字重 |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| 登录标题 | 30 / 38px | 600 |
|
|
48
|
+
| 工作区分组标题 | 18 / 26px | 600 |
|
|
49
|
+
| 卡片身份 | 16 / 24px | 600 |
|
|
50
|
+
| 正文、主要操作 | 14 / 22px | 400–600 |
|
|
51
|
+
| 标签、时间、辅助说明 | 12 / 18px | 400–500 |
|
|
52
|
+
| 用量关键数字 | 26 / 32px;窄窗 20 / 26px | 600 |
|
|
53
|
+
|
|
54
|
+
正常视图不出现小于 12px 的业务信息。英文短标签可使用 0.08em 字距;中文不刻意拉大字距。大写英文只作为少量层级辅助。
|
|
55
|
+
|
|
56
|
+
### 3.3 几何与布局
|
|
57
|
+
|
|
58
|
+
- 间距序列 4/8/12/16/24/32/48px。窗口内容边距 24px;宽度低于 480 时 20px。
|
|
59
|
+
- 展台内容按容器重排;卡片最小宽度按信息密度设定,不能由某个产品窗口宽度推导所有宿主布局。状态区预留文字换行空间。
|
|
60
|
+
- 卡片圆角 4px,按钮 3px;保留原生窗口圆角,不在 WebView 内再画一层假窗口。
|
|
61
|
+
- 控件最小高度 40px,主要操作 44px;移动预览/触屏按至少 44px 目标校验。
|
|
62
|
+
- 1px 分隔线;选中卡以 2px 深绿边界与「已选择」文字共同表达。焦点环 2px,偏移 3px。
|
|
63
|
+
- 400/520/880px作为展台验收宽度;消费项目按其最小容器尺寸追加验证。
|
|
64
|
+
- 指标与表格按容器重排;数字和说明可换行,必要时表格局部滚动,不以超小字号塞入全部列。
|
|
65
|
+
- 100% 与 200% 字体缩放下允许纵向增长、重排;不固定卡片高度、不裁掉报错和主要操作。
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## 4. 组件与内容契约
|
|
69
|
+
|
|
70
|
+
使用[40项组件契约](../components/README.md)及[常用组合](../components/COMMON_COMPONENTS.md)。表单标签持续可见,提示与错误关联;交互元素使用原生语义或可访问基础。选择、筛选、排序和分页向宿主回传意图,宿主决定数据来源及操作结果。
|
|
71
|
+
|
|
72
|
+
错误说明发生了什么及可执行的下一步,避免内部堆栈和秘密;成功、警告、未知与空态有文字区分。通知不得依赖颜色或短暂动画才能理解。品牌、实体名、时区和业务单位由宿主定义,具体文案规则见[字体与文字](../components/TYPOGRAPHY.md)。
|
|
73
|
+
|
|
74
|
+
## 5. 动效与品牌
|
|
75
|
+
|
|
76
|
+
三角构架以同一SVG母版呈现;默认字标Rhine Design,可配置。几何、颜色和安全间距见[Logo契约](../components/README.md#品牌标志)。完整深色主题仍待补齐,Logo变色不代表全套主题完成。
|
|
77
|
+
|
|
78
|
+
通用状态过渡以120–240ms为基线,按下80ms;循环只表示实际等待。具体配方、减少动态及候选边界见[动效规范](../components/MOTION.md)。[启动编排](../components/STARTUP.md)展示三块旋转压缩、展开归位以及同一标志收拢的连续性,不是业务初始化门槛。
|
|
79
|
+
|
|
80
|
+
## 6. 独立迭代与验收
|
|
81
|
+
|
|
82
|
+
在本仓运行npm run check,新增组件同步契约和默认/禁用/加载/错误/长文本/键盘/动效示例。展台浏览器验收400/520/880宽度、200%缩放、焦点、快速中断、重播和减少动态;独立消费构建验证CSS及SVG资源。
|
|
83
|
+
|
|
84
|
+
变化记录在CHANGELOG并按语义版本提供迁移说明。消费项目显式选择版本,不随主线自动升级。库验收不依赖某个App迁移完成;真实业务、原生平台与辅助技术由宿主按适用范围补验。剩余能力见[系统对照](DESIGN_SYSTEM_AUDIT.md)。
|