Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225<script lang="ts"> import type { Snippet } from "svelte"; import ChevronDown from "$icon/chevron-down"; import Menu from "$icon/menu"; import Monitor from "$icon/monitor"; import Moon from "$icon/moon"; import Search from "$icon/search"; import Sun from "$icon/sun"; import X from "$icon/x"; import Button from "$lib/components/ui/Button.svelte"; import Input from "$lib/components/ui/Input.svelte"; import Logo from "$lib/components/ui/Logo.svelte"; import { plural } from "$lib/format";
interface Entry { name: string; slug: string; }
interface Props { title: string; entries: Entry[]; total: number; query?: string; children: Snippet; }
let { title, entries, total, query = $bindable(""), children }: Props = $props();
type Theme = "system" | "light" | "dark";
const themes: Record<Theme, { next: Theme; label: string; icon: typeof Sun }> = { system: { next: "light", label: "System theme", icon: Monitor }, light: { next: "dark", label: "Light theme", icon: Sun }, dark: { next: "system", label: "Dark theme", icon: Moon } };
let theme = $state<Theme>("system"); let sidebarOpen = $state(true); let drawerOpen = $state(false); let componentsOpen = $state(true);
$effect(() => { const root = document.documentElement; if (theme === "system") root.removeAttribute("data-theme"); else root.dataset.theme = theme; return () => root.removeAttribute("data-theme"); });
const sorted = $derived(entries.toSorted((a, b) => a.name.localeCompare(b.name)));</script>
{#snippet themeButton()} <Button variant="ghost" icon={themes[theme].icon} aria-label={themes[theme].label} title={themes[theme].label} onclick={() => (theme = themes[theme].next)} />{/snippet}
{#snippet nav()} <div class="mb-sm"> <Input size="small" iconLeft={Search} placeholder="Search..." bind:value={query} /> </div> <ul class="flex flex-col gap-px"> <li> <a href="#top" class="block rounded-md p-xs text-foreground-default no-underline transition-colors hover:bg-background-subtle hover:no-underline" > Overview </a> </li> </ul> <div class="my-md border-b border-border-default"></div> <div class="mb-md"> <button type="button" class="flex w-full cursor-pointer items-center justify-between rounded-md p-xs font-medium text-foreground-default" aria-expanded={componentsOpen} onclick={() => (componentsOpen = !componentsOpen)} > <span>Components</span> <ChevronDown class={[ "size-4 transition-transform duration-300", !componentsOpen && "-rotate-90" ]} /> </button> {#if componentsOpen} <ul class="mt-2xs flex flex-col gap-px"> {#each sorted as entry (entry.slug)} <li> <a href="#{entry.slug}" class="block rounded-md p-xs text-foreground-subtle no-underline transition-colors hover:bg-background-subtle hover:text-foreground-default hover:no-underline" onclick={() => (drawerOpen = false)} > {entry.name} </a> </li> {:else} <li class="p-xs text-foreground-placeholder">No matches</li> {/each} </ul> {/if} </div>{/snippet}
<div id="top" class="isolate min-h-screen bg-background-default text-foreground-default"> <div class="fixed inset-x-0 top-0 z-50 flex h-12 items-center justify-between border-b border-border-default bg-background-default px-xs md:hidden" > <Button variant="ghost" icon={Menu} aria-label="Open navigation" onclick={() => (drawerOpen = true)} /> <h1 class="typography-paragraph-regular font-medium">{title}</h1> {@render themeButton()} </div>
{#if drawerOpen} <button type="button" class="fixed inset-0 z-40 bg-background-emphasis/40 md:hidden" aria-label="Close navigation" onclick={() => (drawerOpen = false)} ></button> {/if} <aside class={[ "fixed inset-y-0 left-0 z-50 flex w-72 flex-col border-r border-border-default bg-background-default transition-transform duration-300 ease-in-out md:hidden", !drawerOpen && "-translate-x-full" ]} > <div class="flex h-12 flex-none items-center justify-between border-b border-border-default px-sm" > <h1 class="typography-paragraph-regular font-medium">{title}</h1> <Button variant="ghost" icon={X} aria-label="Close navigation" onclick={() => (drawerOpen = false)} /> </div> <div class="min-h-0 grow overflow-y-auto overscroll-contain px-sm py-md typography-paragraph-small text-foreground-subtle" > {@render nav()} </div> </aside>
<div class="fixed inset-y-0 left-0 z-50 hidden w-12 border-r border-border-default bg-background-default md:block" > <div class="relative h-12 border-b border-border-default"> <button type="button" class="absolute inset-0 grid cursor-pointer place-items-center text-foreground-default" aria-label={sidebarOpen ? "Hide navigation" : "Show navigation"} aria-expanded={sidebarOpen} onclick={() => (sidebarOpen = !sidebarOpen)} > <Logo class="size-5" /> </button> </div> </div> <div class="pointer-events-none fixed top-0 left-12 z-50 hidden h-12 items-center px-sm font-medium select-none md:flex" > <h1 class="typography-paragraph-regular font-medium">{title}</h1> </div> <aside class={[ "fixed inset-y-0 left-12 z-40 hidden w-64 flex-col border-r border-border-default bg-background-default transition-transform duration-300 ease-in-out md:flex", !sidebarOpen && "-translate-x-full" ]} > <div class="h-12 flex-none border-b border-border-default"></div> <div class="min-h-0 grow overflow-y-auto overscroll-contain px-sm py-md typography-paragraph-small text-foreground-subtle" > {@render nav()} </div> </aside>
<div class={[ "mt-12 transition-[margin] duration-300 md:mt-0", sidebarOpen ? "md:ml-76" : "md:ml-12" ]} > <div class="flex flex-col"> <header class="sticky top-0 z-10 hidden h-12 border-b border-border-default bg-background-default md:flex" > <div class="flex min-w-0 flex-1 items-center justify-end border-r border-border-default px-md" > <span class="rounded-xs bg-background-inset px-1.5 py-0.5 typography-monospace-small text-foreground-subtle" > {total} {plural(total, "component", "components")} </span> </div> <div class="flex w-12 shrink-0 items-center justify-center"> {@render themeButton()} </div> </header> <main class="flex grow flex-col md:pr-12"> <div class="mx-auto w-full grow md:border-r md:border-border-default"> {@render children()} </div> </main> </div> </div></div>