TanStack
shadcn/ui Charts

shadcn dashboard

application

1,888 lines · 4 files · 47.1 kB

cases/127-shadcn-dashboard/dashboard.tsx633 lines · dependency
cases/127-shadcn-dashboard/dashboard.tsx
import { useEffect, useMemo, useState } from 'react'
import {
  dashboardTableRows,
  filterDashboardData,
  formatDashboardDate,
  type DashboardDatum,
  type DashboardRange,
} from './data'
import { shadcnDashboardStyles } from './styles'
import type { ComponentType, ReactNode, SVGProps } from 'react'

export interface DashboardSize {
  width: number
  height: number
}

export interface DashboardChartProps {
  data: readonly DashboardDatum[]
  input: DashboardSize
}

export function dashboardChartWidth(width: number): number {
  if (width <= 480) return Math.max(1, width - 50)
  if (width < 768) return Math.max(1, width - 82)
  return Math.max(1, width - 354)
}

export function dashboardTickValues(
  data: readonly DashboardDatum[],
  width: number,
): readonly string[] {
  if (data.length < 2) return data.map((datum) => datum.date)

  const labels = data.map((datum) => formatDashboardDate(datum.date))
  const context =
    typeof document === 'undefined' ||
    typeof CanvasRenderingContext2D === 'undefined'
      ? null
      : document.createElement('canvas').getContext('2d')
  if (context) {
    context.font =
      '12px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
  }
  const measure = (label: string) =>
    context?.measureText(label).width ?? label.length * 6.1
  const start = 5
  const axisLength = Math.max(1, width - 10)
  let end = start + axisLength
  const visible: string[] = []

  for (let index = data.length - 1; index >= 0; index -= 1) {
    const size = measure(labels[index]!)
    const coordinate = start + (axisLength * index) / (data.length - 1)
    const tickCoordinate =
      index === data.length - 1
        ? Math.min(coordinate, end - size / 2)
        : coordinate
    if (
      tickCoordinate - size / 2 >= start &&
      tickCoordinate + size / 2 <= end
    ) {
      visible.push(data[index]!.date)
      end = tickCoordinate - size / 2 - 32
    }
  }

  return visible.reverse()
}

interface DashboardProps {
  ChartRenderer: ComponentType<DashboardChartProps>
  input: DashboardSize
}

const ranges: readonly { value: DashboardRange; label: string }[] = [
  { value: '90d', label: 'Last 3 months' },
  { value: '30d', label: 'Last 30 days' },
  { value: '7d', label: 'Last 7 days' },
]

const statCards = [
  {
    label: 'Total Revenue',
    value: '$1,250.00',
    change: '+12.5%',
    trend: 'Trending up this month',
    detail: 'Visitors for the last 6 months',
    direction: 'up' as const,
  },
  {
    label: 'New Customers',
    value: '1,234',
    change: '-20%',
    trend: 'Down 20% this period',
    detail: 'Acquisition needs attention',
    direction: 'down' as const,
  },
  {
    label: 'Active Accounts',
    value: '45,678',
    change: '+12.5%',
    trend: 'Strong user retention',
    detail: 'Engagement exceed targets',
    direction: 'up' as const,
  },
  {
    label: 'Growth Rate',
    value: '4.5%',
    change: '+4.5%',
    trend: 'Steady performance increase',
    detail: 'Meets growth projections',
    direction: 'up' as const,
  },
]

const mainNavigation = [
  ['Dashboard', 'dashboard'],
  ['Lifecycle', 'list'],
  ['Analytics', 'analytics'],
  ['Projects', 'folder'],
  ['Team', 'users'],
] as const

const documentNavigation = [
  ['Data Library', 'database'],
  ['Reports', 'report'],
  ['Word Assistant', 'word'],
] as const

const secondaryNavigation = [
  ['Settings', 'settings'],
  ['Get Help', 'help'],
  ['Search', 'search'],
] as const

export function ShadcnDashboard({ ChartRenderer, input }: DashboardProps) {
  const [range, setRange] = useState<DashboardRange>(
    input.width < 768 ? '7d' : '90d',
  )
  const chartData = useMemo(() => filterDashboardData(range), [range])

  useEffect(() => {
    if (input.width < 768) setRange('7d')
  }, [input.width])

  return (
    <div
      className="shadcn-dashboard"
      data-conformance-view="main"
      data-range={range}
      role="region"
      aria-label="shadcn dashboard example"
      style={{ width: input.width, height: input.height }}
    >
      <style>{shadcnDashboardStyles}</style>
      <div className="sd-viewport">
        <DashboardSidebar />
        <main className="sd-main">
          <div className="sd-main-scroll">
            <DashboardHeader />
            <div className="sd-content">
              <section className="sd-cards" aria-label="Key metrics">
                {statCards.map((card) => (
                  <StatCard key={card.label} {...card} />
                ))}
              </section>
              <section className="sd-card sd-chart-card">
                <header className="sd-chart-header">
                  <h2 className="sd-chart-title">Total Visitors</h2>
                  <p className="sd-chart-description">
                    Total for the last 3 months
                  </p>
                  <div
                    className="sd-range-buttons"
                    role="group"
                    aria-label="Chart time range"
                  >
                    {ranges.map((option) => (
                      <button
                        key={option.value}
                        type="button"
                        className="sd-range-button"
                        data-active={range === option.value}
                        data-range-value={option.value}
                        aria-pressed={range === option.value}
                        onClick={() => setRange(option.value)}
                      >
                        {option.label}
                      </button>
                    ))}
                  </div>
                  <select
                    className="sd-range-select"
                    aria-label="Select a time range"
                    value={range}
                    onChange={(event) =>
                      setRange(event.currentTarget.value as DashboardRange)
                    }
                  >
                    {ranges.map((option) => (
                      <option key={option.value} value={option.value}>
                        {option.label}
                      </option>
                    ))}
                  </select>
                </header>
                <div className="sd-chart-stage">
                  <ChartRenderer data={chartData} input={input} />
                </div>
              </section>
              <DashboardTable />
            </div>
          </div>
        </main>
      </div>
    </div>
  )
}

