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.
This commit is contained in:
132
frontend/src/lib/components/layout/Sidebar.svelte
Normal file
132
frontend/src/lib/components/layout/Sidebar.svelte
Normal file
@@ -0,0 +1,132 @@
|
||||
<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 {
|
||||
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 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);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if ($sidebarState === 'expanded') {
|
||||
if (window.innerWidth < 768) {
|
||||
setSidebarState('hidden');
|
||||
} else if (window.innerWidth < 1280) {
|
||||
setSidebarState('collapsed');
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user