@linhey/rhine-design 0.9.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 +8 -0
- package/README.md +3 -1
- package/components/AdvancedForms.tsx +3 -3
- package/components/README.md +2 -2
- package/components/SPACING.md +6 -0
- package/components/components.tsx +5 -2
- package/components/extensions.css +10 -0
- package/components/index.ts +1 -1
- package/components/styles.css +1 -0
- package/components/timeline.css +2 -1
- package/docs/APPLICATION_COMPONENTS.md +4 -0
- package/docs/COMPACT_ACCOUNT_PATTERNS.md +3 -1
- package/docs/COMPACT_LAYOUT_VERIFICATION.md +53 -0
- package/docs/DESIGN_SYSTEM.md +2 -0
- package/docs/INTERACTION_PATTERNS.md +6 -0
- package/docs/LIST_FILTER_BAR.md +3 -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/CompactAccountGallery.tsx +18 -18
- package/preview/DateRangeFilterGallery.tsx +18 -1
- package/preview/SpacingGallery.tsx +15 -1
- package/preview/preview.css +9 -2
- package/scripts/check-compact-layouts.js +136 -0
- package/scripts/check-filter-native-date.js +1 -1
- 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/DesignSystem.test.tsx +12 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 0.9.0(2026-10-24)
|
|
4
12
|
|
|
5
13
|
- #17:OrbitBackdrop 新增 controls、composition 和 framed;背景画布与静态降级同置右下角,新增按容器宽度重排的入口组合,保留输入和减少动态。
|
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
|
|
|
@@ -47,7 +47,7 @@ export function DateField({ onValueChange, onValidationChange, error, ...props }
|
|
|
47
47
|
};
|
|
48
48
|
// Chromium can change an empty calendar fragment without firing input/change.
|
|
49
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} />;
|
|
50
|
+
return <TextField {...props} className="rhine-date-field" value={current.draft} error={invalid} type="date" onInput={change} onChange={change} onKeyUp={checkFragments} onBlur={checkFragments} />;
|
|
51
51
|
}
|
|
52
52
|
export interface DateRange { start: string; end: string }
|
|
53
53
|
export interface DateRangePickerProps {
|
|
@@ -71,7 +71,7 @@ export function DateRangePicker({ label, value, onValueChange, min, max, disable
|
|
|
71
71
|
setResetRevision(revision => revision + 1);
|
|
72
72
|
onValueChange(next);
|
|
73
73
|
};
|
|
74
|
-
return <fieldset className="rhine-date-range" disabled={disabled} aria-describedby={[hint && `${id}-hint`, invalid && id].filter(Boolean).join(' ') || undefined}>
|
|
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}>
|
|
75
75
|
<legend>{label}</legend><div className="rhine-date-range__fields">
|
|
76
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
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)} />
|
|
@@ -79,7 +79,7 @@ export function DateRangePicker({ label, value, onValueChange, min, max, disable
|
|
|
79
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
80
|
{clearable && <Button variant="quiet" disabled={disabled || readOnly || (!value.start && !value.end && !fieldErrors.start && !fieldErrors.end)} onClick={() => replace({ start: '', end: '' })}>清空日期</Button>}
|
|
81
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>}
|
|
82
|
-
</fieldset>;
|
|
82
|
+
</fieldset></div>;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export interface NumberFieldProps {
|
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
|
|
|
@@ -60,7 +60,7 @@ export function Example() {
|
|
|
60
60
|
| Toolbar | title;description;children;HTML div属性 | 自适应控件组合,role=group与普通Tab顺序;不伪装为需要方向键导航的ARIA toolbar;不含业务状态 |
|
|
61
61
|
| ListFilterBar(0.9.0) | label;search;primary/filters 字段;onReset;refresh;一个 action;result/pending/error | 最多两个常显条件,超出项进入同一 Drawer;宿主维护即时状态、查询和权限;[契约与版本边界](../docs/LIST_FILTER_BAR.md) |
|
|
62
62
|
| Badge | tone=neutral/success/warning/error;children | 只读短状态,颜色必须配文字,不做可点击按钮 |
|
|
63
|
-
| 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) |
|
|
64
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) |
|
|
65
65
|
| SelectionCard | label;selected;onSelectedChange;disabled;showIndicator=true;children | 原生 checkbox 可取消;是否单选由上层控制;showIndicator=false 仅视觉隐藏勾选框,保留键盘与读屏,卡片边界表达选中;内部不嵌套按钮/链接/其他输入 |
|
|
66
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
|
@@ -44,6 +44,6 @@ export { Fieldset, ErrorSummary } from './FormWorkflow';
|
|
|
44
44
|
export type { FieldsetProps, FieldError, ErrorSummaryProps } from './FormWorkflow';
|
|
45
45
|
|
|
46
46
|
export { Button, Badge, Surface, SelectionCard, TextField, Tabs, SegmentedControl, Notice, Progress, Skeleton, Disclosure, EmptyState } from './components';
|
|
47
|
-
export type { ButtonProps, Tone, Choice, TabItem } from './components';
|
|
47
|
+
export type { ButtonProps, SurfaceProps, Tone, Choice, TabItem } from './components';
|
|
48
48
|
|
|
49
49
|
export { ThemePicker, ThemePreview, type ThemePickerProps, type ThemePickerOption, type ThemePreviewProps } from './ThemePicker';
|
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 变化会直接标明当前越界,不伪造用户输入或自动修改宿主值。空值步进按有效范围处理,不再把空字符串当成已达到零边界。
|
|
@@ -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
|
@@ -82,6 +82,8 @@
|
|
|
82
82
|
- 400/520/880px作为展台验收宽度;消费项目按其最小容器尺寸追加验证。
|
|
83
83
|
- 指标与表格按容器重排;数字和说明可换行,必要时表格局部滚动,不以超小字号塞入全部列。
|
|
84
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 裁掉内容来通过检查。
|
|
85
87
|
|
|
86
88
|
|
|
87
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导入并使用同一主题。
|
package/docs/LIST_FILTER_BAR.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
## 先决定常显内容
|
|
8
8
|
|
|
9
9
|
- 搜索在用户确实需要查找记录时提供;统计范围等场景无需填充一个无用搜索框。
|
|
10
|
+
- 搜索 label 持续可见,不以 placeholder 代替。#22 的 `f0f8495` 已移除早期候选隐藏搜索 label 的 CSS;消费旧安装副本时须核对,不能以源码文档描述代替实际安装行为。
|
|
10
11
|
- `primary` 只常显前两项,超出的项目自动进入“更多筛选”。选择高频且互相独立的条件,例如状态和排序;不要为了展开所有控件增加第二个筛选栏。
|
|
11
12
|
- 其他条件进入同一个 Drawer。字段自己的 label 持续可见,组件不重复加可见标题,也不提供任意 `children` 让页面插回一排控件。
|
|
12
13
|
- 非默认条件用短摘要说明当前限制;单项清除与整体重置都由宿主定义。默认排序、默认统计日期等是否需要摘要由宿主的 `activeLabel` 决定,不把所有非空值都视为筛选。
|
|
@@ -71,3 +72,5 @@ Drawer 复用已有模态、Escape 与焦点归还;移除当前聚焦摘要时
|
|
|
71
72
|
`preview/ListFilterBarGallery.tsx` 提供独立会话列表、420px 嵌入列表、无搜索统计范围三种组合。独立例子刻意传入第三项 primary,以验证运行时移入 Drawer;全局状态选择使用真实控件演示正常、加载、无结果、错误与禁用。记录与操作均为合成示例,无业务请求或文件下载。
|
|
72
73
|
|
|
73
74
|
验收至少覆盖:三主题与真实窄容器;400/520/880px、200% CSS zoom;主条件上限;输入法、键盘和焦点;多个活跃条件的完整摘要、单项/全量重置;Drawer 内加载与失败;筛选后总数、分页与迟到响应。图库证明共享组件组合,产品的服务端权限、导出和查询契约仍分别验收。
|
|
75
|
+
|
|
76
|
+
Drawer 中的原生日期还须检查实际文本编辑区:完整值、最后日期片段与日历图标均可见并可操作;input/document 的 scrollWidth 不溢出不代表原生编辑区没有裁字。重复分组内距的共享修复由 [#25](https://github.com/linhay/rhine-design/issues/25) 跟踪;不在业务页复制日期内部 CSS。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
Badge, Button, ButtonLink, Checkbox, Disclosure, Divider,
|
|
3
|
+
Badge, Button, ButtonLink, Checkbox, Disclosure, Divider, Grid, Inline,
|
|
4
4
|
Notice, Progress, Select, Skeleton, Stack, StatusTimelineList, Surface, Text,
|
|
5
5
|
TextField, TimelineLegend, Tooltip, type TimelineSegment, type TimelineStatus,
|
|
6
6
|
} from '../components';
|
|
@@ -45,29 +45,29 @@ export function CompactAccountExample({ state = 'normal' }: { state?: AccountSta
|
|
|
45
45
|
</Stack>
|
|
46
46
|
<Stack gap={1}>
|
|
47
47
|
<StatusTimelineList label="资源用量与异常记录" items={[{
|
|
48
|
-
id: 'resource-a', title: '用量与异常记录',
|
|
49
|
-
note: unknown ? '历史记录不完整,仅展示已确认的时段。' :
|
|
48
|
+
id: 'resource-a', title: '用量与异常记录', summary: '24小时窗口 · 15分钟',
|
|
49
|
+
note: `10/23 00:00 – 10/24 00:00 · 上海${unknown ? ' · 历史记录不完整,仅展示已确认的时段。' : ''}`,
|
|
50
50
|
timeline: { startLabel: '10/23 00:00 · 上海', endLabel: '10/24 00:00 · 上海', segments: unknown ? segments.slice(0, 48) : segments },
|
|
51
51
|
}]} />
|
|
52
52
|
<TimelineLegend compact label="历史状态图例" items={compactAccountLegend} />
|
|
53
53
|
</Stack>
|
|
54
|
-
<Stack gap={
|
|
55
|
-
<
|
|
54
|
+
<Stack gap={3} className="compact-gallery-facts" data-testid="compact-account-facts">
|
|
55
|
+
<Grid gap={2} className="compact-gallery-facts-meta">
|
|
56
56
|
<Tooltip content="数值越小,调度优先级越高;这不是健康评分。"><Text as="span" variant="caption" tabIndex={0}>优先级 <strong>0</strong></Text></Tooltip>
|
|
57
57
|
<Text as="span" variant="caption">API-Key <strong>{unknown ? '—' : zero ? '0' : '3'}</strong></Text>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
</
|
|
58
|
+
<Text as="span" variant="caption" className="compact-gallery-facts-expiry">到期 <strong>{long ? '2027-10-24 23:59 · 上海(运营设置的账号期限)' : '未设置'}</strong></Text>
|
|
59
|
+
{long && <Text as="span" variant="caption" className="compact-gallery-facts-subscription">订阅到期 <strong>2026-11-24 08:30 · 上海(当前服务套餐期限)</strong></Text>}
|
|
60
|
+
</Grid>
|
|
61
|
+
<Stack gap={2}>
|
|
62
|
+
<Grid gap={2} className="compact-gallery-fact-pair">
|
|
63
|
+
<Text as="span" variant="caption">今日消费 <strong>{unknown ? '—' : zero ? '$0.00' : long ? '$12,345.67 / $24,691.34' : '$2.40'}</strong></Text>
|
|
64
|
+
<Text as="span" variant="caption">本周期 <strong>{unknown ? '—' : zero ? '$0.00' : long ? '$123,456.78 / $246,913.56' : '$18.50'}</strong></Text>
|
|
65
|
+
</Grid>
|
|
66
|
+
<Grid gap={2} className="compact-gallery-fact-pair">
|
|
67
|
+
<Text as="span" variant="caption">今日生图 <strong>{unknown ? '—' : zero ? '0' : '8'}</strong></Text>
|
|
68
|
+
<Text as="span" variant="caption">本周期生图 <strong>{unknown ? '—' : zero ? '0' : '24'}</strong></Text>
|
|
69
|
+
</Grid>
|
|
70
|
+
</Stack>
|
|
71
71
|
</Stack>
|
|
72
72
|
</Stack></Surface>;
|
|
73
73
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { Button, DateRangePicker, Divider, Icon, Inline, Popover, SegmentedControl, Stack, Surface, Text, type DateRange } from '../components';
|
|
2
|
+
import { Button, DateField, DateRangePicker, Divider, Icon, Inline, Popover, SegmentedControl, Select, Stack, Surface, Text, type DateRange } from '../components';
|
|
3
3
|
|
|
4
4
|
const quickRanges = [
|
|
5
5
|
{ value: 'today', label: '今天', range: { start: '2026-10-24', end: '2026-10-24' } },
|
|
@@ -78,6 +78,10 @@ export function DateRangeFilterExample({ label = '时间范围', initialValue =
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export function DateRangeFilterGallery() {
|
|
81
|
+
const [narrowWidth, setNarrowWidth] = useState('150');
|
|
82
|
+
const [narrowRange, setNarrowRange] = useState<DateRange>({ start: '2026-10-24', end: '2026-10-25' });
|
|
83
|
+
const [singleDate, setSingleDate] = useState('2026-10-24');
|
|
84
|
+
const [resetRevision, setResetRevision] = useState(0);
|
|
81
85
|
return <section id="date-range-filter">
|
|
82
86
|
<div className="ds-section-title"><span>RANGE</span><h2>日期筛选 · 快捷选择与自定义</h2></div>
|
|
83
87
|
<p className="ds-caption">常用日期是一组互斥选项;具体日期持续可见。更多预设即选即用,自定义日期在确认后生效。</p>
|
|
@@ -103,6 +107,19 @@ export function DateRangeFilterGallery() {
|
|
|
103
107
|
<Text variant="caption" tone="muted">归档结果的统计范围固定,保持日期可读。</Text>
|
|
104
108
|
</Stack>
|
|
105
109
|
</Surface>
|
|
110
|
+
<Stack gap={3}>
|
|
111
|
+
<Text as="h3" variant="heading">窄容器中的完整日期</Text>
|
|
112
|
+
<Text variant="caption" tone="muted">只改变实际容器宽度,不缩小字号。请聚焦日期并编辑最后一段,确认完整年月日与日历入口仍可见;尺寸适配自 0.9.1 提供。</Text>
|
|
113
|
+
<Select label="日期容器宽度" value={narrowWidth} onChange={event => setNarrowWidth(event.target.value)}>
|
|
114
|
+
<option value="150">150 CSS px</option><option value="200">200 CSS px</option><option value="280">280 CSS px</option>
|
|
115
|
+
</Select>
|
|
116
|
+
<Stack gap={3} style={{ width: Number(narrowWidth), maxWidth: '100%' }}>
|
|
117
|
+
<DateRangePicker key={resetRevision} label="局部日期" value={narrowRange} onValueChange={setNarrowRange} />
|
|
118
|
+
<Inline><DateField key={resetRevision} label="独立日期" value={singleDate} onValueChange={setSingleDate} /></Inline>
|
|
119
|
+
<Button variant="secondary" onClick={() => { setNarrowRange({ start: '2026-10-24', end: '2026-10-25' }); setSingleDate('2026-10-24'); setResetRevision(value => value + 1); }}>重置示例日期</Button>
|
|
120
|
+
</Stack>
|
|
121
|
+
<Text variant="caption" role="status">已接受日期:{narrowRange.start || '不限开始'} — {narrowRange.end || '不限结束'}</Text>
|
|
122
|
+
</Stack>
|
|
106
123
|
<Text variant="caption" tone="muted">方向键切换快捷项;Escape 或取消退出草稿并归还焦点,点击外部控件按原生焦点行为继续。所有主题使用同一组公开组件。</Text>
|
|
107
124
|
</Stack>
|
|
108
125
|
</section>;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { Button, Grid, Inline, Notice, Select, Stack, Surface, Text, type Spacing } from '../components';
|
|
2
|
+
import { Button, DataTable, Grid, Inline, Notice, Select, Stack, Surface, Text, type Spacing } from '../components';
|
|
3
3
|
import './spacing.css';
|
|
4
4
|
|
|
5
5
|
const scale: Spacing[] = [1, 2, 3, 4, 6, 8, 12];
|
|
6
6
|
export function SpacingGallery() {
|
|
7
7
|
const [state, setState] = useState('normal');
|
|
8
|
+
const [selected, setSelected] = useState<string[]>(['research']);
|
|
8
9
|
return <section id="spacing">
|
|
9
10
|
<div className="ds-section-title"><span>SPACE</span><h2>间距与组合 · 让关系可见</h2></div>
|
|
10
11
|
<Stack gap={6}>
|
|
@@ -27,6 +28,19 @@ export function SpacingGallery() {
|
|
|
27
28
|
<Text variant="caption" tone="muted">Surface只负责边界与内边距。避免父gap与子margin重复累加;不固定卡高,不为空白增加动效。</Text>
|
|
28
29
|
</Stack></Surface>
|
|
29
30
|
</Grid>
|
|
31
|
+
<Stack gap={3}>
|
|
32
|
+
<Text as="h3" variant="heading">纯表格 · 取消容器外围内距</Text>
|
|
33
|
+
<Text variant="caption" tone="muted">Surface padding="none" 自 0.9.1 提供;表头和单元格保留原有间距。切换主题并勾选行,检查边界和文字。</Text>
|
|
34
|
+
<Surface padding="none">
|
|
35
|
+
<DataTable caption="示例部门用量" rows={[
|
|
36
|
+
{ id: 'research', name: '研发协作组', requests: 128 },
|
|
37
|
+
{ id: 'accessibility', name: '跨部门体验与可访问性协作小组', requests: 42 },
|
|
38
|
+
]} columns={[
|
|
39
|
+
{ key: 'name', header: '部门', render: row => row.name, rowHeader: true },
|
|
40
|
+
{ key: 'requests', header: '请求', render: row => row.requests, align: 'right' },
|
|
41
|
+
]} rowKey={row => row.id} rowLabel={row => row.name} selection={{ keys: selected, onChange: setSelected }} />
|
|
42
|
+
</Surface>
|
|
43
|
+
</Stack>
|
|
30
44
|
</Stack>
|
|
31
45
|
</section>;
|
|
32
46
|
}
|
package/preview/preview.css
CHANGED
|
@@ -3,8 +3,15 @@ body { margin:0; }
|
|
|
3
3
|
.compact-gallery-stage { max-width:100%; min-width:0; }
|
|
4
4
|
.compact-gallery-examples { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,320px),1fr)); gap:var(--rhine-space-6); align-items:start; }
|
|
5
5
|
.compact-gallery-examples>* { min-width:0; }
|
|
6
|
-
.compact-gallery-
|
|
7
|
-
.compact-gallery-
|
|
6
|
+
.compact-gallery-facts { container-type:inline-size; }
|
|
7
|
+
.compact-gallery-facts-meta { grid-template-columns:minmax(0,1fr) minmax(0,1fr) minmax(0,2fr); align-items:baseline; }
|
|
8
|
+
.compact-gallery-facts-subscription { grid-column:1 / -1; }
|
|
9
|
+
.compact-gallery-fact-pair { grid-template-columns:repeat(2,minmax(0,1fr)); align-items:baseline; }
|
|
10
|
+
.compact-gallery-facts-meta>*,.compact-gallery-fact-pair>* { min-width:0; overflow-wrap:anywhere; }
|
|
11
|
+
@container (max-width:360px) {
|
|
12
|
+
.compact-gallery-facts-meta { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
|
13
|
+
.compact-gallery-facts-expiry { grid-column:1 / -1; }
|
|
14
|
+
}
|
|
8
15
|
.ds-type-row { display:grid; grid-template-columns:180px minmax(0,1fr); align-items:baseline; gap:12px 24px; padding:16px 0; border-bottom:1px solid var(--rhine-line); }
|
|
9
16
|
.ds-type-row:last-child { border:0; }
|
|
10
17
|
.ds-type-copy { display:grid; gap:12px; margin-top:16px; max-width:60ch; }
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Open a fixed production preview, then run with playwright-cli run-code --filename.
|
|
2
|
+
// CSS zoom evidence is separate from native browser-menu zoom.
|
|
3
|
+
async page => {
|
|
4
|
+
page.setDefaultTimeout(10000);
|
|
5
|
+
const origin = new URL(page.url()).origin;
|
|
6
|
+
const failures = [], errors = [], counts = { drawer: 0, date: 0, table: 0, account: 0 };
|
|
7
|
+
page.on('pageerror', error => errors.push(String(error)));
|
|
8
|
+
const check = (pass, context) => { if (!pass) failures.push(context); };
|
|
9
|
+
const open = async (section, theme, width, zoom) => {
|
|
10
|
+
await page.setViewportSize({ width, height: 1000 });
|
|
11
|
+
await page.goto(`${origin}/?theme=${theme}#${section}`);
|
|
12
|
+
await page.locator(`#${section}`).waitFor();
|
|
13
|
+
await page.evaluate(value => { document.documentElement.style.zoom = String(value); }, zoom);
|
|
14
|
+
};
|
|
15
|
+
const editLastSegment = async (input, context) => {
|
|
16
|
+
await input.fill('2026-10-24');
|
|
17
|
+
await input.focus();
|
|
18
|
+
// The preview's zh-CN native date order is year/month/day.
|
|
19
|
+
await page.keyboard.press('ArrowRight');
|
|
20
|
+
await page.keyboard.press('ArrowRight');
|
|
21
|
+
await page.keyboard.press('ArrowUp');
|
|
22
|
+
check(await input.inputValue() === '2026-10-25', { ...context, kind: 'edit-last-segment' });
|
|
23
|
+
await page.keyboard.press('ArrowDown');
|
|
24
|
+
check(await input.inputValue() === '2026-10-24', { ...context, kind: 'restore-last-segment' });
|
|
25
|
+
};
|
|
26
|
+
const measureDate = async (input, context) => {
|
|
27
|
+
const dimensions = await input.evaluate(e => {
|
|
28
|
+
const style = getComputedStyle(e);
|
|
29
|
+
return { width: parseFloat(style.width), height: parseFloat(style.height), font: parseFloat(style.fontSize), padding: parseFloat(style.paddingLeft) };
|
|
30
|
+
});
|
|
31
|
+
check(dimensions.width >= 148 && dimensions.height >= 44 && dimensions.font >= 14, { ...context, kind: 'date-dimensions', dimensions });
|
|
32
|
+
};
|
|
33
|
+
for (const theme of ['rhine', 'arknights', 'neo-brutalism']) {
|
|
34
|
+
for (const width of [400, 520, 880]) for (const zoom of [1, 2]) {
|
|
35
|
+
const context = { theme, width, zoom };
|
|
36
|
+
await open('list-filter-bar', theme, width, zoom);
|
|
37
|
+
const bar = page.getByRole('group', { name: '录制会话筛选', exact: true });
|
|
38
|
+
const trigger = bar.getByRole('button', { name: /^更多筛选/ });
|
|
39
|
+
await trigger.click();
|
|
40
|
+
const dialog = page.getByRole('dialog', { name: '录制会话筛选', exact: true });
|
|
41
|
+
await dialog.evaluate(e => Promise.all(e.getAnimations().map(a => a.finished)));
|
|
42
|
+
const start = dialog.getByLabel('开始日期', { exact: true });
|
|
43
|
+
await measureDate(start, context);
|
|
44
|
+
await editLastSegment(start, context);
|
|
45
|
+
await dialog.getByLabel('结束日期', { exact: true }).fill('2026-10-26');
|
|
46
|
+
await dialog.getByRole('button', { name: '清空日期', exact: true }).click();
|
|
47
|
+
check(await start.inputValue() === '', { ...context, kind: 'clear-range' });
|
|
48
|
+
await dialog.getByRole('checkbox', { name: '只看已审查' }).check();
|
|
49
|
+
await start.focus(); await page.keyboard.type('2');
|
|
50
|
+
check(await start.getAttribute('aria-invalid') === 'true', { ...context, kind: 'incomplete-date' });
|
|
51
|
+
await dialog.getByRole('button', { name: '重置筛选', exact: true }).click();
|
|
52
|
+
check(await start.evaluate(e => !e.value && !e.validity.badInput && e.getAttribute('aria-invalid') !== 'true'), { ...context, kind: 'reset-draft' });
|
|
53
|
+
if (width === 400 && zoom === 2) {
|
|
54
|
+
await start.fill('2026-10-24');
|
|
55
|
+
await start.screenshot({ path: `output/playwright/issues-23-25/drawer-${theme}.png` });
|
|
56
|
+
}
|
|
57
|
+
await page.keyboard.press('Escape'); await dialog.waitFor({ state: 'detached' });
|
|
58
|
+
await page.waitForFunction(e => e === document.activeElement, await trigger.elementHandle());
|
|
59
|
+
counts.drawer++;
|
|
60
|
+
|
|
61
|
+
await open('spacing', theme, width, zoom);
|
|
62
|
+
const surface = page.locator('#spacing .rhine-surface--padding-none');
|
|
63
|
+
check(await surface.evaluate(e => getComputedStyle(e).padding === '0px' && getComputedStyle(e).overflow !== 'hidden'), { ...context, kind: 'surface-padding' });
|
|
64
|
+
check(await surface.locator('td').first().evaluate(e => parseFloat(getComputedStyle(e).paddingLeft) > 0), { ...context, kind: 'table-cell-spacing' });
|
|
65
|
+
const selection = surface.getByRole('checkbox').nth(1);
|
|
66
|
+
await selection.uncheck(); await selection.check();
|
|
67
|
+
await selection.focus(); await page.keyboard.press('Space');
|
|
68
|
+
check(!await selection.isChecked(), { ...context, kind: 'table-keyboard-selection' });
|
|
69
|
+
check(await surface.evaluate(e => e.getBoundingClientRect().right <= innerWidth + 1), { ...context, kind: 'table-bounds' });
|
|
70
|
+
const scroll = surface.getByRole('region', { name: /可横向滚动/ });
|
|
71
|
+
await scroll.focus();
|
|
72
|
+
await scroll.evaluate(e => { e.scrollLeft = e.scrollWidth; });
|
|
73
|
+
check(await scroll.evaluate(e => e === document.activeElement && e.querySelector('tbody tr td:last-child').getBoundingClientRect().right <= e.getBoundingClientRect().right + 1), { ...context, kind: 'table-last-column-reachable' });
|
|
74
|
+
await scroll.evaluate(e => { e.scrollLeft = 0; });
|
|
75
|
+
if (width === 400 && zoom === 2) await surface.screenshot({ path: `output/playwright/issues-23-25/table-${theme}.png` });
|
|
76
|
+
counts.table++;
|
|
77
|
+
|
|
78
|
+
await open('compact-account', theme, width, 1);
|
|
79
|
+
await page.getByRole('combobox', { name: '样机宽度' }).selectOption(String(width));
|
|
80
|
+
await page.getByRole('combobox', { name: '内容缩放' }).selectOption(String(zoom));
|
|
81
|
+
for (const state of ['normal', 'zero', 'unknown', 'long']) {
|
|
82
|
+
await page.getByRole('combobox', { name: '账号读数' }).selectOption(state);
|
|
83
|
+
const card = page.getByTestId('compact-account-card');
|
|
84
|
+
const layout = await card.evaluate(e => {
|
|
85
|
+
const bars = e.querySelector('.rhine-timeline__bars').getBoundingClientRect();
|
|
86
|
+
const legend = e.querySelector('.rhine-timeline__legend').getBoundingClientRect();
|
|
87
|
+
const slider = e.querySelector('input[type=range]');
|
|
88
|
+
const pairs = [...e.querySelectorAll('.compact-gallery-fact-pair')];
|
|
89
|
+
return {
|
|
90
|
+
gap: legend.top - bars.bottom,
|
|
91
|
+
sliderHeight: parseFloat(getComputedStyle(slider).height),
|
|
92
|
+
triggerHeight: parseFloat(getComputedStyle(e.querySelector('.rhine-timeline__legend-trigger')).height),
|
|
93
|
+
aligned: [...pairs[0].children].every((child, i) => Math.abs(child.getBoundingClientRect().left - pairs[1].children[i].getBoundingClientRect().left) < 1),
|
|
94
|
+
overflow: [...e.querySelectorAll('.compact-gallery-facts, .compact-gallery-facts span')].some(node => node.scrollWidth > node.clientWidth + 1),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
check(Math.abs(layout.gap / zoom - 12) < 1 && layout.sliderHeight >= 44 && layout.triggerHeight >= 44 && layout.aligned && !layout.overflow, { ...context, state, kind: 'account-layout', layout });
|
|
98
|
+
if (state === 'normal') {
|
|
99
|
+
const slider = card.getByRole('slider');
|
|
100
|
+
await slider.focus(); await page.keyboard.press('ArrowRight');
|
|
101
|
+
const detail = page.getByRole('dialog', { name: /时段详情$/ });
|
|
102
|
+
await detail.waitFor(); await page.keyboard.press('Escape'); await detail.waitFor({ state: 'detached' });
|
|
103
|
+
await page.getByRole('combobox', { name: '账号读数' }).focus();
|
|
104
|
+
await slider.hover(); await detail.waitFor();
|
|
105
|
+
await page.mouse.move(0, 0); await detail.waitFor({ state: 'detached' });
|
|
106
|
+
const legend = card.getByRole('button', { name: '有输出', exact: true });
|
|
107
|
+
await legend.click();
|
|
108
|
+
const rule = page.getByRole('dialog', { name: '有输出规则', exact: true });
|
|
109
|
+
await rule.waitFor(); await page.keyboard.press('Escape'); await rule.waitFor({ state: 'detached' });
|
|
110
|
+
}
|
|
111
|
+
if (width === 400 && zoom === 2 && state === 'long') await card.screenshot({ path: `output/playwright/issues-23-25/account-${theme}.png` });
|
|
112
|
+
counts.account++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Leave room for the gallery sidebar: max-width must not clamp the 280px fixture.
|
|
116
|
+
await open('date-range-filter', theme, 1280, 2);
|
|
117
|
+
for (const containerWidth of [150, 200, 280]) {
|
|
118
|
+
await page.getByRole('combobox', { name: '日期容器宽度' }).selectOption(String(containerWidth));
|
|
119
|
+
const context = { theme, containerWidth, zoom: 2 };
|
|
120
|
+
const range = page.getByRole('group', { name: '局部日期', exact: true });
|
|
121
|
+
check(await range.evaluate((e, expected) => Math.abs(parseFloat(getComputedStyle(e.parentElement).width) - expected) < 1, containerWidth), { ...context, kind: 'actual-container-width' });
|
|
122
|
+
const start = range.getByLabel('开始日期', { exact: true });
|
|
123
|
+
const single = page.getByLabel('独立日期', { exact: true });
|
|
124
|
+
await measureDate(start, context); await measureDate(single, context);
|
|
125
|
+
await editLastSegment(start, context); await editLastSegment(single, context);
|
|
126
|
+
await single.fill(''); check(await single.inputValue() === '', { ...context, kind: 'clear-single' });
|
|
127
|
+
await range.getByRole('button', { name: '清空日期', exact: true }).click();
|
|
128
|
+
await page.getByRole('button', { name: '重置示例日期' }).click();
|
|
129
|
+
check(await start.inputValue() === '2026-10-24' && await single.inputValue() === '2026-10-24', { ...context, kind: 'reset-example' });
|
|
130
|
+
check(await page.getByRole('button', { name: '重置示例日期' }).evaluate(e => e === document.activeElement), { ...context, kind: 'reset-example-focus' });
|
|
131
|
+
await single.screenshot({ path: `output/playwright/issues-23-25/single-${theme}-${containerWidth}.png` });
|
|
132
|
+
counts.date++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { pass: !failures.length && !errors.length, counts, failures, errors, scope: 'Chromium; real gallery, CSS zoom, native keyboard editing. Inspect date screenshots for full fragments/calendar visibility.' };
|
|
136
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
async page => {
|
|
2
|
-
await page.goto(
|
|
2
|
+
await page.goto(`${new URL(page.url()).origin}/?fixture=list-filters&theme=neo-brutalism&proof=${Date.now()}#list-filter-bar`);
|
|
3
3
|
const checks = [];
|
|
4
4
|
const check = (name, pass, evidence = {}) => checks.push({ name, pass: Boolean(pass), ...evidence });
|
|
5
5
|
await page.getByRole('group', { name: '录制会话筛选', exact: true }).getByRole('button', { name: /^更多筛选/ }).click();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: rhine-design
|
|
3
3
|
description: Use Rhine Design components and specifications to design, implement, or review React UI, typography, themes, and motion. Apply when a project uses Rhine Design or the user requests its visual language or component system.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "0.
|
|
5
|
+
version: "0.9.1"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Rhine Design · 莱茵设计
|
|
@@ -13,7 +13,7 @@ Use the installed version's components and contracts to build consistent interfa
|
|
|
13
13
|
|
|
14
14
|
## Locate the matching source
|
|
15
15
|
|
|
16
|
-
Package: `@linhey/rhine-design`. GitHub: `linhay/rhine-design` (different account spelling). This skill targets 0.
|
|
16
|
+
Package: `@linhey/rhine-design`. GitHub: `linhay/rhine-design` (different account spelling). This skill targets 0.9.1, including ListFilterBar, Surface padding, narrow date layouts, OrbitBackdrop, application shells, advanced forms, media, controlled tables and monitoring charts. Theme and navigation APIs require at least 0.4.0; application APIs require 0.5.0 and OrbitBackdrop requires 0.6.0. Check installed exports before adoption.
|
|
17
17
|
|
|
18
18
|
Find the package in the host's dependencies, normally `node_modules/@linhey/rhine-design/`, or its registered source checkout. Verify `package.json` name/version before using the paths below. Paths in references are relative to that package root, not the copied skill directory. Read installed types and source when versions differ; do not infer a new API from this skill. If the package is unavailable, use the task's supplied design sources and identify the missing implementation evidence.
|
|
19
19
|
|
|
@@ -56,6 +56,6 @@ Required issue content:
|
|
|
56
56
|
- Host state determines data, selection and operation outcomes. Search and pagination remain host/server responsibilities; busy indicators never imply success.
|
|
57
57
|
- For multi-field failures, preserve input and focus a summary only after failed submission; links return to fields without changing routing. Use SideNav/Breadcrumbs/Steps only when exported by the installed source.
|
|
58
58
|
- Monitoring shows units, freshness and stale/unknown states; errors must not rely on color, flashing or transient toasts.
|
|
59
|
-
- Design from the user's current task: keep identity, available actions, results and decision-relevant consequences; remove duplicate state labels and implementation prose. Reveal history on demand. Inside an existing Surface prefer borderless read-only summaries and paired facts; never compress by shrinking business text, cropping values, hiding unknown/error states or merging distinct costs/expiry meanings. See `docs/COMPACT_ACCOUNT_PATTERNS.md` when present; its
|
|
59
|
+
- Design from the user's current task: keep identity, available actions, results and decision-relevant consequences; remove duplicate state labels and implementation prose. Reveal history on demand. Inside an existing Surface prefer borderless read-only summaries and paired facts; never compress by shrinking business text, cropping values, hiding unknown/error states or merging distinct costs/expiry meanings. See `docs/COMPACT_ACCOUNT_PATTERNS.md` when present; its base APIs require 0.8.0, and the spacing fixes and Surface padding option require 0.9.1.
|
|
60
60
|
- Motion explains a change. Respect reduced motion, cancellation and replay; startup decoration never blocks real readiness. Preview-only experiments are not exported components.
|
|
61
61
|
- Keep independent release boundaries: do not change unrelated products or publish packages merely because this skill was invoked.
|
|
@@ -30,7 +30,7 @@ All paths are relative to the matching package root. `package.json` exports, `co
|
|
|
30
30
|
| SideNav, Breadcrumbs, Steps (0.4.0) | `components/Navigation.tsx` |
|
|
31
31
|
| Fieldset, ErrorSummary (0.4.0) | `components/FormWorkflow.tsx` |
|
|
32
32
|
| Toast | `components/Toast.tsx` |
|
|
33
|
-
| Button, Badge, Surface, SelectionCard, TextField, Tabs, SegmentedControl, Notice, Progress, Skeleton, Disclosure, EmptyState | `components/components.tsx` |
|
|
33
|
+
| Button, Badge, Surface, SelectionCard, TextField, Tabs, SegmentedControl, Notice, Progress, Skeleton, Disclosure, EmptyState | `components/components.tsx`;Surface 的 `padding="none"` 仅取消表格容器外围内距,默认不变;此选项自 0.9.1 提供,见 `CHANGELOG.md` |
|
|
34
34
|
|
|
35
35
|
Primary contracts: `components/README.md` and `components/COMMON_COMPONENTS.md`. Find behavioral evidence in `tests/` and composition examples in `preview/`.
|
|
36
36
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Supported baseline
|
|
4
4
|
|
|
5
|
-
Rhine Design 0.
|
|
5
|
+
Rhine Design 0.9.1 ships TypeScript/TSX, CSS and SVG source for React 19 + Vite. Install `@linhey/rhine-design`; GitHub remains `linhay/rhine-design`. Check the installed package's engine and peer dependencies. Node 24 is the development baseline. Do not assume precompiled JavaScript, arbitrary SSR support or a complete dark theme.
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
8
|
import { Button, Surface, Text } from '@linhey/rhine-design';
|
|
@@ -19,7 +19,8 @@ describe('compact account composition', () => {
|
|
|
19
19
|
view.rerender(<div className="rhine-theme"><CompactAccountExample state="unknown" /></div>);
|
|
20
20
|
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
|
|
21
21
|
expect(screen.getByText('7D 剩余 — · 额度暂不可用')).toBeVisible();
|
|
22
|
-
expect(screen.getByText(
|
|
22
|
+
expect(screen.getByText(/10\/23 00:00 – 10\/24 00:00 · 上海/)).toBeVisible();
|
|
23
|
+
expect(screen.getByText(/历史记录不完整,仅展示已确认的时段。/)).toBeVisible();
|
|
23
24
|
expect(within(screen.getByTestId('compact-account-facts')).getAllByText('—')).toHaveLength(5);
|
|
24
25
|
view.rerender(<div className="rhine-theme"><CompactAccountExample state="long" /></div>);
|
|
25
26
|
expect(screen.getByText('到期')).toBeVisible();
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { Button, Disclosure, Logo, Progress, SegmentedControl, Select, SelectionCard, Tabs, TextField } from '../components';
|
|
4
|
+
import { Button, Disclosure, Logo, Progress, SegmentedControl, Select, SelectionCard, Surface, Tabs, TextField } from '../components';
|
|
5
5
|
|
|
6
6
|
describe('reusable design components', () => {
|
|
7
|
+
it('keeps Surface native attributes and default spacing when opting out of table container padding', () => {
|
|
8
|
+
const { rerender } = render(<Surface role="region" aria-label="表格" className="host-layout">内容</Surface>);
|
|
9
|
+
const surface = screen.getByRole('region', { name: '表格' });
|
|
10
|
+
expect(surface).toHaveClass('rhine-surface', 'host-layout');
|
|
11
|
+
expect(surface).not.toHaveClass('rhine-surface--padding-none');
|
|
12
|
+
rerender(<Surface role="region" aria-label="表格" className="host-layout" padding="none">内容</Surface>);
|
|
13
|
+
expect(surface).toHaveClass('rhine-surface--padding-none', 'host-layout');
|
|
14
|
+
expect(surface).not.toHaveAttribute('padding');
|
|
15
|
+
rerender(<Surface role="region" aria-label="表格" padding="default">内容</Surface>);
|
|
16
|
+
expect(surface).not.toHaveClass('rhine-surface--padding-none');
|
|
17
|
+
});
|
|
7
18
|
it('uses the independent library identity and accepts a host wordmark', () => {
|
|
8
19
|
const { rerender } = render(<Logo variant="lockup" />);
|
|
9
20
|
expect(screen.getByRole('img', { name: 'Rhine Design' })).toHaveTextContent('Rhine Design');
|