function DashboardSidebar() {
  return (
    <aside className="sd-sidebar" aria-label="Sidebar">
      <div className="sd-sidebar-inner">
        <div className="sd-brand">
          <Icon name="brand" className="sd-brand-mark" />
          <span className="sd-brand-name">Acme Inc.</span>
        </div>
        <nav className="sd-nav" aria-label="Dashboard">
          <div className="sd-nav-group">
            <div className="sd-nav-label">Home</div>
            {mainNavigation.map(([label, icon], index) => (
              <NavItem
                key={label}
                label={label}
                icon={icon}
                active={index === 0}
              />
            ))}
          </div>
          <div className="sd-nav-group">
            <div className="sd-nav-label">Documents</div>
            {documentNavigation.map(([label, icon]) => (
              <NavItem key={label} label={label} icon={icon} more />
            ))}
            <NavItem label="More" icon="more" />
          </div>
          <div className="sd-nav-group sd-nav-bottom">
            {secondaryNavigation.map(([label, icon]) => (
              <NavItem key={label} label={label} icon={icon} />
            ))}
          </div>
        </nav>
        <div className="sd-user">
          <span className="sd-avatar">CN</span>
          <span className="sd-user-copy">
            <strong>shadcn</strong>
            <span>m@example.com</span>
          </span>
          <Icon name="more-vertical" className="sd-nav-more" />
        </div>
      </div>
    </aside>
  )
}

function DashboardHeader() {
  return (
    <header className="sd-header">
      <button
        className="sd-icon-button sd-menu-button"
        type="button"
        aria-label="Toggle menu"
      >
        <Icon name="menu" />
      </button>
      <span className="sd-separator" aria-hidden="true" />
      <h1>Documents</h1>
      <button className="sd-quick-create" type="button">
        <Icon name="plus" />
        Quick Create
      </button>
    </header>
  )
}

function NavItem({
  label,
  icon,
  active = false,
  more = false,
}: {
  label: string
  icon: IconName
  active?: boolean
  more?: boolean
}) {
  return (
    <button
      className="sd-nav-item"
      type="button"
      data-active={active}
      aria-current={active ? 'page' : undefined}
    >
      <Icon name={icon} />
      <span>{label}</span>
      {more ? <Icon name="more" className="sd-nav-more" /> : null}
    </button>
  )
}

function StatCard({
  label,
  value,
  change,
  trend,
  detail,
  direction,
}: (typeof statCards)[number]) {
  return (
    <article className="sd-card sd-stat-card">
      <div className="sd-stat-head">
        <span className="sd-stat-label">{label}</span>
        <span className="sd-badge">
          <Icon name={direction === 'up' ? 'trend-up' : 'trend-down'} />
          {change}
        </span>
        <p className="sd-stat-value">{value}</p>
      </div>
      <div className="sd-stat-foot">
        <span className="sd-stat-trend">
          {trend}
          <Icon name={direction === 'up' ? 'trend-up' : 'trend-down'} />
        </span>
        <span className="sd-stat-detail">{detail}</span>
      </div>
    </article>
  )
}

