Last active
February 3, 2026 02:56
-
-
Save damnordicus/9e30e22cd96862800aeb12c95a5bb1d3 to your computer and use it in GitHub Desktop.
app sidebar component using recursive collapsible components
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script lang="ts"> | |
| import * as Sidebar from "$lib/components/ui/sidebar/index.js"; | |
| import Separator from "./ui/separator/separator.svelte"; | |
| import { DoorOpenIcon, User, ChevronDown, LogIn } from "@lucide/svelte"; | |
| import { useSidebar } from "$lib/components/ui/sidebar/index.js"; | |
| import { Collapsible } from "bits-ui"; | |
| import { navItems, type NavItem } from "$lib/utils"; | |
| const settingsMenu = [ | |
| { | |
| title: "Admin", | |
| url: "/admin", | |
| icon: User, | |
| }, | |
| { | |
| title: "Login", | |
| url: "/login", | |
| icon: LogIn | |
| } | |
| ]; | |
| const { setOpenMobile } = useSidebar(); | |
| // Calculate padding based on depth (in rem units) | |
| function getIndentClass(depth: number): string { | |
| const paddings = ['pl-2', 'pl-6', 'pl-10', 'pl-14', 'pl-18']; | |
| return paddings[depth] || paddings[paddings.length - 1]; | |
| } | |
| </script> | |
| <Sidebar.Root class="md:hidden"> | |
| <Sidebar.Content> | |
| <Sidebar.Group> | |
| <Sidebar.GroupLabel>Application</Sidebar.GroupLabel> | |
| <Sidebar.GroupContent> | |
| {#each navItems as item (item.title)} | |
| {@render renderNavItem(item, 0)} | |
| {/each} | |
| </Sidebar.GroupContent> | |
| </Sidebar.Group> | |
| <Separator /> | |
| <Sidebar.Group> | |
| <Sidebar.GroupLabel>Settings</Sidebar.GroupLabel> | |
| <Sidebar.GroupContent> | |
| {#each settingsMenu as item (item.title)} | |
| <a | |
| href={item.url} | |
| class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent pl-2" | |
| onclick={() => setOpenMobile(false)} | |
| > | |
| <item.icon class="h-4 w-4 shrink-0" /> | |
| <span>{item.title}</span> | |
| </a> | |
| {/each} | |
| </Sidebar.GroupContent> | |
| </Sidebar.Group> | |
| </Sidebar.Content> | |
| </Sidebar.Root> | |
| {#snippet renderNavItem(item: NavItem, depth: number)} | |
| {#if item.sub && item.sub.length > 0} | |
| <Collapsible.Root class="group/nav"> | |
| <Collapsible.Trigger class="w-full"> | |
| <button | |
| class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent {getIndentClass(depth)}" | |
| > | |
| {#if item.icon} | |
| <item.icon class="h-4 w-4 shrink-0" /> | |
| {/if} | |
| <span class="flex-1 text-left">{item.title}</span> | |
| <ChevronDown class="h-4 w-4 shrink-0 transition-transform duration-200 group-data-[state=open]/nav:rotate-180" /> | |
| </button> | |
| </Collapsible.Trigger> | |
| <Collapsible.Content> | |
| {#each item.sub as subItem} | |
| {@render renderNavItem(subItem, depth + 1)} | |
| {/each} | |
| </Collapsible.Content> | |
| </Collapsible.Root> | |
| {:else} | |
| <a | |
| href={item.url} | |
| class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent {getIndentClass(depth)}" | |
| onclick={() => setOpenMobile(false)} | |
| > | |
| {#if item.icon} | |
| <item.icon class="h-4 w-4 shrink-0" /> | |
| {/if} | |
| <span class="flex-1 text-left">{item.title}</span> | |
| </a> | |
| {/if} | |
| {/snippet} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export type NavItem = { | |
| title: string; | |
| url: string; | |
| icon?: typeof HouseIcon; | |
| sub?: NavItem[]; | |
| }; | |
| export const navItems = [ | |
| { | |
| title: "Home", | |
| url: "/", | |
| icon: HouseIcon, | |
| }, | |
| { | |
| title: "Facilities", | |
| url: "#", | |
| icon: Building2Icon, | |
| sub: [ | |
| { | |
| title: "Fitness Center", | |
| url: "/facilities/fitness-center", | |
| sub: [ | |
| { | |
| title: "FAC", | |
| url: "/facilities/fitness-center/fac", | |
| sub: [ | |
| { | |
| title: "Score Charts", | |
| url: "/facilities/fitness-center/fac/score-charts" | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| { | |
| title: "Nose Dock", | |
| url: "/facilities/nose-dock", | |
| }, | |
| { | |
| title: "Jungle", | |
| url: "/facilities/jungle", | |
| }, | |
| { | |
| title: "Running Routes", | |
| url: "/facilities/running-routes" | |
| }, | |
| ] | |
| }, | |
| { | |
| title: "Health Promotion", | |
| url: "#", | |
| icon: HeartPlus, | |
| sub: [ | |
| { | |
| title: "Fitness", | |
| url: "/health-promotion/fitness", | |
| }, | |
| { | |
| title: "Sleep Optimization", | |
| url: "/health-promotion/sleep-optimization", | |
| }, | |
| { | |
| title: "Tobacco/ Nicotine Cessation", | |
| url: "/health-promotion/tobacco-nicotine", | |
| }, | |
| { | |
| title: "Body Composition", | |
| url: "/health-promotion/body-composition", | |
| }, | |
| { | |
| title: "Nutrition", | |
| url: "/health-promotion/nutrition", | |
| sub: [ | |
| { | |
| title: "DFAC", | |
| url: "/health-promotion/dfac", | |
| sub: [ | |
| { | |
| title: "Go 4 Green", | |
| url: "/health-promotion/dfac/go-4-green", | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| ] | |
| }, | |
| { | |
| title: "Programs", | |
| url: "#", | |
| icon: NotebookText, | |
| sub: [ | |
| { | |
| title: "FIP Classes", | |
| url: "/programs/fip-classes", | |
| sub: [ | |
| { | |
| title: "Combat Ready", | |
| url: "/programs/fip-classes/combat-ready", | |
| }, | |
| { | |
| title: "Cardio", | |
| url: "/programs/fip-classes/cardio", | |
| }, | |
| { | |
| title: "Strength", | |
| url: "/programs/fip-classes/strength", | |
| }, | |
| ] | |
| }, | |
| { | |
| title: "Self-Directed", | |
| url: "/programs/self-directed", | |
| sub: [ | |
| { | |
| title: "Cardio", | |
| url: "/programs/self-directed/cardio", | |
| }, | |
| { | |
| title: "Strength", | |
| url: "/programs/self-directed/strength", | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| { | |
| title: "Community", | |
| url: "#", | |
| icon: UsersIcon, | |
| sub: [ | |
| { | |
| title: "Running", | |
| url: "/community/running", | |
| }, | |
| { | |
| title: "Cycling", | |
| url: "/community/cycling", | |
| }, | |
| { | |
| title: "Group X", | |
| url: "/community/group-x", | |
| } | |
| ] | |
| }, | |
| { | |
| title: "Challenges", | |
| url: "#", | |
| icon: SwordsIcon, | |
| sub: [ | |
| { | |
| title: "Fitness Incentive", | |
| url: "/challenges/fitness-incentive", | |
| sub: [ | |
| { | |
| title: "Warrior Ready", | |
| url: "/challenges/fitness-incentive/warrior-ready", | |
| } | |
| ] | |
| }, | |
| { | |
| title: "PFA 100 Club", | |
| url: "/challenges/pfa-100-club" | |
| } | |
| ] | |
| } | |
| ]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Collapsibleshould be imported from your components notbits-ui<Collapsible.Root class="group/nav">is using the same group class<Collapsible.Trigger class="w-full">you'll want to use thechild()snippet so you can grabpropsand add it to the<button><Collapsible.Content>inside the#each