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:
@@ -7,17 +7,23 @@
|
||||
.block {
|
||||
--dsl-terminal-radius: 12px;
|
||||
--dsl-terminal-line-height: 22px;
|
||||
/* Reserved strip to the left of the card for the per-line run-state dots.
|
||||
The dots sit outside the card surface, so a reader scans command state
|
||||
down one column without the dots competing with the commands themselves. */
|
||||
--dsl-terminal-gutter: 30px;
|
||||
|
||||
position: relative;
|
||||
margin: 16px 0;
|
||||
margin: 16px 0 16px var(--dsl-terminal-gutter);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
background: var(--dsw-alias-markdown-code-block);
|
||||
border-radius: var(--dsl-terminal-radius);
|
||||
}
|
||||
|
||||
/* Top-aligned: the status pill and copy control stay on the first prompt row
|
||||
however many command lines the card carries. */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 9px 14px;
|
||||
background: var(--dsw-alias-markdown-code-block-banner);
|
||||
@@ -25,22 +31,33 @@
|
||||
border-top-right-radius: var(--dsl-terminal-radius);
|
||||
}
|
||||
|
||||
/* The prompt row is the only element allowed to shrink; the status pill and
|
||||
the copy control keep their intrinsic width. */
|
||||
/* One row per command line. The prompt column is the only element allowed to
|
||||
shrink; the status pill and the copy control keep their intrinsic width. */
|
||||
.prompt {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
font: var(--dsw-font-markdown-code-block);
|
||||
}
|
||||
|
||||
/* The dot sits on the prompt row's baseline box, which is a code-font line, so
|
||||
it is centered against that line's box rather than sitting on the baseline. */
|
||||
.promptLine {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
line-height: var(--dsl-terminal-line-height);
|
||||
}
|
||||
|
||||
/* Out of flow in the gutter, so a dot neither indents its command nor depends
|
||||
on the command's own text metrics to line up with it. Centered against the
|
||||
row's line box rather than sitting on the code font's baseline. */
|
||||
.runState {
|
||||
flex: none;
|
||||
align-self: center;
|
||||
position: absolute;
|
||||
left: calc(-1 * var(--dsl-terminal-gutter));
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
/* The dot is aria-hidden; this is its text label for assistive technology. */
|
||||
|
||||
@@ -147,6 +147,13 @@ export function TerminalBlock({
|
||||
|
||||
const status = statusText(exitCode, signal)
|
||||
const state = runState(running, exitCode, signal)
|
||||
// A multi-line command gets one prompt row per line, so a two-command shell
|
||||
// snippet reads as the two commands it is instead of collapsing into one
|
||||
// ellipsized row. A trailing newline is a terminator, not an empty command.
|
||||
const commandLines = useMemo(() => {
|
||||
const body = command.endsWith('\n') ? command.slice(0, -1) : command
|
||||
return body.split('\n')
|
||||
}, [command])
|
||||
const empty = text.trim() === ''
|
||||
const hidden = lines.length - maxLines
|
||||
const capped = hidden > 0 && !expanded
|
||||
@@ -159,10 +166,18 @@ export function TerminalBlock({
|
||||
<div className={clsx(css.block, className)} data-terminal="" data-running={running ? '' : undefined}>
|
||||
<div className={css.header}>
|
||||
<div className={css.prompt}>
|
||||
<StateDot state={state.state} className={css.runState} />
|
||||
<span className={css.runStateLabel}>{state.label}</span>
|
||||
<span className={css.cwd}>{cwd === undefined ? '$' : promptLabel(cwd, home)}</span>
|
||||
<span className={css.command}>{command}</span>
|
||||
{commandLines.map((line, index) => (
|
||||
<div key={index} className={css.promptLine}>
|
||||
{/* One dot for the 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 row would assert a
|
||||
per-line outcome nothing here knows. */}
|
||||
{index === 0 && <StateDot state={state.state} className={css.runState} />}
|
||||
<span className={css.cwd}>{cwd === undefined ? '$' : promptLabel(cwd, home)}</span>
|
||||
<span className={css.command}>{line}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{status !== undefined && <Pill className={css.status}>{status}</Pill>}
|
||||
{!running && !empty && (
|
||||
|
||||
Reference in New Issue
Block a user