Files
headroom/openspec/changes/archive/2026-02-18-p01-ui-foundation/design.md
Santhosh Janardhanan 493cb78173 feat(layout): finalize p01 and p02 changes
Complete UI foundation and app layout implementation, stabilize container health checks, and archive both OpenSpec changes after verification.
2026-02-18 16:12:11 -05:00

7.1 KiB

Design: UI Foundation

Icon Library: Lucide Svelte

Installation

npm install lucide-svelte

Usage Pattern

<script>
  import { Menu, X, Sun, Moon, Home, Users } from 'lucide-svelte';
</script>

<Menu size={20} />
<Home size={18} class="text-base-content" />

Icon Mapping for Navigation

Nav Item Lucide Icon
Dashboard LayoutDashboard
Team Members Users
Projects Folder
Allocations Calendar
Actuals CheckCircle
Forecast TrendingUp
Utilization BarChart3
Costs DollarSign
Variance AlertTriangle
Settings Settings
Master Data Database

Types

src/lib/types/layout.ts

export type SidebarState = 'expanded' | 'collapsed' | 'hidden';

export interface NavItem {
  label: string;
  href: string;
  icon: string; // Lucide icon name
  badge?: string | number; // Optional notification badge
}

export interface NavSection {
  title: string;
  items: NavItem[];
  roles?: string[]; // If set, only visible to these roles
}

export type Theme = 'light' | 'dark';

Stores

src/lib/stores/layout.ts

import { writable } from 'svelte/store';
import { browser } from '$app/environment';
import type { SidebarState, Theme } from '$lib/types/layout';

const DEFAULT_SIDEBAR_STATE: SidebarState = 'expanded';
const DEFAULT_THEME: Theme = 'light';

function createLayoutStore() {
  // Initialize from localStorage or defaults
  const getInitialState = (): SidebarState => {
    if (!browser) return DEFAULT_SIDEBAR_STATE;
    const stored = localStorage.getItem('headroom_sidebar_state');
    return (stored as SidebarState) || DEFAULT_SIDEBAR_STATE;
  };

  const getInitialTheme = (): Theme => {
    if (!browser) return DEFAULT_THEME;
    const stored = localStorage.getItem('headroom_theme');
    if (stored) return stored as Theme;
    // Respect system preference on first visit
    if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      return 'dark';
    }
    return DEFAULT_THEME;
  };

  const sidebarState = writable<SidebarState>(getInitialState());
  const theme = writable<Theme>(getInitialTheme());

  // Apply theme to document
  if (browser) {
    theme.subscribe((value) => {
      document.documentElement.setAttribute('data-theme', value);
      localStorage.setItem('headroom_theme', value);
    });

    sidebarState.subscribe((value) => {
      localStorage.setItem('headroom_sidebar_state', value);
    });
  }

  return {
    sidebarState: { subscribe: sidebarState.subscribe },
    theme: { subscribe: theme.subscribe },
    toggleSidebar: () => {
      sidebarState.update((current) => {
        if (current === 'expanded') return 'collapsed';
        if (current === 'collapsed') return 'hidden';
        return 'expanded';
      });
    },
    setSidebarState: (state: SidebarState) => sidebarState.set(state),
    toggleTheme: () => {
      theme.update((current) => (current === 'light' ? 'dark' : 'light'));
    },
    setTheme: (newTheme: Theme) => theme.set(newTheme),
  };
}

export const layoutStore = createLayoutStore();

src/lib/stores/period.ts

import { writable, derived } from 'svelte/store';
import { browser } from '$app/environment';

function createPeriodStore() {
  const getCurrentMonth = (): string => {
    const now = new Date();
    return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
  };

  const getInitialPeriod = (): string => {
    if (!browser) return getCurrentMonth();
    const stored = localStorage.getItem('headroom_selected_period');
    return stored || getCurrentMonth();
  };

  const selectedPeriod = writable<string>(getInitialPeriod());

  if (browser) {
    selectedPeriod.subscribe((value) => {
      localStorage.setItem('headroom_selected_period', value);
    });
  }

  // Derived values for convenience
  const selectedMonth = derived(selectedPeriod, ($period) => {
    const [year, month] = $period.split('-').map(Number);
    return { year, month };
  });

  const selectedDate = derived(selectedPeriod, ($period) => {
    const [year, month] = $period.split('-').map(Number);
    return new Date(year, month - 1, 1);
  });

  return {
    selectedPeriod: { subscribe: selectedPeriod.subscribe },
    selectedMonth,
    selectedDate,
    setPeriod: (period: string) => selectedPeriod.set(period),
    previousMonth: () => {
      selectedPeriod.update((current) => {
        const [year, month] = current.split('-').map(Number);
        const date = new Date(year, month - 2, 1);
        return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
      });
    },
    nextMonth: () => {
      selectedPeriod.update((current) => {
        const [year, month] = current.split('-').map(Number);
        const date = new Date(year, month, 1);
        return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
      });
    },
    currentMonth: () => selectedPeriod.set(getCurrentMonth()),
  };
}

export const periodStore = createPeriodStore();

Navigation Configuration

src/lib/config/navigation.ts

import type { NavSection } from '$lib/types/layout';

export const navigationSections: NavSection[] = [
  {
    title: 'PLANNING',
    items: [
      { label: 'Dashboard', href: '/dashboard', icon: 'LayoutDashboard' },
      { label: 'Team Members', href: '/team-members', icon: 'Users' },
      { label: 'Projects', href: '/projects', icon: 'Folder' },
      { label: 'Allocations', href: '/allocations', icon: 'Calendar' },
      { label: 'Actuals', href: '/actuals', icon: 'CheckCircle' },
    ],
  },
  {
    title: 'REPORTS',
    items: [
      { label: 'Forecast', href: '/reports/forecast', icon: 'TrendingUp' },
      { label: 'Utilization', href: '/reports/utilization', icon: 'BarChart3' },
      { label: 'Costs', href: '/reports/costs', icon: 'DollarSign' },
      { label: 'Variance', href: '/reports/variance', icon: 'AlertTriangle' },
      { label: 'Allocation Matrix', href: '/reports/allocation', icon: 'Grid3X3' },
    ],
  },
  {
    title: 'ADMIN',
    roles: ['superuser'],
    items: [
      { label: 'Settings', href: '/settings', icon: 'Settings' },
      { label: 'Master Data', href: '/master-data', icon: 'Database' },
    ],
  },
];

Theme CSS

Update src/app.css

@import 'tailwindcss';
@import 'daisyui';

/* Theme variables - optional customization */
:root {
  --sidebar-width-expanded: 240px;
  --sidebar-width-collapsed: 64px;
  --topbar-height: 56px;
}

/* Ensure theme attribute works */
[data-theme='light'] {
  color-scheme: light;
}

[data-theme='dark'] {
  color-scheme: dark;
}

File Structure

src/lib/
├── types/
│   └── layout.ts          # NEW
├── stores/
│   ├── layout.ts          # NEW
│   ├── period.ts          # NEW
│   └── auth.ts            # EXISTS
├── config/
│   └── navigation.ts      # NEW
└── ...