BotUI

给会思考的东西用的界面组件。

加载态、流式指示、agent 状态 —— agent 产品真正待着的那些界面。用 npx 装一个,代码直接落到你的仓库里。没有运行时依赖,不用追版本。读它、改它、把名字删掉都行。

npx @botharness/botui add dot-matrix

同样支持 shadcn,用的是同一个 registry:npx shadcn@latest add https://ui.botharness.ai/r/dot-matrix.json

cols
5
rows
5
形状 shape

改变整个点阵的外轮廓

动效 motion

悬停或聚焦卡片即可播放

方向

缠绕

方向

哪一端领先

点的形状

改变每一个点本身

节奏
包络
点的细节
尺寸 size
点占格   198%
点占间距  198%    100% = 两个点刚好贴合
紧贴时  点占格 = 1.00
间距        43.5 × 43.5 px
点          86.1 px
整体形状   方形   dot 方形
渲染器     css   速度 1.00×

你自己的遍历

遍历就是「格位置」的单一函数,所以你能按格计算的任何东西都能变成动效。下面这两个不是 preset — 它们是 `order` 函数,就在这里写着,并在下面运行。

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 a pointer is over, and the pixel position of a cell's centre.
 *
 * Both come from the ENGINE's own `layout()` numbers, and that is the whole point: the
 * handle is drawn where this function says and the hit-test is taken against the same
 * function, so "the handle is under the cursor" is true by construction rather than by two
 * approximations agreeing.
 *
 * Two mistakes are written into the old version and are worth naming, because both produce
 * a handle that drifts further out the further out you drag — which is exactly the symptom
 * that sent this back:
 *
 *   · the pitch was `box.width / cols`, which is not the pitch. The field's box is `size`
 *     px including one dot of overhang, so the centre-to-centre distance is
 *     `layout().pitchX` and the box/n is about 3% out before anything else is wrong;
 *   · and the centre-to-cell conversion multiplied by `(cols - 1) / 2` a second time, making
 *     the whole thing FOUR TIMES as sensitive as it should be. At the edge of a 9-wide field
 *     that is a twelve-cell error, clamped — so the handle sat at the rim while the cursor
 *     was halfway out, and dragging further changed nothing at all.
 *
 * The fixed version is the identity: a dot's centre is `dotPx/2 + col * pitchX` from the
 * field's left edge, and a pointer at offset `x` from the field's centre is
 * `x / pitchX` cells from the middle one.
 */
export interface FieldGeometry {
  /** centre-to-centre distance in px, per axis */
  pitchX: number;
  pitchY: number;
  /** one dot's width in px — the overhang at each end of the box */
  dotPx: number;
}

/** where a cell's centre sits, relative to the field's box */
export function cellCentre(
  geo: FieldGeometry,
  cell: { col: number; row: number },
): { x: number; y: number } {
  return {
    x: geo.dotPx / 2 + cell.col * geo.pitchX,
    y: geo.dotPx / 2 + cell.row * geo.pitchY,
  };
}

/** the cell whose centre is nearest a point given as an offset from the field's centre */
export function nearestCell(
  at: { x: number; y: number },
  geo: FieldGeometry,
  cols: number,
  rows: number,
): { col: number; row: number } {
  const col = Math.round((cols - 1) / 2 + at.x / geo.pitchX);
  const row = Math.round((rows - 1) / 2 + at.y / geo.pitchY);
  return {
    // clamped, because a drag that leaves the field should park the origin at the rim
    // rather than index off the lattice
    col: Math.min(cols - 1, Math.max(0, col)),
    row: Math.min(rows - 1, Math.max(0, row)),
  };
}

组件

读自 /registry.json —— 和 CLI 安装用的是同一份文档。

正在读取 registry…

为什么是 copy-and-own

装完就是你的

registry 下发的是真实源码。落进 components/botui/ 的文本和仓库里逐字节相同,CI 里有测试证明。

不用追版本

没有运行时依赖要升级,也没有什么会在你脚下突然坏掉。BotUI 以后有更好的实现,你什么时候想拿 diff 就什么时候拿。

一个组件,五层

点阵、遍历、亮度包络、点字形、渲染器。每一层都是一张表,你可以扩展而不动它上面的层。