28 changed files with 3157 additions and 599 deletions
@ -1 +0,0 @@ |
|||||
Subproject commit a1b8c3d171445b2eebfd4b5bd1e4876f3bbed605 |
|
||||
@ -0,0 +1,22 @@ |
|||||
|
{ |
||||
|
"$schema": "https://ui.shadcn.com/schema.json", |
||||
|
"style": "new-york", |
||||
|
"rsc": false, |
||||
|
"tsx": true, |
||||
|
"tailwind": { |
||||
|
"config": "", |
||||
|
"css": "src/base.css", |
||||
|
"baseColor": "slate", |
||||
|
"cssVariables": true, |
||||
|
"prefix": "" |
||||
|
}, |
||||
|
"iconLibrary": "lucide", |
||||
|
"aliases": { |
||||
|
"components": "@/components", |
||||
|
"utils": "@/lib/utils", |
||||
|
"ui": "@/components/ui", |
||||
|
"lib": "@/lib", |
||||
|
"hooks": "@/hooks" |
||||
|
}, |
||||
|
"registries": {} |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
{ |
||||
|
"name": "@meshtastic/ui", |
||||
|
"version": "0.1.0", |
||||
|
"license": "GPL-3.0-only", |
||||
|
"repository": { |
||||
|
"type": "git", |
||||
|
"url": "git+https://github.com/meshtastic/web.git" |
||||
|
}, |
||||
|
"type": "module", |
||||
|
"files": [ |
||||
|
"dist", |
||||
|
"!dist/**/*.test.*" |
||||
|
], |
||||
|
"sideEffects": [ |
||||
|
"**/*.css" |
||||
|
], |
||||
|
"exports": { |
||||
|
".": { |
||||
|
"types": "./dist/index.d.ts", |
||||
|
"default": "./dist/index.js" |
||||
|
}, |
||||
|
"./theme/default.css": "./dist/theme/default.css" |
||||
|
}, |
||||
|
"main": "./dist/index.js", |
||||
|
"module": "./dist/index.js", |
||||
|
"types": "./dist/index.d.ts", |
||||
|
"scripts": { |
||||
|
"start": "npm run dev", |
||||
|
"dev": "vite dev", |
||||
|
"watch": "vite build --watch", |
||||
|
"build": "vite build && publint", |
||||
|
"typecheck": "tsc -p tsconfig.json --noEmit", |
||||
|
"preview": "vite preview", |
||||
|
"lint": "eslint . --max-warnings 0", |
||||
|
"lint:fix": "npm run lint -- --fix", |
||||
|
"format": "prettier --check .", |
||||
|
"format:fix": "prettier --write .", |
||||
|
"test": "vitest" |
||||
|
}, |
||||
|
"peerDependencies": { |
||||
|
"@radix-ui/react-slot": ">=1.0.2", |
||||
|
"class-variance-authority": ">=0.7.0", |
||||
|
"react": ">=19", |
||||
|
"react-dom": ">=19", |
||||
|
"tailwind-merge": ">=2.5.0", |
||||
|
"tailwindcss": "^4.1.7" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@radix-ui/react-collapsible": "^1.1.12", |
||||
|
"@radix-ui/react-dialog": "^1.1.15", |
||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16", |
||||
|
"@radix-ui/react-separator": "^1.1.7", |
||||
|
"@radix-ui/react-slot": "^1.2.3", |
||||
|
"@radix-ui/react-tooltip": "^1.2.8", |
||||
|
"@tanstack/react-router": "^1.132.47", |
||||
|
"class-variance-authority": "^0.7.1", |
||||
|
"clsx": "^2.1.1", |
||||
|
"lucide-react": "^0.545.0", |
||||
|
"tailwind-merge": "^2.6.0" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@tailwindcss/postcss": "^4.1.7", |
||||
|
"@tailwindcss/vite": "^4.1.14", |
||||
|
"@types/react": "^18.3.5", |
||||
|
"@types/react-dom": "^18.3.0", |
||||
|
"@vitejs/plugin-react": "^4.3.4", |
||||
|
"publint": "^0.3.14", |
||||
|
"tailwindcss": "^4.1.14", |
||||
|
"tw-animate-css": "^1.4.0", |
||||
|
"typescript": "^5.6.3", |
||||
|
"vite": "^7.0.0", |
||||
|
"vite-plugin-dts": "^4.5.4", |
||||
|
"vite-plugin-static-copy": "^3.1.4", |
||||
|
"vitest": "^3.0.0" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
body.dark { |
||||
|
color-scheme: dark; |
||||
|
} |
||||
|
|
||||
|
body:not(.dark) { |
||||
|
color-scheme: light; |
||||
|
} |
||||
@ -0,0 +1,73 @@ |
|||||
|
import { createContext, useContext, useEffect, useState } from "react" |
||||
|
|
||||
|
type Theme = "dark" | "light" | "system" |
||||
|
|
||||
|
type ThemeProviderProps = { |
||||
|
children: React.ReactNode |
||||
|
defaultTheme?: Theme |
||||
|
storageKey?: string |
||||
|
} |
||||
|
|
||||
|
type ThemeProviderState = { |
||||
|
theme: Theme |
||||
|
setTheme: (theme: Theme) => void |
||||
|
} |
||||
|
|
||||
|
const initialState: ThemeProviderState = { |
||||
|
theme: "system", |
||||
|
setTheme: () => null, |
||||
|
} |
||||
|
|
||||
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState) |
||||
|
|
||||
|
export function ThemeProvider({ |
||||
|
children, |
||||
|
defaultTheme = "system", |
||||
|
storageKey = "vite-ui-theme", |
||||
|
...props |
||||
|
}: ThemeProviderProps) { |
||||
|
const [theme, setTheme] = useState<Theme>( |
||||
|
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme |
||||
|
) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const root = window.document.documentElement |
||||
|
|
||||
|
root.classList.remove("light", "dark") |
||||
|
|
||||
|
if (theme === "system") { |
||||
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") |
||||
|
.matches |
||||
|
? "dark" |
||||
|
: "light" |
||||
|
|
||||
|
root.classList.add(systemTheme) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
root.classList.add(theme) |
||||
|
}, [theme]) |
||||
|
|
||||
|
const value = { |
||||
|
theme, |
||||
|
setTheme: (theme: Theme) => { |
||||
|
localStorage.setItem(storageKey, theme) |
||||
|
setTheme(theme) |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<ThemeProviderContext.Provider {...props} value={value}> |
||||
|
{children} |
||||
|
</ThemeProviderContext.Provider> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export const useTheme = () => { |
||||
|
const context = useContext(ThemeProviderContext) |
||||
|
|
||||
|
if (context === undefined) |
||||
|
throw new Error("useTheme must be used within a ThemeProvider") |
||||
|
|
||||
|
return context |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
import * as React from "react" |
||||
|
import { Slot } from "@radix-ui/react-slot" |
||||
|
import { cva, type VariantProps } from "class-variance-authority" |
||||
|
|
||||
|
import { cn } from "@/lib/utils" |
||||
|
|
||||
|
const badgeVariants = cva( |
||||
|
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", |
||||
|
{ |
||||
|
variants: { |
||||
|
variant: { |
||||
|
default: |
||||
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", |
||||
|
secondary: |
||||
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", |
||||
|
destructive: |
||||
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", |
||||
|
outline: |
||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", |
||||
|
}, |
||||
|
}, |
||||
|
defaultVariants: { |
||||
|
variant: "default", |
||||
|
}, |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
function Badge({ |
||||
|
className, |
||||
|
variant, |
||||
|
asChild = false, |
||||
|
...props |
||||
|
}: React.ComponentProps<"span"> & |
||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) { |
||||
|
const Comp = asChild ? Slot : "span" |
||||
|
|
||||
|
return ( |
||||
|
<Comp |
||||
|
data-slot="badge" |
||||
|
className={cn(badgeVariants({ variant }), className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export { Badge, badgeVariants } |
||||
@ -0,0 +1,60 @@ |
|||||
|
import * as React from "react" |
||||
|
import { Slot } from "@radix-ui/react-slot" |
||||
|
import { cva, type VariantProps } from "class-variance-authority" |
||||
|
|
||||
|
import { cn } from "@/lib/utils" |
||||
|
|
||||
|
const buttonVariants = cva( |
||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", |
||||
|
{ |
||||
|
variants: { |
||||
|
variant: { |
||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90", |
||||
|
destructive: |
||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", |
||||
|
outline: |
||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", |
||||
|
secondary: |
||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80", |
||||
|
ghost: |
||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", |
||||
|
link: "text-primary underline-offset-4 hover:underline", |
||||
|
}, |
||||
|
size: { |
||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3", |
||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", |
||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", |
||||
|
icon: "size-9", |
||||
|
"icon-sm": "size-8", |
||||
|
"icon-lg": "size-10", |
||||
|
}, |
||||
|
}, |
||||
|
defaultVariants: { |
||||
|
variant: "default", |
||||
|
size: "default", |
||||
|
}, |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
function Button({ |
||||
|
className, |
||||
|
variant, |
||||
|
size, |
||||
|
asChild = false, |
||||
|
...props |
||||
|
}: React.ComponentProps<"button"> & |
||||
|
VariantProps<typeof buttonVariants> & { |
||||
|
asChild?: boolean |
||||
|
}) { |
||||
|
const Comp = asChild ? Slot : "button" |
||||
|
|
||||
|
return ( |
||||
|
<Comp |
||||
|
data-slot="button" |
||||
|
className={cn(buttonVariants({ variant, size, className }))} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export { Button, buttonVariants } |
||||
@ -0,0 +1,31 @@ |
|||||
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" |
||||
|
|
||||
|
function Collapsible({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) { |
||||
|
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} /> |
||||
|
} |
||||
|
|
||||
|
function CollapsibleTrigger({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) { |
||||
|
return ( |
||||
|
<CollapsiblePrimitive.CollapsibleTrigger |
||||
|
data-slot="collapsible-trigger" |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function CollapsibleContent({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) { |
||||
|
return ( |
||||
|
<CollapsiblePrimitive.CollapsibleContent |
||||
|
data-slot="collapsible-content" |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent } |
||||
@ -0,0 +1,255 @@ |
|||||
|
import * as React from "react" |
||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" |
||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" |
||||
|
|
||||
|
import { cn } from "@/lib/utils" |
||||
|
|
||||
|
function DropdownMenu({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { |
||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuPortal({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { |
||||
|
return ( |
||||
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuTrigger({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { |
||||
|
return ( |
||||
|
<DropdownMenuPrimitive.Trigger |
||||
|
data-slot="dropdown-menu-trigger" |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuContent({ |
||||
|
className, |
||||
|
sideOffset = 4, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { |
||||
|
return ( |
||||
|
<DropdownMenuPrimitive.Portal> |
||||
|
<DropdownMenuPrimitive.Content |
||||
|
data-slot="dropdown-menu-content" |
||||
|
sideOffset={sideOffset} |
||||
|
className={cn( |
||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", |
||||
|
className |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
</DropdownMenuPrimitive.Portal> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuGroup({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { |
||||
|
return ( |
||||
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuItem({ |
||||
|
className, |
||||
|
inset, |
||||
|
variant = "default", |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { |
||||
|
inset?: boolean |
||||
|
variant?: "default" | "destructive" |
||||
|
}) { |
||||
|
return ( |
||||
|
<DropdownMenuPrimitive.Item |
||||
|
data-slot="dropdown-menu-item" |
||||
|
data-inset={inset} |
||||
|
data-variant={variant} |
||||
|
className={cn( |
||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", |
||||
|
className |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuCheckboxItem({ |
||||
|
className, |
||||
|
children, |
||||
|
checked, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { |
||||
|
return ( |
||||
|
<DropdownMenuPrimitive.CheckboxItem |
||||
|
data-slot="dropdown-menu-checkbox-item" |
||||
|
className={cn( |
||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", |
||||
|
className |
||||
|
)} |
||||
|
checked={checked} |
||||
|
{...props} |
||||
|
> |
||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> |
||||
|
<DropdownMenuPrimitive.ItemIndicator> |
||||
|
<CheckIcon className="size-4" /> |
||||
|
</DropdownMenuPrimitive.ItemIndicator> |
||||
|
</span> |
||||
|
{children} |
||||
|
</DropdownMenuPrimitive.CheckboxItem> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuRadioGroup({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { |
||||
|
return ( |
||||
|
<DropdownMenuPrimitive.RadioGroup |
||||
|
data-slot="dropdown-menu-radio-group" |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuRadioItem({ |
||||
|
className, |
||||
|
children, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { |
||||
|
return ( |
||||
|
<DropdownMenuPrimitive.RadioItem |
||||
|
data-slot="dropdown-menu-radio-item" |
||||
|
className={cn( |
||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", |
||||
|
className |
||||
|
)} |
||||
|
{...props} |
||||
|
> |
||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> |
||||
|
<DropdownMenuPrimitive.ItemIndicator> |
||||
|
<CircleIcon className="size-2 fill-current" /> |
||||
|
</DropdownMenuPrimitive.ItemIndicator> |
||||
|
</span> |
||||
|
{children} |
||||
|
</DropdownMenuPrimitive.RadioItem> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuLabel({ |
||||
|
className, |
||||
|
inset, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { |
||||
|
inset?: boolean |
||||
|
}) { |
||||
|
return ( |
||||
|
<DropdownMenuPrimitive.Label |
||||
|
data-slot="dropdown-menu-label" |
||||
|
data-inset={inset} |
||||
|
className={cn( |
||||
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", |
||||
|
className |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuSeparator({ |
||||
|
className, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { |
||||
|
return ( |
||||
|
<DropdownMenuPrimitive.Separator |
||||
|
data-slot="dropdown-menu-separator" |
||||
|
className={cn("bg-border -mx-1 my-1 h-px", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuShortcut({ |
||||
|
className, |
||||
|
...props |
||||
|
}: React.ComponentProps<"span">) { |
||||
|
return ( |
||||
|
<span |
||||
|
data-slot="dropdown-menu-shortcut" |
||||
|
className={cn( |
||||
|
"text-muted-foreground ml-auto text-xs tracking-widest", |
||||
|
className |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuSub({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { |
||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} /> |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuSubTrigger({ |
||||
|
className, |
||||
|
inset, |
||||
|
children, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { |
||||
|
inset?: boolean |
||||
|
}) { |
||||
|
return ( |
||||
|
<DropdownMenuPrimitive.SubTrigger |
||||
|
data-slot="dropdown-menu-sub-trigger" |
||||
|
data-inset={inset} |
||||
|
className={cn( |
||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", |
||||
|
className |
||||
|
)} |
||||
|
{...props} |
||||
|
> |
||||
|
{children} |
||||
|
<ChevronRightIcon className="ml-auto size-4" /> |
||||
|
</DropdownMenuPrimitive.SubTrigger> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function DropdownMenuSubContent({ |
||||
|
className, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { |
||||
|
return ( |
||||
|
<DropdownMenuPrimitive.SubContent |
||||
|
data-slot="dropdown-menu-sub-content" |
||||
|
className={cn( |
||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", |
||||
|
className |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export { |
||||
|
DropdownMenu, |
||||
|
DropdownMenuPortal, |
||||
|
DropdownMenuTrigger, |
||||
|
DropdownMenuContent, |
||||
|
DropdownMenuGroup, |
||||
|
DropdownMenuLabel, |
||||
|
DropdownMenuItem, |
||||
|
DropdownMenuCheckboxItem, |
||||
|
DropdownMenuRadioGroup, |
||||
|
DropdownMenuRadioItem, |
||||
|
DropdownMenuSeparator, |
||||
|
DropdownMenuShortcut, |
||||
|
DropdownMenuSub, |
||||
|
DropdownMenuSubTrigger, |
||||
|
DropdownMenuSubContent, |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
import * as React from "react" |
||||
|
|
||||
|
import { cn } from "@/lib/utils" |
||||
|
|
||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) { |
||||
|
return ( |
||||
|
<input |
||||
|
type={type} |
||||
|
data-slot="input" |
||||
|
className={cn( |
||||
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", |
||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", |
||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", |
||||
|
className |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export { Input } |
||||
@ -0,0 +1,26 @@ |
|||||
|
import * as React from "react" |
||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator" |
||||
|
|
||||
|
import { cn } from "@/lib/utils" |
||||
|
|
||||
|
function Separator({ |
||||
|
className, |
||||
|
orientation = "horizontal", |
||||
|
decorative = true, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) { |
||||
|
return ( |
||||
|
<SeparatorPrimitive.Root |
||||
|
data-slot="separator" |
||||
|
decorative={decorative} |
||||
|
orientation={orientation} |
||||
|
className={cn( |
||||
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", |
||||
|
className |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export { Separator } |
||||
@ -0,0 +1,139 @@ |
|||||
|
"use client" |
||||
|
|
||||
|
import * as React from "react" |
||||
|
import * as SheetPrimitive from "@radix-ui/react-dialog" |
||||
|
import { XIcon } from "lucide-react" |
||||
|
|
||||
|
import { cn } from "@/lib/utils" |
||||
|
|
||||
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { |
||||
|
return <SheetPrimitive.Root data-slot="sheet" {...props} /> |
||||
|
} |
||||
|
|
||||
|
function SheetTrigger({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) { |
||||
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> |
||||
|
} |
||||
|
|
||||
|
function SheetClose({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) { |
||||
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> |
||||
|
} |
||||
|
|
||||
|
function SheetPortal({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) { |
||||
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> |
||||
|
} |
||||
|
|
||||
|
function SheetOverlay({ |
||||
|
className, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) { |
||||
|
return ( |
||||
|
<SheetPrimitive.Overlay |
||||
|
data-slot="sheet-overlay" |
||||
|
className={cn( |
||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", |
||||
|
className |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function SheetContent({ |
||||
|
className, |
||||
|
children, |
||||
|
side = "right", |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & { |
||||
|
side?: "top" | "right" | "bottom" | "left" |
||||
|
}) { |
||||
|
return ( |
||||
|
<SheetPortal> |
||||
|
<SheetOverlay /> |
||||
|
<SheetPrimitive.Content |
||||
|
data-slot="sheet-content" |
||||
|
className={cn( |
||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", |
||||
|
side === "right" && |
||||
|
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", |
||||
|
side === "left" && |
||||
|
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", |
||||
|
side === "top" && |
||||
|
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", |
||||
|
side === "bottom" && |
||||
|
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", |
||||
|
className |
||||
|
)} |
||||
|
{...props} |
||||
|
> |
||||
|
{children} |
||||
|
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> |
||||
|
<XIcon className="size-4" /> |
||||
|
<span className="sr-only">Close</span> |
||||
|
</SheetPrimitive.Close> |
||||
|
</SheetPrimitive.Content> |
||||
|
</SheetPortal> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { |
||||
|
return ( |
||||
|
<div |
||||
|
data-slot="sheet-header" |
||||
|
className={cn("flex flex-col gap-1.5 p-4", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { |
||||
|
return ( |
||||
|
<div |
||||
|
data-slot="sheet-footer" |
||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function SheetTitle({ |
||||
|
className, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof SheetPrimitive.Title>) { |
||||
|
return ( |
||||
|
<SheetPrimitive.Title |
||||
|
data-slot="sheet-title" |
||||
|
className={cn("text-foreground font-semibold", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function SheetDescription({ |
||||
|
className, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof SheetPrimitive.Description>) { |
||||
|
return ( |
||||
|
<SheetPrimitive.Description |
||||
|
data-slot="sheet-description" |
||||
|
className={cn("text-muted-foreground text-sm", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export { |
||||
|
Sheet, |
||||
|
SheetTrigger, |
||||
|
SheetClose, |
||||
|
SheetContent, |
||||
|
SheetHeader, |
||||
|
SheetFooter, |
||||
|
SheetTitle, |
||||
|
SheetDescription, |
||||
|
} |
||||
@ -0,0 +1,726 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import * as React from "react"; |
||||
|
import { Slot } from "@radix-ui/react-slot"; |
||||
|
import { cva, type VariantProps } from "class-variance-authority"; |
||||
|
import { PanelLeftIcon } from "lucide-react"; |
||||
|
|
||||
|
import { useIsMobile } from "@/hooks/use-mobile"; |
||||
|
import { cn } from "@/lib/utils"; |
||||
|
import { Button } from "@/components/ui/button"; |
||||
|
import { Input } from "@/components/ui/input"; |
||||
|
import { Separator } from "@/components/ui/separator"; |
||||
|
import { |
||||
|
Sheet, |
||||
|
SheetContent, |
||||
|
SheetDescription, |
||||
|
SheetHeader, |
||||
|
SheetTitle, |
||||
|
} from "@/components/ui/sheet"; |
||||
|
import { Skeleton } from "@/components/ui/skeleton"; |
||||
|
import { |
||||
|
Tooltip, |
||||
|
TooltipContent, |
||||
|
TooltipProvider, |
||||
|
TooltipTrigger, |
||||
|
} from "@/components/ui/tooltip"; |
||||
|
|
||||
|
const SIDEBAR_COOKIE_NAME = "sidebar_state"; |
||||
|
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; |
||||
|
const SIDEBAR_WIDTH = "16rem"; |
||||
|
const SIDEBAR_WIDTH_MOBILE = "18rem"; |
||||
|
const SIDEBAR_WIDTH_ICON = "3rem"; |
||||
|
const SIDEBAR_KEYBOARD_SHORTCUT = "b"; |
||||
|
|
||||
|
type SidebarContextProps = { |
||||
|
state: "expanded" | "collapsed"; |
||||
|
open: boolean; |
||||
|
setOpen: (open: boolean) => void; |
||||
|
openMobile: boolean; |
||||
|
setOpenMobile: (open: boolean) => void; |
||||
|
isMobile: boolean; |
||||
|
toggleSidebar: () => void; |
||||
|
}; |
||||
|
|
||||
|
const SidebarContext = React.createContext<SidebarContextProps | null>(null); |
||||
|
|
||||
|
function useSidebar() { |
||||
|
const context = React.useContext(SidebarContext); |
||||
|
if (!context) { |
||||
|
throw new Error("useSidebar must be used within a SidebarProvider."); |
||||
|
} |
||||
|
|
||||
|
return context; |
||||
|
} |
||||
|
|
||||
|
function SidebarProvider({ |
||||
|
defaultOpen = true, |
||||
|
open: openProp, |
||||
|
onOpenChange: setOpenProp, |
||||
|
className, |
||||
|
style, |
||||
|
children, |
||||
|
...props |
||||
|
}: React.ComponentProps<"div"> & { |
||||
|
defaultOpen?: boolean; |
||||
|
open?: boolean; |
||||
|
onOpenChange?: (open: boolean) => void; |
||||
|
}) { |
||||
|
const isMobile = useIsMobile(); |
||||
|
const [openMobile, setOpenMobile] = React.useState(false); |
||||
|
|
||||
|
// This is the internal state of the sidebar.
|
||||
|
// We use openProp and setOpenProp for control from outside the component.
|
||||
|
const [_open, _setOpen] = React.useState(defaultOpen); |
||||
|
const open = openProp ?? _open; |
||||
|
const setOpen = React.useCallback( |
||||
|
(value: boolean | ((value: boolean) => boolean)) => { |
||||
|
const openState = typeof value === "function" ? value(open) : value; |
||||
|
if (setOpenProp) { |
||||
|
setOpenProp(openState); |
||||
|
} else { |
||||
|
_setOpen(openState); |
||||
|
} |
||||
|
|
||||
|
// This sets the cookie to keep the sidebar state.
|
||||
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; |
||||
|
}, |
||||
|
[setOpenProp, open], |
||||
|
); |
||||
|
|
||||
|
// Helper to toggle the sidebar.
|
||||
|
const toggleSidebar = React.useCallback(() => { |
||||
|
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); |
||||
|
}, [isMobile, setOpen, setOpenMobile]); |
||||
|
|
||||
|
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
|
React.useEffect(() => { |
||||
|
const handleKeyDown = (event: KeyboardEvent) => { |
||||
|
if ( |
||||
|
event.key === SIDEBAR_KEYBOARD_SHORTCUT && |
||||
|
(event.metaKey || event.ctrlKey) |
||||
|
) { |
||||
|
event.preventDefault(); |
||||
|
toggleSidebar(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
window.addEventListener("keydown", handleKeyDown); |
||||
|
return () => window.removeEventListener("keydown", handleKeyDown); |
||||
|
}, [toggleSidebar]); |
||||
|
|
||||
|
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
|
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
|
const state = open ? "expanded" : "collapsed"; |
||||
|
|
||||
|
const contextValue = React.useMemo<SidebarContextProps>( |
||||
|
() => ({ |
||||
|
state, |
||||
|
open, |
||||
|
setOpen, |
||||
|
isMobile, |
||||
|
openMobile, |
||||
|
setOpenMobile, |
||||
|
toggleSidebar, |
||||
|
}), |
||||
|
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], |
||||
|
); |
||||
|
|
||||
|
return ( |
||||
|
<SidebarContext.Provider value={contextValue}> |
||||
|
<TooltipProvider delayDuration={0}> |
||||
|
<div |
||||
|
data-slot="sidebar-wrapper" |
||||
|
style={ |
||||
|
{ |
||||
|
"--sidebar-width": SIDEBAR_WIDTH, |
||||
|
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON, |
||||
|
...style, |
||||
|
} as React.CSSProperties |
||||
|
} |
||||
|
className={cn( |
||||
|
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", |
||||
|
className, |
||||
|
)} |
||||
|
{...props} |
||||
|
> |
||||
|
{children} |
||||
|
</div> |
||||
|
</TooltipProvider> |
||||
|
</SidebarContext.Provider> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function Sidebar({ |
||||
|
side = "left", |
||||
|
variant = "sidebar", |
||||
|
collapsible = "offcanvas", |
||||
|
className, |
||||
|
children, |
||||
|
...props |
||||
|
}: React.ComponentProps<"div"> & { |
||||
|
side?: "left" | "right"; |
||||
|
variant?: "sidebar" | "floating" | "inset"; |
||||
|
collapsible?: "offcanvas" | "icon" | "none"; |
||||
|
}) { |
||||
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); |
||||
|
|
||||
|
if (collapsible === "none") { |
||||
|
return ( |
||||
|
<div |
||||
|
data-slot="sidebar" |
||||
|
className={cn( |
||||
|
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", |
||||
|
className, |
||||
|
)} |
||||
|
{...props} |
||||
|
> |
||||
|
{children} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (isMobile) { |
||||
|
return ( |
||||
|
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> |
||||
|
<SheetContent |
||||
|
data-sidebar="sidebar" |
||||
|
data-slot="sidebar" |
||||
|
data-mobile="true" |
||||
|
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" |
||||
|
style={ |
||||
|
{ |
||||
|
"--sidebar-width": SIDEBAR_WIDTH_MOBILE, |
||||
|
} as React.CSSProperties |
||||
|
} |
||||
|
side={side} |
||||
|
> |
||||
|
<SheetHeader className="sr-only"> |
||||
|
<SheetTitle>Sidebar</SheetTitle> |
||||
|
<SheetDescription>Displays the mobile sidebar.</SheetDescription> |
||||
|
</SheetHeader> |
||||
|
<div className="flex h-full w-full flex-col">{children}</div> |
||||
|
</SheetContent> |
||||
|
</Sheet> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
className="group peer text-sidebar-foreground hidden md:block" |
||||
|
data-state={state} |
||||
|
data-collapsible={state === "collapsed" ? collapsible : ""} |
||||
|
data-variant={variant} |
||||
|
data-side={side} |
||||
|
data-slot="sidebar" |
||||
|
> |
||||
|
{/* This is what handles the sidebar gap on desktop */} |
||||
|
<div |
||||
|
data-slot="sidebar-gap" |
||||
|
className={cn( |
||||
|
"relative 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)+(--spacing(4)))]" |
||||
|
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)", |
||||
|
)} |
||||
|
/> |
||||
|
<div |
||||
|
data-slot="sidebar-container" |
||||
|
className={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)+(--spacing(4))+2px)]" |
||||
|
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l border-sidebar-border", |
||||
|
className, |
||||
|
)} |
||||
|
{...props} |
||||
|
> |
||||
|
<div |
||||
|
data-sidebar="sidebar" |
||||
|
data-slot="sidebar-inner" |
||||
|
className="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-sm" |
||||
|
> |
||||
|
{children} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarTrigger({ |
||||
|
className, |
||||
|
onClick, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof Button>) { |
||||
|
const { toggleSidebar } = useSidebar(); |
||||
|
|
||||
|
return ( |
||||
|
<Button |
||||
|
data-sidebar="trigger" |
||||
|
data-slot="sidebar-trigger" |
||||
|
variant="ghost" |
||||
|
size="icon" |
||||
|
className={cn("size-7", className)} |
||||
|
onClick={(event) => { |
||||
|
onClick?.(event); |
||||
|
toggleSidebar(); |
||||
|
}} |
||||
|
{...props} |
||||
|
> |
||||
|
<PanelLeftIcon /> |
||||
|
<span className="sr-only">Toggle Sidebar</span> |
||||
|
</Button> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { |
||||
|
const { toggleSidebar } = useSidebar(); |
||||
|
|
||||
|
return ( |
||||
|
<button |
||||
|
data-sidebar="rail" |
||||
|
data-slot="sidebar-rail" |
||||
|
aria-label="Toggle Sidebar" |
||||
|
tabIndex={-1} |
||||
|
onClick={toggleSidebar} |
||||
|
title="Toggle Sidebar" |
||||
|
className={cn( |
||||
|
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex", |
||||
|
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", |
||||
|
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", |
||||
|
"hover:group-data-[collapsible=offcanvas]: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, |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { |
||||
|
return ( |
||||
|
<main |
||||
|
data-slot="sidebar-inset" |
||||
|
className={cn( |
||||
|
"bg-background relative flex w-full flex-1 flex-col", |
||||
|
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", |
||||
|
className, |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarInput({ |
||||
|
className, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof Input>) { |
||||
|
return ( |
||||
|
<Input |
||||
|
data-slot="sidebar-input" |
||||
|
data-sidebar="input" |
||||
|
className={cn("bg-background h-8 w-full shadow-none", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { |
||||
|
return ( |
||||
|
<div |
||||
|
data-slot="sidebar-header" |
||||
|
data-sidebar="header" |
||||
|
className={cn("flex flex-col gap-2 p-2", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { |
||||
|
return ( |
||||
|
<div |
||||
|
data-slot="sidebar-footer" |
||||
|
data-sidebar="footer" |
||||
|
className={cn("flex flex-col gap-2 p-2", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarSeparator({ |
||||
|
className, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof Separator>) { |
||||
|
return ( |
||||
|
<Separator |
||||
|
data-slot="sidebar-separator" |
||||
|
data-sidebar="separator" |
||||
|
className={cn("bg-sidebar-border mx-2 w-auto", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { |
||||
|
return ( |
||||
|
<div |
||||
|
data-slot="sidebar-content" |
||||
|
data-sidebar="content" |
||||
|
className={cn( |
||||
|
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", |
||||
|
className, |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { |
||||
|
return ( |
||||
|
<div |
||||
|
data-slot="sidebar-group" |
||||
|
data-sidebar="group" |
||||
|
className={cn("relative flex w-full min-w-0 flex-col p-2", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarGroupLabel({ |
||||
|
className, |
||||
|
asChild = false, |
||||
|
...props |
||||
|
}: React.ComponentProps<"div"> & { asChild?: boolean }) { |
||||
|
const Comp = asChild ? Slot : "div"; |
||||
|
|
||||
|
return ( |
||||
|
<Comp |
||||
|
data-slot="sidebar-group-label" |
||||
|
data-sidebar="group-label" |
||||
|
className={cn( |
||||
|
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] 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, |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarGroupAction({ |
||||
|
className, |
||||
|
asChild = false, |
||||
|
...props |
||||
|
}: React.ComponentProps<"button"> & { asChild?: boolean }) { |
||||
|
const Comp = asChild ? Slot : "button"; |
||||
|
|
||||
|
return ( |
||||
|
<Comp |
||||
|
data-slot="sidebar-group-action" |
||||
|
data-sidebar="group-action" |
||||
|
className={cn( |
||||
|
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden 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 md:after:hidden", |
||||
|
"group-data-[collapsible=icon]:hidden", |
||||
|
className, |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarGroupContent({ |
||||
|
className, |
||||
|
...props |
||||
|
}: React.ComponentProps<"div">) { |
||||
|
return ( |
||||
|
<div |
||||
|
data-slot="sidebar-group-content" |
||||
|
data-sidebar="group-content" |
||||
|
className={cn("w-full text-sm", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { |
||||
|
return ( |
||||
|
<ul |
||||
|
data-slot="sidebar-menu" |
||||
|
data-sidebar="menu" |
||||
|
className={cn("flex w-full min-w-0 flex-col gap-1", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { |
||||
|
return ( |
||||
|
<li |
||||
|
data-slot="sidebar-menu-item" |
||||
|
data-sidebar="menu-item" |
||||
|
className={cn("group/menu-item relative", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const sidebarMenuButtonVariants = cva( |
||||
|
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground 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]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground 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 shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground 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", |
||||
|
}, |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
function SidebarMenuButton({ |
||||
|
asChild = false, |
||||
|
isActive = false, |
||||
|
variant = "default", |
||||
|
size = "default", |
||||
|
tooltip, |
||||
|
className, |
||||
|
...props |
||||
|
}: React.ComponentProps<"button"> & { |
||||
|
asChild?: boolean; |
||||
|
isActive?: boolean; |
||||
|
tooltip?: string | React.ComponentProps<typeof TooltipContent>; |
||||
|
} & VariantProps<typeof sidebarMenuButtonVariants>) { |
||||
|
const Comp = asChild ? Slot : "button"; |
||||
|
const { isMobile, state } = useSidebar(); |
||||
|
|
||||
|
const button = ( |
||||
|
<Comp |
||||
|
data-slot="sidebar-menu-button" |
||||
|
data-sidebar="menu-button" |
||||
|
data-size={size} |
||||
|
data-active={isActive} |
||||
|
className={cn(sidebarMenuButtonVariants({ variant, size }), className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
|
||||
|
if (!tooltip) { |
||||
|
return button; |
||||
|
} |
||||
|
|
||||
|
if (typeof tooltip === "string") { |
||||
|
tooltip = { |
||||
|
children: tooltip, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Tooltip> |
||||
|
<TooltipTrigger asChild>{button}</TooltipTrigger> |
||||
|
<TooltipContent |
||||
|
side="right" |
||||
|
align="center" |
||||
|
hidden={state !== "collapsed" || isMobile} |
||||
|
{...tooltip} |
||||
|
/> |
||||
|
</Tooltip> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarMenuAction({ |
||||
|
className, |
||||
|
asChild = false, |
||||
|
showOnHover = false, |
||||
|
...props |
||||
|
}: React.ComponentProps<"button"> & { |
||||
|
asChild?: boolean; |
||||
|
showOnHover?: boolean; |
||||
|
}) { |
||||
|
const Comp = asChild ? Slot : "button"; |
||||
|
|
||||
|
return ( |
||||
|
<Comp |
||||
|
data-slot="sidebar-menu-action" |
||||
|
data-sidebar="menu-action" |
||||
|
className={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 top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden 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 md:after: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, |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarMenuBadge({ |
||||
|
className, |
||||
|
...props |
||||
|
}: React.ComponentProps<"div">) { |
||||
|
return ( |
||||
|
<div |
||||
|
data-slot="sidebar-menu-badge" |
||||
|
data-sidebar="menu-badge" |
||||
|
className={cn( |
||||
|
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none", |
||||
|
"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, |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarMenuSkeleton({ |
||||
|
className, |
||||
|
showIcon = false, |
||||
|
...props |
||||
|
}: React.ComponentProps<"div"> & { |
||||
|
showIcon?: boolean; |
||||
|
}) { |
||||
|
// Random width between 50 to 90%.
|
||||
|
const width = React.useMemo(() => { |
||||
|
return `${Math.floor(Math.random() * 40) + 50}%`; |
||||
|
}, []); |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
data-slot="sidebar-menu-skeleton" |
||||
|
data-sidebar="menu-skeleton" |
||||
|
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)} |
||||
|
{...props} |
||||
|
> |
||||
|
{showIcon && ( |
||||
|
<Skeleton |
||||
|
className="size-4 rounded-md" |
||||
|
data-sidebar="menu-skeleton-icon" |
||||
|
/> |
||||
|
)} |
||||
|
<Skeleton |
||||
|
className="h-4 max-w-(--skeleton-width) flex-1" |
||||
|
data-sidebar="menu-skeleton-text" |
||||
|
style={ |
||||
|
{ |
||||
|
"--skeleton-width": width, |
||||
|
} as React.CSSProperties |
||||
|
} |
||||
|
/> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { |
||||
|
return ( |
||||
|
<ul |
||||
|
data-slot="sidebar-menu-sub" |
||||
|
data-sidebar="menu-sub" |
||||
|
className={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, |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarMenuSubItem({ |
||||
|
className, |
||||
|
...props |
||||
|
}: React.ComponentProps<"li">) { |
||||
|
return ( |
||||
|
<li |
||||
|
data-slot="sidebar-menu-sub-item" |
||||
|
data-sidebar="menu-sub-item" |
||||
|
className={cn("group/menu-sub-item relative", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function SidebarMenuSubButton({ |
||||
|
asChild = false, |
||||
|
size = "md", |
||||
|
isActive = false, |
||||
|
className, |
||||
|
...props |
||||
|
}: React.ComponentProps<"a"> & { |
||||
|
asChild?: boolean; |
||||
|
size?: "sm" | "md"; |
||||
|
isActive?: boolean; |
||||
|
}) { |
||||
|
const Comp = asChild ? Slot : "a"; |
||||
|
|
||||
|
return ( |
||||
|
<Comp |
||||
|
data-slot="sidebar-menu-sub-button" |
||||
|
data-sidebar="menu-sub-button" |
||||
|
data-size={size} |
||||
|
data-active={isActive} |
||||
|
className={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-hidden 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, |
||||
|
)} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export { |
||||
|
Sidebar, |
||||
|
SidebarContent, |
||||
|
SidebarFooter, |
||||
|
SidebarGroup, |
||||
|
SidebarGroupAction, |
||||
|
SidebarGroupContent, |
||||
|
SidebarGroupLabel, |
||||
|
SidebarHeader, |
||||
|
SidebarInput, |
||||
|
SidebarInset, |
||||
|
SidebarMenu, |
||||
|
SidebarMenuAction, |
||||
|
SidebarMenuBadge, |
||||
|
SidebarMenuButton, |
||||
|
SidebarMenuItem, |
||||
|
SidebarMenuSkeleton, |
||||
|
SidebarMenuSub, |
||||
|
SidebarMenuSubButton, |
||||
|
SidebarMenuSubItem, |
||||
|
SidebarProvider, |
||||
|
SidebarRail, |
||||
|
SidebarSeparator, |
||||
|
SidebarTrigger, |
||||
|
useSidebar, |
||||
|
}; |
||||
@ -0,0 +1,13 @@ |
|||||
|
import { cn } from "@/lib/utils" |
||||
|
|
||||
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) { |
||||
|
return ( |
||||
|
<div |
||||
|
data-slot="skeleton" |
||||
|
className={cn("bg-accent animate-pulse rounded-md", className)} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export { Skeleton } |
||||
@ -0,0 +1,59 @@ |
|||||
|
import * as React from "react" |
||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip" |
||||
|
|
||||
|
import { cn } from "@/lib/utils" |
||||
|
|
||||
|
function TooltipProvider({ |
||||
|
delayDuration = 0, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) { |
||||
|
return ( |
||||
|
<TooltipPrimitive.Provider |
||||
|
data-slot="tooltip-provider" |
||||
|
delayDuration={delayDuration} |
||||
|
{...props} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function Tooltip({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Root>) { |
||||
|
return ( |
||||
|
<TooltipProvider> |
||||
|
<TooltipPrimitive.Root data-slot="tooltip" {...props} /> |
||||
|
</TooltipProvider> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function TooltipTrigger({ |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { |
||||
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} /> |
||||
|
} |
||||
|
|
||||
|
function TooltipContent({ |
||||
|
className, |
||||
|
sideOffset = 0, |
||||
|
children, |
||||
|
...props |
||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) { |
||||
|
return ( |
||||
|
<TooltipPrimitive.Portal> |
||||
|
<TooltipPrimitive.Content |
||||
|
data-slot="tooltip-content" |
||||
|
sideOffset={sideOffset} |
||||
|
className={cn( |
||||
|
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", |
||||
|
className |
||||
|
)} |
||||
|
{...props} |
||||
|
> |
||||
|
{children} |
||||
|
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> |
||||
|
</TooltipPrimitive.Content> |
||||
|
</TooltipPrimitive.Portal> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } |
||||
@ -0,0 +1,19 @@ |
|||||
|
import * as React from "react" |
||||
|
|
||||
|
const MOBILE_BREAKPOINT = 768 |
||||
|
|
||||
|
export function useIsMobile() { |
||||
|
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) |
||||
|
|
||||
|
React.useEffect(() => { |
||||
|
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) |
||||
|
const onChange = () => { |
||||
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) |
||||
|
} |
||||
|
mql.addEventListener("change", onChange) |
||||
|
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) |
||||
|
return () => mql.removeEventListener("change", onChange) |
||||
|
}, []) |
||||
|
|
||||
|
return !!isMobile |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
export { ThemeProvider, useTheme } from "@/components/theme-provider"; |
||||
|
export * from "@/components/ui/badge.tsx"; |
||||
|
export * from "@/components/ui/sidebar"; |
||||
|
export { AppSidebar } from "./lib/components/index.ts"; |
||||
|
|
||||
|
// Types
|
||||
|
export type { |
||||
|
AppSidebarProps, |
||||
|
NavLink, |
||||
|
SidebarSectionProps, |
||||
|
} from "./lib/components/Sidebar/AppSidebar.tsx"; |
||||
|
|
||||
|
export { ThemeToggle } from "./lib/components/theme-toggle.tsx"; |
||||
@ -0,0 +1,218 @@ |
|||||
|
import type { LucideIcon } from "lucide-react"; |
||||
|
import { ChevronRight } from "lucide-react"; |
||||
|
import type * as React from "react"; |
||||
|
import { Badge } from "@/components/ui/badge"; |
||||
|
import { |
||||
|
Collapsible, |
||||
|
CollapsibleContent, |
||||
|
CollapsibleTrigger, |
||||
|
} from "@/components/ui/collapsible"; |
||||
|
import { |
||||
|
Sidebar, |
||||
|
SidebarContent, |
||||
|
SidebarFooter, |
||||
|
SidebarGroup, |
||||
|
SidebarGroupContent, |
||||
|
SidebarGroupLabel, |
||||
|
SidebarHeader, |
||||
|
SidebarMenu, |
||||
|
SidebarMenuButton, |
||||
|
SidebarMenuItem, |
||||
|
SidebarMenuSub, |
||||
|
SidebarMenuSubButton, |
||||
|
SidebarMenuSubItem, |
||||
|
useSidebar, |
||||
|
} from "@/components/ui/sidebar"; |
||||
|
import { cn } from "@/lib/utils"; |
||||
|
|
||||
|
export interface AppSidebarProps { |
||||
|
children?: React.ReactNode; |
||||
|
logo?: { |
||||
|
src: string; |
||||
|
alt: string; |
||||
|
}; |
||||
|
title?: string; |
||||
|
navigationLabel?: string; |
||||
|
variant?: "sidebar" | "inset"; |
||||
|
footer?: React.ReactNode; |
||||
|
} |
||||
|
|
||||
|
export interface NavLink { |
||||
|
name: string; |
||||
|
icon: LucideIcon; |
||||
|
page: string; |
||||
|
count?: number; |
||||
|
active?: boolean; |
||||
|
disabled?: boolean; |
||||
|
items?: NavLink[]; |
||||
|
onClick?: () => void; |
||||
|
} |
||||
|
|
||||
|
export interface SidebarSectionProps { |
||||
|
label?: string; |
||||
|
items: NavLink[]; |
||||
|
} |
||||
|
|
||||
|
const AppSidebar = ({ |
||||
|
children, |
||||
|
logo, |
||||
|
title, |
||||
|
navigationLabel, |
||||
|
items, |
||||
|
footer, |
||||
|
variant, |
||||
|
...props |
||||
|
}: SidebarSectionProps & AppSidebarProps) => { |
||||
|
const { state } = useSidebar(); |
||||
|
const isCollapsed = state === "collapsed"; |
||||
|
|
||||
|
return ( |
||||
|
<Sidebar collapsible="icon" variant="sidebar" {...props}> |
||||
|
<SidebarHeader |
||||
|
className={cn( |
||||
|
"h-14 mt-2 flex-shrink-0 transition-all duration-300 ease-in-out", |
||||
|
)} |
||||
|
> |
||||
|
<div className="flex items-center"> |
||||
|
{logo && ( |
||||
|
<img |
||||
|
src={logo.src} |
||||
|
alt={logo.alt} |
||||
|
className="size-10 flex-shrink-0 rounded-xl" |
||||
|
/> |
||||
|
)} |
||||
|
{title && ( |
||||
|
<h2 |
||||
|
className={cn( |
||||
|
"text-xl font-semibold whitespace-nowrap", |
||||
|
"transition-all duration-300 ease-in-out", |
||||
|
isCollapsed |
||||
|
? "opacity-0 max-w-0 invisible ml-0" |
||||
|
: "opacity-100 max-w-xs visible ml-2", |
||||
|
)} |
||||
|
> |
||||
|
{title} |
||||
|
</h2> |
||||
|
)} |
||||
|
</div> |
||||
|
</SidebarHeader> |
||||
|
|
||||
|
<SidebarContent> |
||||
|
<SidebarGroup className="mt-4"> |
||||
|
{navigationLabel && ( |
||||
|
<SidebarGroupLabel |
||||
|
className={cn( |
||||
|
"transition-all duration-300 ease-in-out", |
||||
|
"whitespace-nowrap overflow-hidden", |
||||
|
isCollapsed ? "max-w-0 opacity-0 invisible" : "max-w-full", |
||||
|
)} |
||||
|
> |
||||
|
{navigationLabel} |
||||
|
</SidebarGroupLabel> |
||||
|
)} |
||||
|
<SidebarGroupContent> |
||||
|
<SidebarMenu> |
||||
|
{items.map((link) => { |
||||
|
// Check if this item has sub-items
|
||||
|
if (link.items && link.items.length > 0) { |
||||
|
return ( |
||||
|
<Collapsible |
||||
|
key={link.name} |
||||
|
asChild |
||||
|
defaultOpen={link.active} |
||||
|
className="group/collapsible" |
||||
|
> |
||||
|
<SidebarMenuItem> |
||||
|
<CollapsibleTrigger asChild> |
||||
|
<SidebarMenuButton |
||||
|
onClick={link.onClick} |
||||
|
disabled={link.disabled} |
||||
|
tooltip={isCollapsed ? link.name : undefined} |
||||
|
> |
||||
|
{link.icon && <link.icon className="size-5" />} |
||||
|
<span>{link.name}</span> |
||||
|
{link.count !== undefined && link.count > 0 && ( |
||||
|
<Badge |
||||
|
variant="default" |
||||
|
className="ml-auto bg-blue-500 text-white text-xs" |
||||
|
> |
||||
|
{link.count} |
||||
|
</Badge> |
||||
|
)} |
||||
|
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" /> |
||||
|
</SidebarMenuButton> |
||||
|
</CollapsibleTrigger> |
||||
|
<CollapsibleContent> |
||||
|
<SidebarMenuSub> |
||||
|
{link.items.map((subLink) => ( |
||||
|
<SidebarMenuSubItem key={subLink.name}> |
||||
|
<SidebarMenuSubButton |
||||
|
onClick={subLink.onClick} |
||||
|
isActive={subLink.active} |
||||
|
> |
||||
|
{subLink.icon && ( |
||||
|
<subLink.icon className="size-4" /> |
||||
|
)} |
||||
|
<span>{subLink.name}</span> |
||||
|
{subLink.count !== undefined && |
||||
|
subLink.count > 0 && ( |
||||
|
<Badge |
||||
|
variant="default" |
||||
|
className="ml-auto bg-blue-500 text-white text-xs" |
||||
|
> |
||||
|
{subLink.count} |
||||
|
</Badge> |
||||
|
)} |
||||
|
</SidebarMenuSubButton> |
||||
|
</SidebarMenuSubItem> |
||||
|
))} |
||||
|
</SidebarMenuSub> |
||||
|
</CollapsibleContent> |
||||
|
</SidebarMenuItem> |
||||
|
</Collapsible> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<SidebarMenuItem key={link.name}> |
||||
|
<SidebarMenuButton |
||||
|
onClick={link.onClick} |
||||
|
isActive={link.active} |
||||
|
disabled={link.disabled} |
||||
|
tooltip={isCollapsed ? link.name : undefined} |
||||
|
> |
||||
|
{link.icon && <link.icon className="size-5" />} |
||||
|
<span>{link.name}</span> |
||||
|
{link.count !== undefined && link.count > 0 && ( |
||||
|
<Badge |
||||
|
variant="default" |
||||
|
className="ml-auto bg-blue-500 text-white text-xs" |
||||
|
> |
||||
|
{link.count} |
||||
|
</Badge> |
||||
|
)} |
||||
|
</SidebarMenuButton> |
||||
|
</SidebarMenuItem> |
||||
|
); |
||||
|
})} |
||||
|
</SidebarMenu> |
||||
|
</SidebarGroupContent> |
||||
|
</SidebarGroup> |
||||
|
|
||||
|
{children && ( |
||||
|
<div |
||||
|
className={cn("flex-1 min-h-0", isCollapsed && "overflow-hidden")} |
||||
|
> |
||||
|
{children} |
||||
|
</div> |
||||
|
)} |
||||
|
</SidebarContent> |
||||
|
|
||||
|
{footer && <SidebarFooter className="pt-4">{footer}</SidebarFooter>} |
||||
|
</Sidebar> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
AppSidebar.displayName = "AppSidebar"; |
||||
|
|
||||
|
export default AppSidebar; |
||||
@ -0,0 +1 @@ |
|||||
|
export { default as AppSidebar } from "./Sidebar/AppSidebar.tsx"; |
||||
@ -0,0 +1,38 @@ |
|||||
|
import * as React from "react"; |
||||
|
import { Moon, Sun } from "lucide-react"; |
||||
|
|
||||
|
import { Button } from "@/components/ui/button"; |
||||
|
import { |
||||
|
DropdownMenu, |
||||
|
DropdownMenuContent, |
||||
|
DropdownMenuItem, |
||||
|
DropdownMenuTrigger, |
||||
|
} from "@/components/ui/dropdown-menu"; |
||||
|
import { useTheme } from "@/components/theme-provider"; |
||||
|
|
||||
|
export function ThemeToggle() { |
||||
|
const { setTheme } = useTheme(); |
||||
|
|
||||
|
return ( |
||||
|
<DropdownMenu> |
||||
|
<DropdownMenuTrigger asChild> |
||||
|
<Button variant="outline" size="icon"> |
||||
|
<Sun className="size-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> |
||||
|
<Moon className="absolute size-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> |
||||
|
<span className="sr-only">Toggle theme</span> |
||||
|
</Button> |
||||
|
</DropdownMenuTrigger> |
||||
|
<DropdownMenuContent align="end"> |
||||
|
<DropdownMenuItem onClick={() => setTheme("light")}> |
||||
|
Light |
||||
|
</DropdownMenuItem> |
||||
|
<DropdownMenuItem onClick={() => setTheme("dark")}> |
||||
|
Dark |
||||
|
</DropdownMenuItem> |
||||
|
<DropdownMenuItem onClick={() => setTheme("system")}> |
||||
|
System |
||||
|
</DropdownMenuItem> |
||||
|
</DropdownMenuContent> |
||||
|
</DropdownMenu> |
||||
|
); |
||||
|
} |
||||
@ -0,0 +1,124 @@ |
|||||
|
@import "tailwindcss"; |
||||
|
@import "tw-animate-css"; |
||||
|
|
||||
|
@custom-variant dark (&:is(.dark *)); |
||||
|
|
||||
|
:root { |
||||
|
--background: oklch(1 0 0); |
||||
|
--foreground: oklch(0.145 0 0); |
||||
|
--card: oklch(1 0 0); |
||||
|
--card-foreground: oklch(0.145 0 0); |
||||
|
--popover: oklch(1 0 0); |
||||
|
--popover-foreground: oklch(0.145 0 0); |
||||
|
--primary: oklch(0.205 0 0); |
||||
|
--primary-foreground: oklch(0.985 0 0); |
||||
|
--secondary: oklch(0.97 0 0); |
||||
|
--secondary-foreground: oklch(0.205 0 0); |
||||
|
--muted: oklch(0.97 0 0); |
||||
|
--muted-foreground: oklch(0.556 0 0); |
||||
|
--accent: oklch(0.97 0 0); |
||||
|
--accent-foreground: oklch(0.205 0 0); |
||||
|
--destructive: oklch(0.577 0.245 27.325); |
||||
|
--destructive-foreground: oklch(0.577 0.245 27.325); |
||||
|
--border: oklch(0.922 0 0); |
||||
|
--input: oklch(0.922 0 0); |
||||
|
--ring: oklch(0.708 0 0); |
||||
|
--chart-1: oklch(0.646 0.222 41.116); |
||||
|
--chart-2: oklch(0.6 0.118 184.704); |
||||
|
--chart-3: oklch(0.398 0.07 227.392); |
||||
|
--chart-4: oklch(0.828 0.189 84.429); |
||||
|
--chart-5: oklch(0.769 0.188 70.08); |
||||
|
--radius: 0.625rem; |
||||
|
--sidebar: oklch(0.985 0 0); |
||||
|
--sidebar-foreground: oklch(0.145 0 0); |
||||
|
--sidebar-primary: oklch(0.205 0 0); |
||||
|
--sidebar-primary-foreground: oklch(0.985 0 0); |
||||
|
--sidebar-accent: oklch(0.97 0 0); |
||||
|
--sidebar-accent-foreground: oklch(0.205 0 0); |
||||
|
--sidebar-border: oklch(0.922 0 0); |
||||
|
--sidebar-ring: oklch(0.708 0 0); |
||||
|
} |
||||
|
|
||||
|
.dark { |
||||
|
--background: oklch(0.145 0 0); |
||||
|
--foreground: oklch(0.985 0 0); |
||||
|
--card: oklch(0.145 0 0); |
||||
|
--card-foreground: oklch(0.985 0 0); |
||||
|
--popover: oklch(0.145 0 0); |
||||
|
--popover-foreground: oklch(0.985 0 0); |
||||
|
--primary: oklch(0.985 0 0); |
||||
|
--primary-foreground: oklch(0.205 0 0); |
||||
|
--secondary: oklch(0.269 0 0); |
||||
|
--secondary-foreground: oklch(0.985 0 0); |
||||
|
--muted: oklch(0.269 0 0); |
||||
|
--muted-foreground: oklch(0.708 0 0); |
||||
|
--accent: oklch(0.269 0 0); |
||||
|
--accent-foreground: oklch(0.985 0 0); |
||||
|
--destructive: oklch(0.396 0.141 25.723); |
||||
|
--destructive-foreground: oklch(0.637 0.237 25.331); |
||||
|
--border: oklch(0.269 0 0); |
||||
|
--input: oklch(0.269 0 0); |
||||
|
--ring: oklch(0.439 0 0); |
||||
|
--chart-1: oklch(0.488 0.243 264.376); |
||||
|
--chart-2: oklch(0.696 0.17 162.48); |
||||
|
--chart-3: oklch(0.769 0.188 70.08); |
||||
|
--chart-4: oklch(0.627 0.265 303.9); |
||||
|
--chart-5: oklch(0.645 0.246 16.439); |
||||
|
--sidebar: oklch(0.205 0 0); |
||||
|
--sidebar-foreground: oklch(0.985 0 0); |
||||
|
--sidebar-primary: oklch(0.488 0.243 264.376); |
||||
|
--sidebar-primary-foreground: oklch(0.985 0 0); |
||||
|
--sidebar-accent: oklch(0.269 0 0); |
||||
|
--sidebar-accent-foreground: oklch(0.985 0 0); |
||||
|
--sidebar-border: oklch(0.269 0 0); |
||||
|
--sidebar-ring: oklch(0.439 0 0); |
||||
|
} |
||||
|
|
||||
|
@theme inline { |
||||
|
--color-background: var(--background); |
||||
|
--color-foreground: var(--foreground); |
||||
|
--color-card: var(--card); |
||||
|
--color-card-foreground: var(--card-foreground); |
||||
|
--color-popover: var(--popover); |
||||
|
--color-popover-foreground: var(--popover-foreground); |
||||
|
--color-primary: var(--primary); |
||||
|
--color-primary-foreground: var(--primary-foreground); |
||||
|
--color-secondary: var(--secondary); |
||||
|
--color-secondary-foreground: var(--secondary-foreground); |
||||
|
--color-muted: var(--muted); |
||||
|
--color-muted-foreground: var(--muted-foreground); |
||||
|
--color-accent: var(--accent); |
||||
|
--color-accent-foreground: var(--accent-foreground); |
||||
|
--color-destructive: var(--destructive); |
||||
|
--color-destructive-foreground: var(--destructive-foreground); |
||||
|
--color-border: var(--border); |
||||
|
--color-input: var(--input); |
||||
|
--color-ring: var(--ring); |
||||
|
--color-chart-1: var(--chart-1); |
||||
|
--color-chart-2: var(--chart-2); |
||||
|
--color-chart-3: var(--chart-3); |
||||
|
--color-chart-4: var(--chart-4); |
||||
|
--color-chart-5: var(--chart-5); |
||||
|
--radius-sm: calc(var(--radius) - 4px); |
||||
|
--radius-md: calc(var(--radius) - 2px); |
||||
|
--radius-lg: var(--radius); |
||||
|
--radius-xl: calc(var(--radius) + 4px); |
||||
|
--color-sidebar: var(--sidebar); |
||||
|
--color-sidebar-foreground: var(--sidebar-foreground); |
||||
|
--color-sidebar-primary: var(--sidebar-primary); |
||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground); |
||||
|
--color-sidebar-accent: var(--sidebar-accent); |
||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); |
||||
|
--color-sidebar-border: var(--sidebar-border); |
||||
|
--color-sidebar-ring: var(--sidebar-ring); |
||||
|
} |
||||
|
|
||||
|
@layer base { |
||||
|
* { |
||||
|
@apply border-border outline-ring/50; |
||||
|
} |
||||
|
|
||||
|
body { |
||||
|
@apply bg-background text-foreground; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
import { clsx, type ClassValue } from "clsx" |
||||
|
import { twMerge } from "tailwind-merge" |
||||
|
|
||||
|
export function cn(...inputs: ClassValue[]) { |
||||
|
return twMerge(clsx(inputs)) |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
{ |
||||
|
"extends": "../../tsconfig.json", |
||||
|
"compilerOptions": { |
||||
|
"target": "ES2021", |
||||
|
"lib": ["ES2021", "DOM"], |
||||
|
"jsx": "react-jsx", |
||||
|
"module": "ESNext", |
||||
|
"moduleResolution": "Bundler", |
||||
|
"skipLibCheck": true, |
||||
|
"declaration": true, |
||||
|
"declarationMap": true, |
||||
|
"emitDeclarationOnly": false, |
||||
|
"noUncheckedIndexedAccess": true, |
||||
|
"erasableSyntaxOnly": true, |
||||
|
"outDir": "dist", |
||||
|
"rootDir": "src", |
||||
|
"strict": true, |
||||
|
"types": ["react", "react-dom"], |
||||
|
"baseUrl": ".", |
||||
|
"paths": { |
||||
|
"@/*": ["./src/*"] |
||||
|
} |
||||
|
}, |
||||
|
"include": ["src"] |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
import path from "node:path"; |
||||
|
import tailwindcss from "@tailwindcss/vite"; |
||||
|
import react from "@vitejs/plugin-react"; |
||||
|
import { defineConfig } from "vite"; |
||||
|
import dts from "vite-plugin-dts"; |
||||
|
import { viteStaticCopy } from "vite-plugin-static-copy"; |
||||
|
|
||||
|
export default defineConfig({ |
||||
|
plugins: [ |
||||
|
react(), |
||||
|
tailwindcss(), |
||||
|
dts({ |
||||
|
entryRoot: "src", |
||||
|
outDir: "dist", |
||||
|
insertTypesEntry: true, |
||||
|
copyDtsFiles: true, |
||||
|
}), |
||||
|
viteStaticCopy({ |
||||
|
targets: [ |
||||
|
{ |
||||
|
src: "src/lib/theme/default.css", |
||||
|
dest: "theme", |
||||
|
}, |
||||
|
], |
||||
|
}), |
||||
|
], |
||||
|
resolve: { |
||||
|
alias: { |
||||
|
"@": path.resolve(__dirname, "./src"), |
||||
|
}, |
||||
|
}, |
||||
|
build: { |
||||
|
emptyOutDir: true, |
||||
|
lib: { |
||||
|
entry: "src/index.ts", |
||||
|
name: "MeshtasticUI", |
||||
|
formats: ["es"], |
||||
|
fileName: () => "index.js", |
||||
|
}, |
||||
|
rollupOptions: { |
||||
|
external: [ |
||||
|
"react", |
||||
|
"react-dom", |
||||
|
"tailwindcss", |
||||
|
"class-variance-authority", |
||||
|
"tailwind-merge", |
||||
|
"@radix-ui/react-slot", |
||||
|
], |
||||
|
}, |
||||
|
sourcemap: true, |
||||
|
target: "es2021", |
||||
|
}, |
||||
|
}); |
||||
File diff suppressed because it is too large
Loading…
Reference in new issue