Files
headroom/openspec/changes/archive/2026-02-18-p03-dashboard-enhancement/design.md
Santhosh Janardhanan 8e7bfbe517 feat(ui): Create content pattern components - DataTable, FilterBar, EmptyState, LoadingState
- Add LoadingState with table, card, list, and text skeleton patterns
- Add EmptyState with customizable icon, title, description, and action slot
- Add FilterBar with search input, clear button, and custom filter slot
- Add DataTable with TanStack Table integration, sorting, and row click
- Create barrel export index.ts for common components
- Install tanstack-table-8-svelte-5 for Svelte 5 compatibility
- Sync auth spec with authenticated user redirect requirements
- Archive p03-dashboard-enhancement

Refs: openspec/changes/p04-content-patterns
Closes: p04-content-patterns
2026-02-18 18:40:47 -05:00

5.7 KiB

Design: Dashboard Enhancement

PageHeader Component

src/lib/components/layout/PageHeader.svelte

<script lang="ts">
  import type { Snippet } from 'svelte';

  interface Props {
    title: string;
    description?: string;
    children?: Snippet; // Action buttons
  }

  let { title, description, children }: Props = $props();
</script>

<div class="page-header mb-6">
  <div class="flex items-start justify-between gap-4">
    <div>
      <h1 class="text-2xl font-bold text-base-content">{title}</h1>
      {#if description}
        <p class="text-base-content/70 mt-1">{description}</p>
      {/if}
    </div>
    {#if children}
      <div class="flex items-center gap-2">
        {@render children()}
      </div>
    {/if}
  </div>
</div>

StatCard Component

src/lib/components/common/StatCard.svelte

<script lang="ts">
  import type { Component } from 'svelte';
  import { TrendingUp, TrendingDown, Minus } from 'lucide-svelte';

  interface Props {
    title: string;
    value: string | number;
    description?: string;
    trend?: 'up' | 'down' | 'neutral';
    trendValue?: string;
    icon?: Component;
  }

  let { title, value, description, trend = 'neutral', trendValue, icon: Icon }: Props = $props();

  $derived trendColor = {
    up: 'text-success',
    down: 'text-error',
    neutral: 'text-base-content/50'
  }[trend];

  $derived TrendIcon = {
    up: TrendingUp,
    down: TrendingDown,
    neutral: Minus
  }[trend];
</script>

<div class="stat-card card bg-base-100 shadow-sm border border-base-300">
  <div class="card-body p-4">
    <div class="flex items-start justify-between">
      <div class="stat-title text-sm text-base-content/70">{title}</div>
      {#if Icon}
        <div class="text-base-content/50">
          <Icon size={20} />
        </div>
      {/if}
    </div>
    <div class="stat-value text-3xl font-bold mt-1">{value}</div>
    <div class="stat-desc flex items-center gap-1 mt-1">
      {#if trendValue}
        <span class={trendColor}>
          <TrendIcon size={14} />
        </span>
        <span class={trendColor}>{trendValue}</span>
      {/if}
      {#if description}
        <span class="text-base-content/50">{description}</span>
      {/if}
    </div>
  </div>
</div>

Enhanced Dashboard

src/routes/dashboard/+page.svelte

<script lang="ts">
  import PageHeader from '$lib/components/layout/PageHeader.svelte';
  import StatCard from '$lib/components/common/StatCard.svelte';
  import { authStore } from '$lib/stores/auth';
  import { periodStore } from '$lib/stores/period';
  import { 
    Folder, 
    Users, 
    Calendar, 
    BarChart3,
    Plus,
    ArrowRight
  } from 'lucide-svelte';

  // TODO: Fetch from API in future
  const stats = {
    activeProjects: 14,
    teamMembers: 8,
    allocationsThisMonth: 186,
    avgUtilization: 87
  };
</script>

<svelte:head>
  <title>Dashboard | Headroom</title>
</svelte:head>

<PageHeader 
  title="Dashboard" 
  description="Overview of your resource allocation"
>
  <button slot="actions" class="btn btn-primary btn-sm gap-2">
    <Plus size={16} />
    New Allocation
  </button>
</PageHeader>

<!-- KPI Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
  <StatCard 
    title="Active Projects" 
    value={stats.activeProjects}
    trend="up"
    trendValue="+2"
    description="from last month"
    icon={Folder}
  />
  <StatCard 
    title="Team Members" 
    value={stats.teamMembers}
    trend="neutral"
    description="active"
    icon={Users}
  />
  <StatCard 
    title="Allocations (hrs)" 
    value={stats.allocationsThisMonth}
    trend="down"
    trendValue="-12"
    description="vs capacity"
    icon={Calendar}
  />
  <StatCard 
    title="Avg Utilization" 
    value="{stats.avgUtilization}%"
    trend="up"
    trendValue="+5%"
    description="from last month"
    icon={BarChart3}
  />
</div>

<!-- Quick Links -->
<div class="card bg-base-100 shadow-sm border border-base-300 mb-6">
  <div class="card-body">
    <h2 class="card-title text-lg">Quick Actions</h2>
    <div class="grid grid-cols-2 md:grid-cols-4 gap-3 mt-2">
      <a href="/team-members" class="btn btn-ghost justify-start gap-2">
        <Users size={18} />
        Team
        <ArrowRight size={14} class="ml-auto opacity-50" />
      </a>
      <a href="/projects" class="btn btn-ghost justify-start gap-2">
        <Folder size={18} />
        Projects
        <ArrowRight size={14} class="ml-auto opacity-50" />
      </a>
      <a href="/allocations" class="btn btn-ghost justify-start gap-2">
        <Calendar size={18} />
        Allocate
        <ArrowRight size={14} class="ml-auto opacity-50" />
      </a>
      <a href="/reports/forecast" class="btn btn-ghost justify-start gap-2">
        <BarChart3 size={18} />
        Forecast
        <ArrowRight size={14} class="ml-auto opacity-50" />
      </a>
    </div>
  </div>
</div>

<!-- Placeholder for Allocation Preview -->
<div class="card bg-base-100 shadow-sm border border-base-300">
  <div class="card-body">
    <h2 class="card-title text-lg">Allocation Preview</h2>
    <p class="text-base-content/50 text-sm">
      Allocation matrix for {$periodStore.selectedPeriod} will appear here.
    </p>
    <div class="skeleton h-48 w-full mt-4"></div>
  </div>
</div>

Login Page Polish

Updates to src/routes/login/+page.svelte

  • Center vertically using flexbox
  • Add app logo/branding above form
  • Consistent card styling with new layout
  • Better error/success states

File Structure

src/lib/components/
├── layout/
│   └── PageHeader.svelte     # NEW
└── common/
    └── StatCard.svelte       # NEW

src/routes/
├── login/+page.svelte        # UPDATE
└── dashboard/+page.svelte    # UPDATE