@linhey/rhine-design 0.8.0 → 0.9.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 +7 -0
- package/components/AdvancedForms.tsx +38 -8
- package/components/ListFilterBar.tsx +104 -0
- package/components/OrbitBackdrop.tsx +9 -3
- package/components/README.md +1 -0
- package/components/index.ts +1 -0
- package/components/list-filter-bar.css +33 -0
- package/components/orbit.css +21 -0
- package/docs/BACKGROUND_FILTER_VERIFICATION.md +12 -0
- package/docs/DESIGN_SYSTEM.md +4 -0
- package/docs/INTERACTION_PATTERNS.md +5 -1
- package/docs/LIST_FILTER_BAR.md +73 -0
- package/docs/ORBIT_BACKDROP.md +25 -0
- package/package.json +1 -1
- package/preview/BackgroundFiltersFixture.tsx +28 -0
- package/preview/GalleryShell.tsx +1 -1
- package/preview/ListFilterBarGallery.tsx +170 -0
- package/preview/OrbitGallery.tsx +14 -0
- package/preview/main.tsx +4 -1
- package/preview/preview.css +15 -0
- package/scripts/check-background-filters.js +58 -0
- package/scripts/check-background-motion.js +169 -0
- package/scripts/check-filter-native-date.js +47 -0
- package/tests/DateNativeInput.test.tsx +147 -0
- package/tests/ListFilterBar.test.tsx +198 -0
- package/tests/OrbitBackdrop.test.tsx +28 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# 版本记录
|
|
2
2
|
|
|
3
|
+
## 0.9.0(2026-10-24)
|
|
4
|
+
|
|
5
|
+
- #17:OrbitBackdrop 新增 controls、composition 和 framed;背景画布与静态降级同置右下角,新增按容器宽度重排的入口组合,保留输入和减少动态。
|
|
6
|
+
- #19:新增 ListFilterBar,最多两个主筛选,超出进入 Drawer;保留可见搜索标签,统一条件摘要、清除、重置、刷新和请求反馈,业务查询与权限由宿主管理。
|
|
7
|
+
- 修复原生日期未完成片段被当作空值提交的问题;DateRangePicker 清空和同值预设可恢复本地草稿;有效输入继续服从宿主受控值。展台整体重置清除同值日期的未完成草稿。清除最后一个不可聚焦字段的摘要后,焦点保留在筛选区域。
|
|
8
|
+
- 独立验收覆盖三主题、窄容器与 200% CSS zoom、抽屉焦点、日期键盘、真实 GPU 播放与动态偏好、静态降级及卸载清理。
|
|
9
|
+
|
|
3
10
|
## 0.8.0(2026-10-24)
|
|
4
11
|
|
|
5
12
|
- 审查修复:保留 Tooltip 与嵌套 Dialog 的 Escape 层级;主题元数据独立于可选 CSS;设置示例保存中重开时同步已保存值并保留后续编辑。
|
|
@@ -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} 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,16 +60,24 @@ 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);
|
|
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
|
+
};
|
|
44
74
|
return <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
82
|
</fieldset>;
|
|
53
83
|
}
|
|
@@ -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
|
@@ -58,6 +58,7 @@ 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
63
|
| Surface | HTML div 属性 | 静态分区,无点击语义;不是弹窗或导航 |
|
|
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) |
|
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';
|
|
@@ -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
|
+
}
|
|
@@ -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 重复注册。公开脚本不包含认证或业务数据。
|
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"`。消费项目的品牌名称只通过内容与组件参数传入,不进入共享命名空间。
|
|
@@ -49,8 +49,12 @@ SideNav采用44px最小操作行、14/22文字、4px行间距,组间16px;图
|
|
|
49
49
|
|
|
50
50
|
## 快速决策
|
|
51
51
|
|
|
52
|
+
列表顶部的搜索、排序与条件使用 [ListFilterBar](LIST_FILTER_BAR.md) 约束,最多两个常显条件,其余在同一 Drawer 内即时修改;结果反馈在抽屉内也可见。无搜索场景不增加空搜索,批量操作不混入筛选。该组件在 0.9.0 新增,`Toolbar` 的任意 children 组合不能代替这些约束。
|
|
53
|
+
|
|
52
54
|
日期过滤优先采用[日期筛选组合](DATE_RANGE_FILTER.md):少量常用预设使用一个 SegmentedControl,具体范围由独立日期按钮持续显示,Popover 内组合低频预设与 DateRangePicker 草稿。预设即时生效,自定义显式应用;取消与关闭不提交。真实展台为 `#date-range-filter`,组合使用现有 0.6.0 公开组件,没有新增公共 API。
|
|
53
55
|
|
|
56
|
+
原生日期框的未完成片段可能同时满足 `value === ''` 与 `validity.badInput`,不能当作用户清空范围。DateField 保留未完成草稿并报告校验,DateRangePicker 汇总字段错误;补全原生片段后才回传日期值,范围限制继续由校验与宿主提交边界处理,明确清空与快捷项可丢弃草稿。整体重置必须通知局部日期状态,即使已接受日期字符串没有变化。验收使用真实键盘编辑日期片段,并证明期间没有扩大查询范围;只设置完整 ISO 值的测试不覆盖此行为。此修复随本地列表筛选候选验证,不倒推为既有已发布版本能力。
|
|
57
|
+
|
|
54
58
|
| 场景 | 组件组合 | 交互与状态 | 不宜使用 |
|
|
55
59
|
| --- | --- | --- | --- |
|
|
56
60
|
| 跨工作区、跨模块 | SideNav;仅真实父子层级按需加 Breadcrumbs | 当前页有文字、边界与 aria-current;平级工作区顶部保留状态与操作,不重复页名;路由变化后由宿主聚焦主内容 | Tabs 不代替全局导航;面包屑不显示浏览历史、不作必填标题 |
|
|
@@ -65,7 +69,7 @@ SideNav采用44px最小操作行、14/22文字、4px行间距,组间16px;图
|
|
|
65
69
|
| 持续错误、离线或权限状态 | Notice | 就地说明原因、影响、下一步;需行动的信息持续显示 | 不用自动消失通知掩盖失败 |
|
|
66
70
|
| 首次加载 | Skeleton / Progress | 已知结构用骨架,未知进度不写百分比 | 不伪造进度,不循环品牌标志 |
|
|
67
71
|
| 保存、刷新 | Button busy + 保留旧数据 | 防重复提交;刷新明确旧值和更新时间;真实响应决定成败 | 不清空阅读上下文,不提前显示成功 |
|
|
68
|
-
| 大列表查询 |
|
|
72
|
+
| 大列表查询 | ListFilterBar + DataTable + Pagination;候选 API 先核对安装版本 | 最多两个常显条件;宿主处理防抖、IME、取消与过期响应;服务端返回筛选排序分页 | 不为 UI 便利拉取全量本地筛选,不把低频条件继续堆在顶部 |
|
|
69
73
|
| 少量/较多互斥选项 | RadioGroup / Select | Radio 展开选项便于比较,Select 压缩较长清单 | Menu 是命令,不能代替选择器 |
|
|
70
74
|
| 多选 / 即时二态设置 | Checkbox / Switch | Checkbox 选择项;Switch 即时偏好,若需保存明确说明 | 不混同“选中”与“已生效” |
|
|
71
75
|
| 补充说明 / 局部交互 | Tooltip / Popover | Tooltip 不放操作;Popover 可交互;鼠标、键盘均可触发 | 必要说明必须常驻,不能只在 hover 下出现 |
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 列表筛选栏
|
|
2
|
+
|
|
3
|
+
`ListFilterBar` 把列表顶部限制为搜索、最多两个常显条件、更多筛选、结果反馈、刷新与一个业务操作。它统一信息层级与布局,不承接查询、权限、分页或业务筛选状态。需求来源为 [#19](https://github.com/linhay/rhine-design/issues/19),真实展台为 `/#list-filter-bar`。
|
|
4
|
+
|
|
5
|
+
**版本边界:本组件在 0.9.0 新增。** 0.8.0 及更早版本不提供该组件;消费时必须核对实际导出及固定源码/安装快照。图库跟随源码,不能代替 npm 发布证据。
|
|
6
|
+
|
|
7
|
+
## 先决定常显内容
|
|
8
|
+
|
|
9
|
+
- 搜索在用户确实需要查找记录时提供;统计范围等场景无需填充一个无用搜索框。
|
|
10
|
+
- `primary` 只常显前两项,超出的项目自动进入“更多筛选”。选择高频且互相独立的条件,例如状态和排序;不要为了展开所有控件增加第二个筛选栏。
|
|
11
|
+
- 其他条件进入同一个 Drawer。字段自己的 label 持续可见,组件不重复加可见标题,也不提供任意 `children` 让页面插回一排控件。
|
|
12
|
+
- 非默认条件用短摘要说明当前限制;单项清除与整体重置都由宿主定义。默认排序、默认统计日期等是否需要摘要由宿主的 `activeLabel` 决定,不把所有非空值都视为筛选。
|
|
13
|
+
- 结果数量、更新中与错误反馈在栏内呈现;Drawer 打开时同样显示反馈。失败不能藏在遮罩后的页面上,也不能把旧数量当作新条件已经成功。
|
|
14
|
+
- 刷新之外最多一个业务操作。多记录的选择、批量操作属于结果区,录制启停等对象设置也不塞进筛选栏。
|
|
15
|
+
- 平级工作区已有侧栏身份时不重复大标题。组件的 `label` 提供可访问分组名称,宿主仍负责页面标题、主内容名称及路由焦点。
|
|
16
|
+
|
|
17
|
+
布局按组件实际容器宽度重排,窄嵌入区域和 200% 内容缩放保持完整文字、可达操作和 44px 目标;不以省略日期、缩小文字或横向撑破页面换取一行。三套主题使用同一组件结构与 tokens。
|
|
18
|
+
|
|
19
|
+
## 公共契约
|
|
20
|
+
|
|
21
|
+
| 属性 | 输入与责任 |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `label` | 必填分组名称,也是更多筛选 Drawer 的标题 |
|
|
24
|
+
| `search` | 可选原生 input 属性加必填 `label`;支持受控 value、change、composition、disabled;内部为 TextField |
|
|
25
|
+
| `primary` | `ListFilterField[]`;最多前两项常显,其他顺序移入 Drawer |
|
|
26
|
+
| `filters` | `ListFilterField[]`;按顺序追加到 Drawer |
|
|
27
|
+
| `onReset` | 宿主清除搜索与恢复各条件默认值;有搜索或 activeLabel 才显示重置入口 |
|
|
28
|
+
| `refresh` | `{onClick,busy?,disabled?}`;原生共享 Button,不自动执行请求 |
|
|
29
|
+
| `action` | 可选单个 ReactElement,代表一个业务操作;消费方不能用 Fragment 或自建按钮组绕过数量限制 |
|
|
30
|
+
| `result` | 宿主提供的结果摘要;数量未知时明确显示未知,不用已加载条数冒充总数 |
|
|
31
|
+
| `pending` / `error` | 正在更新 / 失败说明;在栏与已展开 Drawer 中保持一致 |
|
|
32
|
+
| `className` | 只用于外层组合布局,不覆盖组件内部视觉或状态 |
|
|
33
|
+
|
|
34
|
+
字段包含 `{id,label,control,activeLabel?,onClear?}`。`id` 在当前栏内唯一,用于稳定标识;它不是输入框 DOM id。`control` 提供真实受控组件及自己的可见/可访问标签;字段包装只添加分组语义。`activeLabel` 是宿主判断后的非默认条件摘要;`onClear` 缺省时摘要只读。不同实例的输入 DOM id 仍须唯一,优先使用组件自动生成的 id。
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<ListFilterBar
|
|
38
|
+
label="会话筛选"
|
|
39
|
+
search={{ label: '搜索会话', value: query, onChange: event => setQuery(event.target.value) }}
|
|
40
|
+
primary={[
|
|
41
|
+
{
|
|
42
|
+
id: 'state', label: '会话状态',
|
|
43
|
+
control: <Select label="会话状态" value={state} onChange={event => setState(event.target.value)}>
|
|
44
|
+
<option value="all">全部状态</option><option value="done">已完成</option>
|
|
45
|
+
</Select>,
|
|
46
|
+
activeLabel: state === 'all' ? undefined : '已完成',
|
|
47
|
+
onClear: () => setState('all'),
|
|
48
|
+
},
|
|
49
|
+
]}
|
|
50
|
+
filters={additionalFields}
|
|
51
|
+
onReset={resetFilters}
|
|
52
|
+
refresh={{ onClick: reload, busy: loading }}
|
|
53
|
+
result={total === null ? '总数待确认' : `${total} 条结果`}
|
|
54
|
+
pending={loading}
|
|
55
|
+
error={error}
|
|
56
|
+
/>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
示例中的状态、函数与额外字段由宿主提供。使用受控 `search.value` 才能准确决定搜索是否需要重置;组件不解析其他控件的值。
|
|
60
|
+
|
|
61
|
+
## 状态与查询边界
|
|
62
|
+
|
|
63
|
+
更多筛选是同一组状态的展开视图,不建立另一套草稿/应用流程。普通选择即时回传宿主;关闭、点击外部与 Escape 只收起抽屉,不撤销已经接受的条件。业务本来要求完整范围显式应用的日期控件可以保留其局部草稿,但应在 Drawer 内内联展示,不再嵌套日期 Drawer。
|
|
64
|
+
|
|
65
|
+
服务端搜索、权限裁剪、默认值、URL 与偏好、请求防抖、IME、取消和过期响应保护都留在宿主。输入中的值不等于最后接受的结果;失败保留还是回滚条件按接口契约处理,并把结果/错误传入组件。不要为了接入组件改成拉取全量数据在浏览器过滤。
|
|
66
|
+
|
|
67
|
+
Drawer 复用已有模态、Escape 与焦点归还;移除当前聚焦摘要时焦点回到有效控件。忙碌与禁用不同:busy 保留焦点,禁用字段由宿主明确传入;不因一次后台刷新禁用所有条件。只读身份应从传入字段中移除其无权使用的条件,不能只改变颜色或 CSS 隐藏。
|
|
68
|
+
|
|
69
|
+
## 真实展台与验收
|
|
70
|
+
|
|
71
|
+
`preview/ListFilterBarGallery.tsx` 提供独立会话列表、420px 嵌入列表、无搜索统计范围三种组合。独立例子刻意传入第三项 primary,以验证运行时移入 Drawer;全局状态选择使用真实控件演示正常、加载、无结果、错误与禁用。记录与操作均为合成示例,无业务请求或文件下载。
|
|
72
|
+
|
|
73
|
+
验收至少覆盖:三主题与真实窄容器;400/520/880px、200% CSS zoom;主条件上限;输入法、键盘和焦点;多个活跃条件的完整摘要、单项/全量重置;Drawer 内加载与失败;筛选后总数、分页与迟到响应。图库证明共享组件组合,产品的服务端权限、导出和查询契约仍分别验收。
|
package/docs/ORBIT_BACKDROP.md
CHANGED
|
@@ -71,3 +71,28 @@ CSS zoom 不能替代浏览器原生缩放;`document.hidden` 事件模拟不
|
|
|
71
71
|

|
|
72
72
|
|
|
73
73
|

|
|
74
|
+
|
|
75
|
+
## 入口文字与全页背景(0.9.0 新增)
|
|
76
|
+
|
|
77
|
+
| 输入 | 契约 |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `controls?: boolean`(0.9.0) | 默认 `true`;`false` 不渲染局部暂停按钮,并移除为控件预留的额外底部内边距。它不解除已经暂停的状态,也不覆盖系统、prop 或祖先减少动态 |
|
|
80
|
+
| `composition?: 'full' \| 'corner'`(0.9.0) | 默认 `full` 保留现有构图;`corner` 将实际绘制区域和静态降级艺术一起放至右下角,宽度最多 480px、高度为容器的 44% 且最多 360px |
|
|
81
|
+
| `framed?: boolean`(0.9.0) | 默认 `true` 保留主题面板边框、圆角与阴影;`false` 移除这三项装饰,用于宿主定位的全页背景,背景艺术与内容内边距仍保留 |
|
|
82
|
+
|
|
83
|
+
[Issue #17](https://github.com/linhay/rhine-design/issues/17) 补充入口页面的公共构图能力。需要保留上方文字阅读空间时使用 `composition="corner"`,让完整艺术在右下方较小的绘制区域内重绘;Canvas2D、可选 WebGL 和 Canvas 不可用时的静态 SVG 使用相同几何,不裁去艺术的一部分来伪造位置变化。宿主将标题和正文组合在上方;`corner` 不替宿主判断任意内容高度,长文案仍需真实布局验证。
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<OrbitBackdrop composition="corner" controls={false} style={{ minHeight: 640 }}>
|
|
87
|
+
<Stack gap={4}>
|
|
88
|
+
<Text as="h1" variant="title">继续创造</Text>
|
|
89
|
+
<Text>从这里进入工作空间。</Text>
|
|
90
|
+
</Stack>
|
|
91
|
+
</OrbitBackdrop>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
宽度不足以保留独立艺术栏时,宿主可将同一组件定位为整个页面的背景,使用 `framed={false}` 去掉面板装饰;前景表单使用 `Surface` 保障可读性。响应式断点、业务内容和页面高度归宿主布局负责,禁止通过业务选择器覆盖内部 Canvas 或控件。
|
|
95
|
+
|
|
96
|
+
`controls={false}` 只移除局部控制界面,不关闭动画、不禁用系统或手动减少动态,也不创建额外帧循环。默认用法继续提供暂停入口;用户明确采用无局部按钮的装饰背景时,可由宿主统一动效偏好控制。重新显示控件保留其原暂停状态;改变构图和面板边框不重挂载画布或重置子元素状态。该组合在 0.9.0 新增,旧版本不提供这些选项。
|
|
97
|
+
|
|
98
|
+
展台中的 `OrbitEntryExample` 按实际容器 760px 重排:宽容器独立艺术栏,窄容器扩展为整页背景,表单保留实色 Surface。布局变更不重挂载画布或输入;`?fixture=orbit-entry` 提供独立验收入口。
|
package/package.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Button, Checkbox, OrbitBackdrop, Select, Stack, Surface, Text, TextField } from '../components';
|
|
3
|
+
import { ThemeProvider, themes, type ThemeName } from '../components/theme';
|
|
4
|
+
import { ListFilterBarGallery } from './ListFilterBarGallery';
|
|
5
|
+
|
|
6
|
+
export function OrbitEntryExample({ reduced = false, appearance = 'theme' }: { reduced?: boolean; appearance?: 'theme' | 'original' }) {
|
|
7
|
+
return <div className="ds-orbit-entry-container"><div className="ds-orbit-entry">
|
|
8
|
+
<OrbitBackdrop className="ds-orbit-entry-background" appearance={appearance} composition="corner" controls={false} framed={false} reducedMotion={reduced} aria-hidden="true" />
|
|
9
|
+
<Stack gap={4} className="ds-orbit-entry-intro"><Text variant="overline">RHINE STUDIO</Text><Text as="h3" variant="title">从这里,继续创造。</Text><Text>让内容拥有清晰的阅读空间。</Text></Stack>
|
|
10
|
+
<Surface className="ds-orbit-entry-form"><Stack gap={4}><Text as="h3" variant="heading">欢迎回来</Text><TextField label="示例昵称" placeholder="输入你的昵称" /><Button>进入工作空间</Button></Stack></Surface>
|
|
11
|
+
</div></div>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function BackgroundFiltersFixture() {
|
|
15
|
+
const params = new URLSearchParams(location.search);
|
|
16
|
+
const zoom = Number(params.get('zoom')) || 1;
|
|
17
|
+
const [theme, setTheme] = useState<ThemeName>((params.get('theme') || 'rhine') as ThemeName);
|
|
18
|
+
const [reduced, setReduced] = useState(false), [mounted, setMounted] = useState(true);
|
|
19
|
+
useEffect(() => { document.documentElement.style.zoom = String(zoom); return () => { document.documentElement.style.zoom = ''; }; }, [zoom]);
|
|
20
|
+
return <ThemeProvider theme={theme} onThemeChange={setTheme}><div style={{ padding: 12 }}>
|
|
21
|
+
{params.get('fixture') === 'list-filters' ? <ListFilterBarGallery /> : <Stack gap={4}>
|
|
22
|
+
<Select label="验收主题" value={theme} onChange={event => setTheme(event.target.value as ThemeName)}>{themes.map(item => <option key={item.name} value={item.name}>{item.label}</option>)}</Select>
|
|
23
|
+
<Checkbox label="减少入口动态" checked={reduced} onChange={event => setReduced(event.target.checked)} />
|
|
24
|
+
<Checkbox label="挂载入口" checked={mounted} onChange={event => setMounted(event.target.checked)} />
|
|
25
|
+
{mounted && <OrbitEntryExample reduced={reduced} />}
|
|
26
|
+
</Stack>}
|
|
27
|
+
</div></ThemeProvider>;
|
|
28
|
+
}
|
package/preview/GalleryShell.tsx
CHANGED
|
@@ -10,7 +10,7 @@ export const galleryGroups = [
|
|
|
10
10
|
{ label: '应用能力', items: [['app-shell', '背景与多侧栏'], ['advanced-forms', '复杂表单'], ['advanced-data', '受控数据表'], ['media', '媒体卡片与查看器'], ['monitoring', '健康详情与图表']] },
|
|
11
11
|
{ label: '设计语言', items: [['foundation', '视觉基础'], ['backgrounds', '场景背景'], ['spacing', '间距与组合'], ['typography', '字体与文字'], ['brand', '品牌标志']] },
|
|
12
12
|
{ label: '通用组件', items: [['actions', '按钮与输入'], ['selection', '选择与切换'], ['feedback', '状态与加载'], ['disclosure', '展开与组合'], ['data', '数据与分页'], ['forms', '表单与选择'], ['utilities', '操作与布局'], ['overlays', '浮层与反馈'], ['overlay-bounds', '浮层缩放与边界'], ['settings-dialog', '设置导航弹窗'], ['theme-picker', '主题预览与选择'], ['navigation', '导航与步骤'], ['form-workflow', '表单流程']] },
|
|
13
|
-
{ label: '交互与动效', items: [['patterns', '场景与交互指南'], ['
|
|
13
|
+
{ label: '交互与动效', items: [['patterns', '场景与交互指南'], ['compact-account', '紧凑账号与凭证'], ['list-filter-bar', '列表筛选栏'], ['date-range-filter', '日期筛选组合'], ['motion', '动效实验室'], ['startup', '启动动画'], ['orbit', '轨道星空'], ['neutron-star', '中子星']] },
|
|
14
14
|
] as const;
|
|
15
15
|
export type GallerySection = typeof galleryGroups[number]['items'][number][0];
|
|
16
16
|
const entries = galleryGroups.flatMap(group => [...group.items]);
|