# Agent Note: 预设卡片截断自身描述，而不是由描述决定整份名单的高度

Status: implemented

[English](2026-08-11-preset-card-description-clamp.md) | 中文

## 问题

preset 自行发布 `description`，长度不限，而设置分区把名单渲染为卡片网格。描述只有 `min-height` 没有上限，网格则以 `grid-auto-rows: 1fr` 排布行——该取值让每一个隐式行等高，而不只是承载高卡片的那一行。因此一条长描述决定了整份名单的高度：自定义组里放入一条 250 字的描述后，四张卡片全部量得 421px，短描述卡片被大片空白填满。

描述同时又是区分各个 preset 的字段，因此不能藏起来；卡片必须既给它设上限，又让全文仍然可达。

## 决定

描述截断为四行，其余内容通过共享的 `Tooltip` 呈现，且仅在元素确实溢出时才挂载（`scrollHeight > clientHeight`，并经 ResizeObserver 重新测量，因为设置面板宽度跟随窗口）。这与聊天统计行一致：它按同样的「先测量再挂载」规则截断为一行。

卡片高度仍是推导得出而非固定。描述有了上限之后，`grid-auto-rows: 1fr` 本身就让网格等高，而承载损坏原因或已展示目录的卡片仍能按自身内容定高——写死像素高度会把两者一并裁掉。

随之而来三个更小的决定：

- `.cardId` 以 `margin-top: auto` 吃掉卡片的空余空间，描述不再拉伸。被 flex 拉伸的盒子会让截断高度与盒子高度不一致；让截断盒子只按内容定高，行为便不依赖这层交互。
- 描述带有 `title=""`。空 `title` 表示该元素没有提示信息，查找就此停止，因此卡片主体的原生 tooltip 不会向上找到描述，被裁切的描述只回应一个气泡而不是两个。
- `Tooltip` 新增可选的 `maxWidth`。它默认的半视口上限会把描述渲染成比所属设置弹窗还宽的一整块，溢出到背后的应用界面上。
- `Tooltip` 同时在视口放不下时把 `top` 或 `bottom` 气泡翻到另一侧，此前它只做水平收敛。自定义 preset 位于名单末尾、又恰恰承载最长的描述，因此常见情形正是让一个高气泡挂在页面靠下的锚点之下。翻转只会移向确实放得下的一侧，两侧都放不下时保持请求的位置而不来回摆动；改为垂直滑动则会盖住正在阅读的文本。

形状检查未通过的名单行，徽记从 `Broken`（`已损坏`）改为 `Failed to load`（`加载失败`）。discovery 在组装文件缺失、读不出或格式错误时置位 `broken`——最常见的是用户刚编辑或删除的文件——因此断言损坏超出了观察到的事实，而徽记下方原样展示的原因本就点名了文件与修法。

## 备选方案

- **写死卡片高度。** 它直接表达了意图，却会裁掉两处高度本就可变的行：损坏预设的原因行和已展示的预设目录。
- **用原生 `title` 属性承载完整描述。** 无需测量也无需组件，代价是约一秒的延迟、操作系统的样式，以及在卡片大部分区域内顶替掉「设为默认」的提示。
- **无条件挂载 tooltip。** 省掉 ResizeObserver，代价是把鼠标停在短描述上时，弹出一个重复卡片已有内容的气泡。
- **hover 时展开截断。** 它就地展示文本，同时让网格在指针下方发生位移。

## 后果

分区多了一个带测量的小组件，共享基元多了一个可选 prop。换来的是：任何卡片的高度都不再跟随名单中最长的那条描述；而且截断由 CSS 完成而非截短文本，完整描述始终留在无障碍树中。

`title=""` 的抑制作用由一条 DOM 断言钉住，而非通过观察原生 tooltip：浏览器 tooltip 画在页面之外，无法被捕获。若某个浏览器日后重新越过空 `title` 继续向上查找，退路是去掉卡片主体的 `title`——它的内容已经在主体的 `aria-label` 里。

## 测试

包内测试覆盖三种测量结果（被裁切、放得下、运行时没有 `ResizeObserver`）以及 tooltip 的宽度上限。web e2e golden 除 `damaged.expected.md` 按徽记文案重录外，其余原样回放通过。
