update sidebar to use shadcn svelte next

main
TZGyn 1 year ago
parent cf258e3f40
commit 4014cf024b
Signed by: TZGyn
GPG Key ID: 122EAF77AE81FD4A

Binary file not shown.

@ -52,7 +52,7 @@
"drizzle-orm": "^0.32.1",
"formsnap": "^2.0.0-next.1",
"he": "^1.2.0",
"lucide-svelte": "^0.456.0",
"lucide-svelte": "0.456.0",
"mode-watcher": "^0.4.1",
"nanoid": "^5.0.3",
"node-html-parser": "^6.1.12",

@ -0,0 +1,97 @@
<script lang="ts" module>
import AudioWaveform from 'lucide-svelte/icons/audio-waveform'
import Command from 'lucide-svelte/icons/command'
import GalleryVerticalEnd from 'lucide-svelte/icons/gallery-vertical-end'
</script>
<script lang="ts">
import NavMain from '$lib/components/nav-main.svelte'
import NavUser from '$lib/components/nav-user.svelte'
import TeamSwitcher from '$lib/components/team-switcher.svelte'
import * as Sidebar from '$lib/components/ui/sidebar/index.js'
import type { ComponentProps } from 'svelte'
import {
BlocksIcon,
CreditCardIcon,
HomeIcon,
LinkIcon,
SettingsIcon,
} from 'lucide-svelte'
import { page } from '$app/stores'
import type { User } from 'lucia'
let {
ref = $bindable(null),
user,
collapsible = 'icon',
...restProps
}: ComponentProps<typeof Sidebar.Root> & { user: User } = $props()
let data = $derived({
user: {
name: user.username || '',
email: user.email,
avatar: '/avatars/shadcn.jpg',
isPro: user.plan === 'pro' || user.plan === 'owner',
},
teams: [
{
name: 'Acme Inc',
logo: GalleryVerticalEnd,
plan: 'Enterprise',
},
{
name: 'Acme Corp.',
logo: AudioWaveform,
plan: 'Startup',
},
{
name: 'Evil Corp.',
logo: Command,
plan: 'Free',
},
],
navMain: [
{
title: 'Home',
url: '/dashboard',
icon: HomeIcon,
isActive: $page.url.pathname === '/dashboard',
},
{
title: 'Links',
url: '/dashboard/links',
icon: LinkIcon,
isActive: $page.url.pathname.startsWith('/dashboard/links'),
},
{
title: 'Projects',
url: '/dashboard/projects',
icon: BlocksIcon,
isActive: $page.url.pathname.startsWith(
'/dashboard/projects',
),
},
{
title: 'Settings',
url: '/dashboard/settings/account',
icon: SettingsIcon,
isActive: $page.url.pathname.startsWith(
'/dashboard/settings',
),
},
],
})
</script>
<Sidebar.Root bind:ref {collapsible} {...restProps}>
<Sidebar.Header>
<NavUser user={data.user} />
<!-- <TeamSwitcher teams={data.teams} /> -->
</Sidebar.Header>
<Sidebar.Content>
<NavMain items={data.navMain} />
</Sidebar.Content>
<Sidebar.Footer></Sidebar.Footer>
<Sidebar.Rail />
</Sidebar.Root>

