Mikkel Manniche
Dansk English Deutsch

Vorlage · React · HTML

Delivery board

Ein Projekt-Board in Tiles: Fortschritt pro Kunde, ein Tagesstreifen, der heute öffnet, Roadmap-Bahnen mit Initialen und eine kleine Assistenzkachel.

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/delivery-board.json
npx shadcn@latest add @manniche/delivery-board
delivery-board.tsx
// A delivery board: client progress, a date strip (opens on `now` when it is in the strip), roadmap lanes with initials avatars and a small assistant tile.
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { DataTile } from '@/registry/manniche/data-tile/data-tile'

export type BoardClient = { id: string; name: string; progress: number }
export type BoardItem = { id: string; title: string; owners: string[] }
export type BoardLane = { id: string; title: string; items: BoardItem[] }

export type DeliveryBoardData = {
  clients: { title: string; items: BoardClient[] }
  /** ISO dates "YYYY-MM-DD", one per chip. */
  days: string[]
  roadmap: { title: string; lanes: BoardLane[] }
  assistant: { title: string; text: string; action: string }
}

export type DeliveryBoardLabels = { note?: string; daysLabel?: string }

export type DeliveryBoardProps = {
  data: DeliveryBoardData
  labels?: DeliveryBoardLabels
  now?: Date
  onAssistant?: () => void
  className?: string
}

/** Local calendar date as "YYYY-MM-DD". */
const isoDay = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`

const initials = (n: string) => n.split(' ').map((w) => w[0]).join('').slice(0, 2).toUpperCase()

export function DeliveryBoard({ data, labels, now, onAssistant, className }: DeliveryBoardProps) {
  const note = labels?.note ?? 'Example data.'
  const today = now && isoDay(now)
  const [day, setDay] = useState(today && data.days.includes(today) ? today : data.days[0])
  return (
    <div className={cn('@container w-full rounded-2xl bg-background p-3 text-foreground', className)}>
      <div className="grid grid-cols-1 gap-3 @2xl:grid-cols-6">
        <div className="@2xl:col-span-4">
          <DataTile className="h-full" title={data.clients.title} footer={note}>
            <ul className="flex flex-col gap-3">
              {data.clients.items.map((c) => (
                <li key={c.id} className="flex flex-col gap-1.5">
                  <div className="flex justify-between text-sm">
                    <span className="font-medium">{c.name}</span>
                    <span className="tabular-nums text-muted-foreground">{c.progress}%</span>
                  </div>
                  <div role="progressbar" aria-label={c.name} aria-valuenow={c.progress} aria-valuemin={0} aria-valuemax={100} className="h-1.5 overflow-hidden rounded-full bg-muted">
                    <div className="h-full rounded-full bg-foreground" style={{ width: `${c.progress}%` }} />
                  </div>
                </li>
              ))}
            </ul>
          </DataTile>
        </div>
        <div className="@2xl:col-span-2">
          <DataTile className="h-full" title={data.assistant.title} inverted>
            <p className="text-sm opacity-80">{data.assistant.text}</p>
            <button type="button" onClick={onAssistant} className="mt-4 rounded-full bg-foreground px-4 py-2 text-sm font-medium text-background transition-transform active:scale-[0.97] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring">
              {data.assistant.action}
            </button>
          </DataTile>
        </div>
        <div className="@2xl:col-span-6">
          <div role="group" aria-label={labels?.daysLabel ?? 'Day'} className="flex gap-1 overflow-x-auto rounded-full border border-border bg-card p-1">
            {data.days.map((d) => {
              const date = new Date(`${d}T12:00:00Z`)
              return (
                <button
                  key={d}
                  type="button"
                  aria-pressed={d === day}
                  aria-current={d === today ? 'date' : undefined}
                  onClick={() => setDay(d)}
                  className={cn('relative flex min-w-11 flex-1 flex-col items-center rounded-full px-1 py-1 text-xs text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring', d === day && 'bg-foreground text-background hover:text-background')}
                >
                  <span>{date.toLocaleDateString('en-GB', { weekday: 'short', timeZone: 'UTC' })}</span>
                  <span className="text-sm font-medium tabular-nums">{date.getUTCDate()}</span>
                  {d === today && <span aria-hidden className="absolute bottom-0.5 size-1 rounded-full bg-current" />}
                </button>
              )
            })}
          </div>
        </div>
        <div className="@2xl:col-span-6">
          <DataTile className="h-full" title={data.roadmap.title} footer={note}>
            <div className="grid grid-cols-1 gap-4 @xl:grid-cols-3">
              {data.roadmap.lanes.map((lane) => (
                <section key={lane.id} aria-label={lane.title} className="flex flex-col gap-2">
                  <h3 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">{lane.title}</h3>
                  <ul className="flex flex-col gap-2">
                    {lane.items.map((it) => (
                      <li key={it.id} className="flex items-center justify-between gap-2 rounded-xl border border-border bg-card px-3 py-2 text-sm">
                        <span className="min-w-0 truncate">{it.title}</span>
                        <span className="flex -space-x-1.5">
                          {it.owners.map((o) => (
                            <span key={o} title={o} role="img" aria-label={o} className="grid size-6 place-items-center rounded-full border border-card bg-muted text-[10px] font-medium">
                              {initials(o)}
                            </span>
                          ))}
                        </span>
                      </li>
                    ))}
                  </ul>
                </section>
              ))}
            </div>
          </DataTile>
        </div>
      </div>
    </div>
  )
}
delivery-board-demo.tsx
import { DeliveryBoard, type DeliveryBoardData } from '@/components/delivery-board'

// Example data only. All names are invented.
const data: DeliveryBoardData = {
  clients: {
    title: 'Client progress',
    items: [
      { id: 'a', name: 'Sample Studio', progress: 72 },
      { id: 'b', name: 'Demo Works', progress: 45 },
      { id: 'c', name: 'Example Labs', progress: 18 },
    ],
  },
  days: ['2026-10-05', '2026-10-06', '2026-10-07', '2026-10-08', '2026-10-09', '2026-10-12'],
  roadmap: {
    title: 'Roadmap',
    lanes: [
      { id: 'next', title: 'Next', items: [{ id: 'i1', title: 'Sample brief', owners: ['Alex Example'] }, { id: 'i2', title: 'Demo kickoff', owners: ['Sam Sample', 'Robin Demo'] }] },
      { id: 'doing', title: 'In progress', items: [{ id: 'i3', title: 'Example prototype', owners: ['Robin Demo'] }] },
      { id: 'done', title: 'Delivered', items: [{ id: 'i4', title: 'Test handover', owners: ['Alex Example', 'Sam Sample'] }] },
    ],
  },
  assistant: { title: 'Assistant', text: 'Example tile: ask for a status summary.', action: 'Summarise' },
}

export default function DeliveryBoardDemo() {
  return <DeliveryBoard data={data} now={new Date('2026-10-06T10:00:00')} />
}

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/delivery-board.json

Abhängigkeiten

    Dateien

    • delivery-board.tsx
    • delivery-board.html

    Manniche Lab · Open Source

    Kostenlose Werkzeuge für KI-Agenten und Webentwicklung.