b5166d9768
- Replace manual click-outside menu with DropdownMenu from bits-ui - Add collapsible boards section in sidebar with chevron toggle - Add max-height scroll for boards list
300 lines
9.2 KiB
Svelte
300 lines
9.2 KiB
Svelte
<script lang="ts">
|
|
import { t } from 'svelte-i18n';
|
|
import { ui } from '$lib/stores/ui.svelte.js';
|
|
import { keyboard } from '$lib/stores/keyboard.svelte.js';
|
|
import { page } from '$app/stores';
|
|
import DynamicIcon from '$lib/components/ui/DynamicIcon.svelte';
|
|
|
|
interface BoardLink {
|
|
id: string;
|
|
name: string;
|
|
icon: string | null;
|
|
}
|
|
|
|
interface Props {
|
|
boards: BoardLink[];
|
|
isAdmin: boolean;
|
|
collapsed: boolean;
|
|
}
|
|
|
|
let { boards, isAdmin, collapsed }: Props = $props();
|
|
|
|
let boardsExpanded = $state(true);
|
|
|
|
function isActive(path: string): boolean {
|
|
return $page.url.pathname.startsWith(path);
|
|
}
|
|
</script>
|
|
|
|
<aside
|
|
class="flex h-full flex-col border-r border-sidebar-border bg-sidebar transition-all duration-200"
|
|
class:w-64={!collapsed}
|
|
class:w-16={collapsed}
|
|
>
|
|
<!-- Brand -->
|
|
<div class="flex h-14 items-center border-b border-sidebar-border px-4">
|
|
{#if !collapsed}
|
|
<a href="/" class="flex items-center gap-2 text-sidebar-foreground">
|
|
<svg
|
|
class="h-6 w-6 text-sidebar-primary"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<rect x="3" y="3" width="7" height="7" />
|
|
<rect x="14" y="3" width="7" height="7" />
|
|
<rect x="14" y="14" width="7" height="7" />
|
|
<rect x="3" y="14" width="7" height="7" />
|
|
</svg>
|
|
<span class="text-sm font-semibold">{$t('app_name')}</span>
|
|
</a>
|
|
{:else}
|
|
<a href="/" class="mx-auto text-sidebar-primary" title={$t('app_name')}>
|
|
<svg
|
|
class="h-6 w-6"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<rect x="3" y="3" width="7" height="7" />
|
|
<rect x="14" y="3" width="7" height="7" />
|
|
<rect x="14" y="14" width="7" height="7" />
|
|
<rect x="3" y="14" width="7" height="7" />
|
|
</svg>
|
|
</a>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Navigation -->
|
|
<nav class="flex-1 overflow-y-auto px-2 py-3">
|
|
<!-- Main Links -->
|
|
<div class="mb-3">
|
|
{#if !collapsed}
|
|
<p class="mb-1 px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50">
|
|
{$t('nav.navigation')}
|
|
</p>
|
|
{/if}
|
|
|
|
<a
|
|
href="/boards"
|
|
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/boards')
|
|
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
|
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
|
|
title={collapsed ? $t('nav.boards') : undefined}
|
|
>
|
|
<svg
|
|
class="h-4 w-4 shrink-0"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
<line x1="3" y1="9" x2="21" y2="9" />
|
|
<line x1="9" y1="21" x2="9" y2="9" />
|
|
</svg>
|
|
{#if !collapsed}<span>{$t('nav.boards')}</span>{/if}
|
|
</a>
|
|
|
|
<a
|
|
href="/apps"
|
|
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/apps')
|
|
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
|
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
|
|
title={collapsed ? $t('nav.apps') : undefined}
|
|
>
|
|
<svg
|
|
class="h-4 w-4 shrink-0"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<line x1="2" y1="12" x2="22" y2="12" />
|
|
<path
|
|
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
|
|
/>
|
|
</svg>
|
|
{#if !collapsed}<span>{$t('nav.apps')}</span>{/if}
|
|
</a>
|
|
|
|
<a
|
|
href="/status"
|
|
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/status')
|
|
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
|
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
|
|
title={collapsed ? 'Status Page' : undefined}
|
|
>
|
|
<svg
|
|
class="h-4 w-4 shrink-0"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
|
</svg>
|
|
{#if !collapsed}<span>Status</span>{/if}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Board List -->
|
|
{#if boards.length > 0}
|
|
<div class="mb-3">
|
|
{#if !collapsed}
|
|
<button
|
|
type="button"
|
|
onclick={() => (boardsExpanded = !boardsExpanded)}
|
|
class="mb-1 flex w-full items-center justify-between px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50 transition-colors hover:text-sidebar-foreground/80"
|
|
>
|
|
<span>{$t('nav.boards')}</span>
|
|
<svg
|
|
class="h-3 w-3 transition-transform duration-200"
|
|
class:rotate-180={boardsExpanded}
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<polyline points="6 9 12 15 18 9" />
|
|
</svg>
|
|
</button>
|
|
{/if}
|
|
|
|
{#if boardsExpanded || collapsed}
|
|
<div class="max-h-48 overflow-y-auto">
|
|
{#each boards as board (board.id)}
|
|
<a
|
|
href="/boards/{board.id}"
|
|
class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors {isActive(`/boards/${board.id}`)
|
|
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
|
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
|
|
title={collapsed ? board.name : undefined}
|
|
onclick={() => ui.closeMobileSidebar()}
|
|
>
|
|
{#if board.icon}
|
|
<span class="shrink-0"><DynamicIcon name={board.icon} size={18} /></span>
|
|
{:else}
|
|
<span
|
|
class="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-sidebar-accent text-[10px] font-medium text-sidebar-foreground"
|
|
>
|
|
{board.name.charAt(0).toUpperCase()}
|
|
</span>
|
|
{/if}
|
|
{#if !collapsed}
|
|
<span class="truncate">{board.name}</span>
|
|
{/if}
|
|
</a>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Admin -->
|
|
{#if isAdmin}
|
|
<div class="mt-auto border-t border-sidebar-border pt-3">
|
|
{#if !collapsed}
|
|
<p
|
|
class="mb-1 px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50"
|
|
>
|
|
{$t('nav.admin')}
|
|
</p>
|
|
{/if}
|
|
|
|
<a
|
|
href="/admin/users"
|
|
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/admin')
|
|
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
|
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
|
|
title={collapsed ? $t('nav.admin_panel') : undefined}
|
|
>
|
|
<svg
|
|
class="h-4 w-4 shrink-0"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<path
|
|
d="M12.22 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2h.44a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0l-.43-.25a2 2 0 01-1-1.73V4a2 2 0 00-2-2z"
|
|
/>
|
|
<circle cx="12" cy="12" r="3" />
|
|
</svg>
|
|
{#if !collapsed}<span>{$t('nav.admin_panel')}</span>{/if}
|
|
</a>
|
|
</div>
|
|
{/if}
|
|
</nav>
|
|
|
|
<!-- Keyboard Shortcuts Hint + Collapse Toggle -->
|
|
{#if !ui.isMobile}
|
|
<div class="border-t border-sidebar-border p-2 flex items-center {collapsed ? 'flex-col gap-1' : 'gap-1'}">
|
|
<button
|
|
type="button"
|
|
onclick={() => keyboard.toggleOverlay()}
|
|
class="flex items-center justify-center rounded-md p-2 text-sidebar-foreground/50 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
|
title="Keyboard Shortcuts (?)"
|
|
>
|
|
<svg
|
|
class="h-4 w-4"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onclick={() => ui.toggleSidebar()}
|
|
class="flex w-full items-center justify-center rounded-md p-2 text-sidebar-foreground transition-colors hover:bg-sidebar-accent"
|
|
title={collapsed ? $t('sidebar.expand') : $t('sidebar.collapse')}
|
|
>
|
|
<svg
|
|
class="h-4 w-4 transition-transform duration-200"
|
|
class:rotate-180={collapsed}
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<polyline points="15 18 9 12 15 6" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</aside>
|