767b8015fa
Add AppSidebar built from the shadcn sidebar primitives (collapsible icon rail, editable title, nav with active states, footer theme toggle and connection status) and wrap the app in a sidebar provider with an inset top bar. Preserves the always-mounted Playground pattern. - add src/components/AppSidebar.svelte - restructure App.svelte around Sidebar.Provider/Inset - remove Header.svelte Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UmuGqwNBJNEAMaWsdCDqUC
146 lines
4.9 KiB
Svelte
146 lines
4.9 KiB
Svelte
<script lang="ts">
|
|
import { link } from "svelte-spa-router";
|
|
import { House, Boxes, Activity, ScrollText, Gauge, Sun, Moon, Monitor } from "@lucide/svelte";
|
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
|
import { Button } from "$lib/components/ui/button/index.js";
|
|
import { toggleTheme, themeMode, appTitle } from "../stores/theme";
|
|
import { currentRoute } from "../stores/route";
|
|
import { playgroundActivity } from "../stores/playgroundActivity";
|
|
import { performanceEnabled } from "../stores/api";
|
|
import ConnectionStatus from "./ConnectionStatus.svelte";
|
|
|
|
function handleTitleChange(newTitle: string): void {
|
|
const sanitized = newTitle.replace(/\n/g, "").trim().substring(0, 64) || "llama-swap";
|
|
appTitle.set(sanitized);
|
|
}
|
|
|
|
function handleKeyDown(e: KeyboardEvent): void {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
const target = e.currentTarget as HTMLElement;
|
|
handleTitleChange(target.textContent || "(set title)");
|
|
target.blur();
|
|
}
|
|
}
|
|
|
|
function handleBlur(e: FocusEvent): void {
|
|
const target = e.currentTarget as HTMLElement;
|
|
handleTitleChange(target.textContent || "(set title)");
|
|
}
|
|
|
|
function isActive(path: string, current: string): boolean {
|
|
return path === "/" ? current === "/" : current.startsWith(path);
|
|
}
|
|
</script>
|
|
|
|
<Sidebar.Root collapsible="icon">
|
|
<Sidebar.Header>
|
|
<div class="flex items-center gap-2 px-2 py-1.5">
|
|
<div
|
|
class="bg-primary text-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg font-bold"
|
|
>
|
|
ll
|
|
</div>
|
|
<h1
|
|
contenteditable="true"
|
|
class="truncate pb-0 text-base font-semibold outline-none rounded px-1 hover:bg-sidebar-accent group-data-[collapsible=icon]:hidden"
|
|
onblur={handleBlur}
|
|
onkeydown={handleKeyDown}
|
|
>
|
|
{$appTitle}
|
|
</h1>
|
|
</div>
|
|
</Sidebar.Header>
|
|
|
|
<Sidebar.Content>
|
|
<Sidebar.Group>
|
|
<Sidebar.GroupContent>
|
|
<Sidebar.Menu>
|
|
<Sidebar.MenuItem>
|
|
<Sidebar.MenuButton isActive={isActive("/", $currentRoute)} tooltipContent="Playground">
|
|
{#snippet child({ props })}
|
|
<a href="/" use:link {...props}>
|
|
<House />
|
|
<span class={$playgroundActivity ? "activity-link" : ""}>Playground</span>
|
|
</a>
|
|
{/snippet}
|
|
</Sidebar.MenuButton>
|
|
</Sidebar.MenuItem>
|
|
|
|
<Sidebar.MenuItem>
|
|
<Sidebar.MenuButton isActive={isActive("/models", $currentRoute)} tooltipContent="Models">
|
|
{#snippet child({ props })}
|
|
<a href="/models" use:link {...props}>
|
|
<Boxes />
|
|
<span>Models</span>
|
|
</a>
|
|
{/snippet}
|
|
</Sidebar.MenuButton>
|
|
</Sidebar.MenuItem>
|
|
|
|
<Sidebar.MenuItem>
|
|
<Sidebar.MenuButton isActive={isActive("/activity", $currentRoute)} tooltipContent="Activity">
|
|
{#snippet child({ props })}
|
|
<a href="/activity" use:link {...props}>
|
|
<Activity />
|
|
<span>Activity</span>
|
|
</a>
|
|
{/snippet}
|
|
</Sidebar.MenuButton>
|
|
</Sidebar.MenuItem>
|
|
|
|
<Sidebar.MenuItem>
|
|
<Sidebar.MenuButton isActive={isActive("/logs", $currentRoute)} tooltipContent="Logs">
|
|
{#snippet child({ props })}
|
|
<a href="/logs" use:link {...props}>
|
|
<ScrollText />
|
|
<span>Logs</span>
|
|
</a>
|
|
{/snippet}
|
|
</Sidebar.MenuButton>
|
|
</Sidebar.MenuItem>
|
|
|
|
{#if $performanceEnabled}
|
|
<Sidebar.MenuItem>
|
|
<Sidebar.MenuButton isActive={isActive("/performance", $currentRoute)} tooltipContent="Performance">
|
|
{#snippet child({ props })}
|
|
<a href="/performance" use:link {...props}>
|
|
<Gauge />
|
|
<span>Performance</span>
|
|
</a>
|
|
{/snippet}
|
|
</Sidebar.MenuButton>
|
|
</Sidebar.MenuItem>
|
|
{/if}
|
|
</Sidebar.Menu>
|
|
</Sidebar.GroupContent>
|
|
</Sidebar.Group>
|
|
</Sidebar.Content>
|
|
|
|
<Sidebar.Footer>
|
|
<div
|
|
class="flex items-center justify-between gap-2 px-1 group-data-[collapsible=icon]:flex-col-reverse"
|
|
>
|
|
<div class="flex items-center gap-2 px-1">
|
|
<ConnectionStatus />
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onclick={toggleTheme}
|
|
title="Toggle theme (current: {$themeMode})"
|
|
>
|
|
{#if $themeMode === "system"}
|
|
<Monitor />
|
|
{:else if $themeMode === "light"}
|
|
<Sun />
|
|
{:else}
|
|
<Moon />
|
|
{/if}
|
|
<span class="sr-only">Toggle theme</span>
|
|
</Button>
|
|
</div>
|
|
</Sidebar.Footer>
|
|
<Sidebar.Rail />
|
|
</Sidebar.Root>
|