@linhey/rhine-design 0.2.0 → 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 +6 -0
- package/CONTRIBUTING.md +6 -0
- package/README.md +19 -14
- package/components/Actions.tsx +4 -4
- package/components/COMMON_COMPONENTS.md +1 -1
- package/components/Data.tsx +8 -8
- package/components/Forms.tsx +11 -11
- package/components/Layout.tsx +4 -4
- package/components/Logo.tsx +7 -7
- package/components/MOTION.md +1 -1
- package/components/Overlays.tsx +9 -9
- package/components/README.md +6 -6
- package/components/Select.tsx +5 -5
- package/components/TYPOGRAPHY.md +1 -1
- package/components/Toast.tsx +2 -2
- package/components/Toolbar.tsx +3 -3
- package/components/Typography.tsx +1 -1
- package/components/common.css +69 -69
- package/components/components.tsx +28 -28
- package/components/styles.css +101 -101
- package/components/tokens.css +55 -55
- package/components/typography.css +31 -31
- package/docs/DESIGN_SYSTEM.md +12 -11
- package/package.json +1 -1
- package/preview/MotionGallery.tsx +3 -3
- package/preview/StartupGallery.tsx +1 -1
- package/preview/brand-assembly.css +8 -8
- package/preview/main.tsx +1 -1
- package/preview/motion.css +14 -14
- package/preview/preview.css +19 -19
- package/preview/startup.css +15 -15
- package/scripts/check-build.mjs +1 -1
- package/skills/rhine-design/SKILL.md +3 -3
- package/skills/rhine-design/references/integration.md +4 -4
- package/tests/CommonComponents.test.tsx +1 -1
- package/tests/StartupGallery.test.tsx +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# 版本记录
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- **破坏性变更:**统一使用莱茵设计的 `rhine-` 命名空间,无旧前缀别名。
|
|
6
|
+
- 升级时将主题根 `.tod-theme` 改为 `.rhine-theme`,CSS 变量 `--tod-*` 改为 `--rhine-*`,类名及关键帧 `tod-*` 改为 `rhine-*`,减少动态属性 `data-tod-motion` 改为 `data-rhine-motion`。请同步更新自定义样式、DOM 查询和复制到项目中的 skill。
|
|
7
|
+
- 组件、浮层主题挂载、动效展台、文档与测试一起迁移;React 导出与 props、视觉参数保持一致。
|
|
8
|
+
|
|
3
9
|
## 0.2.0
|
|
4
10
|
|
|
5
11
|
- 定位为个人通用设计系统 linhay/rhine-design,独立迭代。
|
package/CONTRIBUTING.md
CHANGED
|
@@ -42,3 +42,9 @@ README 面向首次接触项目的开发者,优先解释用途、安装、示
|
|
|
42
42
|
问题反馈请提供包版本、组件名、预期与实际行为、最小复现。删除复现中的私人数据和凭据。
|
|
43
43
|
|
|
44
44
|
维护者发布步骤见 [RELEASING](docs/RELEASING.md)。项目采用 [Apache-2.0](LICENSE) 许可证。
|
|
45
|
+
|
|
46
|
+
## 在线展台
|
|
47
|
+
|
|
48
|
+
[GitHub Pages](https://linhay.github.io/rhine-design/) 展示 `main` 的组件与演示。现有 CI 先执行 `npm run check`,成功后只上传 `dist/` 并部署;Pull Request 运行检查,不发布站点。工作流也支持在 `main` 手动重跑。
|
|
49
|
+
|
|
50
|
+
Vite 使用相对资源路径,兼容 `/rhine-design/` 子路径。调整构建或静态资源时,检查在线页面、CSS、JS 和 Logo SVG 均能正常加载,以及 `#motion`、`#startup` 直接访问和重播。
|
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<p align="center">暖白与苔绿,三角与细线。让界面拥有清晰的秩序与克制的节奏。</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
+
<a href="https://linhay.github.io/rhine-design/">在线预览</a> ·
|
|
10
11
|
<a href="#quick-start">快速接入</a> ·
|
|
11
12
|
<a href="#components">组件</a> ·
|
|
12
13
|
<a href="#documentation">设计规范</a> ·
|
|
@@ -30,20 +31,20 @@ Rhine Design 是面向 React 应用的开源设计系统,适合桌面工具、
|
|
|
30
31
|
|
|
31
32
|
需要 **React 19**。当前已验证的构建环境为 **Vite**,开发建议使用 **Node 24**。
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
在你的 React 项目中安装:
|
|
34
35
|
|
|
35
36
|
```sh
|
|
36
|
-
npm install
|
|
37
|
+
npm install @linhey/rhine-design@0.3.0
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
包与版本信息见 [npm](https://www.npmjs.com/package/@linhey/rhine-design)。如需使用未发版的源码,可安装 `git+https://github.com/linhay/rhine-design.git#<commit>`;建议固定版本并提交 lockfile。
|
|
40
41
|
|
|
41
42
|
```tsx
|
|
42
43
|
import { Button, Stack, Surface, Text } from '@linhey/rhine-design';
|
|
43
44
|
|
|
44
45
|
export function Welcome() {
|
|
45
46
|
return (
|
|
46
|
-
<div className="
|
|
47
|
+
<div className="rhine-theme">
|
|
47
48
|
<Surface>
|
|
48
49
|
<Stack gap={4}>
|
|
49
50
|
<Text as="h1" variant="title">开始新的工作</Text>
|
|
@@ -56,12 +57,14 @@ export function Welcome() {
|
|
|
56
57
|
}
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
入口自动加载样式,用 `.
|
|
60
|
+
入口自动加载样式,用 `.rhine-theme` 包裹页面即可使用默认主题。布局和颜色变量使用 `--rhine-*` 前缀;完整定义见 [tokens.css](components/tokens.css)。
|
|
61
|
+
|
|
62
|
+
类名、CSS 变量、动画名称与动态设置统一使用 `rhine-` 命名空间。升级到 0.3.0 时请同步迁移主题根、自定义样式与动效属性,具体见 [迁移说明](CHANGELOG.md#030)。
|
|
60
63
|
|
|
61
64
|
### 兼容范围
|
|
62
65
|
|
|
63
66
|
- 包导出 TS/TSX、CSS 和 SVG 源码,构建器需支持 CSS 导入与 Vite 的 SVG `?no-inline` 引用;目前未验证其他构建器和 SSR。
|
|
64
|
-
- 浮层挂载到最近的 `.
|
|
67
|
+
- 浮层挂载到最近的 `.rhine-theme`。请将主题根放在页面根部,避免对它使用 `transform`、裁剪或 `contain`。
|
|
65
68
|
- 组件提供呈现与交互,应用负责数据请求、搜索、分页和操作结果。
|
|
66
69
|
- 当前提供默认暖白主题;Logo 支持独立的深浅配色,完整组件深色主题尚未提供。
|
|
67
70
|
|
|
@@ -86,16 +89,18 @@ export function Welcome() {
|
|
|
86
89
|
|
|
87
90
|
## 设计规范与展台
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
无需安装即可打开 **[在线组件展台](https://linhay.github.io/rhine-design/)**,体验组件与动效。
|
|
93
|
+
|
|
94
|
+
| 主题 | 文档 | 在线预览 |
|
|
90
95
|
| --- | --- | --- |
|
|
91
|
-
| 视觉基础与组合原则 | [设计规范](docs/DESIGN_SYSTEM.md) |
|
|
92
|
-
| 字体、文字层级与数值 | [Typography](components/TYPOGRAPHY.md) |
|
|
93
|
-
| 品牌几何与 Logo | [Logo 契约](components/README.md#品牌标志) |
|
|
94
|
-
| 动效、交互与减少动态 | [Motion](components/MOTION.md) |
|
|
95
|
-
| 启动动画与就绪衔接 | [Startup](components/STARTUP.md) |
|
|
96
|
+
| 视觉基础与组合原则 | [设计规范](docs/DESIGN_SYSTEM.md) | [视觉基础](https://linhay.github.io/rhine-design/#foundation) |
|
|
97
|
+
| 字体、文字层级与数值 | [Typography](components/TYPOGRAPHY.md) | [字体与文字](https://linhay.github.io/rhine-design/#typography) |
|
|
98
|
+
| 品牌几何与 Logo | [Logo 契约](components/README.md#品牌标志) | [品牌标志](https://linhay.github.io/rhine-design/#brand) |
|
|
99
|
+
| 动效、交互与减少动态 | [Motion](components/MOTION.md) | [动效实验室](https://linhay.github.io/rhine-design/#motion) |
|
|
100
|
+
| 启动动画与就绪衔接 | [Startup](components/STARTUP.md) | [启动动画](https://linhay.github.io/rhine-design/#startup) |
|
|
96
101
|
| 系统覆盖与演进方向 | [组件系统对照](docs/DESIGN_SYSTEM_AUDIT.md) | — |
|
|
97
102
|
|
|
98
|
-
|
|
103
|
+
展台包含组件状态、键盘操作、长文本、加载与错误示例。动效可播放、重播、复位和慢放,并支持减少动态模式。在线版本跟随 `main` 更新;如需本地运行:
|
|
99
104
|
|
|
100
105
|
```sh
|
|
101
106
|
git clone https://github.com/linhay/rhine-design.git
|
|
@@ -104,7 +109,7 @@ npm ci
|
|
|
104
109
|
npm run dev
|
|
105
110
|
```
|
|
106
111
|
|
|
107
|
-
打开 `http://127.0.0.1:18309
|
|
112
|
+
打开 `http://127.0.0.1:18309/`,使用相同的 `#foundation`、`#motion` 等锚点访问对应章节。
|
|
108
113
|
|
|
109
114
|
<a id="ai-skills"></a>
|
|
110
115
|
|
package/components/Actions.tsx
CHANGED
|
@@ -16,13 +16,13 @@ const paths = {
|
|
|
16
16
|
} as const;
|
|
17
17
|
export type IconName = keyof typeof paths;
|
|
18
18
|
export function Icon({ name, size = 20 }: { name: IconName; size?: 16 | 20 }) {
|
|
19
|
-
return <svg className="
|
|
19
|
+
return <svg className="rhine-icon" width={size} height={size} viewBox="0 0 20 20" aria-hidden="true" focusable="false"><path d={paths[name]} /></svg>;
|
|
20
20
|
}
|
|
21
21
|
export const IconButton = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement> & { label: string; icon: IconName }>(function IconButton({ label, icon, className = '', type = 'button', ...props }, ref) {
|
|
22
|
-
return <button {...props} ref={ref} type={type} aria-label={label} className={`
|
|
22
|
+
return <button {...props} ref={ref} type={type} aria-label={label} className={`rhine-button rhine-button--quiet rhine-icon-button ${className}`}><Icon name={icon} /></button>;
|
|
23
23
|
});
|
|
24
24
|
export function Link({ children, className = '', target, rel, ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) {
|
|
25
|
-
return <a {...props} target={target} rel={target === '_blank' ? `${rel || ''} noopener noreferrer`.trim() : rel} className={`
|
|
25
|
+
return <a {...props} target={target} rel={target === '_blank' ? `${rel || ''} noopener noreferrer`.trim() : rel} className={`rhine-link ${className}`}>{children}{target === '_blank' && <><Icon name="external" size={16} /><span className="rhine-sr-only">(新窗口打开)</span></>}</a>;
|
|
26
26
|
}
|
|
27
27
|
export function CopyButton({ value, label = '复制', copy = text => navigator.clipboard.writeText(text) }: { value: string; label?: string; copy?: (value: string) => Promise<void> }) {
|
|
28
28
|
const [status, setStatus] = useState<'idle' | 'busy' | 'success' | 'error'>('idle');
|
|
@@ -36,5 +36,5 @@ export function CopyButton({ value, label = '复制', copy = text => navigator.c
|
|
|
36
36
|
catch { if (request === epoch.current) setStatus('error'); }
|
|
37
37
|
finally { if (request === epoch.current) pending.current = false; }
|
|
38
38
|
};
|
|
39
|
-
return <span className="
|
|
39
|
+
return <span className="rhine-copy"><Button variant="secondary" busy={status === 'busy'} busyLabel="正在复制" onClick={run}><Icon name="copy" size={16} /> {label}</Button><span role={status === 'error' ? 'alert' : 'status'} className={status === 'error' ? 'rhine-text--error' : 'rhine-text--muted'}>{status === 'success' ? '已复制' : status === 'error' ? '复制失败,请重试' : ''}</span></span>;
|
|
40
40
|
}
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
## 接入契约
|
|
42
42
|
|
|
43
|
-
统一从 套件的`components/index.ts` 导入,并在页面根部使用 `.
|
|
43
|
+
统一从 套件的`components/index.ts` 导入,并在页面根部使用 `.rhine-theme`。主题根不要放在带transform、overflow裁剪或contain的局部面板;浮层挂到最近主题根,保持token和减少动态偏好。Dialog、Popover、Menu的trigger与Tooltip的children必须是单个可聚焦、可接收ref和事件的元素;使用Button/IconButton即可。Dialog受控时必须同时传open与onOpenChange。
|
|
44
44
|
|
|
45
45
|
```tsx
|
|
46
46
|
<Stack gap={4}>
|
package/components/Data.tsx
CHANGED
|
@@ -3,10 +3,10 @@ import { Button, EmptyState, Notice, Skeleton } from './components';
|
|
|
3
3
|
import { Text } from './Typography';
|
|
4
4
|
|
|
5
5
|
export function Metric({ label, value, unit, description, loading = false }: { label: string; value: ReactNode; unit?: string; description?: string; loading?: boolean }) {
|
|
6
|
-
return <div className="
|
|
6
|
+
return <div className="rhine-metric" aria-busy={loading}><Text variant="caption" tone="muted">{label}</Text>{loading ? <Skeleton lines={1} label={`正在读取${label}`} /> : <Text variant="metric">{value ?? '—'}{unit && <span className="rhine-metric__unit"> {unit}</span>}</Text>}{description && <Text variant="caption" tone="muted">{description}</Text>}</div>;
|
|
7
7
|
}
|
|
8
8
|
export function KeyValue({ items }: { items: { key: string; label: string; value: ReactNode }[] }) {
|
|
9
|
-
return <dl className="
|
|
9
|
+
return <dl className="rhine-key-value">{items.map(item => <div key={item.key}><dt>{item.label}</dt><dd>{item.value ?? '—'}</dd></div>)}</dl>;
|
|
10
10
|
}
|
|
11
11
|
export interface DataColumn<T> { key: string; header: string; render: (row: T) => ReactNode; align?: 'left' | 'right'; sortable?: boolean }
|
|
12
12
|
export interface TableSort { key: string; direction: 'asc' | 'desc' }
|
|
@@ -16,11 +16,11 @@ export interface DataTableProps<T> {
|
|
|
16
16
|
onSortChange?: (sort: TableSort) => void; onRetry?: () => void;
|
|
17
17
|
}
|
|
18
18
|
export function DataTable<T>({ caption, columns, rows, rowKey, loading = false, error, emptyTitle = '暂无记录', sort, onSortChange, onRetry }: DataTableProps<T>) {
|
|
19
|
-
return <div className="
|
|
19
|
+
return <div className="rhine-data" aria-busy={loading}>
|
|
20
20
|
{error && <Notice tone="error" title="读取失败" action={onRetry && <Button variant="secondary" onClick={onRetry} disabled={loading}>重试</Button>}>{error}{rows.length > 0 && ' · 以下保留上次读取的记录。'}</Notice>}
|
|
21
|
-
{loading && <p role="status" className="
|
|
22
|
-
<div className="
|
|
23
|
-
<table className="
|
|
21
|
+
{loading && <p role="status" className="rhine-text rhine-text--caption rhine-text--muted">{rows.length ? '正在刷新,保留当前记录…' : '正在读取…'}</p>}
|
|
22
|
+
<div className="rhine-table-scroll" role="region" aria-label={`${caption},可横向滚动`} tabIndex={0}>
|
|
23
|
+
<table className="rhine-table"><caption>{caption}</caption><thead><tr>{columns.map(column => <th scope="col" key={column.key} data-align={column.align} aria-sort={column.sortable ? sort?.key === column.key ? sort.direction === 'asc' ? 'ascending' : 'descending' : 'none' : undefined}>{column.sortable && onSortChange ? <button type="button" disabled={loading} onClick={() => onSortChange({ key: column.key, direction: sort?.key === column.key && sort.direction === 'asc' ? 'desc' : 'asc' })}>{column.header}<span aria-hidden="true">{sort?.key === column.key ? sort.direction === 'asc' ? ' ↑' : ' ↓' : ' ↕'}</span></button> : column.header}</th>)}</tr></thead>
|
|
24
24
|
<tbody>{rows.map(row => <tr key={rowKey(row)}>{columns.map(column => <td key={column.key} data-align={column.align}>{column.render(row) ?? '—'}</td>)}</tr>)}</tbody></table>
|
|
25
25
|
</div>
|
|
26
26
|
{!rows.length && !loading && !error && <EmptyState title={emptyTitle} description="当前条件下没有可展示的数据。" />}
|
|
@@ -34,8 +34,8 @@ export function Pagination({ page, pageSize, total, hasNext = false, loading = f
|
|
|
34
34
|
const pages = count === undefined ? undefined : Math.max(1, Math.ceil(count / size));
|
|
35
35
|
const current = Math.max(1, Math.floor(page));
|
|
36
36
|
const next = pages === undefined ? hasNext : current < pages;
|
|
37
|
-
return <nav className="
|
|
38
|
-
<span className="
|
|
37
|
+
return <nav className="rhine-pagination" aria-label="分页">
|
|
38
|
+
<span className="rhine-text rhine-text--caption rhine-text--numeric">{count === undefined ? `第 ${current} 页` : `第 ${current} / ${pages} 页 · 共 ${count} 条`}</span>
|
|
39
39
|
<Button variant="secondary" disabled={loading || current <= 1} onClick={() => onPageChange(current - 1)}>上一页</Button>
|
|
40
40
|
<Button variant="secondary" disabled={loading || !next} onClick={() => onPageChange(current + 1)}>下一页</Button>
|
|
41
41
|
</nav>;
|
package/components/Forms.tsx
CHANGED
|
@@ -7,23 +7,23 @@ export function Checkbox({ label, hint, indeterminate = false, id: suppliedId, c
|
|
|
7
7
|
const generatedId = useId(), id = suppliedId || generatedId;
|
|
8
8
|
const ref = useRef<HTMLInputElement>(null);
|
|
9
9
|
useEffect(() => { if (ref.current) ref.current.indeterminate = indeterminate; }, [indeterminate]);
|
|
10
|
-
return <div className={`
|
|
10
|
+
return <div className={`rhine-check ${className}`}><label htmlFor={id}><input {...props} ref={ref} id={id} type="checkbox" aria-checked={indeterminate ? 'mixed' : undefined} aria-describedby={[props['aria-describedby'], hint && `${id}-hint`].filter(Boolean).join(' ') || undefined} /><span>{label}</span></label>{hint && <p id={`${id}-hint`} className="rhine-text rhine-text--caption rhine-text--muted">{hint}</p>}</div>;
|
|
11
11
|
}
|
|
12
12
|
export function RadioGroup({ label, items, value, onValueChange, disabled = false, name }: { label: string; items: Choice[]; value: string; onValueChange: (value: string) => void; disabled?: boolean; name?: string }) {
|
|
13
13
|
const id = useId();
|
|
14
|
-
return <fieldset className="
|
|
14
|
+
return <fieldset className="rhine-radio-group" disabled={disabled}><legend>{label}</legend>{items.map(item => <label key={item.value}><input type="radio" name={name || id} value={item.value} checked={value === item.value} disabled={item.disabled} onChange={() => onValueChange(item.value)} /><span>{item.label}</span></label>)}</fieldset>;
|
|
15
15
|
}
|
|
16
16
|
export function Switch({ label, checked, onCheckedChange, disabled = false, hint }: { label: string; checked: boolean; onCheckedChange: (checked: boolean) => void; disabled?: boolean; hint?: string }) {
|
|
17
17
|
const id = useId();
|
|
18
|
-
return <div className="
|
|
18
|
+
return <div className="rhine-switch"><label><input type="checkbox" role="switch" checked={checked} aria-checked={checked} disabled={disabled} aria-describedby={hint ? id : undefined} onChange={event => onCheckedChange(event.target.checked)} /><span className="rhine-switch__track" aria-hidden="true"><span /></span><span>{label}</span></label>{hint && <p id={id} className="rhine-text rhine-text--caption rhine-text--muted">{hint}</p>}</div>;
|
|
19
19
|
}
|
|
20
20
|
export function Textarea({ label, hint, error, id: suppliedId, className = '', rows = 3, ...props }: TextareaHTMLAttributes<HTMLTextAreaElement> & { label: string; hint?: string; error?: string }) {
|
|
21
21
|
const generatedId = useId(), id = suppliedId || generatedId;
|
|
22
|
-
return <div className={`
|
|
22
|
+
return <div className={`rhine-field ${className}`}><label htmlFor={id}>{label}</label><textarea {...props} id={id} rows={rows} aria-invalid={error ? true : props['aria-invalid']} aria-describedby={[props['aria-describedby'], hint && `${id}-hint`, error && `${id}-error`].filter(Boolean).join(' ') || undefined} />{hint && <span id={`${id}-hint`} className="rhine-field__hint">{hint}</span>}{error && <span id={`${id}-error`} className="rhine-field__error" role="alert">{error}</span>}</div>;
|
|
23
23
|
}
|
|
24
24
|
export function SearchField({ label, value, onValueChange, onSearch, disabled = false, loading = false }: { label: string; value: string; onValueChange: (value: string) => void; onSearch: (query: string) => void; disabled?: boolean; loading?: boolean }) {
|
|
25
25
|
const id = useId(); const composing = useRef(false);
|
|
26
|
-
return <div className="
|
|
26
|
+
return <div className="rhine-search rhine-field" role="search" aria-label={label}><label htmlFor={id}>{label}</label><div className="rhine-inline">
|
|
27
27
|
<input id={id} type="search" value={value} disabled={disabled} onChange={event => onValueChange(event.target.value)} onCompositionStart={() => { composing.current = true; }} onCompositionEnd={() => { composing.current = false; }} onKeyDown={event => { if (event.key === 'Enter') { if (composing.current || event.nativeEvent.isComposing || event.keyCode === 229) return; event.preventDefault(); if (!loading && !disabled) onSearch(value); } }} />
|
|
28
28
|
{value && <IconButton icon="close" label="清空搜索" disabled={disabled || loading} onClick={() => { onValueChange(''); onSearch(''); document.getElementById(id)?.focus(); }} />}
|
|
29
29
|
<Button variant="secondary" disabled={disabled} busy={loading} busyLabel="正在搜索" onClick={() => { if (!composing.current) onSearch(value); }}>搜索</Button>
|
|
@@ -38,15 +38,15 @@ export function Combobox({ label, query, onQueryChange, value, onValueChange, op
|
|
|
38
38
|
const id = useId(); const composing = useRef(false);
|
|
39
39
|
const root = useRef<HTMLDivElement>(null);
|
|
40
40
|
const [container, setContainer] = useState<HTMLElement>();
|
|
41
|
-
useLayoutEffect(() => { setContainer((root.current?.closest('.
|
|
41
|
+
useLayoutEffect(() => { setContainer((root.current?.closest('.rhine-theme') as HTMLElement) || document.body); }, []);
|
|
42
42
|
const [open, setOpen] = useState(false), [active, setActive] = useState<string | null>(null), [draft, setDraft] = useState<string | null>(null);
|
|
43
43
|
const available = loading || error ? [] : options.filter(option => !option.disabled);
|
|
44
44
|
const activeIndex = options.findIndex(option => option.value === active && !option.disabled);
|
|
45
45
|
const activeId = activeIndex >= 0 && !loading && !error ? `${id}-option-${activeIndex}` : undefined;
|
|
46
46
|
useEffect(() => { if (!open || !activeId) return; document.getElementById(activeId)?.scrollIntoView?.({ block: 'nearest' }); }, [activeId, open]);
|
|
47
47
|
const choose = (option: Choice) => { onValueChange(option); setOpen(false); setActive(null); };
|
|
48
|
-
return <div ref={root} className="
|
|
49
|
-
<PopoverBase.Root open={open && !disabled} onOpenChange={setOpen}><PopoverBase.Anchor asChild><div className="
|
|
48
|
+
return <div ref={root} className="rhine-combobox rhine-field" onBlur={event => { if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false); }}><label htmlFor={id}>{label}</label>
|
|
49
|
+
<PopoverBase.Root open={open && !disabled} onOpenChange={setOpen}><PopoverBase.Anchor asChild><div className="rhine-combobox__input"><input id={id} role="combobox" aria-autocomplete="list" aria-expanded={open && !disabled} aria-controls={open && !disabled ? `${id}-list` : undefined} aria-activedescendant={open ? activeId : undefined} aria-invalid={!!error || undefined} aria-describedby={`${id}-status`} value={draft ?? query} disabled={disabled}
|
|
50
50
|
onClick={() => setOpen(true)} onFocus={() => setOpen(true)} onChange={event => { if (composing.current) setDraft(event.target.value); else onQueryChange(event.target.value); setActive(null); setOpen(true); }}
|
|
51
51
|
onCompositionStart={() => { composing.current = true; setDraft(query); }} onCompositionEnd={event => { composing.current = false; setDraft(null); onQueryChange(event.currentTarget.value); }}
|
|
52
52
|
onKeyDown={event => {
|
|
@@ -55,10 +55,10 @@ export function Combobox({ label, query, onQueryChange, value, onValueChange, op
|
|
|
55
55
|
if (['ArrowDown', 'ArrowUp'].includes(event.key)) { event.preventDefault(); setOpen(true); const index = available.findIndex(option => option.value === active); const next = event.key === 'ArrowDown' ? (index + 1) % available.length : (index <= 0 ? available.length : index) - 1; setActive(available[next]?.value ?? null); }
|
|
56
56
|
if (open && ['Home', 'End'].includes(event.key)) { event.preventDefault(); setActive((event.key === 'Home' ? available[0] : available.at(-1))?.value ?? null); }
|
|
57
57
|
if (open && event.key === 'Enter') { event.preventDefault(); const option = available.find(item => item.value === active); if (option) choose(option); }
|
|
58
|
-
}} /><span className="
|
|
58
|
+
}} /><span className="rhine-combobox__arrow"><Icon name="chevronDown" size={16} /></span></div></PopoverBase.Anchor>
|
|
59
59
|
<PopoverBase.Portal container={container}><PopoverBase.Content asChild sideOffset={4} collisionPadding={12} onOpenAutoFocus={event => event.preventDefault()} onCloseAutoFocus={event => event.preventDefault()} onInteractOutside={event => { if (root.current?.contains(event.target as Node)) event.preventDefault(); }}>
|
|
60
|
-
<div className="
|
|
60
|
+
<div className="rhine-combobox__list" role="listbox" id={`${id}-list`} aria-label={label} aria-busy={loading}>{!loading && !error && options.map((option, index) => <div role="option" id={`${id}-option-${index}`} key={option.value} aria-selected={option.value === value} aria-disabled={option.disabled || undefined} data-active={option.value === active} onMouseDown={event => event.preventDefault()} onMouseMove={() => { if (!option.disabled) setActive(option.value); }} onClick={() => { if (!option.disabled) choose(option); }}>{option.label}</div>)}{(loading || error || !options.length) && <div className="rhine-combobox__empty" role="presentation">{loading ? '正在读取…' : error ? '暂不可用' : '暂无匹配结果'}</div>}</div>
|
|
61
61
|
</PopoverBase.Content></PopoverBase.Portal></PopoverBase.Root>
|
|
62
|
-
<span id={`${id}-status`} role="status" className={`
|
|
62
|
+
<span id={`${id}-status`} role="status" className={`rhine-text rhine-text--caption ${error ? 'rhine-text--error' : 'rhine-text--muted'}`}>{loading ? '正在读取候选项…' : error || (options.length ? `${options.length} 个候选项` : '暂无匹配结果')}</span>
|
|
63
63
|
</div>;
|
|
64
64
|
}
|
package/components/Layout.tsx
CHANGED
|
@@ -2,12 +2,12 @@ import type { CSSProperties, HTMLAttributes } from 'react';
|
|
|
2
2
|
export type Spacing = 1 | 2 | 3 | 4 | 6 | 8 | 12;
|
|
3
3
|
type LayoutProps = HTMLAttributes<HTMLDivElement> & { gap?: Spacing };
|
|
4
4
|
export function Stack({ gap = 4, className = '', style, ...props }: LayoutProps) {
|
|
5
|
-
return <div {...props} className={`
|
|
5
|
+
return <div {...props} className={`rhine-stack ${className}`} style={{ gap: `var(--rhine-space-${gap})`, ...style }} />;
|
|
6
6
|
}
|
|
7
7
|
export function Inline({ gap = 3, className = '', style, ...props }: LayoutProps) {
|
|
8
|
-
return <div {...props} className={`
|
|
8
|
+
return <div {...props} className={`rhine-inline ${className}`} style={{ gap: `var(--rhine-space-${gap})`, ...style }} />;
|
|
9
9
|
}
|
|
10
10
|
export function Grid({ gap = 4, minWidth = 240, className = '', style, ...props }: LayoutProps & { minWidth?: number }) {
|
|
11
|
-
return <div {...props} className={`
|
|
11
|
+
return <div {...props} className={`rhine-grid ${className}`} style={{ '--rhine-grid-min': `${Math.max(80, minWidth)}px`, gap: `var(--rhine-space-${gap})`, ...style } as CSSProperties} />;
|
|
12
12
|
}
|
|
13
|
-
export function Divider() { return <hr className="
|
|
13
|
+
export function Divider() { return <hr className="rhine-divider" />; }
|
package/components/Logo.tsx
CHANGED
|
@@ -16,19 +16,19 @@ export interface LogoProps {
|
|
|
16
16
|
export function Logo({ size = 48, variant = 'mark', theme = 'light', animate = false, decorative = false, wordmark = 'Rhine Design', label = wordmark, className = '' }: LogoProps) {
|
|
17
17
|
const dimension = Number.isFinite(size) ? Math.max(16, size) : 48;
|
|
18
18
|
return <span
|
|
19
|
-
className={`
|
|
19
|
+
className={`rhine-logo rhine-logo--${variant} ${className}`}
|
|
20
20
|
data-theme={theme}
|
|
21
21
|
data-animate={animate}
|
|
22
22
|
role={decorative ? undefined : 'img'}
|
|
23
23
|
aria-label={decorative ? undefined : label}
|
|
24
24
|
aria-hidden={decorative || undefined}
|
|
25
|
-
style={{ '--
|
|
25
|
+
style={{ '--rhine-logo-size': `${dimension}px`, '--rhine-logo-travel': `${2400 / dimension}px` } as CSSProperties}
|
|
26
26
|
>
|
|
27
|
-
<svg className="
|
|
28
|
-
<use className="
|
|
29
|
-
<use className="
|
|
30
|
-
<use className="
|
|
27
|
+
<svg className="rhine-logo__symbol" viewBox="0 0 800 800" aria-hidden="true" focusable="false">
|
|
28
|
+
<use className="rhine-logo__plate rhine-logo__plate--left" href={`${markUrl}#left`} />
|
|
29
|
+
<use className="rhine-logo__plate rhine-logo__plate--right" href={`${markUrl}#right`} />
|
|
30
|
+
<use className="rhine-logo__plate rhine-logo__plate--base" href={`${markUrl}#base`} />
|
|
31
31
|
</svg>
|
|
32
|
-
{variant === 'lockup' && <span className="
|
|
32
|
+
{variant === 'lockup' && <span className="rhine-logo__wordmark" aria-hidden="true">{wordmark}</span>}
|
|
33
33
|
</span>;
|
|
34
34
|
}
|
package/components/MOTION.md
CHANGED
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
## 参数与中断约束
|
|
52
52
|
|
|
53
53
|
- 继续使用[tokens.css](./tokens.css):按下80ms、交互120ms、浮层160ms、面板180ms、通知/折叠200ms、进度/品牌240ms;Toast用面板180ms,Notice用通知200ms。正文与数值不做缩放或模糊。
|
|
54
|
-
- `--
|
|
54
|
+
- `--rhine-ease-standard`为`cubic-bezier(.2,0,0,1)`;Disclosure收起用现有exit曲线。`--rhine-motion-distance`为3px;新增配方先复用这些变量,不新增逐组件硬编码时长。
|
|
55
55
|
- 动画仅声明需要变化的属性,不用`transition: all`。一般用opacity/transform;高度测量仅用于必要的Disclosure布局变化,不用整页高度动画掩盖布局跳动。
|
|
56
56
|
- 快速反向从当前画面续接,旧动画取消;卸载清理动画和计时器。初始渲染、窗口恢复或普通重渲染不重复播放操作成功效果。
|
|
57
57
|
- 语义先于装饰:checked、aria-expanded、真实结果和焦点即时更新;内容淡入不延迟可读状态。不要为了动画同时保留两份可聚焦控件或重复live播报。
|
package/components/Overlays.tsx
CHANGED
|
@@ -11,14 +11,14 @@ import { IconButton } from './Actions';
|
|
|
11
11
|
function useScope() {
|
|
12
12
|
const ref = useRef<HTMLSpanElement>(null);
|
|
13
13
|
const [container, setContainer] = useState<HTMLElement>();
|
|
14
|
-
useLayoutEffect(() => { setContainer((ref.current?.closest('.
|
|
14
|
+
useLayoutEffect(() => { setContainer((ref.current?.closest('.rhine-theme') as HTMLElement) || document.body); }, []);
|
|
15
15
|
return { ref, container };
|
|
16
16
|
}
|
|
17
17
|
export interface DialogProps { trigger: ReactElement; title: string; description: string; children: ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void }
|
|
18
18
|
export function Dialog({ trigger, title, description, children, open, onOpenChange }: DialogProps) {
|
|
19
19
|
const { ref, container } = useScope();
|
|
20
|
-
return <span className="
|
|
21
|
-
<DialogBase.Overlay className="
|
|
20
|
+
return <span className="rhine-overlay-anchor" ref={ref}><DialogBase.Root open={open} onOpenChange={onOpenChange}><DialogBase.Trigger asChild>{trigger}</DialogBase.Trigger><DialogBase.Portal container={container}>
|
|
21
|
+
<DialogBase.Overlay className="rhine-overlay" /><DialogBase.Content className="rhine-dialog"><div className="rhine-dialog__head"><DialogBase.Title className="rhine-text rhine-text--section">{title}</DialogBase.Title><DialogBase.Close asChild><IconButton label="关闭对话框" icon="close" /></DialogBase.Close></div><DialogBase.Description className="rhine-text rhine-text--body rhine-text--muted">{description}</DialogBase.Description><div className="rhine-dialog__body">{children}</div></DialogBase.Content>
|
|
22
22
|
</DialogBase.Portal></DialogBase.Root></span>;
|
|
23
23
|
}
|
|
24
24
|
export function ConfirmDialog({ trigger, title, description, confirmLabel = '确认操作', danger = false, onConfirm }: { trigger: ReactElement; title: string; description: string; confirmLabel?: string; danger?: boolean; onConfirm: () => Promise<void> | void }) {
|
|
@@ -32,22 +32,22 @@ export function ConfirmDialog({ trigger, title, description, confirmLabel = '确
|
|
|
32
32
|
catch { if (mounted.current) setError(true); }
|
|
33
33
|
finally { pending.current = false; if (mounted.current) setBusy(false); }
|
|
34
34
|
};
|
|
35
|
-
return <span className="
|
|
36
|
-
<AlertBase.Title className="
|
|
37
|
-
<div className="
|
|
35
|
+
return <span className="rhine-overlay-anchor" ref={ref}><AlertBase.Root open={open} onOpenChange={next => { if (!pending.current) { setOpen(next); setError(false); } }}><AlertBase.Trigger asChild>{trigger}</AlertBase.Trigger><AlertBase.Portal container={container}><AlertBase.Overlay className="rhine-overlay" /><AlertBase.Content className="rhine-dialog" aria-busy={busy} onEscapeKeyDown={event => { if (busy) event.preventDefault(); }}>
|
|
36
|
+
<AlertBase.Title className="rhine-text rhine-text--section">{title}</AlertBase.Title><AlertBase.Description className="rhine-text rhine-text--body rhine-text--muted">{description}</AlertBase.Description>{error && <Notice tone="error" title="操作未完成">请重试,或取消后检查。</Notice>}
|
|
37
|
+
<div className="rhine-dialog__actions"><AlertBase.Cancel asChild><Button variant="secondary" disabled={busy}>取消</Button></AlertBase.Cancel><Button variant={danger ? 'danger' : 'primary'} busy={busy} onClick={run}>{confirmLabel}</Button></div>
|
|
38
38
|
</AlertBase.Content></AlertBase.Portal></AlertBase.Root></span>;
|
|
39
39
|
}
|
|
40
40
|
interface OverlayControl { open?: boolean; onOpenChange?: (open: boolean) => void; side?: 'top' | 'right' | 'bottom' | 'left' }
|
|
41
41
|
export function Popover({ trigger, title, children, open, onOpenChange, side }: OverlayControl & { trigger: ReactElement; title: string; children: ReactNode }) {
|
|
42
42
|
const { ref, container } = useScope();
|
|
43
|
-
return <span className="
|
|
43
|
+
return <span className="rhine-overlay-anchor" ref={ref}><PopoverBase.Root open={open} onOpenChange={onOpenChange}><PopoverBase.Trigger asChild>{trigger}</PopoverBase.Trigger><PopoverBase.Portal container={container}><PopoverBase.Content className="rhine-popover" aria-label={title} side={side} sideOffset={8} collisionPadding={12}><div className="rhine-dialog__head"><strong>{title}</strong><PopoverBase.Close asChild><IconButton label="关闭详情" icon="close" /></PopoverBase.Close></div>{children}</PopoverBase.Content></PopoverBase.Portal></PopoverBase.Root></span>;
|
|
44
44
|
}
|
|
45
45
|
export interface MenuItem { id: string; label: string; disabled?: boolean; danger?: boolean; onSelect: () => void }
|
|
46
46
|
export function Menu({ trigger, label, items, open, onOpenChange, side }: OverlayControl & { trigger: ReactElement; label: string; items: MenuItem[] }) {
|
|
47
47
|
const { ref, container } = useScope();
|
|
48
|
-
return <span className="
|
|
48
|
+
return <span className="rhine-overlay-anchor" ref={ref}><MenuBase.Root open={open} onOpenChange={onOpenChange}><MenuBase.Trigger asChild>{trigger}</MenuBase.Trigger><MenuBase.Portal container={container}><MenuBase.Content className="rhine-menu" aria-label={label} side={side} sideOffset={8} collisionPadding={12}>{items.map(item => <MenuBase.Item key={item.id} disabled={item.disabled} onSelect={item.onSelect} className="rhine-menu__item" data-danger={item.danger}>{item.label}</MenuBase.Item>)}</MenuBase.Content></MenuBase.Portal></MenuBase.Root></span>;
|
|
49
49
|
}
|
|
50
50
|
export function Tooltip({ children, content, open, onOpenChange, side }: OverlayControl & { children: ReactElement; content: string }) {
|
|
51
51
|
const { ref, container } = useScope();
|
|
52
|
-
return <span className="
|
|
52
|
+
return <span className="rhine-overlay-anchor" ref={ref}><TooltipBase.Provider delayDuration={300}><TooltipBase.Root open={open} onOpenChange={onOpenChange}><TooltipBase.Trigger asChild>{children}</TooltipBase.Trigger><TooltipBase.Portal container={container}><TooltipBase.Content className="rhine-tooltip" side={side} sideOffset={8} collisionPadding={12}>{content}</TooltipBase.Content></TooltipBase.Portal></TooltipBase.Root></TooltipBase.Provider></span>;
|
|
53
53
|
}
|
package/components/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 莱茵设计 · 通用组件 V1.4
|
|
2
2
|
|
|
3
|
-
本目录提供[莱茵设计 · Rhine Design](../README.md)的 React 组件、设计变量和 API 契约。组件接收应用传入的数据与动作,不绑定特定后端或原生平台。包名为`@linhey/rhine-design`;样式变量和类名使用`
|
|
3
|
+
本目录提供[莱茵设计 · Rhine Design](../README.md)的 React 组件、设计变量和 API 契约。组件接收应用传入的数据与动作,不绑定特定后端或原生平台。包名为`@linhey/rhine-design`;样式变量和类名使用`rhine-`命名空间。
|
|
4
4
|
|
|
5
5
|
共40个组件。已有16个基础组件,新增24个数据、表单、操作、浮层和布局组件,见[常用组件契约](./COMMON_COMPONENTS.md)。[字体与文字规范](./TYPOGRAPHY.md)与[主流系统对照与缺口](../docs/DESIGN_SYSTEM_AUDIT.md)共同约束复用。静态页面样稿也使用同源Toolbar/Select CSS,不运行React。
|
|
6
6
|
|
|
@@ -15,7 +15,7 @@ import { Button, Notice, Progress, Surface } from '@linhey/rhine-design';
|
|
|
15
15
|
|
|
16
16
|
export function Example() {
|
|
17
17
|
return (
|
|
18
|
-
<div className="
|
|
18
|
+
<div className="rhine-theme">
|
|
19
19
|
<Surface>
|
|
20
20
|
<Notice tone="warning" title="需要处理">请检查后重试。</Notice>
|
|
21
21
|
<Progress label="任务进度" value={null} />
|
|
@@ -26,10 +26,10 @@ export function Example() {
|
|
|
26
26
|
}
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
入口 `index.ts` 自动导入组件 CSS。使用一个 `.
|
|
29
|
+
入口 `index.ts` 自动导入组件 CSS。使用一个 `.rhine-theme` 包裹组件树,获得字体、盒模型、焦点和动效降级规则。主题根放在页面根部,不加transform、裁剪或contain;浮层Portal挂在最近的主题根以继承变量和减少动态偏好。独立运行与跨项目导入方式见[套件入口](../README.md);跨仓复用核对React19与套件package.json中的5个Radix依赖,不导入展台。
|
|
30
30
|
|
|
31
31
|
- `tokens.css`:唯一色彩、尺寸与动画变量来源。
|
|
32
|
-
- `styles.css`:`
|
|
32
|
+
- `styles.css`:`rhine-` 前缀组件样式,业务页面可组合布局但不覆盖组件状态。
|
|
33
33
|
- `components.tsx`:原有基础组件;`Actions/Data/Forms/Layout/Overlays/Toast.tsx`承载新增组件。表单与业务数据由调用方控制,浮层开关、通知计时及复制结果只管理局部交互。
|
|
34
34
|
- `../preview/`:使用同一组件实现的开发预览,独立 HTML 入口,无业务客户端依赖。
|
|
35
35
|
- [index.html](../index.html):Vite 开发入口,在套件根运行 `npm run dev` 后访问 `http://127.0.0.1:18309/`。
|
|
@@ -113,11 +113,11 @@ export function Example() {
|
|
|
113
113
|
| Dialog / ConfirmDialog / Popover / Menu / Tooltip / Combobox | 160ms / standard | opacity,非模态最多3px位移 | 关闭立即归还焦点;Tooltip悬停延迟300ms,键盘无延迟 |
|
|
114
114
|
| Toast | 180ms / standard | opacity + translateY(3px→0) | 成功默认5秒;hover/focus/页面隐藏暂停;错误持久;计时不依赖动画 |
|
|
115
115
|
|
|
116
|
-
缓动变量:`--
|
|
116
|
+
缓动变量:`--rhine-ease-standard: cubic-bezier(.2,0,0,1)`;`--rhine-ease-exit: cubic-bezier(.4,0,1,1)`。
|
|
117
117
|
|
|
118
118
|
### 降级与中断
|
|
119
119
|
|
|
120
|
-
- 系统 `prefers-reduced-motion: reduce` 始终优先;组件容器也可用 `data-
|
|
120
|
+
- 系统 `prefers-reduced-motion: reduce` 始终优先;组件容器也可用 `data-rhine-motion="reduced"` 显式减少动态。
|
|
121
121
|
- 减少动态时过渡与循环动画全部关闭,直接到达最终状态。未知进度保留静态轨迹与「处理中」文字,骨架保留静态结构。
|
|
122
122
|
- Disclosure 的在途 Web Animation 在系统偏好变化或预览切换减少动态时取消并完成当前意图。没有 Web Animations 支持时立即展开/收起。
|
|
123
123
|
- 原生 details 保证完全关闭后隐藏内容不再接收 Tab。收起时把内部焦点送回 summary;卸载取消未完成动画。
|
package/components/Select.tsx
CHANGED
|
@@ -10,13 +10,13 @@ export function Select({ label, hint, error, id: providedId, className = '', chi
|
|
|
10
10
|
const generatedId = useId();
|
|
11
11
|
const id = providedId || generatedId;
|
|
12
12
|
const description = [props['aria-describedby'], hint && `${id}-hint`, error && `${id}-error`].filter(Boolean).join(' ');
|
|
13
|
-
return <div className={`
|
|
13
|
+
return <div className={`rhine-field ${className}`}>
|
|
14
14
|
<label htmlFor={id}>{label}</label>
|
|
15
|
-
<div className="
|
|
15
|
+
<div className="rhine-select">
|
|
16
16
|
<select {...props} id={id} aria-invalid={error ? true : props['aria-invalid']} aria-describedby={description || undefined}>{children}</select>
|
|
17
|
-
<svg className="
|
|
17
|
+
<svg className="rhine-select__arrow" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="m4 6 4 4 4-4" /></svg>
|
|
18
18
|
</div>
|
|
19
|
-
{hint && <span id={`${id}-hint`} className="
|
|
20
|
-
{error && <span id={`${id}-error`} className="
|
|
19
|
+
{hint && <span id={`${id}-hint`} className="rhine-field__hint">{hint}</span>}
|
|
20
|
+
{error && <span id={`${id}-error`} className="rhine-field__error" role="alert">{error}</span>}
|
|
21
21
|
</div>;
|
|
22
22
|
}
|
package/components/TYPOGRAPHY.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- 代码与路径:`ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace`。模型 ID、路径、协议值用等宽;普通英文说明仍用界面字体。
|
|
9
9
|
- 常规400、标签500、标题600。避免轻字重、小字号粗细堆叠、650等中间字重和浏览器伪斜体;不全局使用700。Logo字标遵循品牌组件,不作为业务文字模板。
|
|
10
10
|
- 系统字体选择与语义字号层级参考 [Fluent Typography](https://fluent2.microsoft.design/typography);具体尺寸和中文规则是本项目的设计决策,不复制其全部字号。
|
|
11
|
-
- `.
|
|
11
|
+
- `.rhine-theme` 禁止字体伪造;不同系统的字面宽度可能不同,不能用固定字符串宽度或固定容器高度实现布局。
|
|
12
12
|
|
|
13
13
|
## 文字层级
|
|
14
14
|
|
package/components/Toast.tsx
CHANGED
|
@@ -16,8 +16,8 @@ export function Toast({ open, onOpenChange, title, description, tone = 'success'
|
|
|
16
16
|
const started = Date.now(); const timer = setTimeout(() => close.current(false), Math.max(0, remaining.current));
|
|
17
17
|
return () => { clearTimeout(timer); remaining.current = Math.max(0, remaining.current - (Date.now() - started)); };
|
|
18
18
|
}, [open, hover, focus, hidden, tone, duration, title, description]);
|
|
19
|
-
return <div className="
|
|
20
|
-
<div role={tone === 'error' ? 'alert' : 'status'} aria-atomic="true" className={open ? `
|
|
19
|
+
return <div className="rhine-toast-region" aria-label="通知">
|
|
20
|
+
<div role={tone === 'error' ? 'alert' : 'status'} aria-atomic="true" className={open ? `rhine-toast rhine-tone-${tone}` : 'rhine-sr-only'}
|
|
21
21
|
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} onFocus={() => setFocus(true)} onBlur={event => { if (!event.currentTarget.contains(event.relatedTarget)) setFocus(false); }}>
|
|
22
22
|
{open && <><div><strong>{title}</strong>{description && <p>{description}</p>}</div><IconButton label="关闭通知" icon="close" onClick={() => onOpenChange(false)} /></>}
|
|
23
23
|
</div>
|
package/components/Toolbar.tsx
CHANGED
|
@@ -7,8 +7,8 @@ export interface ToolbarProps extends Omit<HTMLAttributes<HTMLDivElement>, 'titl
|
|
|
7
7
|
|
|
8
8
|
// A native-control group keeps normal Tab navigation, not toolbar arrow-key semantics.
|
|
9
9
|
export function Toolbar({ title, description, children, className = '', ...props }: ToolbarProps) {
|
|
10
|
-
return <div {...props} role="group" aria-label={props['aria-label'] || title} className={`
|
|
11
|
-
<div className="
|
|
12
|
-
<div className="
|
|
10
|
+
return <div {...props} role="group" aria-label={props['aria-label'] || title} className={`rhine-toolbar ${className}`}>
|
|
11
|
+
<div className="rhine-toolbar__intro"><strong className="rhine-text rhine-text--label">{title}</strong>{description && <p className="rhine-text rhine-text--caption rhine-text--muted">{description}</p>}</div>
|
|
12
|
+
<div className="rhine-toolbar__controls">{children}</div>
|
|
13
13
|
</div>;
|
|
14
14
|
}
|
|
@@ -9,5 +9,5 @@ export interface TextProps extends HTMLAttributes<HTMLElement> {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function Text({ as: Element = 'p', variant = 'body', tone = 'default', numeric = false, className = '', ...props }: TextProps) {
|
|
12
|
-
return <Element {...props} className={`
|
|
12
|
+
return <Element {...props} className={`rhine-text rhine-text--${variant} rhine-text--${tone}${numeric ? ' rhine-text--numeric' : ''} ${className}`} />;
|
|
13
13
|
}
|