Mikkel Manniche
Dansk English Deutsch

Skabelon · React · HTML

Widget wall

En væg med tretten widgets i Tiles: et mørkt højdepunkt, handler, seks nøgletal med sparklines, en prikmatrix, en profilring, team, søjler og en rangliste.

Forhåndsvisning

React

Installér skabelonen med shadcn. Den lægges i dit projekt som almindelige komponenter, som du selv retter i.

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/widget-wall.json
npx shadcn@latest add @manniche/widget-wall
widget-wall.tsx
// A wall of small widgets (KPI sparkline tiles, one inverted highlight, profile ring, team, trades, profit matrix, lollipop, bars). Built from Tiles primitives.
import { cn } from '@/lib/utils'
import { BarChart, type BarPoint } from '@/registry/manniche/bar-chart/bar-chart'
import { BigNumber, DeltaPill } from '@/registry/manniche/chart-kit/chart-kit'
import type { ValueFormat } from '@/registry/manniche/chart-kit/chart-utils'
import { DataTile, TileFact } from '@/registry/manniche/data-tile/data-tile'
import { DotMatrix } from '@/registry/manniche/dot-matrix/dot-matrix'
import { TransactionList, type Transaction } from '@/registry/manniche/transaction-list/transaction-list'
import { Donut, type DonutDatum } from '@/registry/manniche/donut/donut'
import { Lollipop, type LollipopDatum } from '@/registry/manniche/lollipop/lollipop'
import { Sparkline } from '@/registry/manniche/sparkline/sparkline'

export type WidgetKpi = { id: string; title: string; value: number; change: number; series: number[]; format?: ValueFormat; lowerIsBetter?: boolean }

export type WidgetPerson = { id: string; name: string; role: string }

export type WidgetWallData = {
  highlight: { title: string; value: number; format?: ValueFormat; text: string }
  trades: { title: string; items: Transaction[]; today?: string; format?: ValueFormat }
  matrix: { title: string; data: number[][]; rows: string[]; columns: string[] }
  profile: { title: string; items: DonutDatum[]; total?: number; format?: ValueFormat }
  team: { title: string; people: WidgetPerson[] }
  kpis: WidgetKpi[]
  ranking: { title: string; items: LollipopDatum[]; format?: ValueFormat }
  weekly: { title: string; points: BarPoint[]; format?: ValueFormat }
}

export type WidgetWallLabels = { note?: string }

export type WidgetWallProps = { data: WidgetWallData; labels?: WidgetWallLabels; className?: string }

export function WidgetWall({ data, labels, className }: WidgetWallProps) {
  const note = labels?.note ?? 'Example data.'
  return (
    <div className={cn('@container w-full rounded-2xl bg-background p-3 text-foreground', className)}>
      <div className="grid grid-cols-1 gap-3 @lg:grid-cols-2 @3xl:grid-cols-6">
        <div className="@3xl:col-span-3">
          <DataTile className="h-full" title={data.highlight.title} inverted>
            <BigNumber value={data.highlight.value} format={data.highlight.format} size="xl" />
            <p className="mt-2 text-sm opacity-80">{data.highlight.text}</p>
          </DataTile>
        </div>
        <div className="@3xl:col-span-3">
          <DataTile className="h-full" title={data.trades.title} footer={note}>
            <TransactionList data={data.trades.items} label={data.trades.title} today={data.trades.today} format={data.trades.format} />
          </DataTile>
        </div>
        {data.kpis.map((k) => (
          <div key={k.id} className="@3xl:col-span-2">
            <DataTile className="h-full" title={k.title} density="compact">
              <TileFact aside={<DeltaPill value={k.change} goodWhen={k.lowerIsBetter ? 'down' : 'up'} />}>
                <BigNumber value={k.value} format={k.format} size="md" />
              </TileFact>
              <Sparkline data={k.series} label={k.title} format={k.format} />
            </DataTile>
          </div>
        ))}
        <div className="@lg:col-span-2 @3xl:col-span-6">
          <DataTile className="h-full" title={data.matrix.title} footer={note}>
            <DotMatrix data={data.matrix.data} rows={data.matrix.rows} columns={data.matrix.columns} label={data.matrix.title} />
          </DataTile>
        </div>
        <div className="@3xl:col-span-3">
          <DataTile className="h-full" title={data.profile.title} footer={note}>
            <Donut data={data.profile.items} label={data.profile.title} total={data.profile.total} format={data.profile.format} />
          </DataTile>
        </div>
        <div className="@3xl:col-span-3">
          <DataTile className="h-full" title={data.team.title} footer={note}>
            <ul className="flex flex-col gap-3">
              {data.team.people.map((p) => (
                <li key={p.id} className="flex items-center gap-3">
                  <span aria-hidden className="grid size-9 place-items-center rounded-full bg-muted text-xs font-medium">
                    {p.name.split(' ').map((w) => w[0]).join('').slice(0, 2)}
                  </span>
                  <span className="min-w-0 flex-1">
                    <span className="block truncate text-sm font-medium">{p.name}</span>
                    <span className="block truncate text-xs text-muted-foreground">{p.role}</span>
                  </span>
                </li>
              ))}
            </ul>
          </DataTile>
        </div>
        <div className="@lg:col-span-2 @3xl:col-span-3">
          <DataTile className="h-full" title={data.weekly.title} footer={note}>
            <BarChart data={data.weekly.points} label={data.weekly.title} format={data.weekly.format} />
          </DataTile>
        </div>
        <div className="@lg:col-span-2 @3xl:col-span-3">
          <DataTile className="h-full" title={data.ranking.title} footer={note}>
            <Lollipop data={data.ranking.items} label={data.ranking.title} format={data.ranking.format} />
          </DataTile>
        </div>
      </div>
    </div>
  )
}
widget-wall-demo.tsx
import { WidgetWall, type WidgetWallData } from '@/components/widget-wall'

