Presence Sync Indicator

Compact pill that surfaces live connection + sync health for a collaborative canvas.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/presence-sync-indicator.json
bash

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

4 stories available:

Code

"use client";

import {
  type ComponentPropsWithoutRef,
  forwardRef,
  type ReactNode,
} from "react";

import { cn } from "../../lib/utils";

/**
 * Connection / sync state — drives the dot color + animation.
 *
 * @public
 */
export type PresenceSyncState =
  | "error"
  | "live"
  | "offline"
  | "reconnecting"
  | "syncing";

const STATE_LABEL: Record<PresenceSyncState, string> = {
  error: "Sync error",
  live: "Live",
  offline: "Offline",
  reconnecting: "Reconnecting",
  syncing: "Syncing",
};

const STATE_DOT: Record<PresenceSyncState, string> = {
  error: "bg-red-500",
  live: "bg-emerald-500",
  offline: "bg-muted-foreground",
  reconnecting: "bg-amber-500 animate-pulse",
  syncing: "bg-blue-500 animate-pulse",
};

const STATE_TEXT: Record<PresenceSyncState, string> = {
  error: "text-red-700 dark:text-red-300",
  live: "text-emerald-700 dark:text-emerald-300",
  offline: "text-muted-foreground",
  reconnecting: "text-amber-700 dark:text-amber-300",
  syncing: "text-blue-700 dark:text-blue-300",
};

/**
 * Localizable strings.
 *
 * @public
 */
export type PresenceSyncIndicatorLabels = {
  /** Aria-label override. Defaults to `"Presence sync"`. */
  region?: string;
};

const DEFAULT_LABELS = {
  region: "Presence sync",
} as const satisfies Required<PresenceSyncIndicatorLabels>;

/**
 * Props for {@link PresenceSyncIndicator}.
 *
 * @public
 */
export type PresenceSyncIndicatorProps = {
  /** Optional override label (defaults to humanized state name). */
  label?: ReactNode;
  /** Localizable strings. */
  labels?: PresenceSyncIndicatorLabels;
  /** Connection state. */
  state: PresenceSyncState;
  /** Optional secondary line (peer count, latency). */
  status?: ReactNode;
} & ComponentPropsWithoutRef<"div">;

/**
 * Compact pill that surfaces live connection + sync health for the
 * canvas. Stays calm: a single dot + one-line label, with a subtle
 * pulse for transient `syncing` / `reconnecting` states.
 *
 * Pure presentation; the host owns the websocket / CRDT loop and maps
 * its diagnostics into one of five canonical states.
 *
 * @example
 * ```tsx
 * <PresenceSyncIndicator state="live" status="3 peers" />
 * <PresenceSyncIndicator state="reconnecting" status="retry 2/5" />
 * ```
 *
 * @public
 */
export const PresenceSyncIndicator = forwardRef<
  HTMLDivElement,
  PresenceSyncIndicatorProps
>((props, ref) => {
  const { className, label, labels, state, status, ...rest } = props;
  const resolvedLabels = { ...DEFAULT_LABELS, ...labels };
  const text = label ?? STATE_LABEL[state];
  return (
    <div
      aria-label={`${resolvedLabels.region}: ${STATE_LABEL[state]}`}
      className={cn(
        "inline-flex items-center gap-1.5 rounded-full border border-border bg-background px-2 py-1 text-[11px] shadow-sm",
        className,
      )}
      data-presence-state={state}
      data-presence-sync
      ref={ref}
      role="status"
      {...rest}
    >
      <span
        aria-hidden="true"
        className={cn("size-1.5 rounded-full", STATE_DOT[state])}
        data-presence-sync-dot
      />
      <span className={cn("font-medium", STATE_TEXT[state])}>{text}</span>
      {status ? (
        <span
          className="text-[10px] text-muted-foreground"
          data-presence-sync-status
        >
          {status}
        </span>
      ) : null}
    </div>
  );
});
PresenceSyncIndicator.displayName = "PresenceSyncIndicator";
typescript

Dependencies

  • @vllnt/ui@^0.2.1