Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions packages/core/src/renderables/Box.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
})
})
97 changes: 79 additions & 18 deletions packages/web/src/content/docs/components/box.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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

Expand Down