@linhey/rhine-design 0.8.0 → 0.9.1
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 +15 -0
- package/README.md +3 -1
- package/components/AdvancedForms.tsx +40 -10
- package/components/ListFilterBar.tsx +104 -0
- package/components/OrbitBackdrop.tsx +9 -3
- package/components/README.md +3 -2
- package/components/SPACING.md +6 -0
- package/components/components.tsx +5 -2
- package/components/extensions.css +10 -0
- package/components/index.ts +2 -1
- package/components/list-filter-bar.css +33 -0
- package/components/orbit.css +21 -0
- package/components/styles.css +1 -0
- package/components/timeline.css +2 -1
- package/docs/APPLICATION_COMPONENTS.md +4 -0
- package/docs/BACKGROUND_FILTER_VERIFICATION.md +12 -0
- package/docs/COMPACT_ACCOUNT_PATTERNS.md +3 -1
- package/docs/COMPACT_LAYOUT_VERIFICATION.md +53 -0
- package/docs/DESIGN_SYSTEM.md +6 -0
- package/docs/INTERACTION_PATTERNS.md +11 -1
- package/docs/LIST_FILTER_BAR.md +76 -0
- package/docs/ORBIT_BACKDROP.md +25 -0
- package/docs/assets/compact-layouts/account.png +0 -0
- package/docs/assets/compact-layouts/date-arknights.png +0 -0
- package/docs/assets/compact-layouts/date-neo-brutalism.png +0 -0
- package/docs/assets/compact-layouts/date-rhine.png +0 -0
- package/docs/assets/compact-layouts/table.png +0 -0
- package/package.json +1 -1
- package/preview/BackgroundFiltersFixture.tsx +28 -0
- package/preview/CompactAccountGallery.tsx +18 -18
- package/preview/DateRangeFilterGallery.tsx +18 -1
- package/preview/GalleryShell.tsx +1 -1
- package/preview/ListFilterBarGallery.tsx +170 -0
- package/preview/OrbitGallery.tsx +14 -0
- package/preview/SpacingGallery.tsx +15 -1
- package/preview/main.tsx +4 -1
- package/preview/preview.css +24 -2
- package/scripts/check-background-filters.js +58 -0
- package/scripts/check-background-motion.js +169 -0
- package/scripts/check-compact-layouts.js +136 -0
- package/scripts/check-filter-native-date.js +47 -0
- package/skills/rhine-design/SKILL.md +3 -3
- package/skills/rhine-design/references/API_MAP.md +1 -1
- package/skills/rhine-design/references/integration.md +1 -1
- package/tests/CompactAccountGallery.test.tsx +2 -1
- package/tests/DateNativeInput.test.tsx +147 -0
- package/tests/DesignSystem.test.tsx +12 -1
- package/tests/ListFilterBar.test.tsx +198 -0
- package/tests/OrbitBackdrop.test.tsx +28 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# 版本记录
|
|
2
2
|
|
|
3
|
+
## 0.9.1(2026-10-25)
|
|
4
|
+
|
|
5
|
+
- #23:移除 StatusTimelineList 末项重复留白,保留 44px 命中区;紧凑账号展台的基础信息与今日/本周期读数按稳定列对齐,窄容器自然换行。
|
|
6
|
+
- #24:Surface 新增 `padding="default" | "none"` 和公开 `SurfaceProps`;纯表格可取消外围内距,默认 24px、单元格间距与浮层/焦点行为保持不变。
|
|
7
|
+
- #25:DateRangePicker 按实际容器宽度取消窄容器重复横向内距,DateField 收紧窄编辑区内距并保留 Inline 中的固有宽度;增加 150/200/280 CSS px 的范围与独立日期展台,保留原生日期、日历入口和受控草稿语义。
|
|
8
|
+
- 修正固定合成历史的摘要为“24小时窗口”,避免将 2026-10-23~24 的示例误解为当前最近一天。以上修复见 [PR #26](https://github.com/linhay/rhine-design/pull/26)。
|
|
9
|
+
- 默认 API 行为保持兼容,无需迁移;使用 `Surface padding="none"` 需升级至 0.9.1。验证范围与重跑步骤见[紧凑布局回归](docs/COMPACT_LAYOUT_VERIFICATION.md)。
|
|
10
|
+
|
|
11
|
+
## 0.9.0(2026-10-24)
|
|
12
|
+
|
|
13
|
+
- #17:OrbitBackdrop 新增 controls、composition 和 framed;背景画布与静态降级同置右下角,新增按容器宽度重排的入口组合,保留输入和减少动态。
|
|
14
|
+
- #19:新增 ListFilterBar,最多两个主筛选,超出进入 Drawer;保留可见搜索标签,统一条件摘要、清除、重置、刷新和请求反馈,业务查询与权限由宿主管理。
|
|
15
|
+
- 修复原生日期未完成片段被当作空值提交的问题;DateRangePicker 清空和同值预设可恢复本地草稿;有效输入继续服从宿主受控值。展台整体重置清除同值日期的未完成草稿。清除最后一个不可聚焦字段的摘要后,焦点保留在筛选区域。
|
|
16
|
+
- 独立验收覆盖三主题、窄容器与 200% CSS zoom、抽屉焦点、日期键盘、真实 GPU 播放与动态偏好、静态降级及卸载清理。
|
|
17
|
+
|
|
3
18
|
## 0.8.0(2026-10-24)
|
|
4
19
|
|
|
5
20
|
- 审查修复:保留 Tooltip 与嵌套 Dialog 的 Escape 层级;主题元数据独立于可选 CSS;设置示例保存中重开时同步已保存值并保留后续编辑。
|
package/README.md
CHANGED
|
@@ -46,11 +46,13 @@ Rhine Design 是面向 React 应用的开源设计系统,适合桌面工具、
|
|
|
46
46
|
在你的 React 项目中安装:
|
|
47
47
|
|
|
48
48
|
```sh
|
|
49
|
-
npm install --save-exact @linhey/rhine-design@0.
|
|
49
|
+
npm install --save-exact @linhey/rhine-design@0.9.1
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
包与版本信息见 [npm](https://www.npmjs.com/package/@linhey/rhine-design)。如需使用未发版的源码,可安装 `git+https://github.com/linhay/rhine-design.git#<commit>`;建议固定版本并提交 lockfile。
|
|
53
53
|
|
|
54
|
+
**0.9.1 修复**:紧凑账号时间轴尾距与事实列对齐、纯表格 `Surface padding="none"`、窄容器中完整原生日期编辑区。默认行为兼容,完整说明见[版本记录](CHANGELOG.md#0912026-10-25)。0.9.0 的 `ListFilterBar` 搜索、主条件与更多筛选能力继续保留。
|
|
55
|
+
|
|
54
56
|
```tsx
|
|
55
57
|
import { Button, Inline, Stack, Surface, Text } from '@linhey/rhine-design';
|
|
56
58
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useId, useLayoutEffect, useRef, useState, type KeyboardEvent } from 'react';
|
|
1
|
+
import { useEffect, useId, useLayoutEffect, useRef, useState, type KeyboardEvent, type SyntheticEvent } from 'react';
|
|
2
2
|
import { Button, TextField, type Choice } from './components';
|
|
3
3
|
import { Combobox } from './Forms';
|
|
4
4
|
|
|
@@ -23,9 +23,31 @@ function useDateValidation(error: string | undefined, onValidationChange?: (erro
|
|
|
23
23
|
}
|
|
24
24
|
/** Date-only ISO strings: conversion to timestamps and time zones belongs to the host. */
|
|
25
25
|
export function DateField({ onValueChange, onValidationChange, error, ...props }: DateFieldProps) {
|
|
26
|
-
const
|
|
26
|
+
const [editing, setEditing] = useState({ accepted: props.value, draft: props.value, badInput: false });
|
|
27
|
+
const lastEvent = useRef<Event | null>(null);
|
|
28
|
+
const nativeBadInput = useRef(false);
|
|
29
|
+
let current = editing;
|
|
30
|
+
if (editing.accepted !== props.value) {
|
|
31
|
+
current = { accepted: props.value, draft: props.value, badInput: false };
|
|
32
|
+
nativeBadInput.current = false;
|
|
33
|
+
setEditing(current);
|
|
34
|
+
}
|
|
35
|
+
const invalid = error || (current.badInput ? '请填写完整有效的日期。' : dateError(current.draft, props.min, props.max));
|
|
27
36
|
useDateValidation(invalid, onValidationChange);
|
|
28
|
-
|
|
37
|
+
const change = (event: SyntheticEvent<HTMLInputElement>) => {
|
|
38
|
+
if (props.disabled || props.readOnly || lastEvent.current === event.nativeEvent) return;
|
|
39
|
+
lastEvent.current = event.nativeEvent;
|
|
40
|
+
const { value, validity } = event.currentTarget;
|
|
41
|
+
nativeBadInput.current = validity.badInput;
|
|
42
|
+
// Native date fragments also expose an empty value; they are not a cleared boundary.
|
|
43
|
+
// Only incomplete native fragments are local drafts. Valid values remain
|
|
44
|
+
// controlled by the host, which may reject a proposed change.
|
|
45
|
+
setEditing({ accepted: props.value, draft: validity.badInput ? value : props.value, badInput: validity.badInput });
|
|
46
|
+
if (!validity.badInput) onValueChange(value);
|
|
47
|
+
};
|
|
48
|
+
// Chromium can change an empty calendar fragment without firing input/change.
|
|
49
|
+
const checkFragments = (event: SyntheticEvent<HTMLInputElement>) => { if (event.currentTarget.validity.badInput !== nativeBadInput.current) change(event); };
|
|
50
|
+
return <TextField {...props} className="rhine-date-field" value={current.draft} error={invalid} type="date" onInput={change} onChange={change} onKeyUp={checkFragments} onBlur={checkFragments} />;
|
|
29
51
|
}
|
|
30
52
|
export interface DateRange { start: string; end: string }
|
|
31
53
|
export interface DateRangePickerProps {
|
|
@@ -38,18 +60,26 @@ export interface DateRangePickerProps {
|
|
|
38
60
|
}
|
|
39
61
|
export function DateRangePicker({ label, value, onValueChange, min, max, disabled, error, presets = [], readOnly = false, required, clearable = true, hint, startLabel = '开始日期', endLabel = '结束日期', startName, endName, onValidationChange }: DateRangePickerProps) {
|
|
40
62
|
const id = useId();
|
|
63
|
+
const [fieldErrors, setFieldErrors] = useState<{ start: string | null; end: string | null }>({ start: null, end: null });
|
|
64
|
+
const [resetRevision, setResetRevision] = useState(0);
|
|
41
65
|
const rangeError = (range: DateRange) => dateError(range.start, min, max) || dateError(range.end, min, max) || (range.start && range.end && range.start > range.end ? '结束日期不能早于开始日期。' : undefined);
|
|
42
|
-
const invalid = error || rangeError(value);
|
|
66
|
+
const invalid = error || rangeError(value) || fieldErrors.start || fieldErrors.end || undefined;
|
|
43
67
|
useDateValidation(invalid, onValidationChange);
|
|
44
|
-
|
|
68
|
+
const fieldValidation = (field: 'start' | 'end', message: string | null) => setFieldErrors(current => current[field] === message ? current : { ...current, [field]: message });
|
|
69
|
+
const replace = (next: DateRange) => {
|
|
70
|
+
setFieldErrors({ start: null, end: null });
|
|
71
|
+
setResetRevision(revision => revision + 1);
|
|
72
|
+
onValueChange(next);
|
|
73
|
+
};
|
|
74
|
+
return <div className="rhine-date-range-container"><fieldset className="rhine-date-range" disabled={disabled} aria-describedby={[hint && `${id}-hint`, invalid && id].filter(Boolean).join(' ') || undefined}>
|
|
45
75
|
<legend>{label}</legend><div className="rhine-date-range__fields">
|
|
46
|
-
<DateField label={startLabel} name={startName} value={value.start} min={min} max={max} readOnly={readOnly} required={required} disabled={disabled} onValueChange={start => onValueChange({ ...value, start })}
|
|
47
|
-
<DateField label={endLabel} name={endName} value={value.end} min={value.start && (!min || value.start > min) ? value.start : min} max={max} readOnly={readOnly} required={required} disabled={disabled} onValueChange={end => onValueChange({ ...value, end })} />
|
|
76
|
+
<DateField key={`start-${resetRevision}`} label={startLabel} name={startName} value={value.start} min={min} max={max} readOnly={readOnly} required={required} disabled={disabled} onValueChange={start => onValueChange({ ...value, start })} onValidationChange={message => fieldValidation('start', message)} />
|
|
77
|
+
<DateField key={`end-${resetRevision}`} label={endLabel} name={endName} value={value.end} min={value.start && (!min || value.start > min) ? value.start : min} max={max} readOnly={readOnly} required={required} disabled={disabled} onValueChange={end => onValueChange({ ...value, end })} onValidationChange={message => fieldValidation('end', message)} />
|
|
48
78
|
</div><div className="rhine-inline">
|
|
49
|
-
{presets.map(preset => <Button key={preset.label} variant="secondary" disabled={disabled || readOnly || !!rangeError(preset.value) || !!(required && (!preset.value.start || !preset.value.end))} onClick={() =>
|
|
50
|
-
{clearable && <Button variant="quiet" disabled={disabled || readOnly || (!value.start && !value.end)} onClick={() =>
|
|
79
|
+
{presets.map(preset => <Button key={preset.label} variant="secondary" disabled={disabled || readOnly || !!rangeError(preset.value) || !!(required && (!preset.value.start || !preset.value.end))} onClick={() => replace(preset.value)}>{preset.label}</Button>)}
|
|
80
|
+
{clearable && <Button variant="quiet" disabled={disabled || readOnly || (!value.start && !value.end && !fieldErrors.start && !fieldErrors.end)} onClick={() => replace({ start: '', end: '' })}>清空日期</Button>}
|
|
51
81
|
</div>{hint && <p id={`${id}-hint`} className="rhine-field__hint">{hint}</p>}{invalid && <p id={id} role="alert" className="rhine-field__error">{invalid}</p>}
|
|
52
|
-
</fieldset>;
|
|
82
|
+
</fieldset></div>;
|
|
53
83
|
}
|
|
54
84
|
|
|
55
85
|
export interface NumberFieldProps {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useRef, useState, type InputHTMLAttributes, type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
import { Button, Notice, Surface, TextField } from './components';
|
|
3
|
+
import { Drawer } from './Overlays';
|
|
4
|
+
import './list-filter-bar.css';
|
|
5
|
+
|
|
6
|
+
export interface ListFilterField {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
control: ReactNode;
|
|
10
|
+
activeLabel?: string;
|
|
11
|
+
onClear?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ListFilterBarProps {
|
|
15
|
+
label: string;
|
|
16
|
+
search?: InputHTMLAttributes<HTMLInputElement> & { label: string };
|
|
17
|
+
primary?: readonly ListFilterField[];
|
|
18
|
+
filters?: readonly ListFilterField[];
|
|
19
|
+
onReset?: () => void;
|
|
20
|
+
refresh?: { onClick: () => void; busy?: boolean; disabled?: boolean };
|
|
21
|
+
action?: ReactElement;
|
|
22
|
+
result?: ReactNode;
|
|
23
|
+
pending?: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Feedback({ result, pending, error, announce = true }: Pick<ListFilterBarProps, 'result' | 'pending' | 'error'> & { announce?: boolean }) {
|
|
29
|
+
return <>
|
|
30
|
+
{(result != null || pending) && <div className="rhine-list-filter-bar__feedback" role="status" aria-live={announce ? 'polite' : 'off'}>
|
|
31
|
+
{pending && <span>正在更新结果…</span>}
|
|
32
|
+
{result != null && <div>{result}</div>}
|
|
33
|
+
</div>}
|
|
34
|
+
{error && <Notice tone="error" title="未能更新结果">{error}</Notice>}
|
|
35
|
+
</>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const focusable = 'input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), button:not(:disabled), a[href]';
|
|
39
|
+
|
|
40
|
+
export function ListFilterBar({ label, search, primary = [], filters = [], onReset, refresh, action, result, pending = false, error, className = '' }: ListFilterBarProps) {
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
const root = useRef<HTMLDivElement>(null);
|
|
43
|
+
const drawerBody = useRef<HTMLDivElement>(null);
|
|
44
|
+
const visibleFields = primary.slice(0, 2);
|
|
45
|
+
const drawerFields = [...primary.slice(2), ...filters];
|
|
46
|
+
const activeFields = [...primary, ...filters].filter(field => field.activeLabel?.trim());
|
|
47
|
+
const activeDrawerCount = activeFields.filter(field => drawerFields.some(item => item.id === field.id)).length;
|
|
48
|
+
const hasQuery = search?.value != null && String(search.value).trim() !== '';
|
|
49
|
+
const canReset = Boolean(onReset && (activeFields.length > 0 || hasQuery));
|
|
50
|
+
|
|
51
|
+
function stableFocus() {
|
|
52
|
+
const scope = open ? drawerBody.current : root.current?.querySelector('.rhine-list-filter-bar__main');
|
|
53
|
+
const target = scope?.querySelector<HTMLElement>(focusable);
|
|
54
|
+
(target ?? root.current)?.focus();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function reset() {
|
|
58
|
+
stableFocus();
|
|
59
|
+
onReset?.();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function clear(field: ListFilterField) {
|
|
63
|
+
// A removed chip must not leave keyboard focus on the document body.
|
|
64
|
+
const control = Array.from(root.current?.querySelectorAll<HTMLElement>('[data-filter-id]') ?? [])
|
|
65
|
+
.find(node => node.dataset.filterId === field.id)?.querySelector<HTMLElement>(focusable);
|
|
66
|
+
if (control) control.focus(); else stableFocus();
|
|
67
|
+
field.onClear?.();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return <div ref={root} tabIndex={-1} className={`rhine-list-filter-bar ${className}`}>
|
|
71
|
+
<Surface role="group" aria-label={label} className="rhine-list-filter-bar__surface">
|
|
72
|
+
<div className="rhine-list-filter-bar__main">
|
|
73
|
+
{search && <div className="rhine-list-filter-bar__search"><TextField type="search" {...search} /></div>}
|
|
74
|
+
{visibleFields.length > 0 && <div className="rhine-list-filter-bar__primary">
|
|
75
|
+
{visibleFields.map(field => <div key={field.id} data-filter-id={field.id} className="rhine-list-filter-bar__field" role="group" aria-label={field.label}>{field.control}</div>)}
|
|
76
|
+
</div>}
|
|
77
|
+
{(drawerFields.length > 0 || refresh || action) && <div className="rhine-list-filter-bar__actions">
|
|
78
|
+
{drawerFields.length > 0 && <Drawer
|
|
79
|
+
title={label}
|
|
80
|
+
description="更改后立即更新结果。"
|
|
81
|
+
open={open}
|
|
82
|
+
onOpenChange={setOpen}
|
|
83
|
+
trigger={<Button variant="secondary" aria-label={activeDrawerCount > 0 ? `更多筛选,已选 ${activeDrawerCount} 项` : undefined}>更多筛选{activeDrawerCount > 0 && <span className="rhine-list-filter-bar__count" aria-hidden="true">{activeDrawerCount}</span>}</Button>}
|
|
84
|
+
>
|
|
85
|
+
<div ref={drawerBody} className="rhine-list-filter-bar__drawer">
|
|
86
|
+
<Feedback result={result} pending={pending} error={error} />
|
|
87
|
+
{drawerFields.map(field => <div key={field.id} data-filter-id={field.id} className="rhine-list-filter-bar__field" role="group" aria-label={field.label}>{field.control}</div>)}
|
|
88
|
+
{canReset && <div className="rhine-list-filter-bar__drawer-actions"><Button variant="quiet" onClick={reset}>重置筛选</Button></div>}
|
|
89
|
+
</div>
|
|
90
|
+
</Drawer>}
|
|
91
|
+
{refresh && <Button variant="secondary" onClick={refresh.onClick} busy={refresh.busy} disabled={refresh.disabled} busyLabel="正在刷新">刷新</Button>}
|
|
92
|
+
{action && <div className="rhine-list-filter-bar__action">{action}</div>}
|
|
93
|
+
</div>}
|
|
94
|
+
</div>
|
|
95
|
+
{(activeFields.length > 0 || canReset) && <div className="rhine-list-filter-bar__active" role="group" aria-label="已选条件">
|
|
96
|
+
{activeFields.map(field => field.onClear
|
|
97
|
+
? <Button key={field.id} variant="quiet" className="rhine-list-filter-bar__chip" onClick={() => clear(field)} aria-label={`清除${field.label}:${field.activeLabel}`}><span>{field.label}:{field.activeLabel}</span><span aria-hidden="true">×</span></Button>
|
|
98
|
+
: <span key={field.id} className="rhine-list-filter-bar__summary">{field.label}:{field.activeLabel}</span>)}
|
|
99
|
+
{canReset && <Button variant="quiet" onClick={reset}>重置筛选</Button>}
|
|
100
|
+
</div>}
|
|
101
|
+
<Feedback result={result} pending={pending} error={error} announce={!open} />
|
|
102
|
+
</Surface>
|
|
103
|
+
</div>;
|
|
104
|
+
}
|
|
@@ -7,9 +7,15 @@ export interface OrbitBackdropProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
7
7
|
appearance?: 'theme' | 'original';
|
|
8
8
|
/** Adds a static preference; false never overrides the system or an ancestor. */
|
|
9
9
|
reducedMotion?: boolean;
|
|
10
|
+
/** Omits the local pause control without changing motion preferences. */
|
|
11
|
+
controls?: boolean;
|
|
12
|
+
/** Reserves the upper content area by moving the artwork into the lower right. */
|
|
13
|
+
composition?: 'full' | 'corner';
|
|
14
|
+
/** Removes panel decoration when used as a page background. */
|
|
15
|
+
framed?: boolean;
|
|
10
16
|
}
|
|
11
17
|
|
|
12
|
-
export function OrbitBackdrop({ appearance = 'theme', reducedMotion = false, children, className = '', ...props }: OrbitBackdropProps) {
|
|
18
|
+
export function OrbitBackdrop({ appearance = 'theme', reducedMotion = false, controls = true, composition = 'full', framed = true, children, className = '', ...props }: OrbitBackdropProps) {
|
|
13
19
|
const canvas = useRef<HTMLCanvasElement>(null);
|
|
14
20
|
const renderer = useRef<OrbitRenderer | null>(null);
|
|
15
21
|
const [paused, setPaused] = useState(false);
|
|
@@ -27,9 +33,9 @@ export function OrbitBackdrop({ appearance = 'theme', reducedMotion = false, chi
|
|
|
27
33
|
const label = state === 'reduced' ? '已减少动态' : state === 'compact' ? '窄屏静态画面'
|
|
28
34
|
: canControl ? paused ? '继续动效' : '暂停动效' : '静态画面';
|
|
29
35
|
|
|
30
|
-
return <div {...props} className={`rhine-orbit ${className}`} data-rhine-orbit-appearance={appearance} data-rhine-orbit-state={state}>
|
|
36
|
+
return <div {...props} className={`rhine-orbit ${className}`} data-rhine-orbit-appearance={appearance} data-rhine-orbit-state={state} data-rhine-orbit-controls={controls} data-rhine-orbit-composition={composition} data-rhine-orbit-framed={framed}>
|
|
31
37
|
<canvas ref={canvas} className="rhine-orbit-canvas" aria-hidden="true" />
|
|
32
38
|
<div className="rhine-orbit-content">{children}</div>
|
|
33
|
-
<div className="rhine-orbit-controls"><Button variant="secondary" disabled={!canControl} onClick={() => setPaused(value => !value)}>{label}</Button></div>
|
|
39
|
+
{controls && <div className="rhine-orbit-controls"><Button variant="secondary" disabled={!canControl} onClick={() => setPaused(value => !value)}>{label}</Button></div>}
|
|
34
40
|
</div>;
|
|
35
41
|
}
|
package/components/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
## 间距组合
|
|
12
12
|
|
|
13
|
-
[间距规范](SPACING.md)定义尺度、分组与卡片配方:Surface
|
|
13
|
+
[间距规范](SPACING.md)定义尺度、分组与卡片配方:Surface默认负责24px内边距,嵌套Stack提供8px标题/说明和16px内容/操作间隔,Inline提供8px按钮间隔;Surface不自动给子元素加间距。单独承载表格时使用 0.9.1 的 `padding="none"`,只取消容器外围内距,保留表头/单元格间距。展台`#spacing`可检查默认、加载、错误、长文案及无外围内距表格。
|
|
14
14
|
|
|
15
15
|
## 使用
|
|
16
16
|
|
|
@@ -58,8 +58,9 @@ export function Example() {
|
|
|
58
58
|
| Text | variant;as;tone;numeric;HTML属性 | 默认p/body;视觉层级不决定HTML语义;长文本可换行;不转换数值或截断文本 |
|
|
59
59
|
| Select | label;hint;error;原生单选select属性;option子节点 | 标签与描述自动关联;原生菜单和键盘;受控或defaultValue;不支持multiple/size列表模式;箭头装饰性且不接收点击 |
|
|
60
60
|
| Toolbar | title;description;children;HTML div属性 | 自适应控件组合,role=group与普通Tab顺序;不伪装为需要方向键导航的ARIA toolbar;不含业务状态 |
|
|
61
|
+
| ListFilterBar(0.9.0) | label;search;primary/filters 字段;onReset;refresh;一个 action;result/pending/error | 最多两个常显条件,超出项进入同一 Drawer;宿主维护即时状态、查询和权限;[契约与版本边界](../docs/LIST_FILTER_BAR.md) |
|
|
61
62
|
| Badge | tone=neutral/success/warning/error;children | 只读短状态,颜色必须配文字,不做可点击按钮 |
|
|
62
|
-
| Surface | HTML div 属性 |
|
|
63
|
+
| Surface | padding=default/none;HTML div 属性 | 默认24px内距;none仅取消容器padding,适合纯表格;静态分区,无点击语义;padding自0.9.1提供,见[#24](https://github.com/linhay/rhine-design/issues/24) |
|
|
63
64
|
| Background(0.5.0) | roleTone=canvas/content/sidebar/surface/overlay/media;decoration=auto/theme/none;appearance={color,textColor,texture,textureSize};HTML div 属性 | 默认正文纯色;仅画布默认使用主题纹理;局部覆盖不创建主题根或定位上下文;完整配置见[场景背景](../docs/THEMING.md#场景背景050) |
|
|
64
65
|
| SelectionCard | label;selected;onSelectedChange;disabled;showIndicator=true;children | 原生 checkbox 可取消;是否单选由上层控制;showIndicator=false 仅视觉隐藏勾选框,保留键盘与读屏,卡片边界表达选中;内部不嵌套按钮/链接/其他输入 |
|
|
65
66
|
| TextField | label;hint;error;原生 input 属性 | label 与 id 自动关联;错误与提示组成 aria-describedby;输入值由调用方管理 |
|
package/components/SPACING.md
CHANGED
|
@@ -52,6 +52,12 @@
|
|
|
52
52
|
|
|
53
53
|
页面已有h1时卡片通常用h2;标题等级由结构决定,不能用字号决定。不要为布局引入空Text、空段落或连续br。
|
|
54
54
|
|
|
55
|
+
### 只承载表格的 Surface
|
|
56
|
+
|
|
57
|
+
纯表格使用 `<Surface padding="none"><DataTable … /></Surface>`,取消容器外围24px内距,表头和单元格仍使用 DataTable 自身的阅读间距。该公开 prop 自 0.9.1 提供,见 [#24](https://github.com/linhay/rhine-design/issues/24);默认 `padding="default"` 保持原来行为。
|
|
58
|
+
|
|
59
|
+
这不是所有卡片的紧凑开关:正文、表单与混合内容继续采用默认配方。表格上方有标题/操作时,将其放到表格之外或显式分区,不把表头、单元格、分页和焦点空间一并清零;不为了边缘贴合设置 `overflow:hidden` 裁掉焦点。真实选择与长名称示例见 `#spacing`。
|
|
60
|
+
|
|
55
61
|
行高与间距分别管理:正文14/22px、标题16/24px,独立正文段落默认12px段间距;不是在每一行之间再加gap。详见[行高规范](TYPOGRAPHY.md#行高段落与控件)。
|
|
56
62
|
|
|
57
63
|
## 常见组合
|
|
@@ -19,8 +19,11 @@ export function Button({ variant = 'primary', busy = false, busyLabel = '处理
|
|
|
19
19
|
export function Badge({ tone = 'neutral', children }: { tone?: Tone; children: ReactNode }) {
|
|
20
20
|
return <span className={`rhine-badge rhine-tone-${tone}`}>{children}</span>;
|
|
21
21
|
}
|
|
22
|
-
export
|
|
23
|
-
|
|
22
|
+
export interface SurfaceProps extends HTMLAttributes<HTMLDivElement> {
|
|
23
|
+
padding?: 'default' | 'none';
|
|
24
|
+
}
|
|
25
|
+
export function Surface({ className, padding = 'default', ...props }: SurfaceProps) {
|
|
26
|
+
return <div {...props} className={classes('rhine-surface', padding === 'none' && 'rhine-surface--padding-none', className)} />;
|
|
24
27
|
}
|
|
25
28
|
export function SelectionCard({ label, selected, onSelectedChange, disabled = false, showIndicator = true, children }: {
|
|
26
29
|
label: string; selected: boolean; onSelectedChange: (value: boolean) => void; disabled?: boolean; showIndicator?: boolean; children: ReactNode;
|
|
@@ -13,10 +13,20 @@
|
|
|
13
13
|
.rhine-theme[data-rhine-theme=arknights] .rhine-shell__content-header { border-bottom-color:var(--rhine-green); }
|
|
14
14
|
.rhine-shell__content { flex:1; min-width:0; min-height:0; overflow:auto; padding:24px; overflow-wrap:anywhere; }
|
|
15
15
|
@media(max-width:760px) { .rhine-shell__content-header { padding:8px 16px; } .rhine-shell__content { padding:16px; } }
|
|
16
|
+
/* Inline-size containment must retain an intrinsic width in wrapping Inline layouts. */
|
|
17
|
+
.rhine-date-field { container:rhine-date-field / inline-size; contain-intrinsic-inline-size:180px; }
|
|
18
|
+
@container rhine-date-field (max-width:180px) {
|
|
19
|
+
.rhine-date-field input[type=date] { padding-inline:var(--rhine-space-1); }
|
|
20
|
+
}
|
|
21
|
+
.rhine-date-range-container { width:100%; min-width:0; container:rhine-date-range / inline-size; }
|
|
16
22
|
.rhine-date-range { margin:0; min-width:0; padding:16px; border:1px solid var(--rhine-line); background:var(--rhine-field-group-bg); }
|
|
17
23
|
.rhine-date-range legend { padding:0 4px; font-weight:600; }
|
|
18
24
|
.rhine-date-range__fields { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,200px),1fr)); gap:16px; margin-bottom:16px; }
|
|
19
25
|
.rhine-date-range input { max-width:100%; min-width:0; }
|
|
26
|
+
@container rhine-date-range (max-width:280px) {
|
|
27
|
+
.rhine-date-range { padding-inline:0; border-inline:0; }
|
|
28
|
+
.rhine-date-range__fields { grid-template-columns:minmax(0,1fr); }
|
|
29
|
+
}
|
|
20
30
|
.rhine-number-field { display:grid; gap:8px; min-width:0; }
|
|
21
31
|
.rhine-multi-select { min-width:0; }
|
|
22
32
|
.rhine-multi-select__tags { display:flex; flex-wrap:wrap; gap:8px; list-style:none; padding:0; margin:8px 0; }
|
package/components/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export { Select } from './Select';
|
|
|
26
26
|
export type { SelectProps } from './Select';
|
|
27
27
|
export { Toolbar } from './Toolbar';
|
|
28
28
|
export type { ToolbarProps } from './Toolbar';
|
|
29
|
+
export { ListFilterBar, type ListFilterBarProps, type ListFilterField } from './ListFilterBar';
|
|
29
30
|
export { Icon, IconButton, Link, ButtonLink, CopyButton } from './Actions';
|
|
30
31
|
export type { IconName, ButtonLinkProps } from './Actions';
|
|
31
32
|
export { Divider, Stack, Inline, Grid } from './Layout';
|
|
@@ -43,6 +44,6 @@ export { Fieldset, ErrorSummary } from './FormWorkflow';
|
|
|
43
44
|
export type { FieldsetProps, FieldError, ErrorSummaryProps } from './FormWorkflow';
|
|
44
45
|
|
|
45
46
|
export { Button, Badge, Surface, SelectionCard, TextField, Tabs, SegmentedControl, Notice, Progress, Skeleton, Disclosure, EmptyState } from './components';
|
|
46
|
-
export type { ButtonProps, Tone, Choice, TabItem } from './components';
|
|
47
|
+
export type { ButtonProps, SurfaceProps, Tone, Choice, TabItem } from './components';
|
|
47
48
|
|
|
48
49
|
export { ThemePicker, ThemePreview, type ThemePickerProps, type ThemePickerOption, type ThemePreviewProps } from './ThemePicker';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
.rhine-list-filter-bar { min-width:0; container-type:inline-size; }
|
|
2
|
+
.rhine-list-filter-bar__surface { display:grid; gap:var(--rhine-space-3); padding:var(--rhine-space-4); }
|
|
3
|
+
.rhine-list-filter-bar__main { display:flex; flex-wrap:wrap; align-items:flex-end; gap:var(--rhine-space-3); min-width:0; }
|
|
4
|
+
.rhine-list-filter-bar__search { flex:2 1 220px; min-width:0; }
|
|
5
|
+
.rhine-list-filter-bar__primary { display:flex; flex:2 1 300px; flex-wrap:wrap; align-items:flex-end; gap:var(--rhine-space-3); min-width:0; }
|
|
6
|
+
.rhine-list-filter-bar__primary>.rhine-list-filter-bar__field { flex:1 1 140px; }
|
|
7
|
+
.rhine-list-filter-bar__field { min-width:0; overflow-wrap:anywhere; }
|
|
8
|
+
.rhine-list-filter-bar__field>.rhine-field>label { font-size:var(--rhine-text-caption); line-height:var(--rhine-leading-caption); }
|
|
9
|
+
.rhine-list-filter-bar__field>* { max-width:100%; }
|
|
10
|
+
.rhine-list-filter-bar__actions { display:flex; flex:0 1 auto; align-items:center; flex-wrap:wrap; gap:var(--rhine-space-2); min-width:0; margin-inline-start:auto; }
|
|
11
|
+
.rhine-list-filter-bar__actions>* { max-width:100%; }
|
|
12
|
+
.rhine-list-filter-bar__action { min-width:0; }
|
|
13
|
+
.rhine-list-filter-bar__action .rhine-button { max-width:100%; }
|
|
14
|
+
.rhine-list-filter-bar__count { display:inline-flex; align-items:center; justify-content:center; min-width:20px; min-height:20px; margin-inline-start:var(--rhine-space-2); padding-inline:var(--rhine-space-1); border-radius:var(--rhine-radius-control); background:var(--rhine-selected); color:var(--rhine-ink); font-variant-numeric:tabular-nums; }
|
|
15
|
+
.rhine-list-filter-bar__active { display:flex; flex-wrap:wrap; align-items:center; gap:var(--rhine-space-2); min-width:0; }
|
|
16
|
+
.rhine-list-filter-bar__active>.rhine-button { max-width:100%; }
|
|
17
|
+
.rhine-list-filter-bar__chip { text-align:start; }
|
|
18
|
+
.rhine-list-filter-bar__chip>span { display:flex; align-items:center; gap:var(--rhine-space-2); min-width:0; }
|
|
19
|
+
.rhine-list-filter-bar__chip>span>span:first-child { overflow-wrap:anywhere; }
|
|
20
|
+
.rhine-list-filter-bar__chip>span>span:last-child { flex-shrink:0; }
|
|
21
|
+
.rhine-list-filter-bar__summary { padding:var(--rhine-space-2); color:var(--rhine-muted); overflow-wrap:anywhere; }
|
|
22
|
+
.rhine-list-filter-bar__feedback { display:flex; flex-wrap:wrap; gap:var(--rhine-space-1) var(--rhine-space-3); color:var(--rhine-muted); font-size:var(--rhine-text-caption); line-height:var(--rhine-leading-caption); overflow-wrap:anywhere; }
|
|
23
|
+
.rhine-list-filter-bar__feedback>* { min-width:0; }
|
|
24
|
+
.rhine-list-filter-bar__drawer { display:grid; gap:var(--rhine-space-6); min-width:0; }
|
|
25
|
+
.rhine-list-filter-bar__drawer-actions { display:flex; justify-content:flex-start; }
|
|
26
|
+
@container (max-width:640px) {
|
|
27
|
+
.rhine-list-filter-bar__search { flex-basis:100%; }
|
|
28
|
+
.rhine-list-filter-bar__primary { flex-basis:100%; }
|
|
29
|
+
.rhine-list-filter-bar__actions { width:100%; margin-inline-start:0; }
|
|
30
|
+
}
|
|
31
|
+
@container (max-width:360px) {
|
|
32
|
+
.rhine-list-filter-bar__primary>.rhine-list-filter-bar__field { flex-basis:100%; }
|
|
33
|
+
}
|
package/components/orbit.css
CHANGED
|
@@ -118,3 +118,24 @@
|
|
|
118
118
|
.rhine-orbit .rhine-orbit-content .rhine-button--quiet:focus-visible { outline:none; box-shadow:inset 0 0 0 2px currentColor; }
|
|
119
119
|
.rhine-orbit-controls { position:absolute; right:var(--rhine-space-4); bottom:var(--rhine-space-4); z-index:2; max-width:calc(100% - var(--rhine-space-4) * 2); }
|
|
120
120
|
.rhine-orbit-controls .rhine-button { max-width:100%; }
|
|
121
|
+
|
|
122
|
+
/* Public composition options keep both renderers and static artwork in the same region. */
|
|
123
|
+
.rhine-orbit[data-rhine-orbit-controls="false"] > .rhine-orbit-content { padding-bottom:var(--rhine-space-6); }
|
|
124
|
+
.rhine-orbit[data-rhine-orbit-composition="corner"] > .rhine-orbit-canvas,
|
|
125
|
+
.rhine-orbit[data-rhine-orbit-composition="corner"][data-rhine-orbit-state="unavailable"]::after,
|
|
126
|
+
.rhine-theme[data-rhine-theme] .rhine-orbit[data-rhine-orbit-composition="corner"][data-rhine-orbit-state="unavailable"]::after {
|
|
127
|
+
inset:auto 0 0 auto;
|
|
128
|
+
width:min(100%, 480px); height:44%; max-height:360px;
|
|
129
|
+
}
|
|
130
|
+
.rhine-orbit[data-rhine-orbit-framed="false"],
|
|
131
|
+
.rhine-theme[data-rhine-theme] .rhine-orbit[data-rhine-orbit-framed="false"] {
|
|
132
|
+
border:0; border-radius:0; box-shadow:none;
|
|
133
|
+
}
|
|
134
|
+
/* Older packaged artwork painted this fallback on the full root background. */
|
|
135
|
+
.rhine-theme[data-rhine-theme="neo-brutalism"] .rhine-orbit[data-rhine-orbit-appearance="theme"][data-rhine-orbit-composition="corner"][data-rhine-orbit-state="unavailable"] {
|
|
136
|
+
background-image:none;
|
|
137
|
+
}
|
|
138
|
+
.rhine-theme[data-rhine-theme="neo-brutalism"] .rhine-orbit[data-rhine-orbit-appearance="theme"][data-rhine-orbit-composition="corner"][data-rhine-orbit-state="unavailable"]::after {
|
|
139
|
+
display:block;
|
|
140
|
+
background:url('./assets/orbit-donut.svg') center / contain no-repeat;
|
|
141
|
+
}
|
package/components/styles.css
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
.rhine-tone-error { --rhine-tone:var(--rhine-error); --rhine-tone-bg:var(--rhine-error-bg); }
|
|
36
36
|
.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); }
|
|
37
37
|
.rhine-surface { padding:var(--rhine-space-6); border:1px solid var(--rhine-line); border-radius:var(--rhine-radius-card); background:var(--rhine-surface-bg); color:var(--rhine-surface-text); min-width:0; }
|
|
38
|
+
.rhine-surface--padding-none { padding:0; }
|
|
38
39
|
.rhine-selection { display:block; border:1px solid var(--rhine-control); background:var(--rhine-surface); border-radius:var(--rhine-radius-card); padding:var(--rhine-space-6); 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); }
|
|
39
40
|
.rhine-selection:hover:not([data-disabled=true]) { border-color:var(--rhine-green); background:#f4f7ef; }
|
|
40
41
|
.rhine-selection[data-selected=true] { border-color:var(--rhine-green); background:var(--rhine-selected); box-shadow:inset 0 0 0 1px var(--rhine-green); }
|
package/components/timeline.css
CHANGED
|
@@ -26,9 +26,10 @@
|
|
|
26
26
|
.rhine-timeline-popover { z-index:50; width:min(360px,calc(100vw - 24px)); max-height:var(--radix-popover-content-available-height); overflow:auto; padding:16px; border:1px solid var(--rhine-control); border-radius:var(--rhine-radius-card); background:var(--rhine-overlay-bg); color:var(--rhine-overlay-text); box-shadow:0 4px 12px #242b2814; overflow-wrap:anywhere; }
|
|
27
27
|
.rhine-timeline-popover .rhine-key-value { margin-block:16px; }
|
|
28
28
|
.rhine-timeline--compact:not(:hover):not(:focus-within) .rhine-timeline__bars [data-selected=true] { height:28px; outline:none; }
|
|
29
|
-
.rhine-timeline-list { list-style:none; margin:0; padding:4px 4px var(--rhine-space-
|
|
29
|
+
.rhine-timeline-list { list-style:none; margin:0; padding:4px 4px var(--rhine-space-2); min-width:0; }
|
|
30
30
|
.rhine-timeline-list__item { display:grid; gap:var(--rhine-space-2); min-width:0; padding-block:var(--rhine-space-4); }
|
|
31
31
|
.rhine-timeline-list__item:first-child { padding-top:0; }
|
|
32
|
+
.rhine-timeline-list__item:last-child { padding-bottom:0; }
|
|
32
33
|
.rhine-timeline-list__item+.rhine-timeline-list__item { border-top:1px solid var(--rhine-line); }
|
|
33
34
|
.rhine-timeline-list__heading { display:flex; flex-wrap:wrap; justify-content:space-between; align-items:baseline; gap:var(--rhine-space-2); min-width:0; overflow-wrap:anywhere; }
|
|
34
35
|
.rhine-timeline-list__heading>* { min-width:0; }
|
|
@@ -114,6 +114,10 @@ SideNav 继续负责当前项、长名称和 aria-current;Drawer 继续负责
|
|
|
114
114
|
|
|
115
115
|
DateField 新增 `readOnly/required/id/name/hint/onValidationChange`;外部日期或 min/max 改变会更新格式/范围错误,不自动回写值。DateRangePicker 同样支持 readOnly/required/hint/onValidationChange,另有 `clearable`(默认 true)、`startLabel/endLabel/startName/endName`。预设含非法日期、反向区间或任一端越界时禁用。onValidationChange 回传格式/范围/显式 error,required 仍走提交时的原生表单校验。只读时保留浏览与聚焦,预设、清空、步进和选择不能更改值。
|
|
116
116
|
|
|
117
|
+
原生日期的未完成片段可能同时表现为 `value=""` 与 `validity.badInput=true`,不能把它当作用户清空边界。DateField 保留此局部编辑状态并回传“请填写完整有效的日期。”,完整有效提议仍由宿主受控接受或拒绝;DateRangePicker 汇总两端校验。内置清空/预设同时清掉原生草稿;宿主在已接受值不变时强制恢复整个字段,须用独立 reset revision/key 明确重置,不能期待同值 props 推断重置意图。这些行为已包含于 #22 主线修复,消费旧副本需单独升级。
|
|
118
|
+
|
|
119
|
+
窄容器的重复横向内距由共享日期组处理:0.9.1 的 [#25](https://github.com/linhay/rhine-design/issues/25) 修复在实际容器不超过280 CSS px时取消日期组横向 padding/边框,并单列排列两端;DateField 自身不超过180 CSS px时仅将水平 padding 收到已有4px token,保持输入字号、44px 高度、完整日期和原生日历入口。此尺寸修复不更改日期值/API。400px视口、200% CSS zoom 的 Drawer 内输入曾仅116 CSS px,且 `scrollWidth === clientWidth`,因此必须实际读写最后日期片段,不能仅以无 DOM 溢出验收。单个 DateField 的宽度仍由宿主容器提供,同样需核对实际编辑区;本修复不承诺任意小宽度都能完整容纳浏览器原生日期。
|
|
120
|
+
|
|
117
121
|
NumberField 支持小数步进(默认 1);precision 限制为 0–15 位,未指定时普通提交保留解析后的 JS 数值,步进清除至 12 位的浮点尾差;IEEE-754 数值精度不适用于高精度货币账本。`-`、`1.` 等草稿在编辑时不改写;外部 value 真正变化时覆盖草稿。未改变值的失焦/Enter 不触发 onValueChange;输入法组词时不提交或步进。
|
|
118
122
|
|
|
119
123
|
NumberField 另有 `readOnly/required/id/name/hint/controls`(按钮默认显示);Escape 取消未提交草稿并恢复受控值,第二次 Escape 可交给外层浮层。min/max 变化会直接标明当前越界,不伪造用户输入或自动修改宿主值。空值步进按有效范围处理,不再把空字符串当成已达到零边界。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 0.9.0 背景与筛选验收
|
|
2
|
+
|
|
3
|
+
基线:v0.8.0;目标:#17、#19。2026-10-24 Asia/Shanghai,macOS Chromium,固定 Vite production preview。展示记录均为 synthetic,无业务 API;不宣称 Gateway 生产权限、查询或部署已验证。
|
|
4
|
+
|
|
5
|
+
- `npm run check`:31 文件、245 测试、TypeScript 与构建通过。
|
|
6
|
+
- `scripts/check-background-filters.js`:三主题 × 400/520/880px × 1/2 CSS zoom × 三类列表,共54组;主筛选上限、摘要清除、焦点归还、加载/错误/空态/禁用;背景三主题 ×400/520/880/1280px ×1/2 CSS zoom,共24组,文字避让、不透明表单、无控件/边框、容器断点和输入/Canvas节点保留。
|
|
7
|
+
- `scripts/check-filter-native-date.js`:真实日期分段键盘八项,删除片段不扩大已接受范围、补全年份、初始空值的 keyup-only 片段、清空及同值预设恢复、三种展台整体重置清理同值日期草稿。
|
|
8
|
+
- `scripts/check-background-motion.js`:Three.js实际像素变化,手动/系统减少动态、恢复、零尺寸、离屏、模拟 document.hidden、卸载停帧/观察器清理,Canvas不可用时三主题静态几何与实际画布一致。自动化 Chromium 的标签切换不产生真实 document.hidden,保留该限制;不以事件模拟冒充真实后台切换。GPU内部共享资源由renderer.dispose/forceContextLoss收口,不能仅以独立delete调用数量判泄漏。
|
|
9
|
+
|
|
10
|
+
审查修复:搜索 label 保持可见,整体重置同步清理日期内部草稿;独立复核修复:有效日期被宿主拒绝时保持受控值;展台窗口断点改为容器查询;提供宽艺术栏/窄全页的同节点入口组合;仅剩非聚焦字段摘要时,清除后焦点落在筛选区域。首轮浏览器脚本在Drawer动画未结束/焦点恢复前断言,稳定状态复核已修正;原始失败记录与修正后证据保留在根仓库 `.local/artifacts/rhine-design-v0.9.0/`。
|
|
11
|
+
|
|
12
|
+
运行脚本前用 `npm run build`,将 dist 固定服务到127.0.0.1:18484。动效脚本须新浏览器会话,防止旧 addInitScript instrumentation 重复注册。公开脚本不包含认证或业务数据。
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
外层 `Surface` 已定义对象边界时,内部额度摘要不再包细线圆角框。进度、剩余额度、重置时间与预测状态通过 `Progress`、`Text`、`Inline` 和 `Stack` 组合,必要分区使用 `Divider`;不能移除未知或预测失败的文字。
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
事实按共享维度配对:基础信息把优先级、密钥数量与账号到期排列在同一行,窄卡片中到期独占一行;订阅到期独立呈现。今日和本周期占两个稳定等宽列,消费与生图按行对齐。复用 `Grid`、`Inline`、`Stack` 和 `Text`,不把每个字段装成胶囊或小卡片,也不混用不同的标签列宽。压缩来自移除装饰、重复标签和无意义空白,不减小业务字号、不裁切数字、不固定高度。保留双成本的顺序和含义、未知 `—` 与真实 `0`,不相加或相互替换。长内容和 200% 缩放允许自然增高。
|
|
18
18
|
|
|
19
19
|
## ButtonLink:具有按钮外观的原生链接
|
|
20
20
|
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
|
|
52
52
|
`StatusTimelineList` 负责稳定行身份与详情互斥,公共契约见[应用组件](APPLICATION_COMPONENTS.md)。时间窗口、统计范围、unknown/partial/error 与实际业务判定仍由宿主保留;短标签不改变详情证据。
|
|
53
53
|
|
|
54
|
+
列表末项不叠加行间距:最后一项底部为零,列表仅保留 8px 操作余量,容纳 28px 时间条外延的 44px 命中区。与 `Stack gap={1}` 的图例组合时,时间条到图例容器的间距为 12px,不能靠宿主负 margin 或缩小命中区压缩。多条列表中间仍保留分隔留白。此修正及事实列对齐由 [issue #23](https://github.com/linhay/rhine-design/issues/23) 跟踪,自 0.9.1 提供;使用前核对实际安装版本。
|
|
55
|
+
|
|
54
56
|
时间轴与图例的浮层内部归一化定位包装层的祖先 CSS zoom,正文保留等效缩放与视口内滚动。此修复仅覆盖这两类详情,不宣称其他浮层已支持主题根 CSS zoom;浏览器原生网页缩放与 CSS zoom 是不同的证据。跨卡鼠标移走后延迟关闭,并允许移入详情继续阅读;键盘焦点和触屏操作不被鼠标离开逻辑打断。
|
|
55
57
|
|
|
56
58
|
## 验收边界
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# 紧凑布局回归:#23 / #24 / #25
|
|
2
|
+
|
|
3
|
+
2026-10-25,在 `npm run check` 生成的固定生产预览中验证。内容均为合成数据,这是 PR #26 的修复验收记录,修复归入 0.9.1;发布状态以 registry 与 GitHub Release 为准。本次没有重跑 Gateway 消费端或生产链路。
|
|
4
|
+
|
|
5
|
+
## 检查范围
|
|
6
|
+
|
|
7
|
+
| 项目 | 范围与交互 |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| 自动检查 | 31 个测试文件、246 项测试;TypeScript;Vite 构建及 CSS/SVG 产物检查 |
|
|
10
|
+
| 筛选 Drawer | 三主题 × 400/520/880px × 100%/200% CSS zoom,共 18 组;原生日期末段增减、清空、非法片段、整体重置、Escape 归焦 |
|
|
11
|
+
| 精确日期容器 | 三主题 × 实际 150/200/280 CSS px,共 9 组;200% CSS zoom;范围与 Inline 内独立 DateField 分别编辑、清空、重置;断言实际容器宽度 |
|
|
12
|
+
| 无外围内距表格 | 三主题 × 三视口 × 两种 CSS zoom,共 18 组;外围 padding 为 0、单元格内距保留、鼠标勾选和 Space 取消、局部滚动至末列 |
|
|
13
|
+
| 紧凑账号 | 三主题 × 三视口 × 两种内容 CSS zoom × 正常/零值/未知/长值,共 72 组;事实列对齐、局部溢出、12px 尾距、44px 命中区,以及键盘/鼠标详情和图例 |
|
|
14
|
+
| 原生日期既有契约 | 8 项真实键盘回归:未完成片段不覆盖已接受范围、修复年份、空值片段、清空、同值预设及三个筛选示例的整体重置 |
|
|
15
|
+
| 最终重置焦点复核 | 三主题 × 三种实际容器宽度,共 9 项;清除独立日期后输入非法片段,重置恢复日期、移除错误并保留按钮焦点 |
|
|
16
|
+
|
|
17
|
+
`DateField` 的宿主拒绝合法提议、禁用/只读与外部值同步继续由 `tests/DateNativeInput.test.tsx` 覆盖。本次尺寸修复不改变日期字符串、时区或查询归属。
|
|
18
|
+
|
|
19
|
+
精确宽度示例使用 1280px 外层视口容纳展台侧栏;400/520/880px 窄屏另由真实 Drawer 矩阵覆盖。在 880px 外层、200% CSS zoom 下,标称 200/280px 的示例都会被 `max-width:100%` 夹到 192px,因此不能把选项值当成实际宽度证据。
|
|
20
|
+
|
|
21
|
+
## 截图
|
|
22
|
+
|
|
23
|
+
以下为 150 CSS px 容器内独立日期的三主题截图。完整年月日、日历入口与正常字号共同核对;实际末段编辑由浏览器脚本验证,截图或 `scrollWidth` 单独均不足以验收。
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+

|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
表格及长值账号的真实组件展台:
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
## 重跑
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm run check
|
|
38
|
+
npx vite preview --host 127.0.0.1 --port 18525 --strictPort
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
另一终端使用已安装的 `playwright-cli`,打开同一预览后执行:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
mkdir -p output/playwright/issues-23-25
|
|
45
|
+
playwright-cli -s=rhine-layout open 'http://127.0.0.1:18525/#list-filter-bar' --headed
|
|
46
|
+
playwright-cli -s=rhine-layout run-code --filename scripts/check-compact-layouts.js
|
|
47
|
+
playwright-cli -s=rhine-layout run-code --filename scripts/check-filter-native-date.js
|
|
48
|
+
playwright-cli -s=rhine-layout close
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
确认返回结果分别为 `pass: true` 和 `complete: true`,再逐一检查日期截图。脚本以当前页 origin 为预览地址,不依赖固定端口。
|
|
52
|
+
|
|
53
|
+
证据限于本次 Chromium 与 CSS zoom;不等同于浏览器菜单缩放、其他浏览器或任意更小容器验收。构建通过,仍有现有的超过 500kB chunk 提示。
|
package/docs/DESIGN_SYSTEM.md
CHANGED
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
|
|
24
24
|
移除标题不等于移除操作行:窄屏抽屉、收起侧栏后的恢复入口、刷新和必要状态必须持续可达。没有可见内容的操作容器无需为标题预留额外高度;若承载恢复入口,应按实际可见控件保留。只有存在真实父子资源层级且用户需要返回上级时才使用 Breadcrumbs,不把它作为每个页面必填的装饰。组件本身继续提供该能力。组合与验收见[应用布局契约](APPLICATION_COMPONENTS.md#平级工作区顶部操作区)。
|
|
25
25
|
|
|
26
|
+
### 列表顶部的复杂度约束
|
|
27
|
+
|
|
28
|
+
列表搜索与筛选优先使用共享 `ListFilterBar`:可选搜索、最多两个常显条件、统一的更多筛选 Drawer、活跃条件摘要、结果与刷新,再加至多一个业务操作。少条件页面不补空入口,多条件页面不靠新增控件行扩张。筛选业务状态与服务端查询仍归宿主,Drawer 内同步显示加载/错误;批量动作留在结果区。该 API 在 0.9.0 新增,消费时核对安装版本;契约、消融依据与真实展台见[列表筛选栏](LIST_FILTER_BAR.md)。
|
|
29
|
+
|
|
26
30
|
## 3. 视觉变量
|
|
27
31
|
|
|
28
32
|
公开样式接口统一使用 `rhine-`:主题根 `.rhine-theme`、变量 `--rhine-*`、组件类与关键帧 `rhine-*`、减少动态属性 `data-rhine-motion="reduced"`。消费项目的品牌名称只通过内容与组件参数传入,不进入共享命名空间。
|
|
@@ -78,6 +82,8 @@
|
|
|
78
82
|
- 400/520/880px作为展台验收宽度;消费项目按其最小容器尺寸追加验证。
|
|
79
83
|
- 指标与表格按容器重排;数字和说明可换行,必要时表格局部滚动,不以超小字号塞入全部列。
|
|
80
84
|
- 100% 与 200% 字体缩放下允许纵向增长、重排;不固定卡片高度、不裁掉报错和主要操作。
|
|
85
|
+
- Surface 只承载表格时使用 `padding="none"` 取消外围内距;表头、单元格与分页仍保留自身阅读间距,不连带压缩所有卡片。该 prop 自 0.9.1 提供,见 [#24](https://github.com/linhay/rhine-design/issues/24),默认行为不变,示例见 `#spacing` 与[间距规范](../components/SPACING.md#只承载表格的-surface)。
|
|
86
|
+
- 窄屏按真实局部容器验收,不能只检查 document 无横向溢出。抽屉、分栏、卡片中的输入应在 200% 内容缩放后仍看清并可编辑完整值;原生 date 的日期片段和日历图标、搜索文字与清空按钮均须有实际可用宽度。先移除重复分组内距或改为单列,不缩小字体、隐藏关键图标或用 overflow 裁掉内容来通过检查。
|
|
81
87
|
|
|
82
88
|
|
|
83
89
|
## 4. 组件与内容契约
|
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
| 某时段的时间与状态 | 悬停、键盘聚焦时显示 | 同时提供可访问名称;触屏也能聚焦/操作 |
|
|
32
32
|
| 失败、未知、数据不完整、关键陈旧状态 | 相关区域常显 | 不用减少文字来掩盖数据质量问题 |
|
|
33
33
|
|
|
34
|
+
## 局部宽度与输入可用性
|
|
35
|
+
|
|
36
|
+
400px 视口并不代表控件有400px可用:需扣除宿主侧栏、卡片、Drawer 与字段分组的逐层内距。在 200% CSS zoom 下分别测控件边框、实际文本编辑区与邻接按钮,检查完整值、placeholder、标签、错误和焦点;原生日期必须实际聚焦并编辑最后一段,核对年月日与日历入口均未被挤掉。
|
|
37
|
+
|
|
38
|
+
整页 `scrollWidth <= clientWidth` 只是外层检查,不能证明内部没有裁切。验收同时保留局部截图与关键输入交互;控件的 scrollWidth 也不能直接反映浏览器原生 date 的内部编辑区。优先容器查询与单列重排、去掉重复内距,保留正常字号及操作目标,不通过全局 overflow:hidden、字体缩小或隐藏日历入口掩盖问题。
|
|
39
|
+
|
|
34
40
|
## 历史健康记录
|
|
35
41
|
|
|
36
42
|
`StatusTimeline({label,segments,startLabel,endLabel})` 接收按时间排序的 `{id,label,status}`;status仅限 healthy/recovered/degraded/failed/unknown。宿主负责证据分类、时区、缺失时段补unknown和陈旧说明;组件不计算评分、不查询后端。`TimelineLegend()`提供五种状态的文字和色块,恢复态另加纹理。两者从公共barrel导入并使用同一主题。
|
|
@@ -49,8 +55,12 @@ SideNav采用44px最小操作行、14/22文字、4px行间距,组间16px;图
|
|
|
49
55
|
|
|
50
56
|
## 快速决策
|
|
51
57
|
|
|
58
|
+
列表顶部的搜索、排序与条件使用 [ListFilterBar](LIST_FILTER_BAR.md) 约束,最多两个常显条件,其余在同一 Drawer 内即时修改;结果反馈在抽屉内也可见。无搜索场景不增加空搜索,批量操作不混入筛选。该组件在 0.9.0 新增,`Toolbar` 的任意 children 组合不能代替这些约束。
|
|
59
|
+
|
|
52
60
|
日期过滤优先采用[日期筛选组合](DATE_RANGE_FILTER.md):少量常用预设使用一个 SegmentedControl,具体范围由独立日期按钮持续显示,Popover 内组合低频预设与 DateRangePicker 草稿。预设即时生效,自定义显式应用;取消与关闭不提交。真实展台为 `#date-range-filter`,组合使用现有 0.6.0 公开组件,没有新增公共 API。
|
|
53
61
|
|
|
62
|
+
原生日期框的未完成片段可能同时满足 `value === ''` 与 `validity.badInput`,不能当作用户清空范围。DateField 保留未完成草稿并报告校验,DateRangePicker 汇总字段错误;补全原生片段后才回传日期值,范围限制继续由校验与宿主提交边界处理,明确清空与快捷项可丢弃草稿。整体重置必须通知局部日期状态,即使已接受日期字符串没有变化。验收使用真实键盘编辑日期片段,并证明期间没有扩大查询范围;只设置完整 ISO 值的测试不覆盖此行为。此修复随本地列表筛选候选验证,不倒推为既有已发布版本能力。
|
|
63
|
+
|
|
54
64
|
| 场景 | 组件组合 | 交互与状态 | 不宜使用 |
|
|
55
65
|
| --- | --- | --- | --- |
|
|
56
66
|
| 跨工作区、跨模块 | SideNav;仅真实父子层级按需加 Breadcrumbs | 当前页有文字、边界与 aria-current;平级工作区顶部保留状态与操作,不重复页名;路由变化后由宿主聚焦主内容 | Tabs 不代替全局导航;面包屑不显示浏览历史、不作必填标题 |
|
|
@@ -65,7 +75,7 @@ SideNav采用44px最小操作行、14/22文字、4px行间距,组间16px;图
|
|
|
65
75
|
| 持续错误、离线或权限状态 | Notice | 就地说明原因、影响、下一步;需行动的信息持续显示 | 不用自动消失通知掩盖失败 |
|
|
66
76
|
| 首次加载 | Skeleton / Progress | 已知结构用骨架,未知进度不写百分比 | 不伪造进度,不循环品牌标志 |
|
|
67
77
|
| 保存、刷新 | Button busy + 保留旧数据 | 防重复提交;刷新明确旧值和更新时间;真实响应决定成败 | 不清空阅读上下文,不提前显示成功 |
|
|
68
|
-
| 大列表查询 |
|
|
78
|
+
| 大列表查询 | ListFilterBar + DataTable + Pagination;候选 API 先核对安装版本 | 最多两个常显条件;宿主处理防抖、IME、取消与过期响应;服务端返回筛选排序分页 | 不为 UI 便利拉取全量本地筛选,不把低频条件继续堆在顶部 |
|
|
69
79
|
| 少量/较多互斥选项 | RadioGroup / Select | Radio 展开选项便于比较,Select 压缩较长清单 | Menu 是命令,不能代替选择器 |
|
|
70
80
|
| 多选 / 即时二态设置 | Checkbox / Switch | Checkbox 选择项;Switch 即时偏好,若需保存明确说明 | 不混同“选中”与“已生效” |
|
|
71
81
|
| 补充说明 / 局部交互 | Tooltip / Popover | Tooltip 不放操作;Popover 可交互;鼠标、键盘均可触发 | 必要说明必须常驻,不能只在 hover 下出现 |
|