`
+ 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));
+}
diff --git a/frontend/src/lib/components/ui/sidebar/index.ts b/frontend/src/lib/components/ui/sidebar/index.ts
new file mode 100644
index 0000000..318a341
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/index.ts
@@ -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,
+};
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-content.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-content.svelte
new file mode 100644
index 0000000..c6db009
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-content.svelte
@@ -0,0 +1,24 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-footer.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-footer.svelte
new file mode 100644
index 0000000..c62cb41
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-footer.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-group-action.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-group-action.svelte
new file mode 100644
index 0000000..6961e4c
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-group-action.svelte
@@ -0,0 +1,36 @@
+
+
+{#if child}
+ {@render child({ props: propObj })}
+{:else}
+
+{/if}
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-group-content.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-group-content.svelte
new file mode 100644
index 0000000..f5e623b
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-group-content.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-group-label.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-group-label.svelte
new file mode 100644
index 0000000..77c180f
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-group-label.svelte
@@ -0,0 +1,34 @@
+
+
+{#if child}
+ {@render child({ props: mergedProps })}
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-group.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-group.svelte
new file mode 100644
index 0000000..aa8bf31
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-group.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-header.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-header.svelte
new file mode 100644
index 0000000..5656c0b
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-header.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-input.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-input.svelte
new file mode 100644
index 0000000..1d57f43
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-input.svelte
@@ -0,0 +1,23 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-inset.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-inset.svelte
new file mode 100644
index 0000000..b9cd447
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-inset.svelte
@@ -0,0 +1,24 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-action.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-action.svelte
new file mode 100644
index 0000000..5a94f39
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-action.svelte
@@ -0,0 +1,43 @@
+
+
+{#if child}
+ {@render child({ props: mergedProps })}
+{:else}
+
+{/if}
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
new file mode 100644
index 0000000..0fd4c51
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte
@@ -0,0 +1,29 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
new file mode 100644
index 0000000..1513721
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-button.svelte
@@ -0,0 +1,97 @@
+
+
+
+
+{#snippet Button({ props }: { props?: Record })}
+ {@const mergedProps = mergeProps(buttonProps, props)}
+ {#if child}
+ {@render child({ props: mergedProps })}
+ {:else}
+
+ {/if}
+{/snippet}
+
+{#if !tooltipContent}
+ {@render Button({})}
+{:else}
+
+
+ {#snippet child({ props })}
+ {@render Button({ props })}
+ {/snippet}
+
+
+
+{/if}
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-item.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-item.svelte
new file mode 100644
index 0000000..ee82144
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-item.svelte
@@ -0,0 +1,21 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
new file mode 100644
index 0000000..5c2effe
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte
@@ -0,0 +1,36 @@
+
+
+
+ {#if showIcon}
+
+ {/if}
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
new file mode 100644
index 0000000..26b2a90
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte
@@ -0,0 +1,43 @@
+
+
+{#if child}
+ {@render child({ props: mergedProps })}
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
new file mode 100644
index 0000000..6e7346d
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte
@@ -0,0 +1,14 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
new file mode 100644
index 0000000..2a23990
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte
@@ -0,0 +1,25 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-menu.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-menu.svelte
new file mode 100644
index 0000000..72534db
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-menu.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {@render children?.()}
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-provider.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-provider.svelte
new file mode 100644
index 0000000..4583bbe
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-provider.svelte
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+ {@render children?.()}
+
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-rail.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-rail.svelte
new file mode 100644
index 0000000..ee16fce
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-rail.svelte
@@ -0,0 +1,36 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-separator.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-separator.svelte
new file mode 100644
index 0000000..0cc3a25
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-separator.svelte
@@ -0,0 +1,18 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar-trigger.svelte b/frontend/src/lib/components/ui/sidebar/sidebar-trigger.svelte
new file mode 100644
index 0000000..98a4a7f
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar-trigger.svelte
@@ -0,0 +1,34 @@
+
+
+
diff --git a/frontend/src/lib/components/ui/sidebar/sidebar.svelte b/frontend/src/lib/components/ui/sidebar/sidebar.svelte
new file mode 100644
index 0000000..f53363d
--- /dev/null
+++ b/frontend/src/lib/components/ui/sidebar/sidebar.svelte
@@ -0,0 +1,98 @@
+
+
+{#if collapsible === "none"}
+
+ {@render children?.()}
+
+{:else if sidebar.isMobile}
+
+
+
+{:else}
+
+{/if}
diff --git a/frontend/src/lib/hooks/is-mobile.svelte.ts b/frontend/src/lib/hooks/is-mobile.svelte.ts
new file mode 100644
index 0000000..87bea4b
--- /dev/null
+++ b/frontend/src/lib/hooks/is-mobile.svelte.ts
@@ -0,0 +1,27 @@
+import { untrack } from "svelte";
+
+const MOBILE_BREAKPOINT = 768;
+
+export class IsMobile {
+ #current = $state(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;
+ }
+}
diff --git a/frontend/src/routes/(app)/dashboard/+layout.svelte b/frontend/src/routes/(app)/dashboard/+layout.svelte
index 042234a..dd12220 100644
--- a/frontend/src/routes/(app)/dashboard/+layout.svelte
+++ b/frontend/src/routes/(app)/dashboard/+layout.svelte
@@ -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(
- ($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
-
-
-
-
-
-
-
-
-
-
-
-
- {data.user.email}
-
- {
- goto('/dashboard/settings')
- }}>
- Settings
-
-
- (dialogOpen = true)}
- class="text-destructive data-[highlighted]:bg-destructive">
- Log Out
-
-
-
-
-
-
-
-
- {#if $page.data.breadcrumbs}
- {#each $page.data.breadcrumbs as breadcrumb, index}
- {#if index == $page.data.breadcrumbs.length - 1}
-
-
- {breadcrumb.name}
-
-
- {:else}
-
-
- {breadcrumb.name}
-
-
- {/if}
- {#if index != $page.data.breadcrumbs.length - 1}
-
- {/if}
- {/each}
- {:else}
-
- Home
-
- {/if}
-
-
-
-
+
+
+
+
+
+
+
+
+ {#if $page.data.breadcrumbs}
+ {#each $page.data.breadcrumbs as breadcrumb, index}
+ {#if index == $page.data.breadcrumbs.length - 1}
+
+
+ {breadcrumb.name}
+
+
+ {:else}
+
+
+ {breadcrumb.name}
+
+
+
+ {/if}
+ {/each}
+ {:else}
+
+
+ Home
+
+
+ {/if}
+
+
+
-
-
-
- {#each routes as route}
-
- {/each}
-
+ class="flex max-h-[calc(100vh-64px)] flex-grow overflow-hidden">
+
+ {@render children()}
-
-
-
-
-
-
-
- {@render children()}
-
-
-
+
+
diff --git a/frontend/src/routes/(app)/dashboard/billing/+page.server.ts b/frontend/src/routes/(app)/dashboard/billing/+page.server.ts
index f722d82..147210a 100644
--- a/frontend/src/routes/(app)/dashboard/billing/+page.server.ts
+++ b/frontend/src/routes/(app)/dashboard/billing/+page.server.ts
@@ -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),
),