b877464575
- Switch all UI components and tests from sharp box corners (`boxSharp`) to rounded ones (`boxRound`). - Update `Theme` to re-export sharp junction symbols (tees and cross) under `boxRound` to ensure consistent divider rendering in rounded boxes. - Remove outdated architectural notes regarding forced tool-choice queues in documentation.
110 lines
4.4 KiB
TypeScript
110 lines
4.4 KiB
TypeScript
/**
|
|
* Shared box-drawing chrome for fullscreen overlays (the `/copy` picker, the
|
|
* plan-review overlay, …). Every helper paints with `theme.boxRound` glyphs
|
|
* (rounded corners, sharp tee/cross junctions) and the `border`/`accent` theme
|
|
* colors so all outlined overlays read identically.
|
|
*/
|
|
import { padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
import { theme } from "../theme/theme";
|
|
|
|
/** Pad or truncate a (possibly ANSI-styled) string to exactly `width` columns. */
|
|
export function fit(text: string, width: number): string {
|
|
if (width <= 0) return "";
|
|
const w = visibleWidth(text);
|
|
if (w === width) return text;
|
|
if (w < width) return text + padding(width - w);
|
|
const cut = truncateToWidth(text, width);
|
|
const cw = visibleWidth(cut);
|
|
return cw < width ? cut + padding(width - cw) : cut;
|
|
}
|
|
|
|
function paint(s: string): string {
|
|
return theme.fg("border", s);
|
|
}
|
|
|
|
/** Top border with an optional accent-colored title inset into the rule. */
|
|
export function topBorder(width: number, title: string): string {
|
|
const box = theme.boxRound;
|
|
const inner = Math.max(0, width - 2);
|
|
if (!title) return paint(box.topLeft + box.horizontal.repeat(inner) + box.topRight);
|
|
const shown = truncateToWidth(` ${title} `, Math.max(0, inner - 2));
|
|
const fillWidth = Math.max(0, inner - 1 - visibleWidth(shown));
|
|
return (
|
|
paint(box.topLeft + box.horizontal) +
|
|
theme.bold(theme.fg("accent", shown)) +
|
|
paint(box.horizontal.repeat(fillWidth) + box.topRight)
|
|
);
|
|
}
|
|
|
|
/** A horizontal rule with left/right tees, splitting overlay sections. */
|
|
export function divider(width: number): string {
|
|
const box = theme.boxRound;
|
|
return paint(box.teeRight + box.horizontal.repeat(Math.max(0, width - 2)) + box.teeLeft);
|
|
}
|
|
|
|
export function bottomBorder(width: number): string {
|
|
const box = theme.boxRound;
|
|
return paint(box.bottomLeft + box.horizontal.repeat(Math.max(0, width - 2)) + box.bottomRight);
|
|
}
|
|
|
|
/** Wrap pre-styled content in vertical borders with single-column insets. */
|
|
export function row(content: string, width: number): string {
|
|
const box = theme.boxRound;
|
|
return `${paint(box.vertical)} ${fit(content, Math.max(0, width - 4))} ${paint(box.vertical)}`;
|
|
}
|
|
|
|
/**
|
|
* Column index (0-based) of the inner divider for a two-column layout whose
|
|
* sidebar content area is `sidebarWidth` columns wide. The layout is
|
|
* `│ sidebar │ body │` with a single-column inset on every side, so the divider
|
|
* vertical sits at `sidebarWidth + 3` and the body content area is
|
|
* {@link splitBodyWidth} columns.
|
|
*/
|
|
function splitDividerCol(sidebarWidth: number): number {
|
|
return sidebarWidth + 3;
|
|
}
|
|
|
|
/** Body content width for a two-column overlay of total `width`. */
|
|
export function splitBodyWidth(width: number, sidebarWidth: number): number {
|
|
return Math.max(0, width - sidebarWidth - 7);
|
|
}
|
|
|
|
/** Top border carrying the title, split by a `┬` over the column divider. */
|
|
export function topBorderSplit(width: number, title: string, sidebarWidth: number): string {
|
|
const box = theme.boxRound;
|
|
const dividerCol = splitDividerCol(sidebarWidth);
|
|
const leftLen = Math.max(0, dividerCol - 1);
|
|
const rightLen = Math.max(0, width - 2 - dividerCol);
|
|
let left: string;
|
|
if (!title) {
|
|
left = paint(box.topLeft + box.horizontal.repeat(leftLen));
|
|
} else {
|
|
const shown = truncateToWidth(` ${title} `, Math.max(0, leftLen - 1));
|
|
const fillWidth = Math.max(0, leftLen - 1 - visibleWidth(shown));
|
|
left =
|
|
paint(box.topLeft + box.horizontal) +
|
|
theme.bold(theme.fg("accent", shown)) +
|
|
paint(box.horizontal.repeat(fillWidth));
|
|
}
|
|
return left + paint(box.teeDown + box.horizontal.repeat(rightLen) + box.topRight);
|
|
}
|
|
|
|
/** Section rule that closes the sidebar column with a `┴` over the divider. */
|
|
export function dividerSplit(width: number, sidebarWidth: number): string {
|
|
const box = theme.boxRound;
|
|
const dividerCol = splitDividerCol(sidebarWidth);
|
|
const leftLen = Math.max(0, dividerCol - 1);
|
|
const rightLen = Math.max(0, width - 2 - dividerCol);
|
|
return paint(
|
|
box.teeRight + box.horizontal.repeat(leftLen) + box.teeUp + box.horizontal.repeat(rightLen) + box.teeLeft,
|
|
);
|
|
}
|
|
|
|
/** A two-column content row: `│ sidebar │ body │`, each inset by one column. */
|
|
export function splitRow(sidebar: string, body: string, width: number, sidebarWidth: number): string {
|
|
const box = theme.boxRound;
|
|
const bodyWidth = splitBodyWidth(width, sidebarWidth);
|
|
const bar = paint(box.vertical);
|
|
return `${bar} ${fit(sidebar, sidebarWidth)} ${bar} ${fit(body, bodyWidth)} ${bar}`;
|
|
}
|