Files
headroom/frontend/src/lib/components/layout/Sidebar.svelte
Santhosh Janardhanan 96f1d0a6e5 feat(dashboard): Enhance dashboard with PageHeader, StatCard, and auth fixes
- Create PageHeader component with title, description, and action slots
- Create StatCard component with trend indicators and icons
- Update dashboard with KPI cards, Quick Actions, and Allocation Preview
- Polish login page with branding and centered layout
- Fix auth redirect: authenticated users accessing /login go to dashboard
- Fix page refresh: auth state persists, no blank page
- Fix sidebar: visible after login, toggle works, state persists
- Fix CSS import: add app.css to layout, fix DaisyUI import path
- Fix breadcrumbs: home icon links to /dashboard
- Add comprehensive E2E and unit tests

Refs: openspec/changes/p03-dashboard-enhancement
Closes: p03-dashboard-enhancement
2026-02-18 18:14:57 -05:00

144 lines
4.1 KiB
Svelte

<script context="module" lang="ts">
import type { NavSection } from '$lib/types/layout';
export function isSectionVisible(section: NavSection, role?: string | null): boolean {
if (!section.roles?.length) return true;
if (!role) return false;
return section.roles.includes(role);
}
</script>
<script lang="ts">
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import {
Moon,
PanelLeftClose,
PanelLeftOpen,
PanelRightOpen,
Sun
} from 'lucide-svelte';
import { navigationSections } from '$lib/config/navigation';
import {
setSidebarState,
sidebarState,
theme,
toggleSidebar,
toggleTheme
} from '$lib/stores/layout';
import { user } from '$lib/stores/auth';
import type { SidebarState } from '$lib/types/layout';
import SidebarSection from './SidebarSection.svelte';
$: isExpanded = $sidebarState === 'expanded';
$: isCollapsed = $sidebarState === 'collapsed';
$: isHidden = $sidebarState === 'hidden';
$: isDrawerOpen = $sidebarState !== 'hidden';
$: visibleSections = navigationSections.filter((section) => isSectionVisible(section, $user?.role));
function toggleWithShortcut(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) && event.key === '\\') {
event.preventDefault();
toggleSidebar();
}
}
$: if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-sidebar', $sidebarState);
}
function getSidebarStateForWidth(width: number): SidebarState {
if (width < 768) return 'hidden';
if (width < 1280) return 'collapsed';
return 'expanded';
}
onMount(() => {
if (typeof window === 'undefined') return;
const currentState = get(sidebarState);
const desiredState = getSidebarStateForWidth(window.innerWidth);
if (window.innerWidth < 768 && currentState !== 'hidden') {
setSidebarState('hidden');
} else if (currentState === 'hidden' && window.innerWidth >= 768) {
setSidebarState(desiredState);
}
window.addEventListener('keydown', toggleWithShortcut);
return () => window.removeEventListener('keydown', toggleWithShortcut);
});
</script>
<div
class="mobile-backdrop fixed inset-0 z-20 bg-black/40"
class:hidden={!isDrawerOpen}
aria-hidden={!isDrawerOpen}
on:click={() => setSidebarState('hidden')}
data-testid="sidebar-backdrop"
></div>
<aside
class="fixed left-0 top-0 z-30 flex h-full flex-col border-r border-base-300 bg-base-200 transition-all duration-200 ease-out"
class:w-60={isExpanded}
class:w-16={isCollapsed}
class:w-0={isHidden}
class:overflow-hidden={isHidden}
class:-translate-x-full={isHidden}
class:translate-x-0={!isHidden}
data-testid="sidebar"
>
<div class="flex h-14 items-center justify-between border-b border-base-300 px-3">
{#if isExpanded}
<a href="/dashboard" class="text-lg font-bold">Headroom</a>
{/if}
<button class="btn btn-ghost btn-sm btn-circle" on:click={toggleSidebar} aria-label="Toggle sidebar" data-testid="sidebar-toggle">
{#if isExpanded}
<PanelLeftClose size={18} />
{:else if isCollapsed}
<PanelLeftOpen size={18} />
{:else}
<PanelRightOpen size={18} />
{/if}
</button>
</div>
<nav class="flex-1 overflow-y-auto py-2">
{#each visibleSections as section (section.title)}
<SidebarSection {section} expanded={isExpanded} />
{/each}
</nav>
<div class="border-t border-base-300 p-3">
<button class="btn btn-ghost btn-sm w-full justify-start gap-2" class:justify-center={!isExpanded} on:click={toggleTheme} aria-label="Toggle theme" data-testid="theme-toggle">
{#if $theme === 'light'}
<Moon size={16} />
{#if isExpanded}<span>Dark Mode</span>{/if}
{:else}
<Sun size={16} />
{#if isExpanded}<span>Light Mode</span>{/if}
{/if}
</button>
</div>
</aside>
<style>
.mobile-backdrop {
display: block;
}
@media (max-width: 767px) {
aside {
width: 15rem !important;
}
}
@media (min-width: 768px) {
.mobile-backdrop {
display: none;
}
}
</style>