mirror of https://github.com/TZGyn/shortener
update sidebar to use shadcn svelte next
parent
cf258e3f40
commit
4014cf024b
Binary file not shown.
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue