reopt designreopt design
DocsExploreToolsPricingBuilder
Start
Overview
Start
Next.js 설치
Private install
Core Concepts
아키텍처
Composition Patterns
Accessibility
Keyboard Patterns
Styling
Theme System
Advanced Patterns
Build & Operate
Skills
AI Integration
CLI (opt surface add)
Dependency Graph
Tools
Canvas Catalog
Theme Builder
Form Builder
Templates
Templates
Releases
Release Notes
Oopt-ui
reopt designreopt design

A design system for the AI era

  • Docs
  • Pricing
  • Releases
  • GitHub
  • Terms of Service
  • Privacy Policy

© 2026 reopt-ai. All rights reserved.

RealtimeDashboard

surface

실시간 대시보드 Surface. ConnectionIndicator + StatCard + ActivityFeed 조합.

컴포넌트 의존 관계

깊이
▼ USES (5)RealtimeDashboardconnection-indicatoractivity-feedloading-overlaysummary-rowtime-range-selector
100%

기본 사용

Real-time

Connected
Active Users342↑ +28
Events/sec1,205↑ +15%
Errors3↓ -2
Latency45ms↓ -5ms

Live Events

활동 피드

U
user_123 생성 page_view by user_123
Invalid Date
U
user_456 생성 signup — Pro Plan
Invalid Date
U
user_789 완료 purchase $49.99
Invalid Date

테스트 커버리지

2026년 2월 4일

생성된 테스트 결과를 찾지 못했습니다.

RealtimeDashboard 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.

테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.

RealtimeDashboard Props

Prop타입기본값설명
status*ConnectionStatus—연결 상태
stats*RealtimeDashboardStat[]—통계 배열
activities*ActivityDef[]—활동 피드 배열
onLoadMore() => void—더 불러오기 핸들러

Surface 설치

CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.

bash
npx @reopt-ai/opt-cli surface add realtime-dashboard

Consumer target

복사된 파일은 components/surfaces 아래에 저장됩니다.

tsx
import { RealtimeDashboard } from "@/components/surfaces/realtime-dashboard";

Registry metadata

설명
실시간 대시보드 Surface. ConnectionIndicator + StatCard + ActivityFeed 조합.
파일 수
1개
Registry dependencies
없음
Package dependencies
없음
태그
dashboard
Install notes
없음

포함 파일

  • realtime-dashboard.tsx→realtime-dashboard.tsx
Surface 소스 보기
realtime-dashboard.tsx
"use client";

import * as React from "react";
import {
  ConnectionIndicator,
  ActivityFeed,
  SurfaceLayout,
  SummaryRow,
  TimeRangeSelector,
  cn,
  type ConnectionStatus,
  type ActivityDef,
  type DateRange,
} from "@reopt-ai/opt-ui";

/** Statistic shape for `RealtimeDashboard`. */
export interface RealtimeDashboardStat {
  id: string;
  title: string;
  value: string;
  change: string;
  trend: "up" | "down" | "neutral";
  /** Optional variant for gradient-styled stat cards */
  variant?: "default" | "gradient";
}

/** Item shape for `TopEvent`. */
export interface TopEventItem {
  id: string;
  name: string;
  count: number;
}

/** Labels for `RealtimeDashboard`. */
export interface RealtimeDashboardLabels {
  realtime?: string;
  liveEvents?: string;
  topEvents?: string;
  loadMore?: string;
  emptyTitle?: string;
  emptyDescription?: string;
}

const defaultLabels: Required<RealtimeDashboardLabels> = {
  realtime: "Real-time",
  liveEvents: "Live Events",
  topEvents: "Top Events",
  loadMore: "Load more",
  emptyTitle: "No real-time data",
  emptyDescription: "Live events and stats will appear here once available.",
};

/** Props for `RealtimeDashboard`. */
export interface RealtimeDashboardProps {
  status: ConnectionStatus;
  stats: RealtimeDashboardStat[];
  activities: ActivityDef[];
  /** Top events ranked list displayed beside live feed */
  topEvents?: TopEventItem[];
  onLoadMore?: () => void;
  /** Enable CSS animations (pulse for connection, slideIn for feed items) */
  animated?: boolean;
  /** Custom renderer for live event feed items */
  renderEventItem?: (activity: ActivityDef) => React.ReactNode;
  header?: React.ReactNode;
  actions?: React.ReactNode;
  labels?: RealtimeDashboardLabels;
  loading?: boolean;
  timeRange?: DateRange;
  onTimeRangeChange?: (range: DateRange) => void;
  className?: string;
}

