Mikkel Manniche
Dansk English Deutsch

Skabelon · React · HTML

Workspace home

En startside til et arbejdsrum i Tiles: navbar med piller, en stor søgning, en mørk tilbudsflise, søjler med aftaler og ugens skema.

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/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

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/workspace-home.json

Afhængigheder

Filer

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

Manniche Lab · open source

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