feat(setup): paint card and log bodies in brand cyan

Adds a `brandBody` helper in setup/lib/theme.ts that wraps prose in
brand cyan (#2BB7CE), with the same TTY/NO_COLOR/truecolor gating used
by `brand`/`brandBold`/`brandChip`. The helper splits multi-line input
and colors each line independently so the SGR sequence doesn't bleed
across clack's gutter prefix.

Routing:
  - `note()` (the un-dim card wrapper from #2095) now passes
    `brandBody` as its `format` callback, so card bodies render
    cyan line-by-line.
  - Every prose `p.log.{message,info,success,step,warn}` call in the
    setup flow wraps its body argument in `brandBody`. Calls whose
    body is explicitly `k.dim(...)` (failure transcript tails, log
    paths, claude-assist response previews) are left alone — those
    are the "preview/debug" cases the dim-policy comment in
    theme.ts already carves out.
  - Spinner-finish lines in windowed-runner / claude-assist color
    only the message portion; the `(5s)` elapsed suffix stays dim.

Brand cyan accents (chips, wordmark, inline emphasis) are unchanged.
This PR only adds the body color.

A follow-up will add OSC 11 dark/light detection so light-mode
terminals get a brand blue (#2b6fdc) variant — opt-in upgrade with
no regression for the dark-mode default.
This commit is contained in:
exe.dev user
2026-04-29 11:43:30 +00:00
parent e0f813603e
commit ab2d509671
8 changed files with 78 additions and 46 deletions

View File

@@ -39,6 +39,29 @@ export function brandChip(s: string): string {
return k.bgCyan(k.black(k.bold(s)));
}
/**
* Brand body color for setup-flow prose. Used for card bodies (via the
* `note()` formatter) and `p.log.*` body arguments — anywhere the
* previous "dim" treatment was making prose hard to read or washing
* out embedded brand emphasis.
*
* Multi-line input is colored line-by-line so embedded line breaks
* don't bleed the SGR sequence across clack's gutter prefix.
*/
export function brandBody(s: string): string {
if (!USE_ANSI) return s;
if (TRUECOLOR) {
return s
.split('\n')
.map((line) => (line.length > 0 ? `\x1b[38;2;43;183;206m${line}\x1b[39m` : line))
.join('\n');
}
return s
.split('\n')
.map((line) => (line.length > 0 ? k.cyan(line) : line))
.join('\n');
}
/**
* Wrap text so it fits inside clack's gutter without the terminal's soft
* wrap breaking the `│ …` bar on long lines. Works on a single string with
@@ -70,16 +93,13 @@ export function dimWrap(text: string, gutter: number): string {
}
/**
* Wrap clack's `p.note` with the dim formatter disabled. By default
* clack renders note bodies through `styleText("dim", …)`, which the
* project's prose-readability stance (see `dimWrap` above) explicitly
* rejects. Pass-through formatter keeps body text at the terminal's
* regular weight; pre-styled segments (chips, bold, brand color) come
* through unfaded.
* Wrap clack's `p.note` so card bodies render in the brand body color
* (#2b6fdc) instead of clack's default dim. Clack runs the formatter
* on each line individually, so `brandBody` colors each line cleanly
* without bleeding across the gutter prefix.
*/
const passthroughFormat = (s: string): string => s;
export function note(message: string, title?: string): void {
p.note(message, title, { format: passthroughFormat });
p.note(message, title, { format: brandBody });
}
const ANSI_RE = /\x1b\[[0-9;]*m/g;