# 超级块响应式布局最佳实践

超级块的布局应遵循三个步骤：**测量 iframe 的实际内容区，按宽高分配空间，再按信息优先级选择布局。** 比例计算适合字号和间距的小范围调整；信息数量和排列方向需要根据内容预算切换。

## 1. 用页面内容区计算，不用网格占位或屏幕分辨率

在清单中声明的 `cols × rows` 是网格占位，包含宿主保留的间隔和标题区域，并不等于页面可以使用的宽高。当前桌面画布与移动设备采用 160 CSS 像素的逻辑网格；已放置组件的页面尺寸为：

```text
W = cols × 160 − 2 × 24
H = rows × 160 − 2 × 12 − 24
```

| 网格占位 | 已放置组件的页面内容区 |
| --- | --- |
| 1×1 | 112×112 |
| 2×1 | 272×112 |
| 2×2 | 272×272 |
| 4×1 | 592×112 |
| 4×2 | 592×272 |
| 4×4 | 592×592 |

组件目录卡片可能使用未扣除间隔的展示尺寸，例如 160×160。上述数字用于理解当前宿主和建立测试用例，**不要把它们写成插件的运行时尺寸协议**。实际排版应始终读取页面根元素的 `clientWidth`、`clientHeight`，并用 `ResizeObserver` 监听变化。

