Mikkel Manniche
Dansk English Deutsch

Template · React · HTML

Workspace home

A workspace start page in Tiles: a pill navbar, a large search, an inverted offer tile, deal bars and the week's schedule.

Preview

React

Install the template with shadcn. It lands in your project as ordinary components that you edit yourself.

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

The whole page as one file with all CSS built in and no JavaScript. Toggles, tabs and filters work with HTML and CSS alone.

Open in a new tab

The colour in your own app

If you already have shadcn set up, these variables give the same colour. Light in :root, dark 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);
}

Install

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/workspace-home.json

Dependencies

Files

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

Manniche Lab · open source

Free tools for AI agents and web development.