Skip to content

fix: single-side borders vanish on a one-row box - #25

Open
decleezy wants to merge 1 commit into
orchetron:mainfrom
decleezy:fix/single-side-border-on-one-row-box
Open

decleezy wants to merge 1 commit into
orchetron:mainfrom
decleezy:fix/single-side-border-on-one-row-box

Conversation

@decleezy

@decleezy decleezy commented Sep 3, 2026

Copy link
Copy Markdown

What you expected

A Box with borderLeft draws its border regardless of how tall the box is. A left-only border is a single vertical bar, so height 1 is perfectly drawable.

What happened

The border is silently dropped when the box's content is exactly one row tall. Two rows of content, and it appears.

1 Text, 1 line      NO BORDER   |left       one|
1 Text, 2 lines     border      |left      │one|

Minimal reproduction

import React from "react";
import { Box, Text } from "@orchetron/storm";
import { renderForTest } from "@orchetron/storm/testing";

const BORDER = {
  borderStyle: "single" as const,
  borderLeft: true,
  borderTop: false,
  borderBottom: false,
  borderRight: false,
};

function Split({ lines }: { readonly lines: readonly string[] }) {
  return (
    <Box flexDirection="column" width={40} height={6}>
      <Box flexDirection="row" width={40}>
        <Box width={10}>
          <Text>left</Text>
        </Box>
        <Box width={12} flexDirection="column" {...BORDER}>
          {lines.map((line) => (
            <Text key={line}>{line}</Text>
          ))}
        </Box>
      </Box>
    </Box>
  );
}

for (const lines of [["one"], ["one", "two"]]) {
  const result = renderForTest(<Split lines={lines} />, { width: 40, height: 6 });
  const row = (result.lines[0] ?? "").replace(/\s+$/, "");
  console.log(`${lines.length} row(s): ${row.includes("│") ? "border" : "NO BORDER"}  |${row}|`);
  result.unmount();
}

Cause

paintBorder in src/reconciler/renderer.ts opens with:

if (style === "none" || width < 2 || height < 2) return;

The 2x2 minimum is correct for a full border, since one row cannot hold both a top and a bottom edge plus content. It is not correct for a single-side border.

The change

The guard is narrowed rather than removed. Two cells are needed only on an axis that draws both of its sides:

const minWidth = sides.left && sides.right ? 2 : 1;
const minHeight = sides.top && sides.bottom ? 2 : 1;

A box one column wide still refuses a left-and-right border, which the third test covers.

The rest of paintBorder already handles height 1 correctly: the corner branch writes the single , and the for (let i = 1; i < height - 1; i++) edge loop does not run.

Testing

Three regression tests in src/__tests__/component-boundary-layout.test.ts (the one-row case, the two-row case, and the narrowed guard still refusing what cannot fit).

  • npx tsc --noEmit — 0 errors
  • npx vitest run — 547 passed, across three consecutive runs

Context

Found while evaluating Storm as a renderer for a terminal UI with side-by-side panes, where a column's divider is a left-only border. A one-row column loses its divider, which reads as a rendering fault rather than a layout choice. Thanks for the library, the cell diffing is a genuinely different asymptote from a full repaint.

`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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant