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
  );