diff --git a/packages/core/src/renderables/Box.test.ts b/packages/core/src/renderables/Box.test.ts index 1aa16a856..cb03933dc 100644 --- a/packages/core/src/renderables/Box.test.ts +++ b/packages/core/src/renderables/Box.test.ts @@ -407,6 +407,26 @@ describe("BoxRenderable - no-op rendering", () => { expect(called).toBe(true) }) + test("skips drawBox when shouldFill is false even with a visible background color", () => { + const box = new BoxRenderable(testRenderer, { + id: "no-fill-box", + width: 10, + height: 5, + backgroundColor: "#112233", + shouldFill: false, + }) + + let called = false + const buffer = { + drawBox() { + called = true + }, + } + + ;(box as any).renderSelf(buffer) + expect(called).toBe(false) + }) + test("still draws boxes with borders", () => { const box = new BoxRenderable(testRenderer, { id: "bordered-box", @@ -472,3 +492,101 @@ describe("BoxRenderable - no-op rendering", () => { expect(getCellForeground(2, 0)).toEqual([0, 0, 255, 255]) }) }) + +describe("BoxRenderable - customBorderChars", () => { + test("renders custom border characters instead of the default border style", async () => { + const box = new BoxRenderable(testRenderer, { + id: "custom-border-box", + width: 10, + height: 3, + border: true, + customBorderChars: { + topLeft: "+", + topRight: "+", + bottomLeft: "+", + bottomRight: "+", + horizontal: "-", + vertical: "|", + topT: "+", + bottomT: "+", + leftT: "+", + rightT: "+", + cross: "+", + }, + }) + + testRenderer.root.add(box) + await renderOnce() + + const lines = captureFrame().split("\n") + expect(lines[0].slice(0, 10)).toBe("+--------+") + expect(lines[1].slice(0, 10)).toBe("| |") + expect(lines[2].slice(0, 10)).toBe("+--------+") + }) +}) + +describe("BoxRenderable - focusedBorderColor", () => { + test("uses focusedBorderColor when the box is focused", async () => { + const box = new BoxRenderable(testRenderer, { + id: "focused-border-box", + focusable: true, + border: true, + width: 10, + height: 3, + borderColor: "#111111", + focusedBorderColor: "#ff00ff", + }) + + testRenderer.root.add(box) + await renderOnce() + + // Initially unfocused, should use normal borderColor + expect(getCellForeground(0, 0)).toEqual([17, 17, 17, 255]) + + // Focus the box + box.focus() + await renderOnce() + + // Now should use focusedBorderColor + expect(getCellForeground(0, 0)).toEqual([255, 0, 255, 255]) + }) +}) + +describe("BoxRenderable - gaps (rowGap and columnGap)", () => { + test("applies rowGap and columnGap to yoga layout", async () => { + const parent = new BoxRenderable(testRenderer, { + id: "gap-parent", + width: 10, + height: 10, + flexDirection: "row", // row direction means children are side by side + flexWrap: "wrap", + columnGap: 2, + rowGap: 3, + }) + + const child1 = new BoxRenderable(testRenderer, { id: "c1", width: 4, height: 4 }) + const child2 = new BoxRenderable(testRenderer, { id: "c2", width: 4, height: 4 }) + const child3 = new BoxRenderable(testRenderer, { id: "c3", width: 4, height: 4 }) + + parent.add(child1) + parent.add(child2) + parent.add(child3) + + testRenderer.root.add(parent) + await renderOnce() + + // Row direction, parent width 10. + // c1 is at x=0, width=4. + // columnGap is 2, so c2 is at x=6, width=4. Total width = 10. + expect(child1.screenX).toBe(0) + expect(child1.screenY).toBe(0) + + expect(child2.screenX).toBe(6) + expect(child2.screenY).toBe(0) + + // c3 wraps to the next row. + // rowGap is 3, so c3 should be at y = 4 (height of c1) + 3 (rowGap) = 7. + expect(child3.screenX).toBe(0) + expect(child3.screenY).toBe(7) + }) +}) diff --git a/packages/web/src/content/docs/components/box.mdx b/packages/web/src/content/docs/components/box.mdx index 87ed0f31d..3c2191a3d 100644 --- a/packages/web/src/content/docs/components/box.mdx +++ b/packages/web/src/content/docs/components/box.mdx @@ -75,6 +75,26 @@ renderer.root.add( { borderStyle: "heavy" } // Heavy lines: ┏━┓┃┗━┛ + +// Specific border sides +{ + border: ["left", "right"] +} // Only shows left and right borders +``` + +### Custom border characters + +You can supply an object matching the `BorderCharacters` interface to fully override the border drawing characters: + +```typescript +{ + border: true, + customBorderChars: { + topLeft: "+", topRight: "+", bottomLeft: "+", bottomRight: "+", + horizontal: "-", vertical: "|", + topT: "+", bottomT: "+", leftT: "+", rightT: "+", cross: "+" + } +} ``` ## Titles @@ -140,6 +160,22 @@ const container = Box( ) ``` +### Row and column gaps + +Instead of a single `gap`, you can specify vertical and horizontal gaps independently: + +```typescript +const gridContainer = Box( + { + flexDirection: "row", + flexWrap: "wrap", + rowGap: 2, // 2 rows of vertical space between wrapped lines + columnGap: 4, // 4 columns of horizontal space between items + }, + // ...children +) +``` + ## Mouse events Handle mouse interactions on the box: @@ -163,26 +199,51 @@ const button = new BoxRenderable(renderer, { }) ``` +## Focus state + +A Box can receive focus and change its border color automatically when focused. This is especially useful for interactive panels or custom input components. + +```typescript +const interactiveBox = new BoxRenderable(renderer, { + id: "focusable-panel", + width: 20, + height: 5, + border: true, + focusable: true, + borderColor: "#444444", + focusedBorderColor: "#00AAFF", +}) + +// Focus the box programmatically or via keyboard navigation +interactiveBox.focus() +``` + ## Properties -| Property | Type | Default | Description | -| ---------------------- | ------------------ | -------------- | --------------------------------- | -| `width` | `number \| string` | - | Width in characters or percentage | -| `height` | `number \| string` | - | Height in rows or percentage | -| `backgroundColor` | `string \| RGBA` | - | Background fill color | -| `border` | `boolean` | `false` | Show border | -| `borderStyle` | `string` | `"single"` | Border style | -| `borderColor` | `string \| RGBA` | - | Border color | -| `title` | `string` | - | Title text in border | -| `titleColor` | `string \| RGBA` | - | Color of the title text | -| `titleAlignment` | `string` | `"left"` | Title position | -| `bottomTitle` | `string` | - | Bottom title text in border | -| `bottomTitleAlignment` | `string` | `"left"` | Bottom title position | -| `padding` | `number` | `0` | Internal padding | -| `gap` | `number \| string` | - | Gap between children | -| `flexDirection` | `string` | `"column"` | Child layout direction | -| `justifyContent` | `string` | `"flex-start"` | Main axis alignment | -| `alignItems` | `string` | `"stretch"` | Cross axis alignment | +| Property | Type | Default | Description | +| ---------------------- | -------------------------- | -------------- | --------------------------------- | +| `width` | `number \| string` | - | Width in characters or percentage | +| `height` | `number \| string` | - | Height in rows or percentage | +| `backgroundColor` | `string \| RGBA` | - | Background fill color | +| `shouldFill` | `boolean` | `true` | Fill the background | +| `border` | `boolean \| BorderSides[]` | `false` | Show border (or array of sides) | +| `borderStyle` | `string` | `"single"` | Border style | +| `borderColor` | `string \| RGBA` | - | Border color | +| `customBorderChars` | `BorderCharacters` | - | Custom border characters | +| `focusedBorderColor` | `string \| RGBA` | `"#00AAFF"` | Border color when focused | +| `focusable` | `boolean` | `false` | Can the box receive focus | +| `title` | `string` | - | Title text in border | +| `titleColor` | `string \| RGBA` | - | Color of the title text | +| `titleAlignment` | `string` | `"left"` | Title position | +| `bottomTitle` | `string` | - | Bottom title text in border | +| `bottomTitleAlignment` | `string` | `"left"` | Bottom title position | +| `padding` | `number` | `0` | Internal padding | +| `gap` | `number \| string` | - | Gap between children | +| `rowGap` | `number \| string` | - | Vertical gap between children | +| `columnGap` | `number \| string` | - | Horizontal gap between children | +| `flexDirection` | `string` | `"column"` | Child layout direction | +| `justifyContent` | `string` | `"flex-start"` | Main axis alignment | +| `alignItems` | `string` | `"stretch"` | Cross axis alignment | ## Example: Card component