Ka-Note/ka-note/client/src/routes/+layout.svelte

221 lines
6.1 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import "../app.css";
import Sidebar from "$lib/components/Sidebar.svelte";
import BottomTabBar from "$lib/components/BottomTabBar.svelte";
import AiLockBanner from "$lib/components/AiLockBanner.svelte";
import CommandBar from "$lib/components/CommandBar.svelte";
import { commandBarOpen } from "$lib/stores/commandBar";
import { sidebarCollapsed } from "$lib/stores/sidebarCollapsed";
import { seedIfEmpty } from "$lib/db/seed";
import { sync } from "$lib/sync/syncService";
import { refreshLockStatus } from "$lib/stores/aiLock";
import { onMount, onDestroy } from "svelte";
import { afterNavigate } from "$app/navigation";
import type { Snippet } from "svelte";
import {
handleRedirect,
login,
isAuthenticated,
} from "$lib/auth/authStore.js";
import { scopeColor } from "$lib/stores/scopeContext";
let { children }: { children: Snippet } = $props();
let sidebarOpen = $state(false);
let ready = $state(false);
let authReady = $state(false);
let authenticated = $state(false);
let syncInterval: ReturnType<typeof setInterval> | null = null;
let lockInterval: ReturnType<typeof setInterval> | null = null;
isAuthenticated.subscribe((v) => (authenticated = v));
function startSync() {
sync();
syncInterval = setInterval(sync, 30_000);
refreshLockStatus();
lockInterval = setInterval(refreshLockStatus, 30_000);
document.addEventListener("visibilitychange", onVisibilityChange);
}
function onVisibilityChange() {
if (document.visibilityState === "visible") {
sync();
refreshLockStatus();
}
}
onMount(async () => {
await handleRedirect();
authReady = true;
if (authenticated) {
await seedIfEmpty();
ready = true;
startSync();
}
});
onDestroy(() => {
if (syncInterval) clearInterval(syncInterval);
if (lockInterval) clearInterval(lockInterval);
document.removeEventListener("visibilitychange", onVisibilityChange);
});
$effect(() => {
if (authReady && authenticated && !ready) {
seedIfEmpty().then(() => {
ready = true;
startSync();
});
}
});
function toggleSidebar() {
sidebarOpen = !sidebarOpen;
}
function closeSidebar() {
sidebarOpen = false;
}
function handleGlobalKeydown(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
$commandBarOpen = !$commandBarOpen;
}
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
e.preventDefault();
sidebarCollapsed.toggle();
}
}
afterNavigate(({ to }) => {
closeSidebar();
if (to?.url.pathname.startsWith("/context/")) {
const id = to.url.pathname.split("/").pop();
if (id && id !== "daily-log") {
// Exclude daily-log from recents
try {
const stored = sessionStorage.getItem("recentContexts");
let recent = stored ? JSON.parse(stored) : [];
recent = [
id,
...recent.filter((x: string) => x !== id),
].slice(0, 5);
sessionStorage.setItem(
"recentContexts",
JSON.stringify(recent),
);
} catch (e) {
console.error("Failed to save recent context", e);
}
}
}
});
$effect(() => {
document.documentElement.style.setProperty(
"--scope-color",
$scopeColor,
);
});
</script>
<svelte:window onkeydown={handleGlobalKeydown} />
{#if !authReady}
<div class="flex h-screen items-center justify-center bg-bg">
<span class="text-muted">Loading...</span>
</div>
{:else if !authenticated}
<div class="flex h-screen items-center justify-center bg-bg">
<div class="text-center">
<h1 class="mb-4 text-2xl font-bold text-accent">KaNote</h1>
<p class="mb-6 text-muted">Sign in to access your notes</p>
<button
class="rounded bg-accent px-6 py-3 font-semibold text-white transition-colors hover:bg-accent/80"
onclick={() => login()}
>
Sign in with Microsoft
</button>
</div>
</div>
{:else if ready}
<div class="flex h-screen overflow-hidden">
<!-- Sidebar: fullscreen on mobile, static on desktop -->
{#if sidebarOpen}
<!-- Backdrop -->
<div
class="fixed inset-0 z-[59] bg-black/50 backdrop-blur-sm md:hidden"
onclick={closeSidebar}
></div>
<!-- Mobile: full-screen overlay -->
<div
class="fixed inset-0 z-[60] flex flex-col bg-sidebar md:hidden"
>
<!-- Header with close button -->
<div
class="flex items-center justify-between border-b border-border px-5 py-4 shrink-0"
style="padding-top: max(1rem, env(safe-area-inset-top));"
>
<span
class="text-lg font-bold text-accent uppercase tracking-wider"
>KaNote</span
>
<button
class="text-2xl text-muted hover:text-white leading-none"
onclick={closeSidebar}
aria-label="Close">&times;</button
>
</div>
<!-- Scrollable content -->
<div
class="flex-1 overflow-y-auto px-5 pb-8"
style="-webkit-overflow-scrolling: touch;"
>
<Sidebar onnavigate={closeSidebar} hideLogo />
</div>
</div>
{/if}
<!-- Desktop sidebar -->
<aside
class="hidden md:flex md:flex-col md:border-r md:border-border md:bg-sidebar md:overflow-y-auto transition-all duration-200
{$sidebarCollapsed ? 'md:w-0 md:p-0 md:overflow-hidden md:border-r-0' : 'md:w-[250px] md:p-5'}"
>
{#if !$sidebarCollapsed}
<Sidebar onnavigate={closeSidebar} />
{/if}
</aside>
<!-- Main content -->
<main class="flex-1 overflow-y-auto p-5 pt-safe md:pt-5 pb-20 md:pb-5">
<div class="mx-auto max-w-[900px]">
<!-- Desktop sidebar toggle button -->
<div class="hidden md:flex mb-3 -mt-1">
<button
onclick={() => sidebarCollapsed.toggle()}
class="text-muted hover:text-white transition-colors text-xs flex items-center gap-1 opacity-40 hover:opacity-100"
aria-label="Sidebar ein-/ausblenden"
title="Sidebar ein-/ausblenden (Ctrl+B)"
>
{$sidebarCollapsed ? '' : ''}
</button>
</div>
<AiLockBanner />
{@render children()}
</div>
</main>
<!-- Bottom tab bar (mobile only, hidden when sidebar open) -->
{#if !sidebarOpen}
<BottomTabBar onsidebaropen={toggleSidebar} />
{/if}
</div>
{:else}
<div class="flex h-screen items-center justify-center bg-bg">
<span class="text-muted">Loading...</span>
</div>
{/if}
<CommandBar />