Common Patterns
Reusable patterns and Effect.js integration techniques for building layouts with effect-boxes.
Effect.js Integration
Pipe and Data-Last Style
All box operations support data-last style for use with pipe. Functions
are also callable data-first:
import * as Box from "effect-boxes/Box"
import * as Ansi from "effect-boxes/Ansi"
import { pipe } from "effect"
// Data-last with pipe (recommended)
const box1 = pipe(
Box.text("Hello"),
Box.moveRight(5),
Box.annotate(Ansi.bold),
Box.border("rounded")
)
// Data-first (also works)
const box2 = pipe(
Box.moveRight(Box.text("Hello"), 5),
Box.annotate(Ansi.bold),
Box.border("rounded")
)
// Method chaining with .pipe()
const box3 = pipe(
Box.text("Hello").pipe(Box.moveRight(5)),
Box.annotate(Ansi.bold),
Box.border("rounded")
)╭──────────╮
│ Hello│
╰──────────╯Structural Equality
Boxes implement Effect's Equal and Hash interfaces. Two boxes with the
same content and dimensions are structurally equal:
import { Equal } from "effect"
import * as Box from "effect-boxes/Box"
const a = Box.text("hello")
const b = Box.text("hello")
const c = Box.text("world")
Equal.equals(a, b) // true
Equal.equals(a, c) // falseThis means boxes can be used as keys in HashMap or deduplicated in HashSet.
UI Patterns
Status Indicators
const statusIcon = (icon: string, text: string, color: Ansi.AnsiAnnotation) =>
Box.hcat([
pipe(Box.text(`${icon} `), Box.annotate(color)),
pipe(Box.text(text), Box.annotate(color)),
], Box.top)
const statuses = Box.vcat([
statusIcon("✓", "Tests passed", Ansi.green),
statusIcon("✗", "Build failed", Ansi.red),
statusIcon("⚠", "3 warnings", Ansi.yellow),
statusIcon("ℹ", "Running...", Ansi.blue),
], Box.left)✓ Tests passed
✗ Build failed
⚠ 3 warnings
ℹ Running...Progress Bar
const progressBar = (progress: number, total: number, width: number) => {
const ratio = Math.min(progress / total, 1)
const filled = Math.round(ratio * width)
const empty = width - filled
return Box.hcat([
pipe(Box.text("█".repeat(filled)), Box.annotate(Ansi.green)),
pipe(Box.text("░".repeat(empty)), Box.annotate(Ansi.dim)),
Box.text(` ${Math.round(ratio * 100)}%`),
], Box.top)
}
const bar = progressBar(70, 100, 50)╭──────────────────────────────────────────────────────╮
│███████████████████████████████████░░░░░░░░░░░░░░░ 70%│
╰──────────────────────────────────────────────────────╯Data Table
const table = (headers: string[], rows: Box.Box<Ansi.AnsiStyle>[][]) => {
const colWidth = 16
const headerRow = Box.punctuateH(
headers.map((h) => pipe(
Box.text(h), Box.annotate(Ansi.bold), Box.alignHoriz(Box.left, colWidth)
)),
Box.top, Box.text("│")
)
const separator = Box.text("─".repeat(headers.length * colWidth + headers.length - 1))
const dataRows = rows.map((row) =>
Box.punctuateH(
row.map((cell) => pipe(cell, Box.alignHoriz(Box.left, colWidth))),
Box.top, Box.text("│")
)
)
return Box.vcat([headerRow, separator, ...dataRows], Box.left)
}╭──────────────────────────────────────────────────╮
│Name │Role │Status │
│──────────────────────────────────────────────────│
│Alice │Engineer │Active │
│Bob │Designer │Away │
│Carol │Manager │Offline │
╰──────────────────────────────────────────────────╯Key-Value Panel
const kvPanel = (entries: [string, Box.Box<any>][], width: number) =>
pipe(
Container.make({ width, padding: 1 }, (ctx) =>
Box.vcat(
entries.map(([key, value]) =>
Flex.row([
Flex.fixed(pipe(Box.text(`${key}:`), Box.annotate(Ansi.dim))),
Flex.spacer(),
Flex.fixed(value),
], ctx.innerWidth)
),
Box.left
)
),
Box.border("rounded")
)╭────────────────────────────────────────╮
│ │
│ Host: prod-01 │
│ Status: healthy │
│ Uptime: 14d 3h │
│ Load: 0.42 │
│ │
╰────────────────────────────────────────╯Composition Patterns
Building Reusable Components
Create helper functions that return Box values for consistent styling:
const heading = (text: string) =>
pipe(Box.text(text), Box.annotate(Ansi.combine(Ansi.bold, Ansi.white)))
const label = (text: string) =>
pipe(Box.text(text), Box.annotate(Ansi.dim))
const badge = (text: string, color: Ansi.AnsiAnnotation) =>
pipe(Box.text(` ${text} `), Box.annotate(color), Box.border("rounded"))
const section = (title: string, content: Box.Box<Ansi.AnsiStyle>) =>
Box.vcat([heading(title), Box.text("─".repeat(40)), content], Box.left)Container + Flex + Grid
The three Layout primitives compose for complex responsive layouts:
const dashboard = Container.make({ width: 65, padding: 1 }, (ctx) => {
// Header with flex
const header = Flex.row(
[Flex.fixed(heading("Dashboard")), Flex.spacer(), Flex.fixed(label("v1.0"))],
ctx.innerWidth
)
// KPI cards in a grid
const kpis = Grid.auto(
kpiData.map(renderKpiCard),
ctx.innerWidth,
{ minColWidth: 20 }
)
// Vertical stack
return Flex.col(
[Flex.fixed(header), Flex.fixed(kpis), Flex.grow(mainContent)],
ctx.innerHeight
)
})Conditional Styling
const statusColor = (status: string) => {
switch (status) {
case "ok": return Ansi.green
case "warn": return Ansi.yellow
case "error": return Ansi.red
default: return Ansi.dim
}
}
const renderStatus = (name: string, status: string) =>
Flex.row([
Flex.fixed(Box.text(name)),
Flex.spacer(),
Flex.fixed(pipe(Box.text(status), Box.annotate(statusColor(status)))),
], 40)