feat(web): give a multi-line command one prompt row per line

A `command` carrying two shell commands on two lines rendered as one row:
`.command` had `white-space: nowrap`, so the two collapsed into a single
ellipsized line that read as one command with stray arguments.

Render one prompt row per command line, and move the run-state dot out of
flow into a gutter reserved to the left of the card surface, so it neither
indents its command nor depends on the command's text metrics to line up.

The dot stays exactly one per card, on the first row. The exit status the
view carries is the whole call's and bash reports no per-command status, so
a dot per line would assert, of a line that succeeded inside a failing call,
that the line itself failed. The single visually hidden label keeps the same
scope, since one label per row would read to assistive technology as several
distinct outcomes.

Fixture turn 60's command becomes two lines, so the built-bundle snapshot
pins the layout and its dot distribution (`dotsPerPromptRow: [1, 0]`), and
the e2e adds that the dot starts left of the card surface — geometry jsdom
cannot compute. Both READMEs now also record that this package's
user-facing copy is inline Chinese, since zero-cordis atoms have no route to
`ctx.locale`; extracting it belongs to the repo-wide localization work.
This commit is contained in:
Chinesezjc
2026-07-28 18:48:57 +08:00
parent 9d2f7f4362
commit a00678a444
13 changed files with 140 additions and 35 deletions

View File

@@ -222,16 +222,23 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
state: node.getAttribute('data-state'),
color: getComputedStyle(node as HTMLElement).color,
success,
label: node.parentElement?.querySelector('[class*="_runStateLabel_"]')?.textContent ?? null,
// One label per card (the state is the call's), so it hangs off the
// prompt column rather than the row the dot sits in.
label: node.closest('[class*="_prompt_"]')?.querySelector('[class*="_runStateLabel_"]')?.textContent ?? null,
// The dot precedes the prompt label in document order, which is what
// puts it to the left of the `$`.
beforePrompt: node.compareDocumentPosition(node.parentElement!.querySelector('[class*="_cwd_"]')!)
=== Node.DOCUMENT_POSITION_FOLLOWING,
// The dot is out of flow in the card's left gutter, so it starts to the
// left of the card surface itself — the geometry jsdom cannot compute.
leftOfCard: (node as HTMLElement).getBoundingClientRect().left
< node.closest('[data-terminal]')!.getBoundingClientRect().left,
}
})
expect(dot.state).toBe('done')
expect(dot.label).toBe('已完成')
expect(dot.beforePrompt).toBe(true)
expect(dot.leftOfCard).toBe(true)
// Resolved through the theme token, not a literal hex in the component.
expect(dot.success).toMatch(/^rgb/)
expect(dot.color).toBe(dot.success)

View File

@@ -111,7 +111,14 @@ function readCard(card: Element) {
const status = card.querySelector('[class*="_status_"]')
const expander = card.querySelector('button[aria-expanded]')
return {
prompt: `${card.querySelector('[class*="_cwd_"]')?.textContent ?? ''} ${card.querySelector('[class*="_command_"]')?.textContent ?? ''}`,
// One entry per command line: a multi-line command is one row per line.
prompt: [...card.querySelectorAll('[class*="_promptLine_"]')].map(row =>
`${row.querySelector('[class*="_cwd_"]')?.textContent ?? ''} ${row.querySelector('[class*="_command_"]')?.textContent ?? ''}`),
// Dots per prompt row: exactly one, on the first row — the exit status the
// view carries is the whole call's, so a dot per line would assert a
// per-line outcome bash does not report.
dotsPerPromptRow: [...card.querySelectorAll('[class*="_promptLine_"]')].map(row =>
row.querySelectorAll('[data-state]').length),
status: status === null ? null : status.textContent,
copy: card.querySelector('[class*="_copyButton_"]')?.textContent ?? null,
lines: [...card.querySelectorAll('[class*="_line_"]')].map(line => line.textContent),
@@ -187,6 +194,9 @@ it('renders the keyed bash row with a resident terminal card', async () => {
"color: var(--dsw-alias-state-error-primary);",
],
"copy": "复制",
"dotsPerPromptRow": [
1,
],
"expander": {
"expanded": "false",
"label": "展开其余 14 行输出",
@@ -202,7 +212,9 @@ it('renders the keyed bash row with a resident terminal card', async () => {
"1 of 4 checks failed",
"[exit code: 1]",
],
"prompt": "nested pnpm run check",
"prompt": [
"nested pnpm run check",
],
"runState": "error",
"runStateLabel": "失败",
"status": "退出码 1",
@@ -229,13 +241,20 @@ it('the fallback row reaches the same card through its expand control', async ()
{
"colors": [],
"copy": "复制",
"dotsPerPromptRow": [
1,
0,
],
"expander": null,
"lines": [
"total 2",
"drwxr-xr-x fixture",
"-rw-r--r-- demo.txt",
],
"prompt": "fixture ls -la",
"prompt": [
"fixture ls -la",
"fixture echo done",
],
"runState": "done",
"runStateLabel": "已完成",
"status": null,
@@ -302,6 +321,9 @@ it('the details panel Output section renders the same call at full height', asyn
"color: var(--dsw-alias-label-tertiary);",
],
"copy": "复制",
"dotsPerPromptRow": [
1,
],
"expander": {
"expanded": "false",
"label": "展开其余 6 行输出",
@@ -326,7 +348,9 @@ it('the details panel Output section renders the same call at full height', asyn
"[exit code: 1]",
],
"panelLines": 16,
"prompt": "nested pnpm run check",
"prompt": [
"nested pnpm run check",
],
"runState": "error",
"runStateLabel": "失败",
"status": "退出码 1",