@linhey/rhine-design 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/CONTRIBUTING.md +6 -0
- package/README.md +70 -26
- package/components/Actions.tsx +6 -4
- package/components/COMMON_COMPONENTS.md +5 -1
- package/components/Data.tsx +8 -8
- package/components/FormWorkflow.tsx +25 -0
- package/components/Forms.tsx +11 -11
- package/components/Layout.tsx +4 -4
- package/components/Logo.tsx +7 -7
- package/components/MOTION.md +7 -3
- package/components/Navigation.tsx +42 -0
- package/components/Overlays.tsx +17 -11
- package/components/README.md +19 -11
- package/components/SPACING.md +82 -0
- package/components/Select.tsx +5 -5
- package/components/StatusTimeline.tsx +29 -0
- package/components/TYPOGRAPHY.md +35 -3
- package/components/Toast.tsx +2 -2
- package/components/Toolbar.tsx +3 -3
- package/components/Typography.tsx +1 -1
- package/components/common.css +78 -69
- package/components/components.tsx +28 -28
- package/components/index.ts +7 -1
- package/components/patterns.css +39 -0
- package/components/styles.css +105 -101
- package/components/theme-arknights.css +301 -0
- package/components/theme-neo-brutalism.css +84 -0
- package/components/theme.tsx +58 -0
- package/components/timeline.css +23 -0
- package/components/tokens.css +61 -56
- package/components/typography.css +31 -31
- package/docs/DESIGN_SYSTEM.md +24 -13
- package/docs/DESIGN_SYSTEM_AUDIT.md +4 -4
- package/docs/INTERACTION_PATTERNS.md +129 -0
- package/docs/REFERENCE_RESEARCH.md +36 -0
- package/docs/THEME_NEO_BRUTALISM.md +37 -0
- package/docs/THEMING.md +104 -0
- package/package.json +5 -1
- package/preview/CommonGallery.tsx +8 -8
- package/preview/GalleryShell.tsx +106 -0
- package/preview/LineHeightGallery.tsx +27 -0
- package/preview/MotionGallery.tsx +9 -5
- package/preview/PatternGallery.tsx +52 -0
- package/preview/SpacingGallery.tsx +32 -0
- package/preview/StartupGallery.tsx +1 -1
- package/preview/SubscriptionGallery.tsx +84 -0
- package/preview/brand-assembly.css +8 -8
- package/preview/line-height.css +2 -0
- package/preview/main.tsx +39 -25
- package/preview/motion.css +14 -14
- package/preview/patterns.css +7 -0
- package/preview/preview.css +15 -21
- package/preview/shell.css +35 -0
- package/preview/spacing.css +2 -0
- package/preview/startup.css +15 -15
- package/preview/subscriptions.css +27 -0
- package/preview/theme-arknights.css +234 -0
- package/preview/theme-neo-brutalism.css +11 -0
- package/scripts/check-build.mjs +1 -1
- package/skills/rhine-design/SKILL.md +6 -4
- package/skills/rhine-design/references/API_MAP.md +7 -3
- package/skills/rhine-design/references/TASK_MAP.md +4 -1
- package/skills/rhine-design/references/integration.md +6 -4
- package/tests/CommonComponents.test.tsx +1 -1
- package/tests/GalleryShell.test.tsx +98 -0
- package/tests/Patterns.test.tsx +75 -0
- package/tests/StartupGallery.test.tsx +3 -3
- package/tests/StatusTimeline.test.tsx +15 -0
- package/tests/Theme.test.tsx +112 -0
package/components/Overlays.tsx
CHANGED
|
@@ -11,14 +11,20 @@ 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
|
-
export interface DialogProps { trigger: ReactElement; title: string; description: string; children: ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void }
|
|
18
|
-
export function Dialog(
|
|
17
|
+
export interface DialogProps { trigger: ReactElement; title: string; description: string; children: ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void; sidePanel?: ReactNode | ((closeButton: ReactElement) => ReactNode) }
|
|
18
|
+
export function Dialog(props: DialogProps) {
|
|
19
|
+
return <DialogFrame {...props}/>;
|
|
20
|
+
}
|
|
21
|
+
export function Drawer(props: Omit<DialogProps, 'sidePanel'>) { return <DialogFrame {...props} drawer />; }
|
|
22
|
+
function DialogFrame({ trigger, title, description, children, open, onOpenChange, sidePanel, drawer = false }: DialogProps & { drawer?: boolean }) {
|
|
19
23
|
const { ref, container } = useScope();
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
const sideNavigation = typeof sidePanel === 'function';
|
|
25
|
+
const closeButton = <DialogBase.Close asChild><IconButton label={drawer ? '关闭抽屉' : '关闭对话框'} icon="close" /></DialogBase.Close>;
|
|
26
|
+
return <span className="rhine-overlay-anchor" ref={ref}><DialogBase.Root open={open} onOpenChange={onOpenChange}><DialogBase.Trigger asChild>{trigger}</DialogBase.Trigger><DialogBase.Portal container={container}>
|
|
27
|
+
<DialogBase.Overlay className="rhine-overlay" /><DialogBase.Content className={`rhine-dialog${drawer ? ' rhine-drawer' : ''}${sidePanel ? ' rhine-dialog--split' : ''}${sideNavigation ? ' rhine-dialog--side-navigation' : ''}`}><div className="rhine-dialog__main"><div className="rhine-dialog__head"><DialogBase.Title className="rhine-text rhine-text--section">{title}</DialogBase.Title>{!sideNavigation && closeButton}</div><DialogBase.Description className="rhine-text rhine-text--body rhine-text--muted">{description}</DialogBase.Description><div className="rhine-dialog__body">{children}</div></div>{sidePanel && <div className="rhine-dialog__side">{typeof sidePanel === 'function' ? sidePanel(closeButton) : sidePanel}</div>}</DialogBase.Content>
|
|
22
28
|
</DialogBase.Portal></DialogBase.Root></span>;
|
|
23
29
|
}
|
|
24
30
|
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 +38,22 @@ export function ConfirmDialog({ trigger, title, description, confirmLabel = '确
|
|
|
32
38
|
catch { if (mounted.current) setError(true); }
|
|
33
39
|
finally { pending.current = false; if (mounted.current) setBusy(false); }
|
|
34
40
|
};
|
|
35
|
-
return <span className="
|
|
36
|
-
<AlertBase.Title className="
|
|
37
|
-
<div className="
|
|
41
|
+
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(); }}>
|
|
42
|
+
<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>}
|
|
43
|
+
<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
44
|
</AlertBase.Content></AlertBase.Portal></AlertBase.Root></span>;
|
|
39
45
|
}
|
|
40
46
|
interface OverlayControl { open?: boolean; onOpenChange?: (open: boolean) => void; side?: 'top' | 'right' | 'bottom' | 'left' }
|
|
41
47
|
export function Popover({ trigger, title, children, open, onOpenChange, side }: OverlayControl & { trigger: ReactElement; title: string; children: ReactNode }) {
|
|
42
48
|
const { ref, container } = useScope();
|
|
43
|
-
return <span className="
|
|
49
|
+
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
50
|
}
|
|
45
51
|
export interface MenuItem { id: string; label: string; disabled?: boolean; danger?: boolean; onSelect: () => void }
|
|
46
52
|
export function Menu({ trigger, label, items, open, onOpenChange, side }: OverlayControl & { trigger: ReactElement; label: string; items: MenuItem[] }) {
|
|
47
53
|
const { ref, container } = useScope();
|
|
48
|
-
return <span className="
|
|
54
|
+
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
55
|
}
|
|
50
56
|
export function Tooltip({ children, content, open, onOpenChange, side }: OverlayControl & { children: ReactElement; content: string }) {
|
|
51
57
|
const { ref, container } = useScope();
|
|
52
|
-
return <span className="
|
|
58
|
+
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
59
|
}
|
package/components/README.md
CHANGED
|
@@ -1,35 +1,43 @@
|
|
|
1
|
-
# 莱茵设计 · 通用组件
|
|
1
|
+
# 莱茵设计 · 通用组件
|
|
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
|
-
共40
|
|
5
|
+
0.4.0 共48个组件;0.3.0为40个。新增导航、表单流程及健康时间轴组件见[场景与交互规范](../docs/INTERACTION_PATTERNS.md),原24个扩展组件见[常用组件契约](./COMMON_COMPONENTS.md)。[字体与文字规范](./TYPOGRAPHY.md)与[主流系统对照与缺口](../docs/DESIGN_SYSTEM_AUDIT.md)共同约束复用。静态页面样稿也使用同源Toolbar/Select CSS,不运行React。
|
|
6
6
|
|
|
7
7
|
页面改动遵循:优先复用组件 → 先在设计稿/展台修改并验证 → 再按任务授权实现到App。缺组件先补组件与展台,不在业务页另写同用途样式;同一任务已授权设计和实现时连续推进,无需重复确认。
|
|
8
8
|
|
|
9
|
+
## 间距组合
|
|
10
|
+
|
|
11
|
+
[间距规范](SPACING.md)定义尺度、分组与卡片配方:Surface负责24px内边距,嵌套Stack提供8px标题/说明和16px内容/操作间隔,Inline提供8px按钮间隔;Surface不自动给子元素加间距。展台`#spacing`可检查默认、加载、错误和长文案。
|
|
12
|
+
|
|
9
13
|
## 使用
|
|
10
14
|
|
|
11
15
|
先按[套件接入说明](../README.md#quick-start)安装组件包,再导入:
|
|
12
16
|
|
|
13
17
|
```tsx
|
|
14
|
-
import { Button, Notice, Progress, Surface } from '@linhey/rhine-design';
|
|
18
|
+
import { Button, Inline, Notice, Progress, Stack, Surface } from '@linhey/rhine-design';
|
|
15
19
|
|
|
16
20
|
export function Example() {
|
|
17
21
|
return (
|
|
18
|
-
<div className="
|
|
22
|
+
<div className="rhine-theme">
|
|
19
23
|
<Surface>
|
|
24
|
+
<Stack gap={4}>
|
|
20
25
|
<Notice tone="warning" title="需要处理">请检查后重试。</Notice>
|
|
21
26
|
<Progress label="任务进度" value={null} />
|
|
22
|
-
<Button variant="secondary" onClick={() => { /* 业务动作 */ }}>重试</Button>
|
|
27
|
+
<Inline gap={2}><Button variant="secondary" onClick={() => { /* 业务动作 */ }}>重试</Button></Inline>
|
|
28
|
+
</Stack>
|
|
23
29
|
</Surface>
|
|
24
30
|
</div>
|
|
25
31
|
);
|
|
26
32
|
}
|
|
27
33
|
```
|
|
28
34
|
|
|
29
|
-
入口 `index.ts` 自动导入组件 CSS。使用一个 `.
|
|
35
|
+
入口 `index.ts` 自动导入组件 CSS。使用一个 `.rhine-theme` 包裹组件树,获得字体、盒模型、焦点和动效降级规则。主题根放在页面根部,不加transform、裁剪或contain;浮层Portal挂在最近的主题根以继承变量和减少动态偏好。独立运行与跨项目导入方式见[套件入口](../README.md);跨仓复用核对React19与套件package.json中的5个Radix依赖,不导入展台。
|
|
36
|
+
|
|
37
|
+
0.4.0 新增主题入口 `@linhey/rhine-design/theme`,导出 `ThemeProvider`、`useTheme`、`themes`、`ThemeName` 和 `ThemeProviderProps`(自 0.4.0 提供)。Provider 自动生成主题根并加载内置主题;用 `setTheme('arknights')` 切换,受控偏好与存储由宿主管理。支持并列 Provider,暂不支持嵌套覆盖。完整契约及代码见[主题 API](../docs/THEMING.md)。
|
|
30
38
|
|
|
31
39
|
- `tokens.css`:唯一色彩、尺寸与动画变量来源。
|
|
32
|
-
- `styles.css`:`
|
|
40
|
+
- `styles.css`:`rhine-` 前缀组件样式,业务页面可组合布局但不覆盖组件状态。
|
|
33
41
|
- `components.tsx`:原有基础组件;`Actions/Data/Forms/Layout/Overlays/Toast.tsx`承载新增组件。表单与业务数据由调用方控制,浮层开关、通知计时及复制结果只管理局部交互。
|
|
34
42
|
- `../preview/`:使用同一组件实现的开发预览,独立 HTML 入口,无业务客户端依赖。
|
|
35
43
|
- [index.html](../index.html):Vite 开发入口,在套件根运行 `npm run dev` 后访问 `http://127.0.0.1:18309/`。
|
|
@@ -67,7 +75,7 @@ export function Example() {
|
|
|
67
75
|
|
|
68
76
|
启动动画候选见[三角构架归位](./STARTUP.md)与`/#startup`。专属分镜不改变通用Logo入场参数,也不作为真实初始化等待门槛。
|
|
69
77
|
|
|
70
|
-
用户指定的Transitions.dev参考与莱茵设计取舍见[UX与动效补充](./MOTION.md)。`/#motion`已提供
|
|
78
|
+
用户指定的Transitions.dev参考与莱茵设计取舍见[UX与动效补充](./MOTION.md)。`/#motion`已提供12组播放、重播、复位与4倍慢放示例,可逐例切换减少动态。方向展开、内容淡入与结果反馈仅在实验室作为候选实现;通用默认实现仍以下表和实际组件为准。
|
|
71
79
|
|
|
72
80
|
### 品牌标志
|
|
73
81
|
|
|
@@ -113,11 +121,11 @@ export function Example() {
|
|
|
113
121
|
| Dialog / ConfirmDialog / Popover / Menu / Tooltip / Combobox | 160ms / standard | opacity,非模态最多3px位移 | 关闭立即归还焦点;Tooltip悬停延迟300ms,键盘无延迟 |
|
|
114
122
|
| Toast | 180ms / standard | opacity + translateY(3px→0) | 成功默认5秒;hover/focus/页面隐藏暂停;错误持久;计时不依赖动画 |
|
|
115
123
|
|
|
116
|
-
缓动变量:`--
|
|
124
|
+
缓动变量:`--rhine-ease-standard: cubic-bezier(.2,0,0,1)`;`--rhine-ease-exit: cubic-bezier(.4,0,1,1)`。
|
|
117
125
|
|
|
118
126
|
### 降级与中断
|
|
119
127
|
|
|
120
|
-
- 系统 `prefers-reduced-motion: reduce` 始终优先;组件容器也可用 `data-
|
|
128
|
+
- 系统 `prefers-reduced-motion: reduce` 始终优先;组件容器也可用 `data-rhine-motion="reduced"` 显式减少动态。
|
|
121
129
|
- 减少动态时过渡与循环动画全部关闭,直接到达最终状态。未知进度保留静态轨迹与「处理中」文字,骨架保留静态结构。
|
|
122
130
|
- Disclosure 的在途 Web Animation 在系统偏好变化或预览切换减少动态时取消并完成当前意图。没有 Web Animations 支持时立即展开/收起。
|
|
123
131
|
- 原生 details 保证完全关闭后隐藏内容不再接收 Tab。收起时把内部焦点送回 summary;卸载取消未完成动画。
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 间距与组合
|
|
2
|
+
|
|
3
|
+
间距表达信息关系:同组紧、组间松、区域之间留白。颜色和边框不能代替分组。此规范适用于所有消费项目,真实示例见展台 `#spacing`。
|
|
4
|
+
|
|
5
|
+
## 尺度与用途
|
|
6
|
+
|
|
7
|
+
唯一尺寸来源是 [tokens.css](tokens.css)。`Stack / Inline / Grid` 的 `gap` 是 token 后缀,不是 px:`gap={4}` 表示16px。
|
|
8
|
+
|
|
9
|
+
| Token / gap | 尺寸 | 默认用途 |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `--rhine-space-1` / 1 | 4px | 标签与紧随其后的短提示、图标微间隔 |
|
|
12
|
+
| `--rhine-space-2` / 2 | 8px | 标题与说明、字段标签与控件、同组按钮 |
|
|
13
|
+
| `--rhine-space-3` / 3 | 12px | 同组信息行、密集工具栏项目 |
|
|
14
|
+
| `--rhine-space-4` / 4 | 16px | 内容与操作、表单字段、普通列表/网格项 |
|
|
15
|
+
| `--rhine-space-6` / 6 | 24px | Surface内边距、卡片之间、模块内部的独立信息组 |
|
|
16
|
+
| `--rhine-space-8` / 8 | 32px | 页面主要章节之间、宽屏页面边距 |
|
|
17
|
+
| `--rhine-space-12` / 12 | 48px | 独立大区域或品牌留白,不用于普通表单 |
|
|
18
|
+
|
|
19
|
+
0只表示无间隔,不新增token。1px边线、控件高度、焦点环偏移不属于间距尺度。宿主既有窄窗20px边距可作为明确记录的布局例外,不能变成随处使用的新gap。
|
|
20
|
+
|
|
21
|
+
## 视觉间距与操作范围
|
|
22
|
+
|
|
23
|
+
分别定义可见图形、文字行盒、可点击范围与焦点环。视觉对齐按可见内容和行盒测量,操作范围不得为压缩列表而缩小;允许透明命中区向外扩展,但不能覆盖相邻按钮、链接或输入。不能同时叠加父gap、条目padding和控件内部透明留白来表达同一段距离。
|
|
24
|
+
|
|
25
|
+
有分隔线的重复条目,分隔线前后使用同一组间距;首项不重复叠加区域顶部间距,末项为按需详情与焦点环留出空间。不要在分隔线后立刻贴标题、却把全部留白放到前一条末尾。
|
|
26
|
+
|
|
27
|
+
密集健康列表:名称/额度行与20px健康条间隔8px,条目之间分隔线两侧各16px;首项不额外叠加顶部内距。compact时间轴的44px命中区在可见条上下各扩展12px,不占据文档流留白;调用方须保留这段安全空间,避免与相邻可点击控件重叠。不要再把44px命中框的内部空白叠加到视觉gap上。
|
|
28
|
+
|
|
29
|
+
这是紧凑健康列表的配方,不要求所有列表都采用20px图形或16px内距。浮层的自然高度和单层滚动规则见[交互规范](../docs/INTERACTION_PATTERNS.md#弹窗高度与滚动归属)。
|
|
30
|
+
|
|
31
|
+
## 默认卡片配方:24 / 8 / 16
|
|
32
|
+
|
|
33
|
+
- 外层 `Surface` 负责24px内边距与边界,不自动给任意子节点添加margin。`SelectionCard`同样使用24px内边距,组件自身提供标题与内容间的8px;内容内部仍由调用方分组。
|
|
34
|
+
- 标题与说明放进 `Stack gap={2}`,保持8px间隔。
|
|
35
|
+
- 内容组与操作组放进 `Stack gap={4}`,保持16px间隔。
|
|
36
|
+
- 操作组使用 `Inline gap={2}`,按钮间隔8px、允许换行、保持内容宽度。
|
|
37
|
+
- 卡片之间用 `Stack gap={6}` 或 `Grid gap={6}`,保持24px间隔。不要同时设置父级gap与子级margin来叠加同一段距离。
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<Surface>
|
|
41
|
+
<Stack gap={4}>
|
|
42
|
+
<Stack gap={2}>
|
|
43
|
+
<Text as="h2" variant="heading">应用更新</Text>
|
|
44
|
+
<Text tone="muted">当前版本已是最新。</Text>
|
|
45
|
+
</Stack>
|
|
46
|
+
<Inline gap={2}>
|
|
47
|
+
<Button variant="secondary" onClick={checkUpdate}>检查更新</Button>
|
|
48
|
+
</Inline>
|
|
49
|
+
</Stack>
|
|
50
|
+
</Surface>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
页面已有h1时卡片通常用h2;标题等级由结构决定,不能用字号决定。不要为布局引入空Text、空段落或连续br。
|
|
54
|
+
|
|
55
|
+
行高与间距分别管理:正文14/22px、标题16/24px,独立正文段落默认12px段间距;不是在每一行之间再加gap。详见[行高规范](TYPOGRAPHY.md#行高段落与控件)。
|
|
56
|
+
|
|
57
|
+
## 常见组合
|
|
58
|
+
|
|
59
|
+
| 场景 | 内部 | 组间 / 区域 | 组件选择 |
|
|
60
|
+
| --- | --- | --- | --- |
|
|
61
|
+
| 信息与设置卡片 | 标题/说明8px | 内容/动作16px;卡片24px | Surface +嵌套Stack +Inline |
|
|
62
|
+
| 表单 | 标签/控件由字段组件负责 | 字段16px;字段组24px;提交区24px | TextField/Fieldset +Stack |
|
|
63
|
+
| 工具栏 | 同组按钮8px | 独立组16px,窄屏换行 | Toolbar/Inline |
|
|
64
|
+
| 数据列表 | 同项说明4–8px | 列表项16px;筛选/表格/分页16–24px | Stack +DataTable/Pagination |
|
|
65
|
+
| 浮层 | 标题/说明由浮层组件负责 | 正文组16px;操作区24px | Dialog/Drawer +Stack/Inline |
|
|
66
|
+
| 反馈 | 提示内部由Notice负责 | 与前后内容16px | Notice放入Stack,不覆盖其内部样式 |
|
|
67
|
+
|
|
68
|
+
这些是组合默认值,不重新覆盖现有组件内部的padding、gap或状态样式。要改变组件密度,先在库中定义并验证,不能在业务页用后代选择器临时压缩。
|
|
69
|
+
|
|
70
|
+
## 长文本、窄窗与状态变化
|
|
71
|
+
|
|
72
|
+
保持8/16/24的关系,不因窄屏把所有间距归零。长文本自然换行、卡片随内容增高;多列转单列,按钮换行,不缩小字号或点击区域。保持至少44px操作目标及焦点环空间。
|
|
73
|
+
|
|
74
|
+
加载保留已有文案和按钮位置,只改变真实状态;额外进度或错误信息作为独立组加入16px间距。错误文案常驻直至处理,不用固定卡高或裁剪限制。状态元素移除后不保留空白占位组;不要用空的Inline或Stack充当spacer。
|
|
75
|
+
|
|
76
|
+
间距本身不动画。状态变化复用现有组件动效,不为留白增加缩放、弹跳或循环;减少动态保留同样布局与操作。
|
|
77
|
+
|
|
78
|
+
## 验收
|
|
79
|
+
|
|
80
|
+
检查默认、加载、错误、长文案;400/520/880px及200%等效重排;文字、焦点与按钮不裁切;不存在父gap叠加子margin或按钮意外撑满。以真实边界间距为准,行高内部的字形留白不当成额外margin测量。
|
|
81
|
+
|
|
82
|
+
本规范与展台随main更新;现有gap API在npm0.3.0可用,文档章节不代表已发布新版本。
|
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
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useId, useState } from 'react';
|
|
2
|
+
import { Text } from './Typography';
|
|
3
|
+
|
|
4
|
+
export type TimelineStatus = 'healthy' | 'recovered' | 'degraded' | 'failed' | 'unknown';
|
|
5
|
+
export interface TimelineSegment { id: string; label: string; status: TimelineStatus }
|
|
6
|
+
export const timelineLabels: Record<TimelineStatus, string> = { healthy: '有输出·未见异常', recovered: '重试已恢复', degraded: '异常·结果待确认', failed: '存在最终失败', unknown: '未知 / 无数据' };
|
|
7
|
+
|
|
8
|
+
/** Host supplies ordered, validated evidence; this component never computes a health score. */
|
|
9
|
+
export function StatusTimeline({ label, segments, startLabel, endLabel, compact = false }: { label: string; segments: TimelineSegment[]; startLabel: string; endLabel: string; compact?: boolean }) {
|
|
10
|
+
const [index, setIndex] = useState<number>();
|
|
11
|
+
const detailId = useId();
|
|
12
|
+
const selected = Math.min(index ?? segments.length - 1, segments.length - 1);
|
|
13
|
+
const segment = segments[selected];
|
|
14
|
+
return <div className={`rhine-timeline${compact ? ' rhine-timeline--compact' : ''}`}>
|
|
15
|
+
<div className="rhine-timeline__track">
|
|
16
|
+
<div className="rhine-timeline__bars" aria-hidden="true">{segments.map((item, i) => <span key={item.id} data-status={item.status} data-selected={i === selected} />)}</div>
|
|
17
|
+
<input type="range" min={0} max={Math.max(0, segments.length - 1)} value={Math.max(0, selected)} disabled={!segments.length}
|
|
18
|
+
aria-label={label} aria-valuetext={segment ? `${segment.label} · ${timelineLabels[segment.status]}` : '暂无记录'} aria-describedby={detailId}
|
|
19
|
+
onChange={event => setIndex(Number(event.target.value))}
|
|
20
|
+
onPointerMove={event => { if (event.pointerType !== 'mouse' || !segments.length) return; const rect = event.currentTarget.getBoundingClientRect(); setIndex(Math.min(segments.length - 1, Math.max(0, Math.floor((event.clientX - rect.left) / rect.width * segments.length)))); }} />
|
|
21
|
+
</div>
|
|
22
|
+
<div className={compact ? 'rhine-sr-only' : 'rhine-timeline__range'}><Text variant="caption" tone="muted">{startLabel}</Text><Text variant="caption" tone="muted">{endLabel}</Text></div>
|
|
23
|
+
<Text id={detailId} className="rhine-timeline__detail" variant="caption">{segment ? `${segment.label} · ${timelineLabels[segment.status]}` : '暂无记录'}</Text>
|
|
24
|
+
</div>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function TimelineLegend() {
|
|
28
|
+
return <ul className="rhine-timeline__legend" aria-label="健康记录图例">{Object.entries(timelineLabels).map(([status, label]) => <li key={status}><span data-status={status} aria-hidden="true"/>{label}</li>)}</ul>;
|
|
29
|
+
}
|
package/components/TYPOGRAPHY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# 莱茵设计 · 字体与文字规范 V1.
|
|
1
|
+
# 莱茵设计 · 字体与文字规范 V1.4
|
|
2
2
|
|
|
3
|
-
2026-10-
|
|
3
|
+
2026-10-23。适用于使用莱茵设计的中文界面与中英文混排。变量定义在 [tokens.css](./tokens.css),样式与 React 入口分别为 [typography.css](./typography.css)、[Typography.tsx](./Typography.tsx)。归属与接入见[套件入口](../README.md)。
|
|
4
4
|
|
|
5
5
|
## 字体与字重
|
|
6
6
|
|
|
@@ -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
|
|
|
@@ -34,6 +34,38 @@
|
|
|
34
34
|
<Text as="code" variant="code">gpt-6.1-sol</Text>
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
## 行高、段落与控件
|
|
38
|
+
|
|
39
|
+
字号与行高成对使用,`Text`按variant同时设置两者;不在业务页单改字号而继承父级行高,不使用`normal`或固定容器高度猜测文本尺寸。上表是100%缩放的CSS尺寸,缩放后保持比例与自然换行,不以压低行高塞回原高度。
|
|
40
|
+
|
|
41
|
+
| 内容 | 字号 / 行高 | Token | 多行规则 |
|
|
42
|
+
| --- | --- | --- | --- |
|
|
43
|
+
| 大标题 | 30 / 38px | `--rhine-leading-title` | 换行仍为38px,不压成单行 |
|
|
44
|
+
| 分区标题 | 18 / 26px | `--rhine-leading-section` | 行距保持,标题之后的间隔另计 |
|
|
45
|
+
| 卡片标题 | 16 / 24px | `--rhine-leading-heading` | 中英文混排同一行高 |
|
|
46
|
+
| 正文、表单标签、按钮 | 14 / 22px | `--rhine-leading-body` | 两行正文占44px,三行占66px |
|
|
47
|
+
| 时间、来源、辅助说明 | 12 / 18px | `--rhine-leading-caption` | 两行36px;重要正文不要降为辅助字号 |
|
|
48
|
+
| 代码、路径 | 12 / 20px | `--rhine-leading-code` | 长标识可换行,行距不随字体回退变化 |
|
|
49
|
+
| 指标 | 26 / 32px;紧凑20 / 26px | `--rhine-leading-metric` / `--rhine-leading-metric-compact` | 字号与行高一起切换,单位不得把整行撑乱 |
|
|
50
|
+
|
|
51
|
+
- 行高是行框高度,包含字形上下留白,不等于两行字形之间的空白;不能把行高22px理解为额外22px的gap。
|
|
52
|
+
- 一个段落自然换行时不增加margin;两个独立段落默认用`Stack gap={3}`(12px)。标题与说明仍为8px,正文组与操作为16px,遵循[间距规范](SPACING.md)。标题/段落默认无浏览器自带margin。
|
|
53
|
+
- 按钮文字采用14/22,最小操作目标44px通过padding与min-height保证;不要把line-height设置为44px来垂直居中。长按钮文案可换行并增高,禁止固定height裁切。
|
|
54
|
+
- 现有紧凑组件的例外:Badge与Notice说明为12/20px,数据表普通文本为12/22px;这些是组件内部的既有密度,不用后代CSS临时覆盖。数据表中的技术标识仍使用Text code的12/20px。Logo字标是单行品牌几何,line-height=1仅限该品牌组件,不用于正文。
|
|
55
|
+
- 在行内混用不同字号时,行框可能由最大的子元素决定;需要固定对齐的指标、单位和标签使用组件既有组合,不承诺所有混排整行都等于单个Text的行高。
|
|
56
|
+
- 行高、字距与字号不做动画;状态文字立即更新,减少动态不改变排版。
|
|
57
|
+
|
|
58
|
+
展台`#typography`的「多行行高与段落」使用真实Text和Stack,可显示行框辅助线;辅助线只标注每行区域,不模拟字体基线,也不进入产品。
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
<Stack gap={3}>
|
|
62
|
+
<Text>第一段说明自然换行,每行22px;段内不添加额外gap。</Text>
|
|
63
|
+
<Text>第二段由Stack与前一段保持12px距离。</Text>
|
|
64
|
+
</Stack>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
验收:400/520/880px与200%等效重排下检查标题、长正文、路径、操作按钮;计算样式与token一致,文字不裁切,长文增高不挤压操作。系统纯文本放大和原生辅助技术另行验收。此补充随main更新,不代表新npm版本已发布。
|
|
68
|
+
|
|
37
69
|
## 混排、数字与单位
|
|
38
70
|
|
|
39
71
|
- 中文正文不增加字距,不做两端强制分散;段落左对齐,长说明建议不超过60ch。居中仅用于简短空态与品牌展示。
|
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
|
}
|
package/components/common.css
CHANGED
|
@@ -1,69 +1,78 @@
|
|
|
1
|
-
.
|
|
2
|
-
.
|
|
3
|
-
.
|
|
4
|
-
.
|
|
5
|
-
.
|
|
6
|
-
.
|
|
7
|
-
.
|
|
8
|
-
.
|
|
9
|
-
.
|
|
10
|
-
.
|
|
11
|
-
.
|
|
12
|
-
.
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
.
|
|
18
|
-
.
|
|
19
|
-
.
|
|
20
|
-
.
|
|
21
|
-
.
|
|
22
|
-
.
|
|
23
|
-
.
|
|
24
|
-
.
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
.
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
.
|
|
31
|
-
.
|
|
32
|
-
.
|
|
33
|
-
.
|
|
34
|
-
.
|
|
35
|
-
.
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
.
|
|
40
|
-
.
|
|
41
|
-
.
|
|
42
|
-
.
|
|
43
|
-
.
|
|
44
|
-
.
|
|
45
|
-
.
|
|
46
|
-
.
|
|
47
|
-
.
|
|
48
|
-
.
|
|
49
|
-
.
|
|
50
|
-
.
|
|
51
|
-
.
|
|
52
|
-
.
|
|
53
|
-
.
|
|
54
|
-
.
|
|
55
|
-
.
|
|
56
|
-
.
|
|
57
|
-
.
|
|
58
|
-
.
|
|
59
|
-
.
|
|
60
|
-
.
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
.
|
|
64
|
-
.
|
|
65
|
-
.
|
|
66
|
-
.
|
|
67
|
-
.
|
|
68
|
-
|
|
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:var(--rhine-text-caption); line-height:var(--rhine-leading-caption); }.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:var(--rhine-text-heading); line-height:var(--rhine-leading-heading); font-weight:var(--rhine-weight-heading); margin-bottom:var(--rhine-space-4); }
|
|
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,.rhine-dialog__main>p { margin-top:12px; }.rhine-dialog>.rhine-notice { margin-top:16px; }
|
|
54
|
+
.rhine-dialog__main { min-width:0; }
|
|
55
|
+
.rhine-dialog--split { width:min(960px,calc(100vw - 32px)); display:grid; grid-template-columns:minmax(240px,0.8fr) minmax(0,1.2fr); gap:24px; }
|
|
56
|
+
.rhine-dialog--split .rhine-dialog__head { padding-right:24px; }
|
|
57
|
+
.rhine-dialog--split .rhine-dialog__head>.rhine-icon-button { position:absolute; top:16px; right:16px; }
|
|
58
|
+
.rhine-dialog__side { min-width:0; border-left:1px solid var(--rhine-line); padding:40px 0 0 24px; }
|
|
59
|
+
.rhine-dialog--side-navigation .rhine-dialog__side { padding-top:0; }
|
|
60
|
+
.rhine-dialog--side-navigation .rhine-dialog__main>.rhine-dialog__head { min-height:44px; padding-right:0; }
|
|
61
|
+
@media(max-width:720px) { .rhine-dialog--split { grid-template-columns:minmax(0,1fr); }.rhine-dialog__side { border-left:0; border-top:1px solid var(--rhine-line); padding:24px 0 0; } }
|
|
62
|
+
@media(max-width:720px) { .rhine-dialog--side-navigation .rhine-dialog__side { padding-top:16px; } }
|
|
63
|
+
.rhine-dialog__head { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
|
64
|
+
.rhine-dialog__head>:first-child { min-width:0; overflow-wrap:anywhere; }
|
|
65
|
+
.rhine-dialog__body { margin-top:24px; }.rhine-dialog__actions { display:flex; justify-content:flex-end; flex-wrap:wrap; gap:12px; margin-top:24px; }
|
|
66
|
+
.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; }
|
|
67
|
+
.rhine-popover { width:300px; padding:16px; max-height:var(--radix-popover-content-available-height); }
|
|
68
|
+
.rhine-menu { min-width:180px; max-height:var(--radix-dropdown-menu-content-available-height); padding:4px; }
|
|
69
|
+
.rhine-menu__item { min-height:44px; padding:10px 12px; cursor:pointer; outline:none; overflow-wrap:anywhere; }
|
|
70
|
+
.rhine-menu__item[data-highlighted] { background:var(--rhine-selected); box-shadow:inset 0 0 0 2px var(--rhine-green); }
|
|
71
|
+
.rhine-menu__item[data-disabled] { color:var(--rhine-muted); cursor:default; }.rhine-menu__item[data-danger=true] { color:var(--rhine-error); }
|
|
72
|
+
.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; }
|
|
73
|
+
.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); }
|
|
74
|
+
.rhine-toast p { margin:4px 0 0; overflow-wrap:anywhere; }.rhine-toast strong { overflow-wrap:anywhere; }.rhine-toast>div { min-width:0; }
|
|
75
|
+
.rhine-dialog[data-state=open],.rhine-overlay[data-state=open] { animation:rhine-overlay-fade var(--rhine-duration-overlay) var(--rhine-ease-standard); }
|
|
76
|
+
.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); }
|
|
77
|
+
@keyframes rhine-overlay-fade { from { opacity:0; } to { opacity:1; } }
|
|
78
|
+
@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; } }
|