@ -0,0 +1,36 @@
<script lang="ts">
import * as Sidebar from '$lib/components/ui/sidebar/index.js'
let {
items,
}: {
items: {
title: string
url: string
// this should be `Component` after lucide-svelte updates types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
icon?: any
isActive?: boolean
}[]
} = $props()
</script>
<Sidebar.Group>
<Sidebar.GroupLabel>Dashboard</Sidebar.GroupLabel>
<Sidebar.Menu>
{#each items as item (item.title)}
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={item.isActive}>
{#snippet child({ props })}
<a href={item.url} {...props}>
{#if item.icon}
<item.icon class="h-8" />
{/if}
<span>{item.title}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.Group>

@ -0,0 +1,163 @@
<script lang="ts">
import * as Avatar from '$lib/components/ui/avatar/index.js'
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js'
import * as Sidebar from '$lib/components/ui/sidebar/index.js'
import { useSidebar } from '$lib/components/ui/sidebar/index.js'
import { Loader2Icon, UserIcon } from 'lucide-svelte'
import BadgeCheck from 'lucide-svelte/icons/badge-check'
import ChevronsUpDown from 'lucide-svelte/icons/chevrons-up-down'
import CreditCard from 'lucide-svelte/icons/credit-card'
import LogOut from 'lucide-svelte/icons/log-out'
import Sparkles from 'lucide-svelte/icons/sparkles'
import { toast } from 'svelte-sonner'
import * as Dialog from '$lib/components/ui/dialog'
import { Button } from '$lib/components/ui/button'
let dialogOpen = $state(false)
let isLoading = $state(false)
const logout = async () => {
isLoading = true
await fetch('/api/logout', { method: 'post' })
isLoading = false
dialogOpen = false
toast.success('Logged Out Successfully')
goto('/login')
}
let {
user,
}: {
user: {
name: string
email: string
avatar: string
isPro: boolean
}
} = $props()
import { goto } from '$app/navigation'
const sidebar = useSidebar()
</script>
<Sidebar.Menu>
<Sidebar.MenuItem>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Sidebar.MenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
{...props}>
<Avatar.Root class="h-8 w-8 rounded-lg">
<Avatar.Image src={user.avatar} alt={user.name} />
<Avatar.Fallback class="rounded-lg">
<UserIcon />
</Avatar.Fallback>
</Avatar.Root>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{user.name}</span>
<span class="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown class="size-4 ml-auto" />
</Sidebar.MenuButton>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="min-w-56 w-[--bits-dropdown-menu-anchor-width] rounded-lg"
side={sidebar.isMobile ? 'bottom' : 'right'}
align="end"
sideOffset={4}>
<DropdownMenu.Label class="p-0 font-normal">
<div
class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar.Root class="h-8 w-8 rounded-lg">
<Avatar.Image src={user.avatar} alt={user.name} />
<Avatar.Fallback class="rounded-lg">
<UserIcon />
</Avatar.Fallback>
</Avatar.Root>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{user.name}</span>
<span class="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenu.Label>
<DropdownMenu.Separator />
{#if !user.isPro}
<DropdownMenu.Group>
<a href="/dashboard/billing/plan/pro">
<DropdownMenu.Item>
<Sparkles />
Upgrade to Pro
</DropdownMenu.Item>
</a>
</DropdownMenu.Group>
<DropdownMenu.Separator />
{/if}
<DropdownMenu.Group>
<a href="/dashboard/settings/account">
<DropdownMenu.Item>
<BadgeCheck />
Account
</DropdownMenu.Item>
</a>
<a href="/dashboard/billing">
<DropdownMenu.Item>
<CreditCard />
Billing
</DropdownMenu.Item>
</a>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item
onclick={() => (dialogOpen = true)}
class="text-destructive data-[highlighted]:bg-destructive data-[highlighted]:text-white">
<LogOut />
Log out
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Sidebar.MenuItem>
</Sidebar.Menu>
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Content>
<Dialog.Header class="space-y-12">
<div
class="flex w-full flex-col items-center justify-center gap-6 pt-12">
<div class="bg-destructive/30 w-fit rounded-full p-4">
<UserIcon class="text-destructive" size={64} />
</div>
</div>
<div class="space-y-2">
<Dialog.Title class="text-center">Log Out?</Dialog.Title>
<Dialog.Description class="text-center">
You are about to log out of this account.
</Dialog.Description>
</div>
<div class="flex justify-center gap-6">
<Button
variant="outline"
class="w-full"
onclick={() => {
dialogOpen = false
}}
disabled={isLoading}>
Cancel
</Button>
<Button
variant="destructive"
onclick={logout}
class="flex w-full gap-2"
disabled={isLoading}>
{#if isLoading}
<Loader2Icon class="animate-spin" />
{/if}
Log Out
</Button>
</div>
</Dialog.Header>
</Dialog.Content>
</Dialog.Root>

@ -0,0 +1,69 @@
<script lang="ts">
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import { useSidebar } from "$lib/components/ui/sidebar/index.js";
import ChevronsUpDown from "lucide-svelte/icons/chevrons-up-down";
import Plus from "lucide-svelte/icons/plus";
// This should be `Component` after lucide-svelte updates types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let { teams }: { teams: { name: string; logo: any; plan: string }[] } = $props();
const sidebar = useSidebar();
let activeTeam = $state(teams[0]);
</script>
<Sidebar.Menu>
<Sidebar.MenuItem>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Sidebar.MenuButton
{...props}
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg"
>
<activeTeam.logo class="size-4" />
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">
{activeTeam.name}
</span>
<span class="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown class="ml-auto" />
</Sidebar.MenuButton>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-[--bits-dropdown-menu-anchor-width] min-w-56 rounded-lg"
align="start"
side={sidebar.isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenu.Label class="text-muted-foreground text-xs">Teams</DropdownMenu.Label>
{#each teams as team, index (team.name)}
<DropdownMenu.Item onSelect={() => (activeTeam = team)} class="gap-2 p-2">
<div class="flex size-6 items-center justify-center rounded-sm border">
<team.logo class="size-4 shrink-0" />
</div>
{team.name}
<DropdownMenu.Shortcut>{index + 1}</DropdownMenu.Shortcut>
</DropdownMenu.Item>
{/each}
<DropdownMenu.Separator />
<DropdownMenu.Item class="gap-2 p-2">
<div
class="bg-background flex size-6 items-center justify-center rounded-md border"
>
<Plus class="size-4" />
</div>
<div class="text-muted-foreground font-medium">Add team</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Sidebar.MenuItem>
</Sidebar.Menu>

@ -0,0 +1,15 @@
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
const Root = CollapsiblePrimitive.Root;
const Trigger = CollapsiblePrimitive.Trigger;
const Content = CollapsiblePrimitive.Content;
export {
Root,
Content,
Trigger,
//
Root as Collapsible,
Content as CollapsibleContent,
Trigger as CollapsibleTrigger,
};

@ -0,0 +1,6 @@
export const SIDEBAR_COOKIE_NAME = "sidebar:state";
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
export const SIDEBAR_WIDTH = "16rem";
export const SIDEBAR_WIDTH_MOBILE = "18rem";
export const SIDEBAR_WIDTH_ICON = "3rem";
export const SIDEBAR_KEYBOARD_SHORTCUT = "b";

@ -0,0 +1,81 @@
import { IsMobile } from "$lib/hooks/is-mobile.svelte.js";
import { getContext, setContext } from "svelte";
import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js";
type Getter<T> = () => T;
export type SidebarStateProps = {
/**
* A getter function that returns the current open state of the sidebar.
* We use a getter function here to support `bind:open` on the `Sidebar.Provider`
* component.
*/
open: Getter<boolean>;
/**
* A function that sets the open state of the sidebar. To support `bind:open`, we need
* a source of truth for changing the open state to ensure it will be synced throughout
* the sub-components and any `bind:` references.
*/
setOpen: (open: boolean) => void;
};
class SidebarState {
readonly props: SidebarStateProps;
open = $derived.by(() => this.props.open());
openMobile = $state(false);
setOpen: SidebarStateProps["setOpen"];
#isMobile: IsMobile;
state = $derived.by(() => (this.open ? "expanded" : "collapsed"));
constructor(props: SidebarStateProps) {
this.setOpen = props.setOpen;
this.#isMobile = new IsMobile();
this.props = props;
}
// Convenience getter for checking if the sidebar is mobile
// without this, we would need to use `sidebar.isMobile.current` everywhere
get isMobile() {
return this.#isMobile.current;
}
// Event handler to apply to the `<svelte:window>`
handleShortcutKeydown = (e: KeyboardEvent) => {
if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
this.toggle();
}
};
setOpenMobile = (value: boolean) => {
this.openMobile = value;
};
toggle = () => {
return this.#isMobile.current
? (this.openMobile = !this.openMobile)
: this.setOpen(!this.open);
};
}
const SYMBOL_KEY = "scn-sidebar";
/**
* Instantiates a new `SidebarState` instance and sets it in the context.
*
* @param props The constructor props for the `SidebarState` class.
* @returns The `SidebarState` instance.
*/
export function setSidebar(props: SidebarStateProps): SidebarState {
return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props));
}
/**
* Retrieves the `SidebarState` instance from the context. This is a class instance,
* so you cannot destructure it.
* @returns The `SidebarState` instance.
*/
export function useSidebar(): SidebarState {
return getContext(Symbol.for(SYMBOL_KEY));
}

@ -0,0 +1,75 @@
import { useSidebar } from "./context.svelte.js";
import Content from "./sidebar-content.svelte";
import Footer from "./sidebar-footer.svelte";
import GroupAction from "./sidebar-group-action.svelte";
import GroupContent from "./sidebar-group-content.svelte";
import GroupLabel from "./sidebar-group-label.svelte";
import Group from "./sidebar-group.svelte";
import Header from "./sidebar-header.svelte";
import Input from "./sidebar-input.svelte";
import Inset from "./sidebar-inset.svelte";
import MenuAction from "./sidebar-menu-action.svelte";
import MenuBadge from "./sidebar-menu-badge.svelte";
import MenuButton from "./sidebar-menu-button.svelte";
import MenuItem from "./sidebar-menu-item.svelte";
import MenuSkeleton from "./sidebar-menu-skeleton.svelte";
import MenuSubButton from "./sidebar-menu-sub-button.svelte";
import MenuSubItem from "./sidebar-menu-sub-item.svelte";
import MenuSub from "./sidebar-menu-sub.svelte";
import Menu from "./sidebar-menu.svelte";
import Provider from "./sidebar-provider.svelte";
import Rail from "./sidebar-rail.svelte";
import Separator from "./sidebar-separator.svelte";
import Trigger from "./sidebar-trigger.svelte";
import Root from "./sidebar.svelte";
export {
Content,
Footer,
Group,
GroupAction,
GroupContent,
GroupLabel,
Header,
Input,
Inset,
Menu,
MenuAction,
MenuBadge,
MenuButton,
MenuItem,
MenuSkeleton,
MenuSub,
MenuSubButton,
MenuSubItem,
Provider,
Rail,
Root,
Separator,
//
Root as Sidebar,
Content as SidebarContent,
Footer as SidebarFooter,
Group as SidebarGroup,
GroupAction as SidebarGroupAction,
GroupContent as SidebarGroupContent,
GroupLabel as SidebarGroupLabel,
Header as SidebarHeader,
Input as SidebarInput,
Inset as SidebarInset,
Menu as SidebarMenu,
MenuAction as SidebarMenuAction,
MenuBadge as SidebarMenuBadge,
MenuButton as SidebarMenuButton,
MenuItem as SidebarMenuItem,
MenuSkeleton as SidebarMenuSkeleton,
MenuSub as SidebarMenuSub,
MenuSubButton as SidebarMenuSubButton,
MenuSubItem as SidebarMenuSubItem,
Provider as SidebarProvider,
Rail as SidebarRail,
Separator as SidebarSeparator,
Trigger as SidebarTrigger,
Trigger,
useSidebar,
};

@ -0,0 +1,24 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div
bind:this={ref}
data-sidebar="content"
class={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div
bind:this={ref}
data-sidebar="footer"
class={cn("flex flex-col gap-2 p-2", className)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,36 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { Snippet } from "svelte";
import type { HTMLButtonAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
child,
...restProps
}: WithElementRef<HTMLButtonAttributes> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
} = $props();
const propObj = $derived({
class: cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className
),
"data-sidebar": "group-action",
...restProps,
});
</script>
{#if child}
{@render child({ props: propObj })}
{:else}
<button bind:this={ref} {...propObj}>
{@render children?.()}
</button>
{/if}

@ -0,0 +1,21 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-sidebar="group-content"
class={cn("w-full text-sm", className)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,34 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
children,
child,
class: className,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
} = $props();
const mergedProps = $derived({
class: cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
),
"data-sidebar": "group-label",
...restProps,
});
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div bind:this={ref} {...mergedProps}>
{@render children?.()}
</div>
{/if}

@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div
bind:this={ref}
data-sidebar="group"
class={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div
bind:this={ref}
data-sidebar="header"
class={cn("flex flex-col gap-2 p-2", className)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,23 @@
<script lang="ts">
import type { ComponentProps } from "svelte";
import { Input } from "$lib/components/ui/input/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(""),
class: className,
...restProps
}: ComponentProps<typeof Input> = $props();
</script>
<Input
bind:ref
bind:value
data-sidebar="input"
class={cn(
"bg-background focus-visible:ring-sidebar-ring h-8 w-full shadow-none focus-visible:ring-2",
className
)}
{...restProps}
/>

@ -0,0 +1,24 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<main
bind:this={ref}
class={cn(
"bg-background relative flex min-h-svh flex-1 flex-col",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className
)}
{...restProps}
>
{@render children?.()}
</main>

@ -0,0 +1,43 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { Snippet } from "svelte";
import type { HTMLButtonAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
showOnHover = false,
children,
child,
...restProps
}: WithElementRef<HTMLButtonAttributes> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
showOnHover?: boolean;
} = $props();
const mergedProps = $derived({
class: cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
),
"data-sidebar": "menu-action",
...restProps,
});
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<button bind:this={ref} {...mergedProps}>
{@render children?.()}
</button>
{/if}

@ -0,0 +1,29 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div
bind:this={ref}
data-sidebar="menu-badge"
class={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...restProps}
>
{@render children?.()}
</div>

@ -0,0 +1,97 @@
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const sidebarMenuButtonVariants = tv({
base: "peer/menu-button ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none transition-[width,height,padding] focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:font-medium group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type SidebarMenuButtonVariant = VariantProps<
typeof sidebarMenuButtonVariants
>["variant"];
export type SidebarMenuButtonSize = VariantProps<typeof sidebarMenuButtonVariants>["size"];
</script>
<script lang="ts">
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
import { cn } from "$lib/utils.js";
import { mergeProps, type WithElementRef, type WithoutChildrenOrChild } from "bits-ui";
import type { ComponentProps, Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
import { useSidebar } from "./context.svelte.js";
let {
ref = $bindable(null),
class: className,
children,
child,
variant = "default",
size = "default",
isActive = false,
tooltipContent,
tooltipContentProps,
...restProps
}: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> & {
isActive?: boolean;
variant?: SidebarMenuButtonVariant;
size?: SidebarMenuButtonSize;
tooltipContent?: Snippet;
tooltipContentProps?: WithoutChildrenOrChild<ComponentProps<typeof Tooltip.Content>>;
child?: Snippet<[{ props: Record<string, unknown> }]>;
} = $props();
const sidebar = useSidebar();
const buttonProps = $derived({
class: cn(sidebarMenuButtonVariants({ variant, size }), className),
"data-sidebar": "menu-button",
"data-size": size,
"data-active": isActive,
...restProps,
});
</script>
{#snippet Button({ props }: { props?: Record<string, unknown> })}
{@const mergedProps = mergeProps(buttonProps, props)}
{#if child}
{@render child({ props: mergedProps })}
{:else}
<button bind:this={ref} {...mergedProps}>
{@render children?.()}
</button>
{/if}
{/snippet}
{#if !tooltipContent}
{@render Button({})}
{:else}
<Tooltip.Root>
<Tooltip.Trigger>
{#snippet child({ props })}
{@render Button({ props })}
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content
side="right"
align="center"
hidden={sidebar.state !== "collapsed" || sidebar.isMobile}
children={tooltipContent}
{...tooltipContentProps}
/>
</Tooltip.Root>
{/if}

@ -0,0 +1,21 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLLIElement>, HTMLLIElement> = $props();
</script>
<li
bind:this={ref}
data-sidebar="menu-item"
class={cn("group/menu-item relative", className)}
{...restProps}
>
{@render children?.()}
</li>

@ -0,0 +1,36 @@
<script lang="ts">
import { Skeleton } from "$lib/components/ui/skeleton/index.js";
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
showIcon = false,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
showIcon?: boolean;
} = $props();
// Random width between 50% and 90%
const width = `${Math.floor(Math.random() * 40) + 50}%`;
</script>
<div
bind:this={ref}
data-sidebar="menu-skeleton"
class={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...restProps}
>
{#if showIcon}
<Skeleton class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
{/if}
<Skeleton
class="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style="--skeleton-width: {width};"
/>
{@render children?.()}
</div>

@ -0,0 +1,43 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { Snippet } from "svelte";
import type { HTMLAnchorAttributes } from "svelte/elements";
let {
ref = $bindable(null),
children,
child,
class: className,
size = "md",
isActive,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
child?: Snippet<[{ props: Record<string, unknown> }]>;
size?: "sm" | "md";
isActive?: boolean;
} = $props();
const mergedProps = $derived({
class: cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
),
"data-sidebar": "menu-sub-button",
"data-size": size,
"data-active": isActive,
...restProps,
});
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<a bind:this={ref} {...mergedProps}>
{@render children?.()}
</a>
{/if}

@ -0,0 +1,14 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLLIElement>> = $props();
</script>
<li bind:this={ref} data-sidebar="menu-sub-item" {...restProps}>
{@render children?.()}
</li>

@ -0,0 +1,25 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();
</script>
<ul
bind:this={ref}
data-sidebar="menu-sub"
class={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...restProps}
>
{@render children?.()}
</ul>

@ -0,0 +1,21 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLUListElement>, HTMLUListElement> = $props();
</script>
<ul
bind:this={ref}
data-sidebar="menu"
class={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...restProps}
>
{@render children?.()}
</ul>

@ -0,0 +1,59 @@
<script lang="ts">
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import {
SIDEBAR_COOKIE_MAX_AGE,
SIDEBAR_COOKIE_NAME,
SIDEBAR_WIDTH,
SIDEBAR_WIDTH_ICON,
} from "./constants.js";
import { setSidebar } from "./context.svelte.js";
let {
ref = $bindable(null),
open = $bindable(true),
onOpenChange = () => {},
controlledOpen = false,
class: className,
style,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
open?: boolean;
onOpenChange?: (open: boolean) => void;
controlledOpen?: boolean;
} = $props();
const sidebar = setSidebar({
open: () => open,
setOpen: (value: boolean) => {
if (controlledOpen) {
onOpenChange(value);
} else {
open = value;
onOpenChange(value);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
});
</script>
<svelte:window onkeydown={sidebar.handleShortcutKeydown} />
<Tooltip.Provider delayDuration={0}>
<div
style="--sidebar-width: {SIDEBAR_WIDTH}; --sidebar-width-icon: {SIDEBAR_WIDTH_ICON}; {style}"
class={cn(
"group/sidebar-wrapper has-[[data-variant=inset]]:bg-sidebar flex min-h-svh w-full",
className
)}
bind:this={ref}
{...restProps}
>
{@render children?.()}
</div>
</Tooltip.Provider>

@ -0,0 +1,36 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { useSidebar } from "./context.svelte.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement> = $props();
const sidebar = useSidebar();
</script>
<button
bind:this={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onclick={() => sidebar.toggle()}
title="Toggle Sidebar"
class={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...restProps}
>
{@render children?.()}
</button>

@ -0,0 +1,18 @@
<script lang="ts">
import { Separator } from "$lib/components/ui/separator/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
...restProps
}: ComponentProps<typeof Separator> = $props();
</script>
<Separator
bind:ref
data-sidebar="separator"
class={cn("bg-sidebar-border mx-2 w-auto", className)}
{...restProps}
/>

@ -0,0 +1,34 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
import PanelLeft from "lucide-svelte/icons/panel-left";
import type { ComponentProps } from "svelte";
import { useSidebar } from "./context.svelte.js";
let {
ref = $bindable(null),
class: className,
onclick,
...restProps
}: ComponentProps<typeof Button> & {
onclick?: (e: MouseEvent) => void;
} = $props();
const sidebar = useSidebar();
</script>
<Button
type="button"
onclick={(e) => {
onclick?.(e);
sidebar.toggle();
}}
data-sidebar="trigger"
variant="ghost"
size="icon"
class={cn("h-7 w-7", className)}
{...restProps}
>
<PanelLeft />
<span class="sr-only">Toggle Sidebar</span>
</Button>

@ -0,0 +1,98 @@
<script lang="ts">
import * as Sheet from "$lib/components/ui/sheet/index.js";
import { cn } from "$lib/utils.js";
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { SIDEBAR_WIDTH_MOBILE } from "./constants.js";
import { useSidebar } from "./context.svelte.js";
let {
ref = $bindable(null),
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
} = $props();
const sidebar = useSidebar();
</script>
{#if collapsible === "none"}
<div
class={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col",
className
)}
bind:this={ref}
{...restProps}
>
{@render children?.()}
</div>
{:else if sidebar.isMobile}
<Sheet.Root
controlledOpen
open={sidebar.openMobile}
onOpenChange={sidebar.setOpenMobile}
{...restProps}
>
<Sheet.Content
data-sidebar="sidebar"
data-mobile="true"
class="bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden"
style="--sidebar-width: {SIDEBAR_WIDTH_MOBILE};"
{side}
>
<div class="flex h-full w-full flex-col">
{@render children?.()}
</div>
</Sheet.Content>
</Sheet.Root>
{:else}
<div
bind:this={ref}
class="text-sidebar-foreground group peer hidden md:block"
data-state={sidebar.state}
data-collapsible={sidebar.state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
<!-- This is what handles the sidebar gap on desktop -->
<div
class={cn(
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
)}
></div>
<div
class={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...restProps}
>
<div
data-sidebar="sidebar"
class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow"
>
{@render children?.()}
</div>
</div>
</div>
{/if}

@ -0,0 +1,27 @@
import { untrack } from "svelte";
const MOBILE_BREAKPOINT = 768;
export class IsMobile {
#current = $state<boolean>(false);
constructor() {
$effect(() => {
return untrack(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
this.#current = window.innerWidth < MOBILE_BREAKPOINT;
};
mql.addEventListener("change", onChange);
onChange();
return () => {
mql.removeEventListener("change", onChange);
};
});
});
}
get current() {
return this.#current;
}
}

@ -15,6 +15,9 @@
Settings,
UserIcon,
} from 'lucide-svelte'
import AppSidebar from '$lib/components/app-sidebar.svelte'
import { Separator } from '$lib/components/ui/separator/index.js'
import * as Sidebar from '$lib/components/ui/sidebar/index.js'
import { goto } from '$app/navigation'
import { Loader2, User } from 'lucide-svelte'
@ -35,174 +38,53 @@
}
let { data, children } = $props()
let value = $state<string>(
($page.data.project?.id as string) || 'none',
)
const triggerContent = $derived(
data.projects.find((f) => f.id === value)?.name ?? 'None',
)
const routes = [
{
href: '/dashboard',
name: 'Home',
match: (path: string) => path === '/dashboard',
icon: Home,
},
{
href: '/dashboard/links',
name: 'Links',
match: (path: string) => path.startsWith('/dashboard/links'),
icon: Link,
},
{
href: '/dashboard/projects',
name: 'Projects',
match: (path: string) => path.startsWith('/dashboard/projects'),
icon: Blocks,
},
{
href: '/dashboard/billing',
name: 'Billing',
match: (path: string) => path.startsWith('/dashboard/billing'),
icon: CreditCardIcon,
},
{
href: '/dashboard/settings/account',
name: 'Settings',
match: (path: string) => path.startsWith('/dashboard/settings'),
icon: Settings,
},
] as const
</script>
<div
class="max-w-screen flex h-screen max-h-screen w-screen overflow-hidden">
<div class="bg-muted/40 flex max-w-[300px] flex-col border-r">
<div
class="flex h-[60px] max-h-[60px] min-h-[60px] w-full items-center justify-center gap-4 border-b px-2 py-2 lg:px-4">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Avatar.Root>
<Avatar.Image src="" alt="@shadcn" />
<Avatar.Fallback><User /></Avatar.Fallback>
</Avatar.Root>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.Label>{data.user.email}</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item
onclick={() => {
goto('/dashboard/settings')
}}>
Settings
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
onclick={() => (dialogOpen = true)}
class="text-destructive data-[highlighted]:bg-destructive">
Log Out
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
<div class="hidden w-full lg:flex">
<Breadcrumb.Root>
<Breadcrumb.List>
{#if $page.data.breadcrumbs}
{#each $page.data.breadcrumbs as breadcrumb, index}
{#if index == $page.data.breadcrumbs.length - 1}
<Breadcrumb.Item>
<Breadcrumb.Page href={breadcrumb.path}>
{breadcrumb.name}
</Breadcrumb.Page>
</Breadcrumb.Item>
{:else}
<Breadcrumb.Item>
<Breadcrumb.Link href={breadcrumb.path}>
{breadcrumb.name}
</Breadcrumb.Link>
</Breadcrumb.Item>
{/if}
{#if index != $page.data.breadcrumbs.length - 1}
<Breadcrumb.Separator />
{/if}
{/each}
{:else}
<Breadcrumb.Item>
<Breadcrumb.Link href={'/'}>Home</Breadcrumb.Link>
</Breadcrumb.Item>
{/if}
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
</div>
<Sidebar.Provider>
<AppSidebar user={data.user} />
<Sidebar.Inset>
<header
class="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<Sidebar.Trigger class="-ml-1" />
<Separator orientation="vertical" class="mr-2 h-4" />
<Breadcrumb.Root>
<Breadcrumb.List>
{#if $page.data.breadcrumbs}
{#each $page.data.breadcrumbs as breadcrumb, index}
{#if index == $page.data.breadcrumbs.length - 1}
<Breadcrumb.Item>
<Breadcrumb.Page>
{breadcrumb.name}
</Breadcrumb.Page>
</Breadcrumb.Item>
{:else}
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href={breadcrumb.path}>
{breadcrumb.name}
</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
{/if}
{/each}
{:else}
<Breadcrumb.Item>
<Breadcrumb.Link href={'/dashboard'}>
Home
</Breadcrumb.Link>
</Breadcrumb.Item>
{/if}
</Breadcrumb.List>
</Breadcrumb.Root>
</header>
<div
class={'flex h-full flex-col justify-between lg:min-w-[300px]'}>
<div>
<div class="flex flex-col gap-4 p-2 lg:p-4">
<!-- <Select.Root
type="single"
name={'selected_project'}
bind:value>
<Select.Trigger class="hidden lg:flex">
{triggerContent}
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.GroupHeading>Projects</Select.GroupHeading>
<Select.Separator />
<a href={`/dashboard/links`}>
<Select.Item value={'none'} label={'None'}>
None
</Select.Item>
</a>
<Select.Separator />
{#each data.projects as project}
<a href={`/dashboard/projects/${project.uuid}`}>
<Select.Item
value={project.id}
label={project.name}>
{project.name}
</Select.Item>
</a>
{/each}
</Select.Group>
</Select.Content>
</Select.Root> -->
{#each routes as route}
<Button
variant={route.match($page.url.pathname)
? 'secondary'
: 'ghost'}
href={route.href}
class="hover:bg-secondary/50 flex items-center justify-start gap-4 text-base">
<route.icon class="h-4 w-4" />
<div class="hidden lg:flex">
{route.name}
</div>
</Button>
{/each}
</div>
class="flex max-h-[calc(100vh-64px)] flex-grow overflow-hidden">
<div class="flex h-auto w-full flex-col">
{@render children()}
</div>
</div>
<div
class="flex items-center justify-center border-t px-2 py-4 lg:justify-end lg:px-4">
<ThemeToggle />
</div>
</div>
<div class="flex flex-grow overflow-hidden">
<div class="flex h-auto w-full flex-col">
{@render children()}
</div>
</div>
</div>
</Sidebar.Inset>
</Sidebar.Provider>
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Content>

@ -6,7 +6,11 @@ import { zod } from 'sveltekit-superforms/adapters'
import { cancelSubscriptionSchema } from './schema'
export const load = (async () => {
const breadcrumbs = [
{ name: 'Billing', path: '/dashboard/billing' },
]
return {
breadcrumbs,
cancel_subscription_form: await superValidate(
zod(cancelSubscriptionSchema),
),

Loading…
Cancel
Save