133 lines
4.3 KiB
Svelte
133 lines
4.3 KiB
Svelte
<script lang="ts">
|
|
import './layout.css';
|
|
import { page } from '$app/state';
|
|
import { onMount, untrack } from 'svelte';
|
|
import * as m from '$lib/paraglide/messages.js';
|
|
import LanguageSwitcher from '$lib/shared/primitives/LanguageSwitcher.svelte';
|
|
import ThemeToggle from '$lib/shared/primitives/ThemeToggle.svelte';
|
|
import NotificationBell from '$lib/notification/NotificationBell.svelte';
|
|
import AppNav from './AppNav.svelte';
|
|
import UserMenu from './UserMenu.svelte';
|
|
import ConfirmDialog from '$lib/shared/primitives/ConfirmDialog.svelte';
|
|
import { provideConfirmService } from '$lib/shared/services/confirm.svelte.js';
|
|
import { bulkSelectionStore } from '$lib/document/bulkSelection.svelte';
|
|
|
|
let { children, data } = $props();
|
|
|
|
// Provide the confirmation service to the entire component tree.
|
|
// ConfirmDialog below reads it via getConfirmService() and renders the <dialog>.
|
|
provideConfirmService();
|
|
|
|
// Auto-clear the bulk-selection store when the user leaves the routes that
|
|
// surface the BulkSelectionBar. Without this the selection silently follows
|
|
// the user to /persons / /admin etc. and reappears as a stale 3-doc count
|
|
// when they wander back to /documents — Felix C4 on PR #331.
|
|
//
|
|
// `bulkSelectionStore.size` is read inside `untrack` so the effect only
|
|
// re-fires on route change, not on every checkbox toggle (Felix C3 cycle 3).
|
|
$effect(() => {
|
|
const path = page.url.pathname;
|
|
const inBulkContext =
|
|
path === '/documents' ||
|
|
path.startsWith('/documents/') ||
|
|
path === '/enrich' ||
|
|
path.startsWith('/enrich/');
|
|
if (!inBulkContext) {
|
|
untrack(() => {
|
|
if (bulkSelectionStore.size > 0) bulkSelectionStore.clear();
|
|
});
|
|
}
|
|
});
|
|
|
|
const isAdmin = $derived(
|
|
data?.user?.groups?.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN'))
|
|
);
|
|
|
|
// Set after client-side hydration completes. Used by E2E tests to know the
|
|
// page is interactive (event handlers registered) before they interact with it.
|
|
let hydrated = $state(false);
|
|
onMount(() => {
|
|
hydrated = true;
|
|
});
|
|
|
|
const isAuthPage = $derived(
|
|
['/login', '/register', '/forgot-password', '/reset-password'].some((p) =>
|
|
page.url.pathname.startsWith(p)
|
|
)
|
|
);
|
|
|
|
const userInitials = $derived.by(() => {
|
|
const first = data?.user?.firstName?.[0];
|
|
const last = data?.user?.lastName?.[0];
|
|
if (first && last) return (first + last).toUpperCase();
|
|
return null;
|
|
});
|
|
</script>
|
|
|
|
<div class="min-h-screen bg-canvas" data-hydrated={hydrated || undefined}>
|
|
{#if !isAuthPage}
|
|
<header class="sticky top-0 z-50 bg-header">
|
|
<div class="h-1 bg-accent"></div>
|
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
<div class="flex h-16 justify-between">
|
|
<!-- Logo & Nav -->
|
|
<AppNav isAdmin={isAdmin} />
|
|
|
|
<!-- Right Side -->
|
|
<div class="flex items-center gap-3">
|
|
{#if data?.user}
|
|
<a
|
|
href="/documents/new"
|
|
aria-label={m.upload_action()}
|
|
class="inline-flex items-center gap-2 rounded-sm border border-white/25 px-3.5 py-1.5 font-sans text-[11px] font-bold tracking-[.12em] text-white uppercase transition-colors hover:bg-white/10"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2.5"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
aria-hidden="true"
|
|
>
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
<polyline points="17 8 12 3 7 8" />
|
|
<line x1="12" y1="3" x2="12" y2="15" />
|
|
</svg>
|
|
<span class="hidden xl:inline">{m.upload_action()}</span>
|
|
</a>
|
|
{/if}
|
|
<!-- Language selector (desktop only — mobile lives in nav drawer) -->
|
|
<div
|
|
class="hidden items-center gap-1 pr-3 lg:flex [&_button]:px-1.5 [&_button]:py-1 [&_button]:text-xs"
|
|
>
|
|
<LanguageSwitcher inverted />
|
|
</div>
|
|
|
|
<!-- Theme toggle -->
|
|
<ThemeToggle />
|
|
|
|
<!-- Notification bell (authenticated users only) -->
|
|
{#if data?.user}
|
|
<NotificationBell />
|
|
{/if}
|
|
|
|
<!-- User menu -->
|
|
<UserMenu userInitials={userInitials} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
{/if}
|
|
|
|
<main class={isAuthPage ? '' : 'py-6'}>
|
|
{@render children()}
|
|
</main>
|
|
|
|
<!-- Shared confirmation dialog — used by getConfirmService() throughout the app -->
|
|
<ConfirmDialog />
|
|
</div>
|