/** Renders the `RealtimeDashboard` component. */
export function RealtimeDashboard({
  status,
  stats,
  activities,
  topEvents,
  onLoadMore,
  animated = false,
  renderEventItem,
  header,
  actions,
  labels: customLabels,
  loading = false,
  timeRange,
  onTimeRangeChange,
  className,
}: RealtimeDashboardProps) {
  const labels = { ...defaultLabels, ...customLabels };
  const titleId = React.useId();
  const liveEventsId = React.useId();
  const topEventsId = React.useId();

  const hasTopEvents = topEvents && topEvents.length > 0;
  const isEmpty =
    stats.length === 0 && activities.length === 0 && !hasTopEvents;

  const pulseClass = animated && status === "connected" ? "animate-pulse" : "";

  return (
    <SurfaceLayout loading={loading} className={className}>
      {header || actions ? (
        <div className="flex items-center justify-between">
          {header && <div>{header}</div>}
          <div className="gap-element flex items-center">
            <TimeRangeSelector value={timeRange} onChange={onTimeRangeChange} />
            <span className={pulseClass}>
              <ConnectionIndicator status={status} />
            </span>
            {actions}
          </div>
        </div>
      ) : (
        <div className="flex items-center justify-between">
          <h2 id={titleId} className="text-text-primary text-lg font-semibold">
            {labels.realtime}
          </h2>
          <div className="gap-element flex items-center">
            <TimeRangeSelector value={timeRange} onChange={onTimeRangeChange} />
            <span className={pulseClass}>
              <ConnectionIndicator status={status} />
            </span>
          </div>
        </div>
      )}

      {isEmpty ? (
        <div
          role="status"
          className="flex flex-col items-center justify-center py-16 text-center"
        >
          <h3 className="text-text-primary text-lg font-medium">
            {labels.emptyTitle}
          </h3>
          <p className="text-text-tertiary mt-1 text-sm">
            {labels.emptyDescription}
          </p>
        </div>
      ) : (
        <>
          {stats.length > 0 && <SummaryRow stats={stats} columns={4} />}

          {(activities.length > 0 || hasTopEvents) && (
            <div
              className={cn(
                "gap-section grid",
                hasTopEvents ? "grid-cols-1 md:grid-cols-2" : "grid-cols-1",
              )}
            >
              {hasTopEvents && (
                <section aria-labelledby={topEventsId}>
                  <h3
                    id={topEventsId}
                    className="text-text-tertiary mb-2 text-sm font-medium"
                  >
                    {labels.topEvents}
                  </h3>
                  <ul className="flex flex-col gap-1">
                    {topEvents.map((ev, idx) => (
                      <li
                        key={ev.id}
                        className={cn(
                          "hover:bg-bg-subtle flex items-center justify-between rounded-md px-3 py-2 text-sm",
                          animated &&
                            "animate-in fade-in slide-in-from-left-2 duration-300 [animation-fill-mode:backwards]",
                        )}
                        style={
                          animated
                            ? { animationDelay: `${idx * 50}ms` }
                            : undefined
                        }
                      >
                        <span className="text-text-secondary">
                          <span className="text-text-tertiary mr-2 text-xs">
                            {idx + 1}.
                          </span>
                          {ev.name}
                        </span>
                        <span className="text-text-primary font-medium">
                          {ev.count.toLocaleString()}
                        </span>
                      </li>
                    ))}
                  </ul>
                </section>
              )}

              {activities.length > 0 && (
                <section
                  aria-labelledby={liveEventsId}
                  className={
                    animated
                      ? "[&_[data-activity-item]]:animate-in [&_[data-activity-item]]:fade-in [&_[data-activity-item]]:slide-in-from-left-2 [&_[data-activity-item]]:duration-300"
                      : undefined
                  }
                >
                  <h3
                    id={liveEventsId}
                    className="text-text-tertiary mb-2 text-sm font-medium"
                  >
                    {labels.liveEvents}
                  </h3>
                  {renderEventItem ? (
                    <div className="gap-element flex flex-col">
                      {activities.map((activity, i) => (
                        <div key={activity.id ?? i} data-activity-item>
                          {renderEventItem(activity)}
                        </div>
                      ))}
                    </div>
                  ) : (
                    <ActivityFeed activities={activities} />
                  )}
                  {onLoadMore && (
                    <button
                      onClick={onLoadMore}
                      className="text-accent mt-2 text-sm hover:underline"
                    >
                      {labels.loadMore}
                    </button>
                  )}
                </section>
              )}
            </div>
          )}
        </>
      )}
    </SurfaceLayout>
  );
}

RealtimeDashboard.displayName = "RealtimeDashboard";