8000 refactor: 设置卡片缩进归外壳、行节奏归列表 by qqqqqf-q · Pull Request #44 · felinics/ui · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@
| 原始数值(颜色 / 阴影 / 圆角 / 尺寸) | `src/style.css` 的 `:root` / `.dark` / `@theme` | 任何 `.vue` 文件与宿主业务页面 |
| 交互外观(hover / press / focus / open) | `src/style.css` 的 `@layer components`(挂载于 `data-*` 属性) | 任何 `.vue` 文件与宿主业务页面 |
| 静止布局与底色(高度 / 内边距 / 圆角 / 静止文字色) | 组件 `.vue` 文件与 `cva` 定义 | 宿主业务页面不得重写 |
| 设置列表的行节奏(行高 / 上下内边距 / 行间细线) | `src/style.css` 的 `@layer components`(挂载于 `data-settings-list` 下的 `data-settings-row`) | 行组件与宿主业务页面 |
| 页面排布与组合 | 宿主页面与共享布局原语 | — |
| 业务逻辑与状态编排 | 宿主页面与业务层 | 组件库内不植入业务假设 |

设置列表的行节奏是上表“静止布局”的唯一例外:一行长什么样取决于它放在哪种列表里,行组件自己无法知道,所以规则按“列表 → 行”的后代关系写在 `style.css`。横向内缩同理由外壳决定:`SettingsSection` 卡片以 `--settings-inset`(16px)统一留白,行和卡片内的块都不再写 `mx-4` / `px-4`;弹窗等本身已有内边距的表面用 `SettingsList` 只提供行节奏。确需贴到卡片边缘的内容(整行点击目标、表格、图表画布、满宽分隔带)包进 `SettingsBleed`。

新建组件的验收标准:从未读过组件源码的调用方只需填入内容,不需要传任何布局或外观 CSS。可调参数一律设计为枚举型 prop(如 `width="md" | "lg" | "2xl"`),不用自由拼接的 class 字符串,迫使新档位在组件库内集中增补。遇到现有组件必须靠复制一串 class 才能正常工作时,直接向维护者报告并收敛进原语。

## CSS 层叠的四个覆盖平面
Expand Down
17 changes: 12 additions & 5 deletions skills/ui-owners/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ description: 在编写或修改任何设置页、Bot 详情页 Tab、弹窗/气
- `BackButton`:页内详情的唯一返回按钮(幽灵样式,左箭头 + 单行截断标题),无法使用 `DetailPane` 的 Tab 子页也直接复用它。
- `SwapTransition`:列表↔详情的左右推拉过渡容器(`direction: 'forward' | 'back'`,首屏无入场动画,返回时自动恢复列表此前的精确滚动位置)。
- **`SettingsSection` `[稳定]`**
- 顶部静音灰字小标题 + 带边框白底圆角卡片(`bg-card` + `border-border` + 12px 圆角)。Props:`title`(可省略渲染无标题卡片);插槽:`#actions`(标题右侧工具栏)、默认插槽(行列表)、`#footer`(卡片底部的顶边细线操作带,右对齐,供详情表单放取消/保存按钮)。绝不手写 `rounded-... border bg-card`。
- 顶部静音灰字小标题 + 带边框白底圆角卡片(`bg-card` + `border-border` + 12px 圆角)。Props:`title`(可省略渲染无标题卡片);插槽:`#actions`(标题右侧工具栏:组件自带靠右的按钮组和 `gap-2` 间距,多个按钮直接放入,不再自包一层)、默认插槽(行列表)、`#footer`(卡片底部的顶边细线操作带,右对齐,供详情表单放取消/保存按钮)。绝不手写 `rounded-... border bg-card`。
- 卡片同时是外壳和列表:左右内缩由卡片统一提供(`px-(--settings-inset)`,16px),卡片内任意层级的 `SettingsRow` 等行都获得列表的行节奏(规则按后代匹配,中间隔着包裹元素或组件也生效;末行去分隔线按行所在的父元素判断)。卡片内的任何块都不再写 `mx-4` / `px-4`,非列表块只写上下内边距(如 `py-4`)。
- **`SectionGroup` `[稳定]`**
- 带标题的**无外框**内容组,用于内部子项本身已带边框的布局(如 `BackendCard` 网格),避免产生“卡套卡”。仅在一个页面上下堆叠**多个**此类分组时使用(如语音页的 TTS / STT);整页只有一个网格时直接把标题和描述交给 `PageShell`。Props:`tone: 'foreground' | 'muted'`(默认前景深色;与 `SettingsSection` 同级混排时设为 `'muted'`,同页同级标题严禁混用两种色调);`bare`(当内部内容完全无边框时开启,取消标题的 `px-2` 内缩并加大标题与正文的间距)。
- **`StatusDot` `[稳定]`**
Expand All @@ -69,15 +70,19 @@ description: 在编写或修改任何设置页、Bot 详情页 Tab、弹窗/气
## 三、卡片内行原语

