BotUI

UI components for things that think.

Loading states, streaming indicators, agent status — the surfaces an agent product lives on. Install one with npx, and the code lands in your repo. No runtime dependency, nothing to keep current. Read it, change it, delete the name.

npx @botharness/botui add dot-matrix

Also works with shadcn, against the same registry:npx shadcn@latest add https://ui.botharness.ai/r/dot-matrix.json

cols
5
rows
5
shape · 形状

Changes the outline of the whole field

motion · 动效

Hover or focus a card to play it

Direction

Winding

Direction

Which end leads

Dot shape

Changes each individual dot

Timing
Envelope
Dot detail
size · 尺寸
dot / cell   198%
dot / pitch  198%    100% = two dots touching
touching at  dot / cell = 1.00
pitch        43.5 × 43.5 px
dot          86.1 px
silhouette   square   dot square
renderer     css   speed 1.00×

Your own traversal

A traversal is one function of lattice position, so anything you can compute per cell you can animate. These two are not presets — they are `order` functions, written here and running below.

export const chevron: OrderFn = (col, row, cols, rows) => {
  const mid = (rows - 1) / 2;
  const arm = Math.abs(row - mid) / (mid || 1);
  const along = col / (cols - 1 || 1);
  // one band per step of the grid: any fewer and adjacent cells share a band and the arrow
  // thickens, any more and there is no cell left to light
  const bands = Math.max(cols, rows);
  return Math.round(Math.abs(along - arm) * bands) / bands;
};

/**
 * RINGS expanding from a dot the visitor chooses.
 *
 * Chebyshev distance — `max(|dc|, |dr|)` — rather than Euclidean, and that choice is the
 * whole character: on an integer lattice the Chebyshev radius takes only a handful of
 * distinct values, so every dot on a ring shares one phase and the field reads as discrete
 * bands. Euclidean would give nearly every dot its own value and turn the same traversal
 * into a smooth travelling wave. The concentric `ring` preset is this function with the
 * origin pinned to the middle; here the origin is a parameter, which is the request the
 * library's own table could not express.
 *
 * Normalised by the largest distance the lattice can hold, so the outermost dot leads at 1
 * whatever the grid size — otherwise a 9×9 and a 3×3 would animate at different rates for
 * the same nominal shape.
 */
export function ringsFrom(origin: { col: number; row: number }): OrderFn {
  return (col, row, cols, rows) => {
    const d = Math.max(Math.abs(col - origin.col), Math.abs(row - origin.row));
    // The furthest cell in the lattice FROM THE ORIGIN, which is whichever corner is
    // diagonally opposite it. Normalising by a fixed denominator — `cols`, or
    // `max(cols, rows)` — is the tempting one-liner and it is wrong twice over: it makes
    // the outermost dot's value depend on the GRID SIZE rather than on the origin, so a
    // 9×9 and a 3×3 animate at different rates for the same nominal shape, and it leaves
    // the last ring short of 1, so the wave never quite arrives.
    const far = Math.max(origin.col, cols - 1 - origin.col, origin.row, rows - 1 - origin.row);
    return far > 0 ? Math.min(1, d / far) : 0;
  };
}

/** the cell nearest a click, so a visitor can pick the origin with a pointer */
export function nearestCell(
  at: { x: number; y: number },
  pitch: { x: number; y: number },
  cols: number,
  rows: number,
): { col: number; row: number } {
  // the field is centred, so a click at (0,0) in field space is the top-left cell
  const cx = (cols - 1) / 2;
  const cy = (rows - 1) / 2;
  const col = Math.round(cx + (at.x / pitch.x) * cx);
  const row = Math.round(cy + (at.y / pitch.y) * cy);
  return {
    col: Math.min(cols - 1, Math.max(0, col)),
    row: Math.min(rows - 1, Math.max(0, row)),
  };
}

Components

Read from /registry.json — the same document the CLI installs from.

Loading the registry…

Why copy-and-own

Yours after install

The registry ships the real source files. What lands in components/botui/ is the same text as the repository, and a test in CI proves it byte for byte.

No version to track

There is no runtime dependency to upgrade and nothing that can break under you. When BotUI ships something better, you take the diff when you want to.

One component, five layers

Lattice, traversal, brightness envelope, dot polygon, renderer. Each is a table you can extend without touching the layers above.