Mikkel Manniche
Dansk English Deutsch

Vorlage · React · HTML

Workspace home

Eine Startseite für einen Arbeitsbereich in Tiles: Pill-Navigation, eine große Suche, eine dunkle Angebotskachel, Deal-Balken und der Wochenplan.

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/workspace-home.json
npx shadcn@latest add @manniche/workspace-home
workspace-home.tsx
// A workspace home screen (pill navbar, large search, offer tile, deals bars, week schedule).
import { Search } from 'lucide-react'
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { BarChart, type BarPoint } from '@/registry/manniche/bar-chart/bar-chart'
import { BigNumber } from '@/registry/manniche/chart-kit/chart-kit'
import { DataTile, TileFact } from '@/registry/manniche/data-tile/data-tile'
import { WeekSchedule, type ScheduleCategory, type ScheduleEvent } from '@/registry/manniche/week-schedule/week-schedule'

export type WorkspaceHomeData = {
  nav: { id: string; label: string }[]
  offer: { title: string; text: string; action: string }
  schedule: { title: string; weekStart: string; now?: string; events: ScheduleEvent[]; categories?: ScheduleCategory[] }
  deals: { title: string; total: number; points: BarPoint[]; currency?: string }
}

export type WorkspaceHomeLabels = { search?: string; searchLabel?: string; navLabel?: string; note?: string }

export type WorkspaceHomeProps = {
  data: WorkspaceHomeData
  labels?: WorkspaceHomeLabels
  onSearch?: (query: string) => void
  onOffer?: () => void
  className?: string
}

export function WorkspaceHome({ data, labels, onSearch, onOffer, className }: WorkspaceHomeProps) {
  const t = { search: 'Search the workspace', searchLabel: 'Search', navLabel: 'Workspace', note: 'Example data.', ...labels }
  const [current, setCurrent] = useState(data.nav[0]?.id)
  const [q, setQ] = useState('')
  const money = { currency: data.deals.currency ?? 'EUR', decimals: 0 }
  return (
    <div className={cn('@container flex w-full flex-col gap-3 rounded-2xl bg-background p-3 text-foreground', className)}>
      <nav aria-label={t.navLabel} className="flex gap-1 self-start overflow-x-auto rounded-full border border-border bg-card p-1">
        {data.nav.map((n) => (
          <button
            key={n.id}
            type="button"
            aria-current={n.id === current ? 'page' : undefined}
            onClick={() => setCurrent(n.id)}
            className={cn(
              'rounded-full px-4 py-1.5 text-sm text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring',
              n.id === current && 'bg-foreground text-background hover:text-background',
            )}
          >
            {n.label}
          </button>
        ))}
      </nav>
      <form
        role="search"
        onSubmit={(e) => {
          e.preventDefault()
          onSearch?.(q)
        }}
        className="flex items-center gap-3 rounded-2xl border border-border bg-card px-5 py-4 focus-within:outline-2 focus-within:outline-ring"
      >
        <Search className="size-5 text-muted-foreground" aria-hidden />
        <input
          type="search"
          value={q}
          onChange={(e) => setQ(e.target.value)}
          aria-label={t.searchLabel}
          placeholder={t.search}
          className="min-w-0 flex-1 bg-transparent text-lg outline-none placeholder:text-muted-foreground"
        />
      </form>
      <div className="grid grid-cols-1 gap-3 @2xl:grid-cols-5">
        <div className="@2xl:col-span-2">
          <DataTile className="h-full" title={data.offer.title} inverted>
            <p className="text-sm opacity-80">{data.offer.text}</p>
            <button
              type="button"
              onClick={onOffer}
              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.offer.action}
            </button>
          </DataTile>
        </div>
        <div className="@2xl:col-span-3">
          <DataTile className="h-full" title={data.deals.title} footer={t.note}>
            <TileFact>
              <BigNumber value={data.deals.total} format={money} size="lg" />
            </TileFact>
            <BarChart data={data.deals.points} label={data.deals.title} format={money} />
          </DataTile>
        </div>
        <div className="@2xl:col-span-5">
          <DataTile className="h-full" title={data.schedule.title} footer={t.note}>
            <WeekSchedule
              data={data.schedule.events}
              label={data.schedule.title}
              weekStart={data.schedule.weekStart}
              now={data.schedule.now}
              categories={data.schedule.categories}
            />
          </DataTile>
        </div>
      </div>
    </div>
  )
}
workspace-home-demo.tsx
import { WorkspaceHome, type WorkspaceHomeData } from '@/components/workspace-home'

// Example data only. All names and figures are invented.
const data: WorkspaceHomeData = {
  nav: [
    { id: 'home', label: 'Home' },
    { id: 'deals', label: 'Deals' },
    { id: 'people', label: 'People' },
    { id: 'files', label: 'Files' },
  ],
  offer: { title: 'Sample offer', text: 'Example tile: a short offer with one clear action.', action: 'See the offer' },
  schedule: {
    title: 'This week',
    weekStart: '2026-10-05',
    now: '2026-10-06T10:00',
    categories: [
      { id: 'call', label: 'Calls' },
      { id: 'focus', label: 'Focus' },
    ],
    events: [
      { id: 'e1', title: 'Sample kickoff', day: 0, start: '09:00', end: '10:00', category: 'call' },
      { id: 'e2', title: 'Demo review', day: 1, start: '11:00', end: '12:00', category: 'call' },
      { id: 'e3', title: 'Example focus block', day: 2, start: '13:00', end: '15:00', category: 'focus' },
      { id: 'e4', title: 'Test planning', day: 3, start: '10:00', end: '11:30', category: 'focus' },
    ],
  },
  deals: {
    title: 'Deals closed per week',
    total: 84500,
    points: ['W1', 'W2', 'W3', 'W4', 'W5', 'W6'].map((label, i) => ({ label, value: [9800, 14200, 12100, 17600, 15300, 15500][i] })),
  },
}

export default function WorkspaceHomeDemo() {
  return <WorkspaceHome 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/workspace-home.json

Abhängigkeiten

Dateien

  • workspace-home.tsx
  • workspace-home.html

Manniche Lab · Open Source

Kostenlose Werkzeuge für KI-Agenten und Webentwicklung.