- **`SettingsRow` `[稳定]`**
- `SettingsSection` 卡片内的标准水平设置行(`min-h-[3.75rem] py-3 mx-4 border-b last:border-b-0`,自带左右内缩细线)。
- 设置列表里的标准水平设置行,只管行内布局(左标签描述、右控件、对齐与堆叠)。行高、上下内边距和行间细线(`min-h-15 py-3 border-b`,末行与 `:divider="false"` 无细线)由所在列表提供,左右内缩由卡片外壳提供;行本身不带任何外边距。放在列表外(例如弹窗里单独一行)时就是一行纯内容,不再自带缩进。
- Props:`label`、`description`、`align: 'center' | 'start'`(带头像或多行内容的高行用 `'start'` 顶对齐)、`stack: 'never' | 'sm' | 'always'`(默认单行;`'sm'` 在窄屏下折为上下排布防挤压;`'always'` 永久上下排布,用于标签下方放全宽 `Textarea` 或代码块)。
- 插槽:`#leading`(头像/图标/骨架)、`#content`(自定义左侧正文,如需要 `<Label :for>` 绑定焦点或插入状态徽标时替换默认标题描述)、默认插槽(右侧控件)。
- **`ExpandableSettingsRow` `[稳定]`**
- 整行头部可点击展开/收起下属面板的设置行。Props:`label`、`description`、`v-model:open`;插槽:`#leading`、`#content`、`#trailing`(默认带旋转 90° 的箭头)、`#expanded`(展开内容区,通常内放 `FormStack`)。仅用于“该行自身拥有展开内容”的场景。
- **`SettingsList` `[稳定]`**
- 卡片之外的一组设置行(例如弹窗正文里按小标题分组的行),只提供列表的行节奏,不加内缩、不加底色:所在表面(`DialogBody` 等)已经有自己的内边距。`SettingsSection` 卡片本身就是列表,卡片内不要再套它。
- **`SettingsBleed` `[稳定]`**
- 让卡片内的一块内容贴到卡片左右边缘:以同一个 `--settings-inset` 反向拉出(`-mx-(--settings-inset)`),内缩与它的例外共用一个值。用于悬停底色需要横跨整卡的整行点击目标(`ModelListRow` 内部即用它)、表格、图表或图谱画布、带满宽细线的分页条;内部需要与上下行文字对齐时自行补回 `px-(-- 10BC8 settings-inset)`。
- **`BackendCard` `[稳定]`**
- 根节点为真实 `<button>` 的水平对象入口卡片。Props:`name`、`subtitle`、`enabled`(绘制状态点);插槽:`#leading`(图标)、`#trailing`(默认右箭头)。用于提供商、存储后端等对象条目选择。
- **`ModelListRow` `[稳定]`**
- 卡片内紧凑型整行可点击导航行(根节点为 `<button>`,左侧名称与可选次要 `meta` ID,右侧装饰图标默认齿轮,通过 `last` 控制末尾分割线,显式派发 `click` 事件)。专用于“点击整行打开编辑弹窗”的紧凑模型列表;若一行内包含独立的启用开关、测试、删除等多个行内操作按钮(如 `providers/model-item.vue`),交互关系不同,保留独立实现,不强套此原语。
- 卡片内紧凑型整行可点击导航行(整行是一个 `<button>`,经 `SettingsBleed` 贴到卡片两侧边缘,因此只能放在 `SettingsSection` 卡片里;放进弹窗里的 `SettingsList` 会横向溢出 16px。左侧名称与可选次要 `meta` ID,右侧装饰图标默认齿轮,通过 `last` 控制末尾分割线,显式派发 `click` 事件)。专用于“点击整行打开编辑弹窗”的紧凑模型列表;若一行内包含独立的启用开关、测试、删除等多个行内操作按钮(如 `providers/model-item.vue`),交互关系不同,保留独立实现,不强套此原语。

