docs(ui): Add UI layout refactor plan and OpenSpec changes
- Update decision-log with UI layout decisions (Feb 18, 2026) - Update architecture with frontend layout patterns - Update config.yaml with TDD, documentation, UI standards rules - Create p00-api-documentation change (Scribe annotations) - Create p01-ui-foundation change (types, stores, Lucide) - Create p02-app-layout change (AppLayout, Sidebar, TopBar) - Create p03-dashboard-enhancement change (PageHeader, StatCard) - Create p04-content-patterns change (DataTable, FilterBar) - Create p05-page-migrations change (page migrations) - Fix E2E auth tests (11/11 passing) - Add JWT expiry validation to dashboard guard
This commit is contained in:
269
openspec/changes/p01-ui-foundation/design.md
Normal file
269
openspec/changes/p01-ui-foundation/design.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# Design: UI Foundation
|
||||
|
||||
## Icon Library: Lucide Svelte
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
npm install lucide-svelte
|
||||
```
|
||||
|
||||
### Usage Pattern
|
||||
```svelte
|
||||
<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`
|
||||
```typescript
|
||||
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`
|
||||
```typescript
|
||||
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`
|
||||
```typescript
|
||||
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`
|
||||
```typescript
|
||||
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`
|
||||
```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
|
||||
└── ...
|
||||
```
|
||||
66
openspec/changes/p01-ui-foundation/proposal.md
Normal file
66
openspec/changes/p01-ui-foundation/proposal.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Proposal: UI Foundation
|
||||
|
||||
## Overview
|
||||
Establish the foundational UI building blocks for Headroom's layout system. This includes types, stores, icon library, and theme configuration that all subsequent UI changes will depend on.
|
||||
|
||||
## Goals
|
||||
- Install and configure Lucide icons for Svelte
|
||||
- Create layout-related TypeScript types
|
||||
- Create layout/period stores for state management
|
||||
- Set up theme system (light/dark mode with persistence)
|
||||
- Define navigation configuration structure
|
||||
|
||||
## Non-Goals
|
||||
- Creating visual components (done in p02-app-layout)
|
||||
- Building actual pages (done in p03+)
|
||||
- API documentation (done in p00-api-documentation)
|
||||
|
||||
## Priority
|
||||
**HIGH** - Foundation for all UI changes (p02-p05 depend on this)
|
||||
|
||||
## Scope
|
||||
|
||||
### Icon Library
|
||||
- Install `lucide-svelte` package
|
||||
- Create icon usage patterns/documentation
|
||||
- Replace existing inline SVGs where applicable
|
||||
|
||||
### Types
|
||||
- `SidebarState` - 'expanded' | 'collapsed' | 'hidden'
|
||||
- `NavItem` - label, href, icon, roles
|
||||
- `NavSection` - title, items, roles (for role-based visibility)
|
||||
|
||||
### Stores
|
||||
- `layoutStore` - sidebar state, theme preference
|
||||
- `periodStore` - global month/period selection
|
||||
|
||||
### Theme System
|
||||
- DaisyUI theme switching via `data-theme` attribute
|
||||
- Light mode default ("light" theme)
|
||||
- Dark mode option ("dark" theme)
|
||||
- Persistence to localStorage
|
||||
- Respect system preference on first visit
|
||||
|
||||
### Navigation Configuration
|
||||
- Centralized navigation structure
|
||||
- Role-based visibility for admin section
|
||||
|
||||
## Success Criteria
|
||||
- [ ] Lucide icons installed and working
|
||||
- [ ] Types defined and exported
|
||||
- [ ] Stores created with localStorage persistence
|
||||
- [ ] Theme toggle functional
|
||||
- [ ] Navigation config exported
|
||||
- [ ] All tests pass
|
||||
|
||||
## Estimated Effort
|
||||
1-2 hours
|
||||
|
||||
## Dependencies
|
||||
- None (foundation change)
|
||||
|
||||
## Blocks
|
||||
- p02-app-layout
|
||||
- p03-dashboard-enhancement
|
||||
- p04-content-patterns
|
||||
- p05-page-migrations
|
||||
86
openspec/changes/p01-ui-foundation/tasks.md
Normal file
86
openspec/changes/p01-ui-foundation/tasks.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Tasks: UI Foundation
|
||||
|
||||
## Phase 1: Install Dependencies
|
||||
|
||||
- [ ] 1.1 Install Lucide icons: `npm install lucide-svelte`
|
||||
- [ ] 1.2 Verify installation in package.json
|
||||
- [ ] 1.3 Test import in a test file: `import { Menu } from 'lucide-svelte'`
|
||||
|
||||
## Phase 2: Create Types
|
||||
|
||||
- [ ] 1.4 Create `src/lib/types/` directory if not exists
|
||||
- [ ] 1.5 Create `src/lib/types/layout.ts`
|
||||
- [ ] 1.6 Define `SidebarState` type
|
||||
- [ ] 1.7 Define `NavItem` interface
|
||||
- [ ] 1.8 Define `NavSection` interface
|
||||
- [ ] 1.9 Define `Theme` type
|
||||
- [ ] 1.10 Export all types
|
||||
|
||||
## Phase 3: Create Stores
|
||||
|
||||
### Layout Store
|
||||
- [ ] 1.11 Create `src/lib/stores/layout.ts`
|
||||
- [ ] 1.12 Implement `sidebarState` writable with localStorage persistence
|
||||
- [ ] 1.13 Implement `theme` writable with localStorage persistence
|
||||
- [ ] 1.14 Implement `toggleSidebar()` function
|
||||
- [ ] 1.15 Implement `setSidebarState()` function
|
||||
- [ ] 1.16 Implement `toggleTheme()` function
|
||||
- [ ] 1.17 Implement `setTheme()` function
|
||||
- [ ] 1.18 Add system preference detection for initial theme
|
||||
|
||||
### Period Store
|
||||
- [ ] 1.19 Create `src/lib/stores/period.ts`
|
||||
- [ ] 1.20 Implement `selectedPeriod` writable with localStorage persistence
|
||||
- [ ] 1.21 Create `selectedMonth` derived store
|
||||
- [ ] 1.22 Create `selectedDate` derived store
|
||||
- [ ] 1.23 Implement `setPeriod()` function
|
||||
- [ ] 1.24 Implement `previousMonth()` function
|
||||
- [ ] 1.25 Implement `nextMonth()` function
|
||||
- [ ] 1.26 Implement `currentMonth()` function
|
||||
|
||||
## Phase 4: Create Navigation Config
|
||||
|
||||
- [ ] 1.27 Create `src/lib/config/` directory if not exists
|
||||
- [ ] 1.28 Create `src/lib/config/navigation.ts`
|
||||
- [ ] 1.29 Define PLANNING section (Dashboard, Team, Projects, Allocations, Actuals)
|
||||
- [ ] 1.30 Define REPORTS section (Forecast, Utilization, Costs, Variance, Allocation Matrix)
|
||||
- [ ] 1.31 Define ADMIN section with `roles: ['superuser']`
|
||||
- [ ] 1.32 Export `navigationSections` array
|
||||
|
||||
## Phase 5: Theme System
|
||||
|
||||
- [ ] 1.33 Update `src/app.css` with theme CSS variables
|
||||
- [ ] 1.34 Add sidebar width CSS variables
|
||||
- [ ] 1.35 Add theme color-scheme definitions
|
||||
- [ ] 1.36 Test theme switching in browser console
|
||||
|
||||
## Phase 6: Testing
|
||||
|
||||
### Unit Tests
|
||||
- [ ] 1.37 Write test: layoutStore initializes with default values
|
||||
- [ ] 1.38 Write test: layoutStore.toggleSidebar cycles through states
|
||||
- [ ] 1.39 Write test: layoutStore theme toggle works
|
||||
- [ ] 1.40 Write test: periodStore initializes with current month
|
||||
- [ ] 1.41 Write test: periodStore.previousMonth decrements correctly
|
||||
- [ ] 1.42 Write test: periodStore.nextMonth increments correctly
|
||||
- [ ] 1.43 Write test: navigationSections has correct structure
|
||||
|
||||
### Component Tests
|
||||
- [ ] 1.44 Create test: Lucide icon renders correctly
|
||||
|
||||
## Phase 7: Verification
|
||||
|
||||
- [ ] 1.45 Run `npm run check` - no type errors
|
||||
- [ ] 1.46 Run `npm run test:unit` - all tests pass
|
||||
- [ ] 1.47 Verify stores persist to localStorage
|
||||
- [ ] 1.48 Verify theme applies to document
|
||||
|
||||
## Commits
|
||||
|
||||
1. `feat(ui): Install lucide-svelte for icon library`
|
||||
2. `feat(ui): Add layout types (SidebarState, NavItem, NavSection, Theme)`
|
||||
3. `feat(ui): Create layoutStore for sidebar state and theme management`
|
||||
4. `feat(ui): Create periodStore for global month/period selection`
|
||||
5. `feat(ui): Add navigation configuration for sidebar menu`
|
||||
6. `feat(ui): Add CSS variables for theme and layout`
|
||||
7. `test(ui): Add unit tests for layout and period stores`
|
||||
Reference in New Issue
Block a user