// Example data only. All names and figures are invented.
const data: WidgetWallData = {
  highlight: { title: 'Sample profit', value: 12480, format: { currency: 'EUR', decimals: 0 }, text: 'Example figure, not a real result.' },
  trades: {
    title: 'Recent sample trades',
    today: '2026-10-06',
    format: { currency: 'EUR', decimals: 0 },
    items: [
      { id: 't1', name: 'Sample Corp', amount: -420, date: '2026-10-06T09:12', category: 'Buy' },
      { id: 't2', name: 'Demo Ltd', amount: 910, date: '2026-10-05T15:40', category: 'Sell' },
      { id: 't3', name: 'Test Inc', amount: 260, date: '2026-10-04T11:05', category: 'Sell' },
    ],
  },
  matrix: {
    title: 'Profit by weekday and week',
    rows: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
    columns: ['W1', 'W2', 'W3', 'W4', 'W5', 'W6'],
    data: [[1, 3, 2, 5, 4, 6], [2, 2, 3, 4, 5, 5], [0, 1, 2, 3, 3, 4], [3, 4, 5, 6, 6, 7], [1, 2, 2, 3, 4, 4]],
  },
  profile: {
    title: 'Profile steps done',
    items: [
      { label: 'Basics', value: 4 },
      { label: 'Billing', value: 3 },
      { label: 'Team', value: 2 },
    ],
    total: 12,
  },
  team: {
    title: 'Sample team',
    people: [
      { id: 'p1', name: 'Alex Example', role: 'Lead' },
      { id: 'p2', name: 'Sam Sample', role: 'Design' },
      { id: 'p3', name: 'Robin Demo', role: 'Support' },
    ],
  },
  kpis: [
    { id: 'a', title: 'Sessions', value: 18240, change: 6.2, series: [10, 12, 11, 14, 13, 16, 18] },
    { id: 'b', title: 'Sign-ups', value: 412, change: 2.4, series: [8, 9, 12, 10, 11, 12, 13] },
    { id: 'c', title: 'Refund rate', value: 1.8, change: -0.4, lowerIsBetter: true, format: { decimals: 1, suffix: '%' }, series: [5, 4, 5, 4, 3, 3, 2] },
    { id: 'd', title: 'Conversion', value: 3.4, change: 0.3, format: { decimals: 1, suffix: '%' }, series: [3, 3, 4, 3, 4, 4, 5] },
    { id: 'e', title: 'Avg. basket', value: 37.6, change: 1.1, format: { currency: 'EUR', decimals: 2 }, series: [6, 7, 6, 7, 8, 7, 8] },
    { id: 'f', title: 'Open tickets', value: 23, change: -8, lowerIsBetter: true, series: [9, 8, 9, 7, 6, 6, 5] },
  ],
  weekly: {
    title: 'Orders, last 7 days',
    points: ['Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'Mon', 'Tue'].map((label, i) => ({ label, value: [48, 71, 66, 39, 28, 42, 55][i] })),
  },
  ranking: {
    title: 'Top sample products',
    items: [
      { label: 'Sample mug', value: 320 },
      { label: 'Demo bowl', value: 240 },
      { label: 'Test vase', value: 150 },
      { label: 'Mock carafe', value: 90 },
    ],
  },
}

export default function WidgetWallDemo() {
  return <WidgetWall data={data} />
}

HTML

Hele siden som én fil med al CSS indbygget og intet JavaScript. Knapper, faner og filtre virker med HTML og CSS alene.

Åbn i ny fane

Farven i din egen app

Har du allerede shadcn sat op, giver disse variabler samme farve. Lys i :root, mørk i .dark.

:root {
  --primary: oklch(0.52 0.2 262);
  --primary-foreground: oklch(0.99 0 0);
  --accent: oklch(0.95 0.034 262);
  --accent-foreground: oklch(0.36 0.140 262);
  --ring: oklch(0.52 0.2 262);
}

.dark {
  --primary: oklch(0.74 0.150 262);
  --primary-foreground: oklch(0.18 0.024 262);
  --accent: oklch(0.29 0.050 262);
  --accent-foreground: oklch(0.9 0.050 262);
  --ring: oklch(0.74 0.150 262);
}

Installér

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/widget-wall.json

Afhængigheder

    Filer

    • widget-wall.tsx
    • widget-wall.html

    Manniche Lab · open source

    Gratis værktøjer til AI-agenter og webudvikling.