Bottom Activity Strip

Rangee fine a defilement horizontal des evenements recents du canevas pour une activite en direct discrete. Installez Bottom Activity Strip depuis le registre VLLNT UI avec la CLI shadcn.

Signaler un bug

Rangee fine a defilement horizontal des evenements recents du canevas pour une activite en direct discrete. Fait partie de la famille data dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Installation

Ajoutez Bottom Activity Strip a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/bottom-activity-strip.json

Source

"use client"; import type { ComponentPropsWithoutRef, ReactNode } from "react"; import { cn } from "../../lib/utils"; /** * Tone of an event — drives the leading dot color. * * @public */ export type ActivityStripTone = | "danger" | "info" | "neutral" | "success" | "warn"; const TONE_DOT: Record<ActivityStripTone, string> = { danger: "bg-red-500", info: "bg-blue-500", neutral: "bg-muted-foreground", success: "bg-emerald-500", warn: "bg-amber-500", }; /** * One event in the strip. * * @public */ export type ActivityEvent = { /** Stable identifier — used as the React key + analytics hook. */ id: string; /** Short label (e.g. `"deploy completed"`). */ label: ReactNode; /** Optional click handler — when provided, the chip becomes a button. */ onActivate?: () => void; /** Optional tone for the leading dot. Defaults to `"neutral"`. */ tone?: ActivityStripTone; /** Pre-formatted timestamp (host owns formatting). */ ts: ReactNode; }; /** * Localizable strings. * * @public */ export type BottomActivityStripLabels = { /** Empty-state copy. Defaults to `"No recent activity"`. */ empty?: string; /** Aria-label for the strip. Defaults to `"Recent activity"`. */ region?: string; }; const DEFAULT_LABELS = { empty: "No recent activity", region: "Recent activity", } as const satisfies Required<BottomActivityStripLabels>; /** * Props for {@link BottomActivityStrip}. * * @public */ export type BottomActivityStripProps = { /** Event entries — newest first. */ events: ActivityEvent[]; /** Localizable strings. */ labels?: BottomActivityStripLabels; /** Cap the rendered events. The component drops the tail without warning. */ maxEvents?: number; } & ComponentPropsWithoutRef<"section">; const ChipBody = (props: { event: ActivityEvent }): React.ReactElement => { const { event } = props; const tone = event.tone ?? "neutral"; return ( <span className="flex items-center gap-1.5 whitespace-nowrap"> <span aria-hidden="true" className={cn("size-1.5 rounded-full", TONE_DOT[tone])} /> <span className="text-foreground">{event.label}</span> <span className="text-[10px] text-muted-foreground" data-strip-event-ts> {event.ts} </span> </span> ); }; const Chip = (props: { event: ActivityEvent }): React.ReactElement => { const { event } = props; const tone = event.tone ?? "neutral"; if (event.onActivate) { const handleActivateEvent = (): void => { event.onActivate?.(); }; return ( <button className="flex items-center rounded-full border border-border bg-background px-2 py-1 text-[11px] transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" data-strip-event={event.id} data-strip-event-tone={tone} onClick={handleActivateEvent} type="button" > <ChipBody event={event} /> </button> ); } return ( <span className="flex items-center rounded-full border border-border bg-background px-2 py-1 text-[11px]" data-strip-event={event.id} data-strip-event-tone={tone} > <ChipBody event={event} /> </span> ); }; /** * Slim bottom strip showing a horizontally-scrolling row of recent * canvas events. The lowest-noise live execution surface — keep * `ObjectCard`, panels, and overlays for high-value surfaces; let the * strip carry the steady drip of activity. * * Pure presentation; the host computes the event list (newest first) * and supplies an optional `onActivate` per event to jump to the * related object. * * Distinct from `ActivityLog` (vertical, persistent) and `LiveFeed` * (full-height feed): this primitive is a single horizontal row that * lives at the bottom of the canvas. * * @example * ```tsx * <BottomActivityStrip * events={[ * { id: "1", label: "deploy ok", ts: "12s", tone: "success" }, * { id: "2", label: "queue spike", ts: "1m", tone: "warn" }, * ]} * maxEvents={20} * /> * ``` * * @public */ export const BottomActivityStrip = ({ ref, ...props }: BottomActivityStripProps & { ref?: React.Ref<HTMLElement> }) => { const { className, events, labels, maxEvents, ...rest } = props; const resolvedLabels = { ...DEFAULT_LABELS, ...labels }; const visible = maxEvents === undefined || maxEvents >= events.length ? events : events.slice(0, maxEvents); return ( <section aria-label={resolvedLabels.region} className={cn( "flex w-full items-center gap-2 overflow-x-auto rounded-md border border-border bg-background/90 px-2 py-1 text-foreground", className, )} data-bottom-activity-strip ref={ref} {...rest} > {visible.length === 0 ? ( <span className="px-2 text-[11px] text-muted-foreground" data-strip-state="empty" > {resolvedLabels.empty} </span> ) : ( visible.map((event) => <Chip event={event} key={event.id} />) )} </section> ); }; BottomActivityStrip.displayName = "BottomActivityStrip";

Stories

Explorez chaque variante et etat dans le Storybook interactif :

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Dependances

  • @vllnt/ui@^0.3.0