---

Expand Down Expand Up @@ -121,7 +126,7 @@ description: 在编写或修改任何设置页、Bot 详情页 Tab、弹窗/气

1. **仅需旋转图标、无伴随文字**(极少见,如窄侧栏局部指示)→ `<Spinner>` 原子;
2. **点击触发异步请求的按钮自身** → 新代码用宿主封装的 `<LoadingButton>`(带 200ms 延迟与最短停留防闪),底层为 `<Button :loading>`;
3. **文档流内左对齐的“旋转图标 + 静音文字”单行占位** → `<InlineLoadingRow>` `[稳定]`(通过 `surface="card-row"` 渲染 `SettingsSection` 卡片内部行形态,或通过 `surface="tab"` 渲染无外卡的整页 Tab 形态,严禁手抄类名混用两套边距);
3. **文档流内左对齐的“旋转图标 + 静音文字”单行占位** → `<InlineLoadingRow>` `[稳定]`(通过 `surface="card-row"` 渲染设置列表——`SettingsSection` 卡片或 `SettingsList`——里的行形态,或通过 `surface="tab"` 渲染无外卡的整页 Tab 形态,严禁手抄类名混用两套边距);
4. **整块面板尚无任何其他内容时的居中填充** → `<PanePlaceholder>` `[稳定]`(无边框,支持 `loading`、默认空状态与双行强调 `title` 三种模式;父容器需提供高度)。

**`Empty` `[稳定]`**:页面或卡片内的结构化空状态组件(`EmptyHeader` / `EmptyTitle` / `EmptyDescription` / `EmptyContent`)。在 `SettingsSection` 内部时严禁加边框或图标底块;作为独立列表最外层时使用实线 `border`(详见 `../ui-patterns/references/loading-and-empty-states.md`)。
Expand Down Expand Up @@ -152,6 +157,8 @@ description: 在编写或修改任何设置页、Bot 详情页 Tab、弹窗/气
| 带标题的边框白卡(承载行列表) | `SettingsSection` |
| 卡片内单行“左标签描述 + 右控件” | `SettingsRow` |
| 点击行头原地展开下属表单的设置行 | `ExpandableSettingsRow` |
| 卡片之外(如弹窗正文)的一组设置行 | `SettingsList` |
| 卡片内需要贴边的表格 / 画布 / 整行点击目标 | `SettingsBleed` |
| 点击选择某个提供商/后端的对象卡片行 | `BackendCard` |
| 卡片内整行点击打开编辑器的紧凑列表行 | `ModelListRow`(若行内含多个独立开关/操作按钮则保留本地组件) |
| 同页堆叠多组自带边框的网格(如多组卡片) | `SectionGroup` |
Expand Down Expand Up @@ -190,7 +197,7 @@ description: 在编写或修改任何设置页、Bot 详情页 Tab、弹窗/气
## 十、常见陷阱与机械守卫棘轮

