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.

AnalyticsDashboard

surface

분석 대시보드 Surface. StatCard + LineChart + BarChart 조합.

컴포넌트 의존 관계

깊이
▼ USES (5)AnalyticsDashboardline-chartbar-chartloading-overlaysummary-rowtime-range-selector
100%

기본 사용

Analytics

Total Users12,345↑ +12.5%
Events1.2M↑ +8.3%
Sessions45,678↓ -2.1%
Retention68%↑ +1.2%

Trend

테스트 커버리지

2026년 2월 4일

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

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

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

AnalyticsDashboard Props

Prop타입기본값설명
stats*AnalyticsDashboardStat[]—통계 카드 배열
trendDataChartDataPoint[]—추세 차트 데이터
trendSeriesChartSeriesDef[]—추세 시리즈 정의
breakdownDataChartDataPoint[]—분석 차트 데이터
breakdownSeriesChartSeriesDef[]—분석 시리즈 정의

Surface 설치

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

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

Consumer target

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

tsx
import { AnalyticsDashboard } from "@/components/surfaces/analytics-dashboard";

Registry metadata

설명
분석 대시보드 Surface. StatCard + LineChart + BarChart 조합.
파일 수
1개
Registry dependencies
없음
Package dependencies
없음
태그
chartdashboardanalytics
Install notes
없음

포함 파일

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

import * as React from "react";
import {
  SurfaceLayout,
  SummaryRow,
  TimeRangeSelector,
  cn,
  type DateRange,
} from "@reopt-ai/opt-ui";
import {
  LineChart,
  BarChart,
  PieChart,
  type ChartDataPoint,
  type ChartSeriesDef,
  type PieChartDataPoint,
} from "@reopt-ai/opt-charts";

/** Statistic shape for `AnalyticsDashboard`. */
export interface AnalyticsDashboardStat {
  id: string;
  title: string;
  value: string;
  change: string;
  trend: "up" | "down" | "neutral";
  sparklineData?: number[];
}

/** Item shape for `AnalyticsNav`. */
export interface AnalyticsNavItem {
  id: string;
  label: string;
  description?: string;
  icon?: string;
  href?: string;
  onClick?: () => void;
}

/** Labels for `AnalyticsDashboard`. */
export interface AnalyticsDashboardLabels {
  analytics?: string;
  trend?: string;
  breakdown?: string;
  pie?: string;
  tools?: string;
  emptyTitle?: string;
  emptyDescription?: string;
}

const defaultLabels: Required<AnalyticsDashboardLabels> = {
  analytics: "Analytics",
  trend: "Trend",
  breakdown: "Breakdown",
  pie: "Distribution",
  tools: "Analysis Tools",
  emptyTitle: "No analytics data",
  emptyDescription: "Analytics data will appear here once available.",
};

/** Props for `AnalyticsDashboard`. */
export interface AnalyticsDashboardProps {
  stats: AnalyticsDashboardStat[];
  trendData?: ChartDataPoint[];
  trendSeries?: ChartSeriesDef[];
  breakdownData?: ChartDataPoint[];
  breakdownSeries?: ChartSeriesDef[];
  pieData?: PieChartDataPoint[];
  /** Navigation cards for analysis tools */
  navItems?: AnalyticsNavItem[];
  /** Footer content (e.g., SDK info, integration details) */
  footer?: React.ReactNode;
  header?: React.ReactNode;
  actions?: React.ReactNode;
  /** Custom filter/alert area rendered between stats and charts (e.g., metric selector badges, callouts) */
  filterContent?: React.ReactNode;
  /** Additional content rendered between charts and nav items (e.g., top events table, recent activity) */
  tableContent?: React.ReactNode;
  labels?: AnalyticsDashboardLabels;
  loading?: boolean;
  timeRange?: DateRange;
  onTimeRangeChange?: (range: DateRange) => void;
  className?: string;
}

/** Renders the `AnalyticsDashboard` component. */
export function AnalyticsDashboard({
  stats,
  trendData = [],
  trendSeries = [],
  breakdownData = [],
  breakdownSeries = [],
  pieData,
  navItems,
  footer,
  header,
  actions,
  filterContent,
  tableContent,
  labels: customLabels,
  loading = false,
  timeRange,
  onTimeRangeChange,
  className,
}: AnalyticsDashboardProps) {
  const labels = { ...defaultLabels, ...customLabels };
  const titleId = React.useId();
  const trendId = React.useId();
  const breakdownId = React.useId();
  const pieId = React.useId();
  const toolsId = React.useId();

  const isEmpty = stats.length === 0;
  const hasNavItems = navItems && navItems.length > 0;

  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} />
            {actions}
          </div>
        </div>
      ) : (
        <div className="flex items-center justify-between">
          <h2 id={titleId} className="text-text-primary text-lg font-semibold">
            {labels.analytics}
          </h2>
          <TimeRangeSelector value={timeRange} onChange={onTimeRangeChange} />
        </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>
      ) : (
        <>
          <SummaryRow stats={stats} columns={4} />

          {filterContent}

          {trendData.length > 0 && trendSeries.length > 0 && (
            <section aria-labelledby={trendId}>
              <h3
                id={trendId}
                className="text-text-tertiary mb-2 text-sm font-medium"
              >
                {labels.trend}
              </h3>
              <LineChart
                data={trendData}
                series={trendSeries}
                aria-label={labels.trend}
              />
            </section>
          )}

          {breakdownData.length > 0 && breakdownSeries.length > 0 && (
            <section aria-labelledby={breakdownId}>
              <h3
                id={breakdownId}
                className="text-text-tertiary mb-2 text-sm font-medium"
              >
                {labels.breakdown}
              </h3>
              <BarChart
                data={breakdownData}
                series={breakdownSeries}
                aria-label={labels.breakdown}
              />
            </section>
          )}

          {pieData && pieData.length > 0 && (
            <section aria-labelledby={pieId}>
              <h3
                id={pieId}
                className="text-text-tertiary mb-2 text-sm font-medium"
              >
                {labels.pie}
              </h3>
              <PieChart data={pieData} aria-label={labels.pie} />
            </section>
          )}

          {tableContent}

          {hasNavItems && (
            <section aria-labelledby={toolsId}>
              <h3
                id={toolsId}
                className="text-text-tertiary mb-3 text-sm font-medium"
              >
                {labels.tools}
              </h3>
              <nav
                aria-label={labels.tools}
                className="gap-group grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4"
              >
                {navItems.map((item) => {
                  const content = (
                    <>
                      {item.icon && (
                        <span className="text-lg" aria-hidden="true">
                          {item.icon}
                        </span>
                      )}
                      <span className="text-text-primary text-sm font-medium">
                        {item.label}
                      </span>
                      {item.description && (
                        <span className="text-text-tertiary text-xs">
                          {item.description}
                        </span>
                      )}
                    </>
                  );

                  const cls =
                    "gap-element flex flex-col rounded-lg border border-border-subtle p-3 transition-colors hover:bg-bg-subtle";

                  if (item.href) {
                    return (
                      <a key={item.id} href={item.href} className={cls}>
                        {content}
                      </a>
                    );
                  }
                  return (
                    <button
                      key={item.id}
                      type="button"
                      onClick={item.onClick}
                      className={cn(cls, "text-left")}
                    >
                      {content}
                    </button>
                  );
                })}
              </nav>
            </section>
          )}

          {footer && <div>{footer}</div>}
        </>
      )}
    </SurfaceLayout>
  );
}

AnalyticsDashboard.displayName = "AnalyticsDashboard";