mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(web): cap the answer field at six text lines in both variants
The card-wide `border-box` reached `.fieldMirror`, so the growth cap counted text plus padding. The inline variant carries none and landed on the declared line count, while the optionless variant's 16px inset spent two thirds of a line and delivered its last one as an 8px sliver. The e2e measured only the inline shape, so nothing caught the drift. The mirror now takes `box-sizing: content-box`, which states the cap in the units it is written in, and the cap moves to six lines. The e2e asserts whole text lines rather than a box height, and covers the optionless shape — asked straight through the user-questions seam, since a layout metric needs no model round — including that the reserved empty field is fully covered by its own control.
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-20-multiline-question-answer-field.md
|
||||
2026-08-20-multiline-question-answer-field.md: dbe146f1cd56b0f757784283269ba5061c425366
|
||||
2026-08-20-multiline-question-answer-field.zh.md: 7adb5bb37bf4393a5be21f1dc56b2be46fbfe9cb
|
||||
2026-08-20-multiline-question-answer-field.md: 5e411d6728205ce1b72ef8355bbf2493f28873eb
|
||||
2026-08-20-multiline-question-answer-field.zh.md: 6b98f017bbee2e8d89810c5b193b3707c85d1e95
|
||||
|
||||
@@ -16,7 +16,7 @@ Both question shapes answer into one `AnswerField`: a `<textarea rows={1}>` shar
|
||||
|
||||
The mirror sits in normal flow and so sizes the grid row; the textarea stretches to that row, and `rows={1}` keeps the control's own intrinsic height out of the row sizing, leaving the mirror the only input to the height. Soft wraps are invisible to a `'\n'` count, so the mirror is what makes a wrapped answer grow the box rather than scroll one line. The trailing newline covers the last line the textarea's caret can reach and the block container drops. Mirror and textarea must keep identical type, padding, and wrapping rules; a divergence sizes the box wrong for the text being typed.
|
||||
|
||||
Growth stops at the mirror's `max-height` of eight lines, and past that the textarea scrolls itself. It is the only scrollport in the stack: unlike the chat composer, this field paints its own glyphs, so there is no second layer whose scroll offset would have to match.
|
||||
Growth stops at the mirror's `max-height` of six lines, and past that the textarea scrolls itself. The mirror takes `box-sizing: content-box` against the card-wide `border-box` so that cap counts text lines rather than text plus padding: the optionless variant carries 16px of vertical padding, which under `border-box` spends two thirds of a line and delivers the last one as an 8px sliver, while the inline variant has no padding and would land on a different line count from the same declaration. It is the only scrollport in the stack: unlike the chat composer, this field paints its own glyphs, so there is no second layer whose scroll offset would have to match.
|
||||
|
||||
Enter continues the flow and submits the batch on the last question, Shift+Enter breaks the line, and the IME guard is unchanged — Enter during composition confirms the candidate without advancing. The `variant` prop names which of the two looks the field takes, so the field owns both and neither caller assembles one out of class names.
|
||||
|
||||
@@ -32,9 +32,11 @@ Enter continues the flow and submits the batch on the last question, Shift+Enter
|
||||
|
||||
**Uncapped growth.** Rejected because the card tops out at `min(60vh, 520px)` and still owes that budget to the title, the option rows, and the footer actions; an unbounded field pushes the choices the answer belongs to out of view.
|
||||
|
||||
**A per-variant cap that absorbs each variant's padding.** Rejected because it couples the line count to a padding value: changing `.customBlock`'s inset would silently change how many lines the field grows to. `content-box` states the intent once, in the units the cap is written in.
|
||||
|
||||
## Testing
|
||||
|
||||
Component tests pin the round trip: both shapes render a textarea, the mirror follows the draft, Shift+Enter never advances the flow, and line breaks reach the answer batch verbatim. The assembled `question-composer` web e2e measures the live engine — a soft-wrapped draft grows the field without scrolling it, two Shift+Enter presses leave `"\n\n"` in a taller field with the question still open, and a draft past the cap scrolls instead of growing.
|
||||
Component tests pin the round trip: both shapes render a textarea, the mirror follows the draft, Shift+Enter never advances the flow, and line breaks reach the answer batch verbatim. The assembled `question-composer` web e2e measures the live engine — a soft-wrapped draft grows the field without scrolling it, two Shift+Enter presses leave `"\n\n"` in a taller field with the question still open, and a draft past the cap scrolls instead of growing at exactly six text lines in both variants.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Status: implemented
|
||||
|
||||
镜像位于常规流中,因而决定 grid 行的高度;textarea 拉伸到该行,而 `rows={1}` 使控件自身的固有高度不参与行尺寸计算,于是镜像是高度的唯一输入。软换行对 `'\n'` 计数不可见,因此正是镜像让换行后的答案把输入框撑高,而不是在一行内滚动。结尾的换行补上 textarea 光标可达、而块级容器会丢弃的最后一行。镜像与 textarea 必须保持完全一致的字体、内边距与换行规则;一旦偏离,输入框的高度就与正在输入的文本不符。
|
||||
|
||||
增高在镜像 `max-height` 的八行处停止,此后由 textarea 自身滚动。它是这个结构里唯一的滚动容器:与聊天输入框不同,本输入框自己绘制字形,因此不存在第二个需要对齐滚动偏移的图层。
|
||||
增高在镜像 `max-height` 的六行处停止,此后由 textarea 自身滚动。镜像用 `box-sizing: content-box` 覆盖卡片范围内的 `border-box`,使该上限计量的是文本行而不是「文本加内边距」:无选项变体带 16px 纵向内边距,在 `border-box` 下这会吃掉三分之二行、并把最后一行变成 8px 残条,而 inline 变体没有内边距,同一条声明会落到不同的行数上。它是这个结构里唯一的滚动容器:与聊天输入框不同,本输入框自己绘制字形,因此不存在第二个需要对齐滚动偏移的图层。
|
||||
|
||||
Enter 继续流程并在最后一题提交整批,Shift+Enter 换行,IME 保护不变——组合输入期间按 Enter 只确认候选,不前进。`variant` 属性指明该输入框采用两种外观中的哪一种,因此外观归输入框自己所有,任何调用方都不必用类名拼装。
|
||||
|
||||
@@ -32,9 +32,11 @@ Enter 继续流程并在最后一题提交整批,Shift+Enter 换行,IME 保
|
||||
|
||||
**不设增高上限。** 否决,因为卡片上限为 `min(60vh, 520px)`,还要把这份额度分给标题、选项行与底部操作;不设上限的输入框会把答案所属的选项挤出视野。
|
||||
|
||||
**按变体各自吸收内边距的上限。** 否决,因为那会把行数与某个内边距值耦合:改动 `.customBlock` 的内边距会悄悄改变输入框能长到几行。`content-box` 只需声明一次意图,且与上限本身的单位一致。
|
||||
|
||||
## Testing
|
||||
|
||||
组件测试固定了整条往返:两种形状都渲染 textarea、镜像跟随草稿、Shift+Enter 绝不前进流程、换行原样进入答案批次。组装后的 `question-composer` web e2e 则在真实引擎上测量——软换行的草稿把输入框撑高而不滚动,两次 Shift+Enter 之后 `"\n\n"` 留在更高的输入框里且问题仍未关闭,超过上限的草稿改为滚动而不再增高。
|
||||
组件测试固定了整条往返:两种形状都渲染 textarea、镜像跟随草稿、Shift+Enter 绝不前进流程、换行原样进入答案批次。组装后的 `question-composer` web e2e 则在真实引擎上测量——软换行的草稿把输入框撑高而不滚动,两次 Shift+Enter 之后 `"\n\n"` 留在更高的输入框里且问题仍未关闭,超过上限的草稿在两种变体下都恰好于六行文本处改为滚动而不再增高。
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import type { Browser, Locator, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
@@ -30,6 +31,31 @@ const COMPOSED_EXPECTED = join(SNAPSHOT_DIR, 'composed.expected.md')
|
||||
const ANSWERED_EXPECTED = join(SNAPSHOT_DIR, 'answered.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
// The composer's own growth cap, in text lines (QuestionComposer.module.css
|
||||
// .fieldMirror). Asserted as TEXT lines, not as a box height: the two variants
|
||||
// carry different padding, and a cap measured in border-box pixels silently
|
||||
// means a different line count in each — which is exactly how the optionless
|
||||
// field came to stop two thirds of a line short.
|
||||
const CAP_LINES = 6
|
||||
|
||||
/**
|
||||
* Measure a saturated answer field: how many whole text lines it grew to, and
|
||||
* whether it took over the scrolling once it stopped growing.
|
||||
* @param field - the composer's custom-answer textarea.
|
||||
* @returns whole text lines the content box holds, and whether the field scrolls.
|
||||
*/
|
||||
async function capMetrics(field: Locator): Promise<{ textLines: number; scrolls: boolean }> {
|
||||
await field.fill('x\n'.repeat(40))
|
||||
return field.evaluate((el: HTMLTextAreaElement) => {
|
||||
const style = getComputedStyle(el)
|
||||
const text = el.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom)
|
||||
return {
|
||||
textLines: Math.round(text / parseFloat(style.lineHeight)),
|
||||
scrolls: el.scrollHeight > el.clientHeight,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// The options carry long descriptions on purpose: the squeeze assertion below
|
||||
// needs option copy that WRAPS, which is the only text layout that reproduces a
|
||||
// collapsed row painting its copy outside its own box.
|
||||
@@ -41,6 +67,7 @@ describe('web e2e: resident question composer round trip', () => {
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
const sessionEvents: SessionEvent[] = []
|
||||
let answeredSession: SessionId | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 })
|
||||
@@ -141,13 +168,13 @@ describe('web e2e: resident question composer round trip', () => {
|
||||
// the squeeze above: record mode must reach the recording write.
|
||||
const custom = composer.getByRole('textbox')
|
||||
if (MODE !== 'record') {
|
||||
const lineHeight = await custom.evaluate(el => el.getBoundingClientRect().height)
|
||||
const oneLineHeight = await custom.evaluate(el => el.getBoundingClientRect().height)
|
||||
await custom.fill('a'.repeat(120))
|
||||
const wrapped = await custom.evaluate(el => ({
|
||||
height: el.getBoundingClientRect().height,
|
||||
scrolls: el.scrollHeight > el.clientHeight,
|
||||
}))
|
||||
expect(wrapped.height).toBeGreaterThan(lineHeight * 1.5)
|
||||
expect(wrapped.height).toBeGreaterThan(oneLineHeight * 1.5)
|
||||
expect(wrapped.scrolls).toBe(false)
|
||||
|
||||
await custom.fill('')
|
||||
@@ -156,10 +183,9 @@ describe('web e2e: resident question composer round trip', () => {
|
||||
expect(await custom.inputValue()).toBe('\n\n')
|
||||
expect(await composer.getByText('Which color do you prefer?').count()).toBeGreaterThan(0)
|
||||
expect(await custom.evaluate(el => el.getBoundingClientRect().height))
|
||||
.toBeGreaterThan(lineHeight * 2.5)
|
||||
.toBeGreaterThan(oneLineHeight * 2.5)
|
||||
|
||||
await custom.fill('x\n'.repeat(40))
|
||||
expect(await custom.evaluate(el => el.scrollHeight > el.clientHeight)).toBe(true)
|
||||
expect(await capMetrics(custom)).toEqual({ textLines: CAP_LINES, scrolls: true })
|
||||
await custom.fill('')
|
||||
}
|
||||
|
||||
@@ -179,6 +205,7 @@ describe('web e2e: resident question composer round trip', () => {
|
||||
await recordFixture(scaffold, sessionId, FIXTURE)
|
||||
return
|
||||
}
|
||||
answeredSession = sessionId
|
||||
// World state: the tool result carries the chosen answer, and DONE lands.
|
||||
const results = sessionEvents.filter(e => e.type === 'tool/result')
|
||||
const answerText = results.flatMap(event => event.data.message.content.flatMap(block =>
|
||||
@@ -202,6 +229,47 @@ describe('web e2e: resident question composer round trip', () => {
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 200_000)
|
||||
|
||||
// The fixture's question carries options, so the round trip above only ever
|
||||
// exercises the inline shape. The optionless shape is the one that carries
|
||||
// padding, which is where a cap measured in box pixels drifts off the line
|
||||
// count — so it is asked straight through the user-questions seam (the same
|
||||
// service the tool calls; no model round is involved in a layout metric).
|
||||
it.skipIf(MODE === 'record')('grows the optionless answer to the same cap', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-question-optionless'))
|
||||
const sessionId = answeredSession
|
||||
expect(sessionId).toBeDefined()
|
||||
const agent = scaffold.ctx.agents.get(sessionId as SessionId)
|
||||
expect(agent).toBeDefined()
|
||||
const asked = scaffold.ctx.userQuestions.ask({
|
||||
agent: agent as NonNullable<typeof agent>,
|
||||
questions: [{ id: 'free', header: 'More', question: 'Anything else?' }],
|
||||
})
|
||||
|
||||
const composer = page.locator('[data-question-key]')
|
||||
await composer.waitFor({ timeout: 30_000 })
|
||||
const field = composer.getByRole('textbox')
|
||||
// The empty field reserves its two lines AND the textarea fills that frame:
|
||||
// a reserved box the control does not fill leaves a strip that looks like
|
||||
// the field but takes no click.
|
||||
expect(await field.evaluate((el) => {
|
||||
const frame = el.parentElement as HTMLElement
|
||||
const style = getComputedStyle(frame)
|
||||
const inner = frame.getBoundingClientRect().height
|
||||
- parseFloat(style.borderTopWidth) - parseFloat(style.borderBottomWidth)
|
||||
return {
|
||||
reserved: Math.round(frame.getBoundingClientRect().height),
|
||||
fills: Math.abs(el.getBoundingClientRect().height - inner) < 0.5,
|
||||
}
|
||||
})).toEqual({ reserved: 64, fills: true })
|
||||
// The same cap the inline shape stops at — the assertion a border-box cap fails.
|
||||
expect(await capMetrics(field)).toEqual({ textLines: CAP_LINES, scrolls: true })
|
||||
|
||||
// Settle the wait so teardown is not racing a pending question.
|
||||
await composer.getByRole('button', { name: 'Skip this question' }).click()
|
||||
expect(await asked).toEqual({ answers: [{ id: 'free', selected: [] }] })
|
||||
await expect.poll(() => page.locator('[data-question-key]').count(), { timeout: 10_000 }).toBe(0)
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, [
|
||||
'session.jsonl',
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-user-questions/README.md
|
||||
README.md: 90af68315e28a3a89d7eaec1d9e3da1472318164
|
||||
README.zh.md: 33e47eb983c7ec832120a8be2188fb8afe4449fe
|
||||
README.md: 4c7c54ad3c9dd5a06f8bd0f5537044479e4ad7e2
|
||||
README.zh.md: 47d654bb7f687819558e8ef228664a815a4b46d0
|
||||
|
||||
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
Web question feature plugin: its browser half registers the `question` entry in the conversation-owned `conversation.composer` keyed slot. Its host half is empty on purpose — mounting `dsh-tool-ask-user` there put the tool in the registry's GLOBAL layer, which merges into every agent regardless of the preset that composed it, so a two-tool benchmark preset really presented three. Rendering a question is a host UI capability; having the tool is an agent capability, so the `tool-ask-user` row belongs to the presets that want it (and to the TUI composition, which has no presets).
|
||||
|
||||
The component renders one question at a time with progress navigation, single- and multi-select choices, recommendation badges derived from label suffixes, and custom answers. A multi-select draft keeps its selected labels while the user opens or edits the custom answer, so its submitted item may carry both `selected` and `custom`; a single-select custom answer remains exclusive. Question detail reuses the assistant-output `MarkdownText` primitive, including its GFM rendering and untrusted-content policy. The capped card keeps its title, navigation, and submission actions fixed while long detail and choices share an internal scroll region. Both question shapes answer into a textarea over a hidden height mirror, so a long answer soft-wraps and grows the field in place; growth stops at eight lines and the field scrolls from there, keeping the choices the answer belongs to in view. Single-select choices advance immediately, Enter continues the flow and submits once every question is answered or skipped, and Shift+Enter breaks a line instead; Enter during IME composition confirms the input candidate without advancing. It submits one structured answer batch for the whole request: “Skip this question” retains other drafts and emits the existing blank `{ selected: [] }` shape for that item, while close rejects the whole wait as `ASK_CANCELLED`.
|
||||
The component renders one question at a time with progress navigation, single- and multi-select choices, recommendation badges derived from label suffixes, and custom answers. A multi-select draft keeps its selected labels while the user opens or edits the custom answer, so its submitted item may carry both `selected` and `custom`; a single-select custom answer remains exclusive. Question detail reuses the assistant-output `MarkdownText` primitive, including its GFM rendering and untrusted-content policy. The capped card keeps its title, navigation, and submission actions fixed while long detail and choices share an internal scroll region. Both question shapes answer into a textarea over a hidden height mirror, so a long answer soft-wraps and grows the field in place; growth stops at six lines of text — the same count in both variants — and the field scrolls from there, keeping the choices the answer belongs to in view. Single-select choices advance immediately, Enter continues the flow and submits once every question is answered or skipped, and Shift+Enter breaks a line instead; Enter during IME composition confirms the input candidate without advancing. It submits one structured answer batch for the whole request: “Skip this question” retains other drafts and emits the existing blank `{ selected: [] }` shape for that item, while close rejects the whole wait as `ASK_CANCELLED`.
|
||||
|
||||
A request whose single question declares a presentation intent renders as that intent's own surface instead. `plan-review` — set by `dsh-plan-mode` on the `exit_plan_mode` review — takes the waiting-approval card shape: a `Plan review` strip, the plan as the scrolling markdown body, the question text as the card's accessible name, and one decision row of `Chat about it` / `Refuse` / `Approve`. Approve and Refuse answer with the asker's own option labels (the intent names which label approves, so the verdict never rides option order) and keep the asker's descriptions as tooltips; `Chat about it` rejects the wait as `ASK_CANCELLED`, returning the composer so the user can say what they want instead. The card claims a request only when it can send every answer that request allows: one question, the intent declared, the plan present as `detail`, the named approve label offered, and a binary single choice (at most one option besides approve, not multi-select). Anything else — no intent, a batch of several questions, a missing plan, an approve label naming no option, a third option, a multi-select decision — stays on the generic flow, which can express it. An intent changes the layout, never which answers are reachable.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Web 提问功能插件:其浏览器侧把 `question` 条目注册到会话拥有的 `conversation.composer` 键控 slot 中。其主机侧刻意为空——在那里挂载 `dsh-tool-ask-user` 会把工具放进注册表的**全局层**,而全局层会并入每一个 agent(智能体),无论它由哪个 preset 组装,于是一个「两工具」的 benchmark preset 实际会呈现三个。渲染提问是宿主的 UI 能力,拥有该工具则是 agent 的能力,因此 `tool-ask-user` 行属于需要它的各个 preset(以及没有 preset 的 TUI 组装)。
|
||||
|
||||
组件每次渲染一个问题,提供进度导航、单选和多选选项、由标签后缀派生的推荐徽标,以及自定义答案。用户打开或编辑自定义答案时,多选题草稿会保留已选中的标签,因此提交项可以同时携带 `selected` 与 `custom`;单选题的自定义答案仍保持互斥。问题详情复用助手输出的 `MarkdownText` 原语,包括其 GFM 渲染与不受信任内容策略。限高卡片保持标题、导航与提交动作固定,超长的详情与选项共享内部滚动区。两种问题形状的自定义答案都写入一个带隐藏高度镜像的 textarea,因此长答案会软换行并就地把输入框撑高;增高到八行为止,此后由输入框自身滚动,使答案所属的选项仍留在视野内。选择单选选项后会立即前进;Enter 继续流程,所有问题均已回答或跳过后即提交,Shift+Enter 则改为换行;IME 组合输入期间按 Enter 只会确认输入候选,不会前进。组件为整个请求提交一批结构化答案:「跳过此问题」会保留其他草稿,并为该项发出既有的空 `{ selected: [] }` 形状;关闭则以 `ASK_CANCELLED` 拒绝整个等待。
|
||||
组件每次渲染一个问题,提供进度导航、单选和多选选项、由标签后缀派生的推荐徽标,以及自定义答案。用户打开或编辑自定义答案时,多选题草稿会保留已选中的标签,因此提交项可以同时携带 `selected` 与 `custom`;单选题的自定义答案仍保持互斥。问题详情复用助手输出的 `MarkdownText` 原语,包括其 GFM 渲染与不受信任内容策略。限高卡片保持标题、导航与提交动作固定,超长的详情与选项共享内部滚动区。两种问题形状的自定义答案都写入一个带隐藏高度镜像的 textarea,因此长答案会软换行并就地把输入框撑高;增高到六行文本为止——两种形状行数相同——此后由输入框自身滚动,使答案所属的选项仍留在视野内。选择单选选项后会立即前进;Enter 继续流程,所有问题均已回答或跳过后即提交,Shift+Enter 则改为换行;IME 组合输入期间按 Enter 只会确认输入候选,不会前进。组件为整个请求提交一批结构化答案:「跳过此问题」会保留其他草稿,并为该项发出既有的空 `{ selected: [] }` 形状;关闭则以 `ASK_CANCELLED` 拒绝整个等待。
|
||||
|
||||
若某个请求的唯一问题声明了呈现意图,则改为渲染该意图自己的界面。`plan-review`——由 `dsh-plan-mode` 在 `exit_plan_mode` 审阅上设置——采用等待审批卡片的形状:一条 `Plan review` 条带、计划作为可滚动的 markdown 主体、问题文本作为卡片的无障碍名称,以及一行 `Chat about it` / `Refuse` / `Approve` 的决定操作。Approve 与 Refuse 用提问方自己的选项标签回答(意图指名哪个标签表示批准,因此裁决绝不依赖选项顺序),并把提问方的描述保留为 tooltip;`Chat about it` 以 `ASK_CANCELLED` 拒绝该等待,让编辑器归位,用户可以直接说出他想说的话。卡片只在能够发出该请求允许的每一个答案时才接管:只有一个问题、声明了意图、计划以 `detail` 存在、提供了被指名的批准标签,且是二元单选(除批准外最多一个选项,且非多选)。其他任何情形——没有意图、一批含多个问题、缺少计划、批准标签未命中任何选项、出现第三个选项、多选决定——都留在能够表达它的通用流程上。意图改变的只是布局,从不改变可达的答案。
|
||||
|
||||
|
||||
@@ -343,12 +343,18 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* The height ruler, never painted. Growth stops at 8 lines: the card body is
|
||||
/* The height ruler, never painted. Growth stops at 6 lines: the card body is
|
||||
capped at 520px and still owes that budget to the title, the option rows and
|
||||
the footer actions, so a long answer scrolls in the textarea instead of
|
||||
pushing the choices it belongs to out of view. */
|
||||
pushing the choices it belongs to out of view.
|
||||
content-box overrides the card-wide border-box so the cap counts TEXT lines
|
||||
in both variants: under border-box the block variant's 16px of vertical
|
||||
padding eats two thirds of a line and the sixth one arrives as an 8px sliver.
|
||||
Width is unaffected — a stretched grid item's used width comes from the
|
||||
track, not from this box-sizing. */
|
||||
.fieldMirror {
|
||||
max-height: 192px;
|
||||
box-sizing: content-box;
|
||||
max-height: 144px;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user