- **Tailwind 只扫描完整字面量字符串**:动态拼接如 `` `sm:${align}` `` 或 `` `min-h-[${x}]` `` 永远不会生成样式,条件分支必须写出完整的类名字符串。
- **严禁 `<button>` 嵌套 `<button>`**:`BackendCard` 与 `ModelListRow` 的根节点已是 `<button>`,尾部插槽内绝不能再放 `Switch` 或下拉按钮;既要整行点击又要行内独立开关的行需采用拉伸遮罩层写法保留在本地。
- **严禁 `<button>` 嵌套 `<button>`**:`BackendCard` 的根节点与 `ModelListRow` 的整行点击目标都是 `<button>`,尾部插槽内绝不能再放 `Switch` 或下拉按钮;既要整行点击又要行内独立开关的行需采用拉伸遮罩层写法保留在本地。
- **控制兄弟区块显隐的开关不用 `ExpandableSettingsRow`**:该组件只管理自己内部的 `#expanded`;若按钮控制的是下方另一个带独立分割线的兄弟区块,使用普通 `SettingsRow` 放按钮。
- **`SettingsSection #footer` 固定右对齐**:适合放“取消 + 保存”;左统计文字、右分页器的 `justify-between` 底栏暂保持本地写法。
- **守卫棘轮标记必须附带同行理由**:`scripts/check-ui-contract.mjs` 对卡片外手抄 `min-h-[3.75rem]` / `min-h-15` 发出警告(防手抄 `SettingsRow`),确属不同形状需豁免时必须在同一行写明原因(`/* ui-allow-shape: 具体原因 */`,空标记本身报错);对宿主页面新增裸 `animate-spin` 直接硬性报错(存量锁定在 `scripts/ui-spin-baseline.json`,新增必须走四级加载阶梯)。
Expand Down
2 changes: 1 addition & 1 deletion skills/ui-patterns/references/degraded-and-destructive.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
| `focus-visible:ring-success/30` 彩色焦点环 | 键盘焦点只用中灰 `--ring`;输入框聚焦原位换 `--field-edge-solid` |
| `opacity-50 grayscale` 禁用态 | 全系统统一 `opacity-40` |
| 吸顶半透明模糊顶栏 `bg-background/95 backdrop-blur` | 标准 `<PageShell>` 标题栏 |
| 卡片内用满宽 `border-b` 或把 `border-b` 写在 `v-if` 包装 `div` 上 | 卡片内行用内缩 `mx-4 ... last:border-b-0`(`SettingsRow`),包装 `div` 不带边框 |
| 卡片内用满宽 `border-b` 或把 `border-b` 写在 `v-if` 包装 `div` 上 | 卡片内行用 `SettingsRow`,细线由列表提供、内缩由卡片提供,包装 `div` 不带边框 |
| 在 `SettingsSection` 白卡内给 `<Empty>` 加虚线框或灰底图标块 | 白卡内 `<Empty class="py-12">` 无边框无图标;独立列表外层 `<Empty>` 用实线 `border` |
| 在已带边框的卡片行内放自带 `border` 的 `ItemMedia variant="icon"` | 前导图标只用透明尺寸盒,卡片细线是唯一描边 |
| 把手动保存的 `SettingsSection #footer` 抄到根级设置页 | 根级设置页保存放 `PageShell #actions`;二级详情页才放 `#footer` |
Expand Down
7 changes: 4 additions & 3 deletions skills/ui-patterns/references/layout-and-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@
| 页面标题 ↔ 第一个分组 | `mb-6`(个人资料页为 `mb-8`) | 由 `PageShell` 内部管理 |
| 卡片分组 ↔ 卡片分组 | `space-y-8` | 区分不同业务区块的大间距 |
| 分组小标题 ↔ 所属卡片 | `space-y-2.5` | 小标题为 `px-2 text-label font-medium text-muted-foreground` |
| 卡片内行 ↔ 行 | `mx-4 border-b border-border py-3 min-h-[3.75rem]` | 由 `SettingsRow` 管理,末行 `last:border-b-0` |
| 卡片左右边缘 ↔ 卡片内容 | `--settings-inset`(16px) | 由 `SettingsSection` 卡片统一提供,卡片内任何块不再写 `mx-4` / `px-4` |
| 卡片内行 ↔ 行 | `min-h-15 py-3 border-b border-border` | 由设置列表(卡片或 `SettingsList`)提供给其中的 `SettingsRow`,末行无细线 |
| 行标题 ↔ 行描述 | `mt-0.5` | 由 `SettingsRow` 管理 |
| 非列表类的卡片内边距块 | `p-4` 或 `p-5`,内部 `space-y-4` | 用于卡片内承载连续段落或表单块 |
| 非列表类的卡片内边距块 | `py-4` 或 `py-5`,内部 `space-y-4` | 用于卡片内承载连续段落或表单块;左右留白已由卡片提供 |

## 单层卡片原则(严禁卡套卡与双描边)

Expand All @@ -46,7 +47,7 @@