宿主会缩放整张逻辑画布。因此，屏幕截图中的像素大小、`screen.width`、`devicePixelRatio`、外层 iframe 的变换后矩形，都不适合决定页面布局。不要再按屏幕缩放一次页面。`context.surface` 用来对齐内缩描边和圆角，不是需要从页面宽高中再次扣除的网格间隔，详见[外轮廓几何](./widgets.md#最佳实践二-外轮廓圆角与几何裁剪)。

## 2. 先做空间预算，再做有限的比例缩放

推荐以下起点，具体比例和上下限需要结合组件内容验证：

```text
D = min(W, H)
P = clamp(8, 0.07 × D, 14)       // 内边距，CSS px
G = clamp(4, 0.035 × D, 8)       // 区域间距，CSS px
B = H − 2P − T − F − 2G         // 头部、主体、底部三行时的主体高度
```

这里 `T`、`F` 是头部与底部实际预留高度。例如 272×112 的活动卡片取 `P=8`、`G=4`、`T=24`、`F=24`，主体只剩 **40px**。两行 12px 标题约占 32.4px，不能再叠加一行预览。此时应收起预览，或者移除冗余底部操作并重新计算预算。

字号也要同时满足两条约束：

- **高度约束**：主数值字号不得超过主体扣除标签、进度条、间距后的剩余高度。
- **宽度约束**：为 `100%`、负数、单位、长模型名等内容预留空间。对于可变字符串，限制行数或省略；必须完整显示时，测量文本并切换布局。

例如主数值使用 `line-height: 1`，标签占 14px，进度条与间距共占 8px，则可以先计算 `fontSize ≤ B − 22`。再根据数值列宽度和最坏字符串约束字号，最后限制最大字号。**不能只用 `vw` 放大字体**：592×112 的长条高度仍然只有 112px。

当预算小于最小可读字号时，应减少信息、换行布局、提供详情入口，或缩小清单中的 `allowedSizes` 范围。不要持续压缩字号到无法阅读。

## 3. 以内容容量切换布局

为每项信息规定最小空间，再确定展示优先级。推荐从最小布局向外增加内容：

| 组件 | 最小布局保留 | 空间增加后展示 |
| --- | --- | --- |
| 活动 | 状态、当前标题、主要操作 | 消息预览、模型时间、最近对话列表 |
| 用量 | 窗口名称、百分比、进度、重要警告 | 重置时间、积分、次窗口、Token 统计 |
| 运行配置 | 模型入口、当前推理强度、速度开关 | 标题栏、完整选项、说明 |

横向布局至少需要满足 `可用宽度 ≥ 模型最小列宽 + 两个操作列宽 + 间距`；纵向布局至少需要满足 `主体高度 ≥ 标题区域 + 操作行 + 间距`。长宽比只能帮助选择方向，不能代替这两个条件。

在 112×112 的运行配置中，可以让模型独占上半区，推理强度和速度各占下方一列；将完整推理选项收起，由当前强度按钮承担循环切换。模型名最多两行，“标准”等短标签保持不换行。不要把横向三个完整面板直接缩进小方块。

错误提示同样需要预算。紧凑布局可用错误区域替换主体，保留刷新或重试操作；长错误文本在该区域内部滚动。不要把错误作为没有高度预留的额外一行追加。

## 4. 可直接使用的布局骨架

下面的示例显示一个标签、数值、进度条和重要提示，按主体剩余空间决定数值字号。它采用保守的四字符宽度上限，适合 `100%`；换成金额或任意文本时，需要重新计算宽度预算。

```html
<style>
  * { box-sizing: border-box; }
  html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; }
  .card {
    width: 100%; height: 100%; min-width: 0;
    display: grid;
    grid-template-rows: minmax(0, 1fr) 14px;
    padding: var(--pad, 8px); gap: var(--gap, 4px);
    background: #101010; color: #eee; font-family: system-ui;
  }
  .primary {
    min-height: 0; min-width: 0;
    display: grid; grid-template-rows: 14px minmax(0, 1fr) 4px;
    gap: 4px;
  }
  .label, .notice {
    min-width: 0; font-size: 10px; line-height: 14px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .value { min-width: 0; align-self: center; font-size: var(--value-size, 24px); line-height: 1; white-space: nowrap; }
  .track { height: 4px; background: #333; }
  .track > span { display: block; height: 100%; width: 100%; background: #e18d7b; }
  .notice { color: #dd955b; }
</style>
<div class="card" id="card">
  <main class="primary">
    <span class="label">每周用量</span>
    <strong class="value">100%</strong>
    <div class="track"><span></span></div>
  </main>
  <span class="notice">用量暂不可用</span>
</div>
<script>
  const card = document.querySelector('#card');
  const clamp = (min, value, max) => Math.max(min, Math.min(value, max));
  function layout() {
    const W = card.clientWidth, H = card.clientHeight;
    if (!W || !H) return;
    const D = Math.min(W, H);
    const P = clamp(8, D * .07, 14);
    const G = clamp(4, D * .035, 8);
    const B = H - 2 * P - 14 - G;
    // Label 14 + two gaps 8 + track 4. Width allows four full ems.
    const fontSize = Math.min(56, B - 26, (W - 2 * P) / 4);
    card.style.setProperty('--pad', P + 'px');
    card.style.setProperty('--gap', G + 'px');
    card.style.setProperty('--value-size', Math.max(16, fontSize) + 'px');
  }
  new ResizeObserver(layout).observe(card);
  layout();
</script>
```

此骨架按不小于 112×112 的内容区设计。若需要支持更小尺寸，应另设最小布局并验证，不能仅把公式的最小字号调低。实际插件中的数值仍应来自[真实状态](./widgets.md#最佳实践一-桌面端与移动端数据同步)。

使用 Grid 或 Flex 时，主体设置 `min-height: 0`、文本列设置 `min-width: 0`，让浏览器能够分配有限空间；同时确保子内容确实适合该空间。`min-height: 0` 和根元素的 `overflow: hidden` 都不会自动解决重叠，后者只会把超出部分裁掉。

## 5. 验证的是区域关系，不只是页面溢出

发布前，至少完成以下验证：

1. 从清单的 `allowedSizes` 枚举全部规格，检查当前宿主对应的真实内容区；同时检查目录卡片和布局断点前后 1px。默认规格通过不代表其它规格可用。
2. 覆盖加载、空数据、长中文和无空格英文、`0%`、`100%`、长模型名、最多选项、警告、长错误文本，以及隐藏可选字段后的状态。
3. 在浏览器中测量关键区域的 `getBoundingClientRect()`：兄弟区域不能相交，文本和进度条不能越过各自分配的区域，按钮不能互相覆盖。允许滚动的列表只在自己的容器内滚动。
4. 同时检查 `scrollWidth`、`scrollHeight`。它们只能发现越界，**发现不了发生在根元素内部的文字、进度条和底部操作重叠**。仅用模拟 DOM 的业务测试也无法验证 CSS 排版。
5. 对最小方块、最矮长条和最大规格保存截图，检查字号、截断、圆角边缘和实际可点击区域；最后在桌面预览与设备页面中验证宿主缩放效果。

这些公式是可验证的设计起点，不是统一的强制断点。每个插件应根据实际文本、操作数量和展示优先级建立自己的尺寸预算，并把最小支持规格纳入回归检查。
