Mikkel Manniche
Dansk English Deutsch

Vorlage · React · HTML

Widget wall

Eine Wand aus dreizehn Tiles-Widgets: ein dunkles Highlight, Trades, sechs Kennzahlen mit Sparklines, eine Punktmatrix, ein Profilring, Team, Balken und eine Rangliste.

Vorschau

React

Installiere die Vorlage mit shadcn. Sie landet als normale Komponenten in deinem Projekt, die du selbst anpasst.

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

Die ganze Seite als eine Datei mit eingebautem CSS und ohne JavaScript. Umschalter, Tabs und Filter funktionieren allein mit HTML und CSS.

In neuem Tab öffnen

Die Farbe in deiner eigenen App

Wenn shadcn bereits eingerichtet ist, ergeben diese Variablen dieselbe Farbe. Hell in :root, dunkel in .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);
}

Installieren

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

Abhängigkeiten

    Dateien

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

    Manniche Lab · Open Source

    Kostenlose Werkzeuge für KI-Agenten und Webentwicklung.