1. 核心概念与背景

在现代桌面多栏(Multi-Rail/Multi-Column)可拖拽分栏布局中,各列的宽度由用户拖拽分隔条(Resizer)实时决定,而不是取决于浏览器的全屏视口宽度。

  • 媒体查询 (Media Queries, @media) 的局限:
    @media (max-width: ...) 只能感知 window.innerWidth(浏览器视口总宽)。当用户在一个 1920px 宽的显示器上把中间主舞台拖窄到 450px,或者把右侧侧栏拖窄到 260px 时,window.innerWidth 毫发无损,所有的 @media 响应式规则均不会被触发,导致栏内按钮、输入框、标签依然保持固定绝对像素死挤硬抗,引发严重的物理重叠碰撞("打架")。
  • 容器查询 (Container Queries, @container) 的机制:
    根据元素直属祖先容器所声明的 container-type: inline-size,直接查询该容器当前的物理宽度,使组件不论放置在侧栏、弹窗还是中央舞台,都能严格根据自身可用宽度自适应调整。

2. 关键陷阱:A Container Cannot Query Itself (容器无法查询自身)

陷阱现象

开发者经常直觉性地在一个卡片组件(如 .agent-tool-pill)上声明:

.agent-tool-pill {
  container-type: inline-size;
  container-name: tool-pill;
  padding: 6px 12px;
  min-height: 38px;
}

@container tool-pill (max-width: 320px) {
  .agent-tool-pill {
    padding: 3px 6px;   /* 试图在收窄时缩小自身的内边距 */
    min-height: 28px;   /* 试图缩小自身的高度 */
  }
}

结果:样式完全不生效!

根因分析

根据 W3C CSS Container Queries 规范:

A container query applies only to descendants of the container. A container cannot evaluate a container query against itself.

也就是说,容器查询的作用域仅限于容器的子孙节点。容器元素本身(.agent-tool-pill)无法依据自身的宽度来动态计算修改自身的外层属性(如 padding、min-height、border)。

解决方案

必须将 container-type: inline-size; container-name: ... 声明在组件的直接父级容器上(如 .tool-pills-stack 或 .rail.right):

/* 父级声明为容器 */
.tool-pills-stack {
  container-type: inline-size;
  container-name: tool-stack;
}

/* 子级卡片即可被该容器查询调度 */
@container tool-stack (max-width: 340px) {
  .agent-tool-pill {
    padding: 4px 7px;
    min-height: 30px;
  }
  .tool-pill-action {
    font-size: 11px;
  }
}

3. "整条联动比例缩小" (Proportional Scaling) 架构设计模式

如果只零散地修改某个子元素,整条工具栏/卡片在变窄时依旧会发生局部断层。最佳工程实践是通过 CSS 变量集中参数化:

1. 变量统领度量

在工具栏或卡片根节点上集中定义度量令牌:

.custom-pdf-toolbar {
  --tb-h: 40px;
  --tb-px: 10px;
  --tb-gap: 6px;
  --tb-btn-h: 28px;
  --tb-btn-min-w: 28px;
  --tb-btn-px: 6px;
  --tb-btn-font: 11.5px;
  --tb-icon-size: 14px;
  --tb-input-w: 42px;
  --tb-input-h: 26px;
  --tb-input-font: 12px;
  --tb-total-font: 11.5px;
  --tb-select-w: 96px;
  --tb-select-font: 11.5px;
  --tb-divider-h: 16px;
  --ws-ctl-h: var(--tb-btn-h);
}

2. 多梯级渐进收敛 (Multi-Tier Progressive Convergence)

整条算在一起,分级联动微缩各变量:

  1. 轻度收紧 (Stage <= 840px):

    • 隐藏辅助性信息(如文件大小),字号和高度微幅收缩(40px -> 38px);
  2. 中度收紧 (Stage <= 720px):

    • 隐藏长标题(收进 ... 下拉菜单),将高度降至 36px,按钮缩小至 25px,输入框降至 34px;
  3. 高度收紧 (Stage <= 590px):

    • 动作按钮由图文混排优雅降级为纯图标(隐藏文字标签),释放横向空间 120px+;
  4. 超窄极简 (Stage <= 480px):

    • 隐藏占用过大的下拉选择框(功能由 +/- 按钮与 ... 菜单接管),高度收缩至 32px;
  5. 极限超窄 (Stage <= 380px):

    • 隐藏装饰分割线,全要素紧凑排列,高度 30px,输入框 22px,字号 9.5px,完全杜绝换行与元素撞车。

3. 卡片防挤压截断 (Anti-Crumple Pattern)

当工具卡片变窄时,长路径文本(.tool-pill-target)常被挤压至仅剩一个破损单字 [ 文 ]。

  • 阶段 1 (<= 340px):隐藏"详情"汉字,仅保留右箭头 >,立即腾出 26px。
  • 阶段 2 (<= 310px):缩短最大宽度 max-width: 80px,字号等比缩至 10px。
  • 阶段 3 (<= 255px 极限狭窄):直接隐藏长路径 display: none !important,完整展示"动作名称"与"状态点",杜绝残字,点击卡片仍可展开浮层详情。

4. 原生 Select 控件文字与宽度动态自适应模式 (Auto-fit Select with Adaptive Option Text)

痛点根因

  1. 选项少一个都不行:工具栏核心功能(如 PDF 缩放比例下拉框)在紧凑顶栏中必须保留,不能一刀切 display: none;
  2. 原生 Select 宽度机制缺陷:浏览器原生 <select> 的 width: auto 永远由最宽的一个 <option>(如“自适应宽度”)撑开。即使用户选了紧凑的“100%”,外框也无法自动变窄,白白浪费 30~40px;
  3. 长文本挤占物理空间:“自适应宽度”有 5 个汉字,在窄分栏下极易与相邻的翻页按钮发生物理碰撞。

解决方案:双重联动自适应

  1. 阶梯文案映射 (Tiered Option Text):
    保持每个 <option> 的 value(如 fit-width、fit-page)稳定不变,根据当前容器物理宽度动态切换呈现文案:

    • 宽屏(> 640px):自适应宽度 / 自适应整页(完整易懂)
    • 中屏(480px ~ 640px):适合宽 / 适合页(节约 2 字空间)
    • 窄屏(<= 480px):适宽 / 整页(极致精炼)
    • 所有百分比选项(50%、100%、200%)全程完整保留,少一个都不行。
  2. 按选中文本物理宽度实时自适应 (Dynamic Content-Width Auto-Fitting):
    通过 ResizeObserver 配合选项切换回调,根据当前选中文本的字符类型与数量,精确计算目标像素宽度并赋值给 style.width:

    const isChinese = /[\u4e00-\u9fa5]/.test(text);
    const charWidth = isChinese ? 11.5 : 7;
    const basePadding = 22; // 自定义微型小箭头(16px) + 内边距(6px)
    selectEl.style.width = `${Math.ceil(charCount * charWidth + basePadding)}px`;
  3. 轻量 SVG 微型箭头取代原生臃肿箭头:
    使用 appearance: none 去除操作系统原生宽达 20px+ 的粗大下拉三角,改为内嵌数据化 8px 微型墨灰箭头(background-image: url(...)),视觉典雅且为文字腾出 10px+ 宽度。

本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Frontend