From 6ccd576ec3cbec0f73b0dcfa6514a5bdea1d9c68 Mon Sep 17 00:00:00 2001 From: Matt deClercq Date: Wed, 2 Sep 2026 21:32:50 -0500 Subject: [PATCH] fix: single-side borders vanish on a one-row box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `paintBorder` opened with `width < 2 || height < 2`, which is right for a full border, since one row cannot hold both a top and a bottom, but wrong for a single-side one. A left-only border is a single vertical bar and is drawable at any height, so a Box whose content is exactly one row tall silently lost it. Reproduction: two columns side by side, the right one with `borderLeft`. With one line of content no border appears; with two it does. 1 Text, 1 line |left one| 1 Text, 2 lines |left β”‚one| The guard is narrowed rather than removed: two cells are required only on an axis that draws BOTH of its sides, so a box one column wide still refuses a left-and-right border. The rest of the function already handles height 1, as the corner branch writes the single glyph and the edge loop does not run. Three regression tests in component-boundary-layout.test.ts cover the one-row case, the two-row case, and the narrowed guard still refusing what cannot fit. --- .../component-boundary-layout.test.ts | 62 +++++++++++++++++++ src/reconciler/renderer.ts | 8 ++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/__tests__/component-boundary-layout.test.ts b/src/__tests__/component-boundary-layout.test.ts index a024707..bc7a29a 100644 --- a/src/__tests__/component-boundary-layout.test.ts +++ b/src/__tests__/component-boundary-layout.test.ts @@ -203,3 +203,65 @@ describe("commitUpdate layout cache invalidation", () => { expect(result2.lines[1]).toContain("B"); }); }); + +describe("Single-side borders on a one-row box", () => { + const SIDE = { + borderStyle: "single" as const, + borderTop: false, + borderBottom: false, + borderRight: false, + }; + + /** Two columns side by side, the right one carrying a left border. */ + const split = (lines: readonly string[]) => + React.createElement( + Box, + { flexDirection: "column", width: 40, height: 6 }, + React.createElement(Text, null, "header"), + React.createElement( + Box, + { flexDirection: "row", width: 40 }, + React.createElement( + Box, + { width: 10 }, + React.createElement(Text, null, "left"), + ), + React.createElement( + Box, + { width: 12, flexDirection: "column", ...SIDE, borderLeft: true }, + ...lines.map((line) => React.createElement(Text, { key: line }, line)), + ), + ), + ); + + it("draws a left border when the box is only one row tall", () => { + // A left-only border is a single vertical bar, so it is drawable at any + // height. The 2x2 minimum belongs to a border that draws BOTH sides of an + // axis; applying it here dropped the divider from every one-row column. + const { output } = renderToString(split(["one"]), { width: 40, height: 6 }); + expect(output.split("\n")[1]).toContain("β”‚"); + }); + + it("draws it at two rows as well, so the fix is not height-specific", () => { + const { output } = renderToString(split(["one", "two"]), { + width: 40, + height: 6, + }); + expect(output.split("\n")[1]).toContain("β”‚"); + }); + + it("still refuses a full border that cannot fit its own sides", () => { + // The guard is not gone, only narrowed: a box one column wide cannot hold + // a left AND a right border, and must draw neither. + const { output } = renderToString( + React.createElement( + Box, + { flexDirection: "column", width: 40, height: 4 }, + React.createElement(Box, { width: 1, borderStyle: "single" as const }, + React.createElement(Text, null, "x")), + ), + { width: 40, height: 4 }, + ); + expect(output).not.toContain("β”‚"); + }); +}); diff --git a/src/reconciler/renderer.ts b/src/reconciler/renderer.ts index d761c03..7e5f148 100644 --- a/src/reconciler/renderer.ts +++ b/src/reconciler/renderer.ts @@ -1276,7 +1276,13 @@ function paintBorder( dimFlags: BorderDimFlags = NO_DIM, inheritedBg: number = DEFAULT_COLOR, ): void { - if (style === "none" || width < 2 || height < 2) return; + // Two cells are needed only on an axis that draws BOTH of its sides. A + // left-only border is a single vertical bar, drawable at height 1, which is + // exactly what a one-row column is; the same holds for a top-only border at + // width 1. + const minWidth = sides.left && sides.right ? 2 : 1; + const minHeight = sides.top && sides.bottom ? 2 : 1; + if (style === "none" || width < minWidth || height < minHeight) return; const chars = BORDER_CHARS[style]; const fg = color; const bg = DEFAULT_COLOR;