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) // false

This 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)
}
╭──────────────────────────────────────────────────╮
│NameRoleStatus          │
│──────────────────────────────────────────────────│
│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)