function DashboardTable() {
  return (
    <section className="sd-table-section" aria-label="Document sections">
      <div className="sd-toolbar">
        <div className="sd-tabs" role="tablist" aria-label="Document view">
          <button
            className="sd-tab"
            type="button"
            role="tab"
            data-active="true"
          >
            Outline
          </button>
          <button className="sd-tab" type="button" role="tab">
            Past Performance <span className="sd-badge">3</span>
          </button>
          <button className="sd-tab" type="button" role="tab">
            Key Personnel <span className="sd-badge">2</span>
          </button>
          <button className="sd-tab" type="button" role="tab">
            Focus Documents
          </button>
        </div>
        <div className="sd-toolbar-actions">
          <button className="sd-button" type="button">
            <Icon name="columns" />
            <span>Customize Columns</span>
            <Icon name="chevron-down" />
          </button>
          <button className="sd-button" type="button">
            <Icon name="plus" />
            <span>Add Section</span>
          </button>
        </div>
      </div>
      <div className="sd-table-wrap">
        <div className="sd-table-scroll">
          <table className="sd-table">
            <thead>
              <tr>
                <th aria-label="Reorder" />
                <th>
                  <input
                    className="sd-checkbox"
                    type="checkbox"
                    aria-label="Select all"
                  />
                </th>
                <th>Header</th>
                <th>Section Type</th>
                <th>Status</th>
                <th>Target</th>
                <th>Limit</th>
                <th>Reviewer</th>
                <th aria-label="Actions" />
              </tr>
            </thead>
            <tbody>
              {dashboardTableRows.map((row) => (
                <tr key={row.id}>
                  <td>
                    <span className="sd-grip" aria-hidden="true">
                      ⋮⋮
                    </span>
                  </td>
                  <td>
                    <input
                      className="sd-checkbox"
                      type="checkbox"
                      aria-label={`Select ${row.header}`}
                    />
                  </td>
                  <td>
                    <a className="sd-table-link" href="#">
                      {row.header}
                    </a>
                  </td>
                  <td>
                    <span className="sd-badge">{row.type}</span>
                  </td>
                  <td>
                    <span
                      className="sd-badge sd-status"
                      data-done={row.status === 'Done'}
                    >
                      <span className="sd-status-dot" />
                      {row.status}
                    </span>
                  </td>
                  <td>
                    <input
                      className="sd-number-input"
                      aria-label={`${row.header} target`}
                      defaultValue={row.target}
                    />
                  </td>
                  <td>
                    <input
                      className="sd-number-input"
                      aria-label={`${row.header} limit`}
                      defaultValue={row.limit}
                    />
                  </td>
                  <td>{row.reviewer}</td>
                  <td>
                    <button
                      className="sd-icon-button"
                      type="button"
                      aria-label={`Open ${row.header} menu`}
                    >
                      <Icon name="more-vertical" />
                    </button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
      <div className="sd-table-footer">
        <span>0 of 68 row(s) selected.</span>
        <span className="sd-pagination">
          <span>Rows per page</span>
          <button className="sd-button" type="button">
            10 <Icon name="chevron-down" />
          </button>
          <span>Page 1 of 7</span>
          <button
            className="sd-icon-button"
            type="button"
            aria-label="Previous page"
          >
            <Icon name="chevron-left" />
          </button>
          <button
            className="sd-icon-button"
            type="button"
            aria-label="Next page"
          >
            <Icon name="chevron-right" />
          </button>
        </span>
      </div>
    </section>
  )
}

type IconName =
  | 'analytics'
  | 'brand'
  | 'chevron-down'
  | 'chevron-left'
  | 'chevron-right'
  | 'chevrons'
  | 'columns'
  | 'dashboard'
  | 'database'
  | 'folder'
  | 'help'
  | 'list'
  | 'menu'
  | 'more'
  | 'more-vertical'
  | 'plus'
  | 'report'
  | 'search'
  | 'settings'
  | 'trend-down'
  | 'trend-up'
  | 'users'
  | 'word'

function Icon({
  name,
  ...props
}: SVGProps<SVGSVGElement> & { name: IconName }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.8"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      {...props}
    >
      {iconPaths[name]}
    </svg>
  )
}

const iconPaths: Record<IconName, ReactNode> = {
  brand: (
    <>
      <path d="M12 3l8 4.5v9L12 21l-8-4.5v-9z" />
      <path d="M8 9h8v6H8z" />
    </>
  ),
  menu: (
    <>
      <path d="M4 5h16v14H4z" />
      <path d="M9 5v14" />
    </>
  ),
  dashboard: (
    <>
      <rect x="3" y="3" width="7" height="7" rx="1" />
      <rect x="14" y="3" width="7" height="7" rx="1" />
      <rect x="3" y="14" width="7" height="7" rx="1" />
      <rect x="14" y="14" width="7" height="7" rx="1" />
    </>
  ),
  list: (
    <>
      <path d="M9 6h11M9 12h11M9 18h11" />
      <path d="M4 6h.01M4 12h.01M4 18h.01" />
    </>
  ),
  analytics: (
    <>
      <path d="M4 19V9M10 19V5M16 19v-7M22 19H2" />
    </>
  ),
  folder: <path d="M3 6h7l2 2h9v11H3z" />,
  users: (
    <>
      <circle cx="9" cy="8" r="3" />
      <path d="M3 20c0-4 2-6 6-6s6 2 6 6M16 5a3 3 0 010 6M17 14c2.7.3 4 2.3 4 5" />
    </>
  ),
  database: (
    <>
      <ellipse cx="12" cy="5" rx="8" ry="3" />
      <path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6" />
    </>
  ),
  report: (
    <>
      <path d="M5 3h14v18H5zM9 7h6M9 11h6M9 15h4" />
    </>
  ),
  word: (
    <>
      <path d="M5 3h10l4 4v14H5zM15 3v5h4" />
      <path d="M8 11l1.5 6 2.5-4 2.5 4 1.5-6" />
    </>
  ),
  settings: (
    <>
      <circle cx="12" cy="12" r="3" />
      <path d="M19 12a7 7 0 00-.1-1l2-1.6-2-3.4-2.5 1a7 7 0 00-1.7-1L14.3 3h-4.6l-.4 3a7 7 0 00-1.7 1L5 6 3 9.4 5.1 11a7 7 0 000 2L3 14.6 5 18l2.6-1a7 7 0 001.7 1l.4 3h4.6l.4-3a7 7 0 001.7-1l2.5 1 2-3.4-2-1.6a7 7 0 00.1-1z" />
    </>
  ),
  help: (
    <>
      <circle cx="12" cy="12" r="9" />
      <path d="M9.5 9a2.7 2.7 0 115 1.5c-.8 1.2-2.5 1.4-2.5 3M12 17h.01" />
    </>
  ),
  search: (
    <>
      <circle cx="11" cy="11" r="7" />
      <path d="M16.5 16.5L21 21" />
    </>
  ),
  more: (
    <>
      <circle cx="5" cy="12" r="1" fill="currentColor" stroke="none" />
      <circle cx="12" cy="12" r="1" fill="currentColor" stroke="none" />
      <circle cx="19" cy="12" r="1" fill="currentColor" stroke="none" />
    </>
  ),
  'more-vertical': (
    <>
      <circle cx="12" cy="5" r="1" fill="currentColor" stroke="none" />
      <circle cx="12" cy="12" r="1" fill="currentColor" stroke="none" />
      <circle cx="12" cy="19" r="1" fill="currentColor" stroke="none" />
    </>
  ),
  chevrons: <path d="M8 9l4-4 4 4M16 15l-4 4-4-4" />,
  'trend-up': <path d="M3 17l6-6 4 4 8-8M15 7h6v6" />,
  'trend-down': <path d="M3 7l6 6 4-4 8 8M15 17h6v-6" />,
  columns: (
    <>
      <rect x="3" y="4" width="18" height="16" rx="2" />
      <path d="M9 4v16M15 4v16" />
    </>
  ),
  plus: <path d="M12 5v14M5 12h14" />,
  'chevron-down': <path d="M7 9l5 5 5-5" />,
  'chevron-left': <path d="M15 6l-6 6 6 6" />,
  'chevron-right': <path d="M9 6l6 6-6 6" />,
}
cases/127-shadcn-dashboard/data.ts256 lines · dependency
cases/127-shadcn-dashboard/data.ts
export type DashboardRange = '90d' | '30d' | '7d'
export type DashboardSeries = 'mobile' | 'desktop'

export interface DashboardDatum {
  date: string
  desktop: number
  mobile: number
}

export interface DashboardAreaDatum {
  date: Date
  dateKey: string
  series: DashboardSeries
  visitors: number
}

export interface DashboardTableRow {
  id: number
  header: string
  type: string
  status: 'Done' | 'In Process'
  target: string
  limit: string
  reviewer: string
}

export const dashboardChartData: readonly DashboardDatum[] = [
  { date: '2024-04-01', desktop: 222, mobile: 150 },
  { date: '2024-04-02', desktop: 97, mobile: 180 },
  { date: '2024-04-03', desktop: 167, mobile: 120 },
  { date: '2024-04-04', desktop: 242, mobile: 260 },
  { date: '2024-04-05', desktop: 373, mobile: 290 },
  { date: '2024-04-06', desktop: 301, mobile: 340 },
  { date: '2024-04-07', desktop: 245, mobile: 180 },
  { date: '2024-04-08', desktop: 409, mobile: 320 },
  { date: '2024-04-09', desktop: 59, mobile: 110 },
  { date: '2024-04-10', desktop: 261, mobile: 190 },
  { date: '2024-04-11', desktop: 327, mobile: 350 },
  { date: '2024-04-12', desktop: 292, mobile: 210 },
  { date: '2024-04-13', desktop: 342, mobile: 380 },
  { date: '2024-04-14', desktop: 137, mobile: 220 },
  { date: '2024-04-15', desktop: 120, mobile: 170 },
  { date: '2024-04-16', desktop: 138, mobile: 190 },
  { date: '2024-04-17', desktop: 446, mobile: 360 },
  { date: '2024-04-18', desktop: 364, mobile: 410 },
  { date: '2024-04-19', desktop: 243, mobile: 180 },
  { date: '2024-04-20', desktop: 89, mobile: 150 },
  { date: '2024-04-21', desktop: 137, mobile: 200 },
  { date: '2024-04-22', desktop: 224, mobile: 170 },
  { date: '2024-04-23', desktop: 138, mobile: 230 },
  { date: '2024-04-24', desktop: 387, mobile: 290 },
  { date: '2024-04-25', desktop: 215, mobile: 250 },
  { date: '2024-04-26', desktop: 75, mobile: 130 },
  { date: '2024-04-27', desktop: 383, mobile: 420 },
  { date: '2024-04-28', desktop: 122, mobile: 180 },
  { date: '2024-04-29', desktop: 315, mobile: 240 },
  { date: '2024-04-30', desktop: 454, mobile: 380 },
  { date: '2024-05-01', desktop: 165, mobile: 220 },
  { date: '2024-05-02', desktop: 293, mobile: 310 },
  { date: '2024-05-03', desktop: 247, mobile: 190 },
  { date: '2024-05-04', desktop: 385, mobile: 420 },
  { date: '2024-05-05', desktop: 481, mobile: 390 },
  { date: '2024-05-06', desktop: 498, mobile: 520 },
  { date: '2024-05-07', desktop: 388, mobile: 300 },
  { date: '2024-05-08', desktop: 149, mobile: 210 },
  { date: '2024-05-09', desktop: 227, mobile: 180 },
  { date: '2024-05-10', desktop: 293, mobile: 330 },
  { date: '2024-05-11', desktop: 335, mobile: 270 },
  { date: '2024-05-12', desktop: 197, mobile: 240 },
  { date: '2024-05-13', desktop: 197, mobile: 160 },
  { date: '2024-05-14', desktop: 448, mobile: 490 },
  { date: '2024-05-15', desktop: 473, mobile: 380 },
  { date: '2024-05-16', desktop: 338, mobile: 400 },
  { date: '2024-05-17', desktop: 499, mobile: 420 },
  { date: '2024-05-18', desktop: 315, mobile: 350 },
  { date: '2024-05-19', desktop: 235, mobile: 180 },
  { date: '2024-05-20', desktop: 177, mobile: 230 },
  { date: '2024-05-21', desktop: 82, mobile: 140 },
  { date: '2024-05-22', desktop: 81, mobile: 120 },
  { date: '2024-05-23', desktop: 252, mobile: 290 },
  { date: '2024-05-24', desktop: 294, mobile: 220 },
  { date: '2024-05-25', desktop: 201, mobile: 250 },
  { date: '2024-05-26', desktop: 213, mobile: 170 },
  { date: '2024-05-27', desktop: 420, mobile: 460 },
  { date: '2024-05-28', desktop: 233, mobile: 190 },
  { date: '2024-05-29', desktop: 78, mobile: 130 },
  { date: '2024-05-30', desktop: 340, mobile: 280 },
  { date: '2024-05-31', desktop: 178, mobile: 230 },
  { date: '2024-06-01', desktop: 178, mobile: 200 },
  { date: '2024-06-02', desktop: 470, mobile: 410 },
  { date: '2024-06-03', desktop: 103, mobile: 160 },
  { date: '2024-06-04', desktop: 439, mobile: 380 },
  { date: '2024-06-05', desktop: 88, mobile: 140 },
  { date: '2024-06-06', desktop: 294, mobile: 250 },
  { date: '2024-06-07', desktop: 323, mobile: 370 },
  { date: '2024-06-08', desktop: 385, mobile: 320 },
  { date: '2024-06-09', desktop: 438, mobile: 480 },
  { date: '2024-06-10', desktop: 155, mobile: 200 },
  { date: '2024-06-11', desktop: 92, mobile: 150 },
  { date: '2024-06-12', desktop: 492, mobile: 420 },
  { date: '2024-06-13', desktop: 81, mobile: 130 },
  { date: '2024-06-14', desktop: 426, mobile: 380 },
  { date: '2024-06-15', desktop: 307, mobile: 350 },
  { date: '2024-06-16', desktop: 371, mobile: 310 },
  { date: '2024-06-17', desktop: 475, mobile: 520 },
  { date: '2024-06-18', desktop: 107, mobile: 170 },
  { date: '2024-06-19', desktop: 341, mobile: 290 },
  { date: '2024-06-20', desktop: 408, mobile: 450 },
  { date: '2024-06-21', desktop: 169, mobile: 210 },
  { date: '2024-06-22', desktop: 317, mobile: 270 },
  { date: '2024-06-23', desktop: 480, mobile: 530 },
  { date: '2024-06-24', desktop: 132, mobile: 180 },
  { date: '2024-06-25', desktop: 141, mobile: 190 },
  { date: '2024-06-26', desktop: 434, mobile: 380 },
  { date: '2024-06-27', desktop: 448, mobile: 490 },
  { date: '2024-06-28', desktop: 149, mobile: 200 },
  { date: '2024-06-29', desktop: 103, mobile: 160 },
  { date: '2024-06-30', desktop: 446, mobile: 400 },
]

export const dashboardTableRows: readonly DashboardTableRow[] = [
  {
    id: 1,
    header: 'Cover page',
    type: 'Cover page',
    status: 'In Process',
    target: '18',
    limit: '5',
    reviewer: 'Eddie Lake',
  },
  {
    id: 2,
    header: 'Table of contents',
    type: 'Table of contents',
    status: 'Done',
    target: '29',
    limit: '24',
    reviewer: 'Eddie Lake',
  },
  {
    id: 3,
    header: 'Executive summary',
    type: 'Narrative',
    status: 'Done',
    target: '10',
    limit: '13',
    reviewer: 'Eddie Lake',
  },
  {
    id: 4,
    header: 'Technical approach',
    type: 'Narrative',
    status: 'Done',
    target: '27',
    limit: '23',
    reviewer: 'Jamik Tashpulatov',
  },
  {
    id: 5,
    header: 'Design',
    type: 'Narrative',
    status: 'In Process',
    target: '2',
    limit: '16',
    reviewer: 'Jamik Tashpulatov',
  },
  {
    id: 6,
    header: 'Capabilities',
    type: 'Narrative',
    status: 'In Process',
    target: '20',
    limit: '8',
    reviewer: 'Jamik Tashpulatov',
  },
  {
    id: 7,
    header: 'Integration with existing systems',
    type: 'Narrative',
    status: 'In Process',
    target: '19',
    limit: '21',
    reviewer: 'Jamik Tashpulatov',
  },
  {
    id: 8,
    header: 'Innovation and Advantages',
    type: 'Narrative',
    status: 'Done',
    target: '25',
    limit: '26',
    reviewer: 'Assign reviewer',
  },
  {
    id: 9,
    header: "Overview of EMR's Innovative Solutions",
    type: 'Technical content',
    status: 'Done',
    target: '7',
    limit: '23',
    reviewer: 'Assign reviewer',
  },
  {
    id: 10,
    header: 'Advanced Algorithms and Machine Learning',
    type: 'Narrative',
    status: 'Done',
    target: '30',
    limit: '28',
    reviewer: 'Assign reviewer',
  },
]

const referenceDate = Date.parse('2024-06-30T00:00:00Z')
const day = 24 * 60 * 60 * 1_000

export function filterDashboardData(
  range: DashboardRange,
): readonly DashboardDatum[] {
  const days = range === '7d' ? 7 : range === '30d' ? 30 : 90
  const start = referenceDate - days * day
  return dashboardChartData.filter(
    (datum) => Date.parse(`${datum.date}T00:00:00Z`) >= start,
  )
}

export function dashboardAreaRows(
  data: readonly DashboardDatum[],
): readonly DashboardAreaDatum[] {
  return data.flatMap((datum) => {
    const date = new Date(`${datum.date}T00:00:00Z`)
    return [
      {
        date,
        dateKey: datum.date,
        series: 'mobile' as const,
        visitors: datum.mobile,
      },
      {
        date,
        dateKey: datum.date,
        series: 'desktop' as const,
        visitors: datum.desktop,
      },
    ]
  })
}

export function formatDashboardDate(value: string | number | Date): string {
  const date = value instanceof Date ? value : new Date(value)
  return date.toLocaleDateString('en-US', {
    month: 'short',
    day: 'numeric',
    timeZone: 'UTC',
  })
}
cases/127-shadcn-dashboard/example.tsx192 lines · entry
cases/127-shadcn-dashboard/example.tsx
import { useMemo } from 'react'
import { areaY, d3Curve, defineChart, stack } from '@tanstack/charts'
import { motion } from '@tanstack/charts/motion'
import { Chart } from '@tanstack/charts/react/core'
import { tooltip } from '@tanstack/charts/tooltip'
import { scaleLinear, scalePoint } from 'd3-scale'
import { curveNatural } from 'd3-shape'
import {
  dashboardChartWidth,
  dashboardTickValues,
  ShadcnDashboard,
  type DashboardChartProps,
} from './dashboard'
import {
  dashboardAreaRows,
  filterDashboardData,
  formatDashboardDate,
  type DashboardAreaDatum,
  type DashboardDatum,
  type DashboardSeries,
} from './data'
import type { ChartTooltipContent, ChartTooltipOptions } from '@tanstack/charts'

const seriesOrder: readonly DashboardSeries[] = ['mobile', 'desktop']
const primaryColor = 'var(--sd-primary, var(--ts-chart-1, #171717))'
const mutedColor = 'var(--sd-muted-foreground, #737373)'
const borderColor = 'var(--sd-border, #e5e5e5)'

const dashboardTooltip: ChartTooltipOptions<DashboardAreaDatum> = {
  className: 'sd-chart-tooltip',
  anchor: 'group-center',
  placement: ['top', 'right', 'left', 'bottom'],
  sticky: false,
  sort: 'color-domain',
  content(points): ChartTooltipContent {
    return {
      title: formatDashboardDate(points[0]?.xValue ?? ''),
      rows: points.map((point) => ({
        label: point.datum.series === 'desktop' ? 'Desktop' : 'Mobile',
        value: point.datum.visitors.toLocaleString('en-US'),
        color: primaryColor,
      })),
    }
  },
}

export function createExampleChart(
  data: readonly DashboardDatum[],
  width = 288,
) {
  const rows = dashboardAreaRows(data)
  const tickValues = dashboardTickValues(data, width)

  return defineChart(
    {
      marks: [
        areaY(rows, {
          id: 'visitor-areas',
          x: 'dateKey',
          y: 'visitors',
          z: 'series',
          color: 'series',
          key: (row) => `${row.dateKey}:${row.series}`,
          layout: stack({ order: seriesOrder }),
          curve: d3Curve(curveNatural),
          fill: (row) => `url(#fill-${row.series})`,
          fillOpacity: 0.6,
          stroke: primaryColor,
          strokeWidth: 1,
        }),
      ],
      scales: {
        x: {
          scale: scalePoint,
          axis: {
            line: false,
            ticks: {
              values: tickValues,
              size: 0,
              padding: 8,
              format: formatDashboardDate,
            },
            tickLabels: {
              fontSize: 12,
              opacity: 0.72,
              dy: 5,
              anchor: ({ value }) =>
                value === data.at(-1)?.date ? 'end' : 'middle',
              dx: ({ value }) => (value === data.at(-1)?.date ? 5 : 0),
              thin: { minGap: 32, priority: 'ends' },
            },
          },
        },
        y: {
          scale: scaleLinear().domain([0, 1_200]),
          grid: true,
          axis: {
            line: false,
            ticks: { values: [0, 300, 600, 900, 1_200], size: 0 },
            tickLabels: false,
          },
        },
      },

      color: { domain: seriesOrder, range: [primaryColor, primaryColor] },
      gradients: [
        {
          id: 'fill-mobile',
          x1: 0,
          y1: 1,
          x2: 0,
          y2: 0,
          stops: [
            { offset: 0.05, color: primaryColor, opacity: 0.1 },
            { offset: 0.95, color: primaryColor, opacity: 0.8 },
          ],
        },
        {
          id: 'fill-desktop',
          x1: 0,
          y1: 1,
          x2: 0,
          y2: 0,
          stops: [
            { offset: 0.05, color: primaryColor, opacity: 0.1 },
            { offset: 0.95, color: primaryColor, opacity: 1 },
          ],
        },
      ],
      margin: { top: 5, right: 5, bottom: 35, left: 5 },
      theme: {
        foreground: mutedColor,
        grid: borderColor,
        background: 'transparent',
        palette: [primaryColor],
      },
      clip: true,
    },
    {
      svgAnimation: false,
      focus: 'group-x',
      focusRing: true,
      maxFocusDistance: Number.POSITIVE_INFINITY,
      keyboard: true,
      tooltip: { use: tooltip, ...dashboardTooltip },
    },
  )
}

export const definition = createExampleChart(filterDashboardData('90d'), 288)

function DashboardChart({ data, input }: DashboardChartProps) {
  const width = dashboardChartWidth(input.width)
  const renderer = useMemo(
    () =>
      motion({
        initial: 'always',
        transition: {
          type: 'spring',
          stiffness: 170,
          damping: 18,
          mass: 1,
        },
      }),
    [],
  )
  const chartDefinition = useMemo(
    () => createExampleChart(data, width),
    [data, width],
  )

  return (
    <Chart
      definition={chartDefinition}
      renderer={renderer}
      initialWidth={width}
      height={250}
      ariaLabel="Total visitors for the last three months"
    />
  )
}

export interface ExampleProps {
  width?: number
  height?: number
}

export default function Example({ width = 1024, height = 768 }: ExampleProps) {
  return (
    <ShadcnDashboard ChartRenderer={DashboardChart} input={{ width, height }} />
  )
}
cases/127-shadcn-dashboard/styles.ts807 lines · dependency
cases/127-shadcn-dashboard/styles.ts
export const shadcnDashboardStyles = `
  .shadcn-dashboard {
    --sd-background: oklch(1 0 0);
    --sd-foreground: oklch(0.145 0 0);
    --sd-card: oklch(1 0 0);
    --sd-card-foreground: oklch(0.145 0 0);
    --sd-primary: oklch(0.205 0 0);
    --sd-primary-foreground: oklch(0.985 0 0);
    --sd-secondary: oklch(0.97 0 0);
    --sd-muted: oklch(0.97 0 0);
    --sd-muted-foreground: oklch(0.556 0 0);
    --sd-accent: oklch(0.97 0 0);
    --sd-border: oklch(0.922 0 0);
    --sd-input: oklch(0.922 0 0);
    --sd-sidebar: oklch(0.985 0 0);
    --sd-sidebar-accent: oklch(0.97 0 0);
    --sd-radius: 0.625rem;
    width: 100%;
    height: 100%;
    overflow: hidden;
    color: var(--sd-foreground);
    background: var(--sd-background);
    font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    font-size: 14px;
    line-height: 1.45;
    text-rendering: geometricPrecision;
  }

  :root[data-theme='dark'] .shadcn-dashboard {
    --sd-background: oklch(0.145 0 0);
    --sd-foreground: oklch(0.985 0 0);
    --sd-card: oklch(0.205 0 0);
    --sd-card-foreground: oklch(0.985 0 0);
    --sd-primary: oklch(0.922 0 0);
    --sd-primary-foreground: oklch(0.205 0 0);
    --sd-secondary: oklch(0.269 0 0);
    --sd-muted: oklch(0.269 0 0);
    --sd-muted-foreground: oklch(0.708 0 0);
    --sd-accent: oklch(0.269 0 0);
    --sd-border: oklch(1 0 0 / 10%);
    --sd-input: oklch(1 0 0 / 15%);
    --sd-sidebar: oklch(0.205 0 0);
    --sd-sidebar-accent: oklch(0.269 0 0);
  }

  .shadcn-dashboard,
  .shadcn-dashboard * {
    box-sizing: border-box;
  }

  .shadcn-dashboard :where(button, select, input) {
    min-height: 0;
    color: inherit;
    font: inherit;
  }

  .sd-viewport {
    display: flex;
    width: 100%;
    height: 100%;
    overflow: auto;
    background: var(--sd-sidebar);
  }

  .sd-sidebar {
    position: sticky;
    top: 0;
    display: flex;
    flex: 0 0 256px;
    height: 100%;
    flex-direction: column;
    border-right: 1px solid var(--sd-border);
    background: var(--sd-sidebar);
  }

  .sd-sidebar-inner {
    display: flex;
    min-height: 0;
    flex: 1;
    flex-direction: column;
    overflow: hidden;
    border: 0;
    border-radius: 0;
    background: var(--sd-sidebar);
  }

  .sd-brand,
  .sd-user {
    display: flex;
    min-height: 48px;
    align-items: center;
    gap: 10px;
    padding: 8px;
  }

  .sd-brand {
    min-height: 49px;
    border-bottom: 1px solid var(--sd-border);
  }

  .sd-brand-mark {
    width: 20px;
    height: 20px;
    color: var(--sd-foreground);
  }

  .sd-brand-name {
    overflow: hidden;
    font-size: 16px;
    font-weight: 600;
    letter-spacing: -0.015em;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .sd-nav {
    display: flex;
    min-height: 0;
    flex: 1;
    flex-direction: column;
    gap: 18px;
    overflow: auto;
    padding: 4px 8px 12px;
  }

  .sd-nav-group {
    display: grid;
    gap: 2px;
  }

  .sd-nav-label {
    padding: 6px 8px 3px;
    color: var(--sd-muted-foreground);
    font-size: 12px;
    font-weight: 500;
  }

  .sd-nav-bottom {
    margin-top: auto;
  }

  .sd-nav-item {
    display: flex;
    width: 100%;
    height: 32px;
    align-items: center;
    gap: 9px;
    padding: 0 8px;
    border: 0;
    border-radius: 6px;
    color: var(--sd-foreground);
    background: transparent;
    cursor: default;
    text-align: left;
  }

  .sd-nav-item:hover,
  .sd-nav-item[data-active='true'] {
    background: var(--sd-sidebar-accent);
  }

  .sd-nav-item svg,
  .sd-user svg,
  .sd-header svg,
  .sd-toolbar svg,
  .sd-badge svg {
    width: 16px;
    height: 16px;
    flex: none;
  }

  .sd-nav-more {
    margin-left: auto;
    color: var(--sd-muted-foreground);
  }

  .sd-user {
    border-top: 1px solid transparent;
  }

  .sd-avatar {
    display: grid;
    width: 32px;
    height: 32px;
    flex: none;
    place-items: center;
    border-radius: 8px;
    color: var(--sd-primary-foreground);
    background: var(--sd-primary);
    font-size: 11px;
    font-weight: 650;
  }

  .sd-user-copy {
    min-width: 0;
  }

  .sd-user-copy strong,
  .sd-user-copy span {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .sd-user-copy strong {
    font-size: 13px;
    font-weight: 600;
  }

  .sd-user-copy span {
    color: var(--sd-muted-foreground);
    font-size: 11px;
  }

  .sd-main {
    min-width: 0;
    flex: 1;
    margin: 0;
    overflow: hidden;
    border: 0;
    border-radius: 0;
    background: var(--sd-background);
  }

  .sd-main-scroll {
    width: 100%;
    height: 100%;
    overflow: auto;
  }

  .sd-header {
    position: sticky;
    top: 0;
    z-index: 4;
    display: flex;
    height: 49px;
    align-items: center;
    gap: 10px;
    padding: 0 24px;
    border-bottom: 1px solid var(--sd-border);
    background: color-mix(in srgb, var(--sd-background) 94%, transparent);
    backdrop-filter: blur(8px);
  }

  .sd-icon-button {
    display: inline-grid;
    width: 32px;
    height: 32px;
    padding: 0;
    place-items: center;
    border: 0;
    border-radius: 6px;
    background: transparent;
  }

  .sd-separator {
    display: none;
    width: 1px;
    height: 16px;
    margin: 0 6px;
    background: var(--sd-border);
  }

  .sd-menu-button {
    display: none;
  }

  .sd-header h1 {
    margin: 0;
    font-size: 16px;
    font-weight: 500;
    letter-spacing: -0.01em;
  }

  .sd-quick-create {
    display: inline-flex;
    height: 32px;
    align-items: center;
    gap: 6px;
    margin-left: auto;
    padding: 0 11px;
    border: 0;
    border-radius: 6px;
    color: var(--sd-primary-foreground);
    background: var(--sd-primary);
    font-size: 13px;
    font-weight: 600;
  }

  .shadcn-dashboard .sd-quick-create {
    color: var(--sd-primary-foreground);
  }

  .sd-content {
    container-type: inline-size;
    display: flex;
    flex-direction: column;
    gap: 24px;
    padding: 24px;
  }

  .sd-cards {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 16px;
  }

  .sd-card {
    position: relative;
    overflow: hidden;
    border: 1px solid var(--sd-border);
    border-radius: var(--sd-radius);
    color: var(--sd-card-foreground);
    background:
      linear-gradient(to top, color-mix(in srgb, var(--sd-primary) 5%, transparent), transparent 45%),
      var(--sd-card);
    box-shadow: 0 1px 2px rgb(0 0 0 / 3%);
  }

  :root[data-theme='dark'] .sd-card {
    background: var(--sd-card);
  }

  .sd-stat-card {
    min-height: 200px;
    padding: 23px 23px 20px;
  }

  .sd-stat-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 3px 12px;
  }

  .sd-stat-label,
  .sd-chart-description {
    color: var(--sd-muted-foreground);
    font-size: 14px;
  }

  .sd-stat-value {
    grid-column: 1;
    margin: 0;
    font-size: clamp(24px, 2.35cqw, 30px);
    font-weight: 600;
    letter-spacing: -0.035em;
    line-height: 1.2;
    font-variant-numeric: tabular-nums;
  }

  .sd-badge {
    display: inline-flex;
    height: 23px;
    align-items: center;
    gap: 4px;
    padding: 0 7px;
    border: 1px solid var(--sd-border);
    border-radius: 7px;
    color: var(--sd-foreground);
    background: transparent;
    font-size: 12px;
    font-weight: 500;
    line-height: 1;
    white-space: nowrap;
  }

  .sd-stat-foot {
    display: grid;
    gap: 3px;
    margin-top: 30px;
    font-size: 14px;
  }

  .sd-stat-trend {
    display: flex;
    min-width: 0;
    align-items: center;
    gap: 7px;
    overflow: hidden;
    font-weight: 500;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .sd-stat-trend svg {
    width: 16px;
    height: 16px;
    flex: none;
  }

  .sd-stat-detail {
    overflow: hidden;
    color: var(--sd-muted-foreground);
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .sd-chart-card {
    height: 392px;
    background: var(--sd-card);
  }

  .sd-chart-header {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 4px 16px;
    align-items: start;
    padding: 24px 24px 0;
  }

  .sd-chart-title {
    margin: 0;
    font-size: 16px;
    font-weight: 600;
    letter-spacing: -0.015em;
  }

  .sd-chart-description {
    grid-column: 1;
  }

  .sd-range-buttons {
    grid-column: 2;
    grid-row: 1 / span 2;
    display: inline-flex;
    height: 32px;
    overflow: hidden;
    border: 1px solid var(--sd-border);
    border-radius: 8px;
  }

  .sd-range-button {
    height: 30px;
    padding: 0 16px;
    border: 0;
    border-left: 1px solid var(--sd-border);
    background: transparent;
    cursor: pointer;
    font-size: 13px;
    font-weight: 500;
    white-space: nowrap;
  }

  .sd-range-button:first-child {
    border-left: 0;
  }

  .sd-range-button:hover,
  .sd-range-button[data-active='true'] {
    background: var(--sd-accent);
  }

  .sd-range-select {
    display: none;
    grid-column: 2;
    grid-row: 1 / span 2;
    width: 160px;
    height: 32px;
    padding: 0 30px 0 10px;
    border: 1px solid var(--sd-border);
    border-radius: 8px;
    background: var(--sd-background);
    font-size: 13px;
  }

  .sd-chart-stage {
    position: relative;
    width: 100%;
    height: 274px;
    margin-top: 24px;
    padding: 8px 24px 0;
  }

  .sd-chart-stage > * {
    width: 100%;
    height: 100%;
  }

  .sd-chart-stage .ts-chart-host {
    color: var(--sd-muted-foreground);
    font-size: 12px;
  }

  .sd-chart-stage .ts-chart__grid line {
    stroke: var(--sd-border);
  }

  .sd-chart-stage .ts-chart__axis text {
    fill: var(--sd-muted-foreground);
  }

  .ts-chart-tooltip.sd-chart-tooltip {
    min-width: 138px !important;
    padding: 8px 10px !important;
    border: 1px solid var(--sd-border) !important;
    border-radius: 8px !important;
    color: var(--sd-card-foreground) !important;
    background: var(--sd-card) !important;
    box-shadow: 0 2px 6px rgb(0 0 0 / 8%) !important;
    font: 12px/1.45 Inter, ui-sans-serif, system-ui, sans-serif !important;
  }

  .ts-chart-tooltip.sd-chart-tooltip .ts-chart-tooltip__title {
    margin-bottom: 5px !important;
    font-weight: 500 !important;
  }

  .ts-chart-tooltip.sd-chart-tooltip .ts-chart-tooltip__row {
    grid-template-columns: 8px minmax(0, 1fr) auto !important;
    column-gap: 7px !important;
  }

  .ts-chart-tooltip.sd-chart-tooltip .ts-chart-tooltip__swatch {
    width: 8px !important;
    height: 8px !important;
    border-radius: 999px !important;
  }

  .sd-recharts-tooltip {
    min-width: 138px;
    padding: 8px 10px;
    border: 1px solid var(--sd-border);
    border-radius: 8px;
    color: var(--sd-card-foreground);
    background: var(--sd-card);
    box-shadow: 0 2px 6px rgb(0 0 0 / 8%);
    font-size: 12px;
    line-height: 1.45;
  }

  .sd-recharts-tooltip-date {
    margin-bottom: 5px;
    font-weight: 500;
  }

  .sd-recharts-tooltip-row {
    display: grid;
    grid-template-columns: 8px minmax(0, 1fr) auto;
    align-items: center;
    gap: 7px;
  }

  .sd-recharts-tooltip-dot {
    width: 8px;
    height: 8px;
    border-radius: 999px;
    background: var(--sd-primary);
  }

  .sd-table-section {
    display: grid;
    gap: 24px;
  }

  .sd-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
  }

  .sd-tabs {
    display: inline-flex;
    height: 36px;
    align-items: center;
    padding: 3px;
    border-radius: 9px;
    background: var(--sd-muted);
  }

  .sd-tab {
    height: 30px;
    padding: 0 11px;
    border: 0;
    border-radius: 7px;
    background: transparent;
    font-size: 13px;
  }

  .sd-tab[data-active='true'] {
    border: 1px solid var(--sd-border);
    background: var(--sd-background);
    box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
  }

  .sd-toolbar-actions {
    display: flex;
    gap: 8px;
  }

  .sd-button {
    display: inline-flex;
    height: 32px;
    align-items: center;
    justify-content: center;
    gap: 7px;
    padding: 0 11px;
    border: 1px solid var(--sd-border);
    border-radius: 8px;
    background: var(--sd-background);
    font-size: 13px;
    font-weight: 500;
    white-space: nowrap;
  }

  .sd-table-wrap {
    overflow: hidden;
    border: 1px solid var(--sd-border);
    border-radius: 9px;
  }

  .sd-table-scroll {
    overflow-x: auto;
  }

  .sd-table {
    width: 100%;
    min-width: 860px;
    border-collapse: collapse;
    font-size: 13px;
  }

  .sd-table th,
  .sd-table td {
    height: 48px;
    padding: 0 12px;
    border-bottom: 1px solid var(--sd-border);
    text-align: left;
    white-space: nowrap;
  }

  .sd-table th {
    height: 40px;
    color: var(--sd-muted-foreground);
    background: var(--sd-muted);
    font-weight: 500;
  }

  .sd-table tbody tr:last-child td {
    border-bottom: 0;
  }

  .sd-grip {
    color: var(--sd-muted-foreground);
    letter-spacing: -3px;
  }

  .sd-checkbox {
    width: 16px;
    height: 16px;
    accent-color: var(--sd-primary);
  }

  .sd-table-link {
    color: var(--sd-foreground);
    font-weight: 500;
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, var(--sd-muted-foreground) 45%, transparent);
    text-underline-offset: 4px;
  }

  .sd-status {
    display: inline-flex;
    align-items: center;
    gap: 5px;
  }

  .sd-status-dot {
    width: 8px;
    height: 8px;
    border: 1.5px solid currentColor;
    border-radius: 999px;
  }

  .sd-status[data-done='true'] .sd-status-dot {
    border-color: #22c55e;
    background: #22c55e;
    box-shadow: inset 0 0 0 1.5px var(--sd-card);
  }

  .sd-number-input {
    width: 64px;
    height: 32px;
    padding: 0 8px;
    border: 1px solid transparent;
    border-radius: 6px;
    background: transparent;
    text-align: right;
  }

  .sd-number-input:hover {
    background: color-mix(in srgb, var(--sd-input) 30%, transparent);
  }

  .sd-table-footer {
    display: flex;
    min-height: 36px;
    align-items: center;
    gap: 24px;
    color: var(--sd-muted-foreground);
    font-size: 13px;
  }

  .sd-pagination {
    display: flex;
    margin-left: auto;
    align-items: center;
    gap: 8px;
    color: var(--sd-foreground);
    font-weight: 500;
  }

  @container (min-width: 1000px) {
    .sd-cards {
      grid-template-columns: repeat(4, minmax(0, 1fr));
    }
  }

  @container (max-width: 540px) {
    .sd-cards {
      grid-template-columns: 1fr;
    }

    .sd-stat-card {
      min-height: 132px;
    }

    .sd-tabs {
      display: none;
    }

    .sd-toolbar::before {
      content: 'Outline';
      display: inline-flex;
      height: 32px;
      align-items: center;
      padding: 0 10px;
      border: 1px solid var(--sd-border);
      border-radius: 8px;
      font-size: 13px;
    }
  }

  @container (max-width: 767px) {
    .sd-range-buttons {
      display: none;
    }

    .sd-range-select {
      display: block;
    }
  }

  @media (max-width: 767px) {
    .sd-sidebar {
      display: none;
    }

    .sd-main {
      margin: 0;
      border: 0;
      border-radius: 0;
    }

    .sd-header,
    .sd-content {
      padding-inline: 16px;
    }

    .sd-menu-button {
      display: inline-grid;
    }

    .sd-separator {
      display: block;
    }

    .sd-quick-create {
      display: none;
    }
  }

  @media (max-width: 480px) {
    .sd-chart-card {
      height: 414px;
    }

    .sd-chart-header {
      grid-template-columns: 1fr;
    }

    .sd-range-select {
      grid-column: 1;
      grid-row: 3;
      margin-top: 8px;
    }

    .sd-chart-stage {
      padding-inline: 8px;
    }

    .sd-button span {
      display: none;
    }
  }
`