1. **卡片内部行与行之间 → 左右内缩(Inset)**
- 细线绝不能碰到圆角卡片的左右外边框(否则会把一张连续的圆角白卡切成几块叠起来的砖头)。
- 做法是把 `border-b` 放在带 `mx-4` 水平外边距的**行元素**上(即 `SettingsRow` 的默认行为),并在最后一行加 `last:border-b-0`。
- 卡片以 `--settings-inset` 给全部内容统一留白,行间细线画在**行元素**上(由列表规则提供给 `SettingsRow`,最后一行自动去掉),因此天然落在内缩范围内。
- **严禁把 `border-b` 写在包裹 `v-if` 的透明外层包装 `<div>` 上**:当那个包装 `<div>` 恰好成为卡片的最后一个子节点时,它的满宽 `border-b` 会直接压在卡片自身的底边框上,不仅切穿圆角,还会叠出一条粗黑双线。
2. **切分容器整体结构 → 边缘满宽(Full-bleed)**
- 弹窗的顶栏/底栏分隔带、菜单内的分组分隔线(`DropdownMenuSeparator`,严禁加 `mx-1` 留白变悬空短线)、章节标题下划线或区块之间的独立 `<Separator>`,一律横跨容器左右边缘,由内容区自己保留内边距。
Expand Down
9 changes: 4 additions & 5 deletions skills/ui-patterns/references/progressive-disclosure.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,15 @@
```
2. **在 `SettingsSection` 白卡内部 → 细线行 + 右侧 `outline` `sm` 按钮**:
```vue
<div class="mx-4 flex min-h-[3.75rem] items-center justify-between gap-4 border-b border-border py-3 last:border-b-0">
<span class="text-sm font-medium text-foreground">{{ $t('feature.advancedTitle') }}</span>
<Button variant="outline" size="sm" class="shrink-0" @click="open = !open">
<SettingsRow :label="$t('feature.advancedTitle')">
<Button variant="outline" size="sm" @click="open = !open">
<ChevronRight class="size-4 transition-transform" :class="open ? 'rotate-90' : ''" />
{{ open ? $t('feature.collapse') : $t('feature.expand') }}
</Button>
</div>
</SettingsRow>
<template v-if="open"><!-- 展开的设置行 --></template>
```
折叠行必须带 `last:border-b-0`:收起时它是卡片最后一行,底边细线自动隐藏以免与卡片底框叠成双线;展开后下方出现新行,它的底边自然成为分隔线(单行自包含展开也可直接使用 `<ExpandableSettingsRow>`)。
收起时折叠行是卡片最后一行,列表规则会去掉它的底边细线,不与卡片底框叠成双线;展开后下方出现新行,它的底边自然成为分隔线(单行自包含展开也可直接使用 `<ExpandableSettingsRow>`)。

**关键边界:决定“原地折叠”还是“弹独立窗口”的唯一标准是“是否为同一个关注点(Concern)”,而不是字段数量**。
- 如果这些高级选项属于**用户当前正在填写的同一个配置任务**(例如配置网络或渠道提供商时的鉴权、网络、环境变量等分组选填项),哪怕字段很多、分了几个小标题,也必须**留在当前卡片内用上面的原地折叠展开**——因为用户正在配这件事,把同一个表单的后半截拆进另一个弹窗会人为割裂操作流。
Expand Down
6 changes: 4 additions & 2 deletions src/components/inline-loading-row/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
"整 tab"家族,读作半空的高盒子而非紧凑一行。两种具名家族收进 `surface` prop,
调用方不再能抄错;真正局部、未具名的定位仍走 class 传入(单根节点,Vue 把外部
class 与内部基础 class 合并)。 -->
<!-- card-row 只声明自己是设置列表里的一行(data-settings-row):行高、上下边距和
分隔线由所在列表提供,横向缩进由卡片外壳提供,与 SettingsRow 一致。 -->
<div
:data-settings-row="surface === 'card-row' ? '' : undefined"
:class="[
'flex items-center gap-2 text-muted-foreground',
size === 'md' ? 'text-control' : 'text-body',
bordered && 'rounded-md border border-border-soft bg-background p-3',
surface === 'card-row' && 'mx-4 min-h-[3.75rem] border-b border-border py-3 last:border-b-0',
surface === 'tab' && 'px-2 py-8',
]"
>
Expand All @@ -39,7 +41,7 @@ withDefaults(defineProps<{
bordered?: boolean
/**
* 具名定位家族,取代调用方手抄同一串 class:
* `card-row` = SettingsSection 卡片内的列表行占位(带下边框,末行去边框);
* `card-row` = 设置列表(SettingsSection 卡片或 SettingsList)里的行占位,行节奏与分隔线随列表;
* `tab` = 整个 tab 无卡片包裹时的居中留白占位。
* 不传则不加任何定位 class,由调用方经根 class 传入(局部、未具名的定位场景)。
*/
Expand Down
Loading
Loading
0