This commit is contained in:
2025-11-06 10:06:10 -03:00
parent bb667b6636
commit e885f04303
27 changed files with 376 additions and 284 deletions

View File

@@ -75,11 +75,15 @@ export function DataTable<TData, TValue>({
)
const [columnVisibility, setColumnVisibility] =
useState<VisibilityState>(hiddenColumn_)
const [rowSelection, setRowSelection] = useState({})
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
onRowSelectionChange: setRowSelection,
state: {
rowSelection,
columnVisibility,
pagination: {
pageIndex,

View File

@@ -10,6 +10,7 @@ import {
useSidebar
} from '@repo/ui/components/ui/sidebar'
import { useIsMobile } from '@repo/ui/hooks/use-mobile'
import { type LucideIcon } from 'lucide-react'
import { NavLink, useParams } from 'react-router'

View File

@@ -21,7 +21,6 @@ import {
SidebarRail,
useSidebar
} from '@repo/ui/components/ui/sidebar'
import { initials } from '@repo/ui/lib/utils'
type Org = {

View File

@@ -1,5 +0,0 @@
import type { User } from '@/lib/auth'
import { createContext } from 'react-router'
export const userContext = createContext<User | null>(null)
export const requestIdContext = createContext<string | null>(null)

View File

@@ -92,8 +92,8 @@ export default function Route({ loaderData: { data } }) {
<SearchForm
placeholder={
<>
Pressione <Kbd className="border font-mono">/</Kbd> para
filtrar...
Digite <Kbd className="border font-mono">/</Kbd> para
pesquisar
</>
}
defaultValue={term}

View File

@@ -13,6 +13,7 @@ import {
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
import { Badge } from '@repo/ui/components/ui/badge'
import { Checkbox } from '@repo/ui/components/ui/checkbox'
import { Progress } from '@repo/ui/components/ui/progress'
import { cn, initials } from '@repo/ui/lib/utils'
@@ -83,6 +84,26 @@ const statusTranslate: Record<string, string> = {
}
export const columns: ColumnDef<Enrollment>[] = [
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && 'indeterminate')
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Selecionar tudo"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Selecionar linha"
/>
)
},
{
header: 'Colaborador',
enableHiding: false,
@@ -145,30 +166,35 @@ export const columns: ColumnDef<Enrollment>[] = [
{
accessorKey: 'created_at',
header: 'Matriculado em',
enableSorting: true,
enableHiding: true,
cell: cellDate
},
{
accessorKey: 'started_at',
header: 'Iniciado em',
enableSorting: true,
enableHiding: true,
cell: cellDate
},
{
accessorKey: 'completed_at',
header: 'Aprovado em',
enableSorting: true,
enableHiding: true,
cell: cellDate
},
{
accessorKey: 'failed_at',
header: 'Reprovado em',
enableSorting: true,
enableHiding: true,
cell: cellDate
},
{
accessorKey: 'canceled_at',
header: 'Cancelado em',
enableSorting: true,
enableHiding: true,
cell: cellDate
}

View File

@@ -113,8 +113,8 @@ export default function Route({ loaderData: { data } }) {
defaultValue={searchParams.get('q') || ''}
placeholder={
<>
Pressione <Kbd className="border font-mono">/</Kbd> para
pesquisar...
Digite <Kbd className="border font-mono">/</Kbd> para
pesquisar
</>
}
onChange={(value) =>

View File

@@ -72,8 +72,8 @@ export default function Route({ loaderData: { data } }) {
<SearchForm
placeholder={
<>
Pressione <Kbd className="border font-mono">/</Kbd> para
pesquisar...
Digite <Kbd className="border font-mono">/</Kbd> para
pesquisar
</>
}
defaultValue={searchParams.get('q') || ''}

View File

@@ -1,6 +1,10 @@
import type { Route } from './+types'
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
import {
createCookie,
Outlet,
type ShouldRevalidateFunctionArgs
} from 'react-router'
import { AppSidebar } from '@/components/app-sidebar'
import { request as req } from '@/lib/request'
@@ -14,13 +18,14 @@ import {
SidebarProvider,
SidebarTrigger
} from '@repo/ui/components/ui/sidebar'
import { useIsMobile } from '@repo/ui/hooks/use-mobile'
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
export async function loader({ params, context, request }: Route.ActionArgs) {
const rawCookie = request.headers.get('cookie')
const user = context.get(userContext)
const sidebarState = await createCookie('sidebar_state').parse(rawCookie)
const r = await req({
url: `/users/${user.sub}/orgs?limit=25`,
request,
@@ -39,7 +44,7 @@ export async function loader({ params, context, request }: Route.ActionArgs) {
const exists = orgs.some(({ id }) => id === params.orgid)
if (exists) {
return { user, orgs }
return { user, orgs, sidebarState }
}
throw new Response(null, { status: 401 })
@@ -52,12 +57,12 @@ export function shouldRevalidate({
return currentParams.orgid !== nextParams.orgid
}
export default function Layout({ loaderData }: Route.ComponentProps) {
const { user, orgs } = loaderData
const isMobile = useIsMobile()
export default function Route({ loaderData }: Route.ComponentProps) {
const { user, orgs, sidebarState } = loaderData
console.log(sidebarState)
return (
<SidebarProvider className="flex">
<SidebarProvider defaultOpen={sidebarState === 'true'} className="flex">
<AppSidebar orgs={orgs} />
<SidebarInset className="relative flex flex-col flex-1 min-w-0">
@@ -66,7 +71,8 @@ export default function Layout({ loaderData }: Route.ComponentProps) {
px-4 py-2 lg:py-4 sticky top-0 z-5"
>
<div className="container mx-auto flex items-center">
{isMobile ? <SidebarTrigger /> : <ThemedImage />}
<SidebarTrigger className="md:hidden" />
<ThemedImage className="max-md:hidden" />
<div className="ml-auto flex gap-2.5 items-center">
<ModeToggle />

View File

@@ -13,8 +13,8 @@
},
"dependencies": {
"@react-router/fs-routes": "^7.9.5",
"@repo/ui": "*",
"@repo/auth": "*",
"@repo/ui": "*",
"@tanstack/react-table": "^8.21.3",
"date-fns": "^4.1.0",
"fuse.js": "^7.1.0",

View File

@@ -1,9 +1,9 @@
import { useKeyPress } from '@/hooks/use-keypress'
import {
InputGroup,
InputGroupAddon,
InputGroupInput
} from '@repo/ui/components/ui/input-group'
import { useKeyPress } from '@/hooks/use-keypress'
import clsx from 'clsx'
import { debounce } from 'lodash'
import { SearchIcon } from 'lucide-react'

View File

@@ -1,22 +0,0 @@
import { throttle } from 'lodash'
import { useEffect } from 'react'
export function useKeyPress(targetKey, callback) {
useEffect(() => {
const onKeyDown = throttle((event) => {
if (event.key === targetKey) {
event.preventDefault()
callback(event)
}
}, 300)
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
onKeyDown.cancel?.()
}
}, [targetKey, callback])
return null
}

View File

@@ -1,43 +0,0 @@
import type { OAuth2Tokens } from 'arctic'
import { decodeJwt } from 'jose'
import { Authenticator } from 'remix-auth'
import { CodeChallengeMethod, OAuth2Strategy } from 'remix-auth-oauth2'
export type User = {
sub: string
email: string
name: string
scope: string
email_verified: boolean
accessToken: string
refreshToken: string
}
export function createAuth(env: Env) {
const authenticator = new Authenticator()
const strategy = new OAuth2Strategy(
{
clientId: env.CLIENT_ID,
clientSecret: env.CLIENT_SECRET,
redirectURI: env.REDIRECT_URI,
authorizationEndpoint: `${env.ISSUER_URL}/authorize`,
tokenEndpoint: `${env.ISSUER_URL}/token`,
tokenRevocationEndpoint: `${env.ISSUER_URL}/revoke`,
scopes: env.SCOPE.split(' '),
codeChallengeMethod: CodeChallengeMethod.S256
},
async ({ tokens }: { tokens: OAuth2Tokens }) => {
const user = decodeJwt(tokens.idToken())
return {
...user,
accessToken: tokens.accessToken(),
refreshToken: tokens.hasRefreshToken() ? tokens.refreshToken() : null
}
}
)
authenticator.use(strategy, 'oidc')
return authenticator
}

View File

@@ -1,4 +1,4 @@
import { userContext } from '@/context'
import { userContext } from '@repo/auth/context'
import type { LoaderFunctionArgs } from 'react-router'
enum Method {

View File

@@ -1,16 +0,0 @@
import { createCookieSessionStorage } from 'react-router'
export function createSessionStorage(env: Env) {
const sessionStorage = createCookieSessionStorage({
cookie: {
name: '__session',
httpOnly: true,
secure: false,
secrets: [env.SESSION_SECRET],
sameSite: 'lax',
path: '/',
maxAge: 86400 * 7 // 7 days
}
})
return sessionStorage
}

View File

@@ -8,6 +8,7 @@ import {
export default [
layout('routes/layout.tsx', [
index('routes/index.tsx'),
route('certs', 'routes/certs.tsx'),
route('payments', 'routes/payments.tsx'),
route('settings', 'routes/settings.tsx'),
route('player/:course', 'routes/player.tsx'),

View File

@@ -10,10 +10,10 @@ import {
} from '@repo/ui/components/ui/empty'
import {
BanIcon,
BookCopyIcon,
CircleCheckIcon,
CircleIcon,
CircleOffIcon,
CirclePlusIcon,
CircleXIcon,
TimerIcon,
type LucideIcon
@@ -131,7 +131,7 @@ export default function Component({
/>
</div>
<FacetedFilter
icon={BookCopyIcon}
icon={CirclePlusIcon}
value={searchParams.getAll('status')}
onChange={(statuses) => {
setSearchParams((searchParams) => {

View File

@@ -1,12 +1,18 @@
import type { Route } from './+types'
import { Outlet } from 'react-router'
import { Link, NavLink, Outlet } from 'react-router'
import { userContext } from '@repo/auth/context'
import { authMiddleware } from '@repo/auth/middleware/auth'
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
import { NavUser } from '@repo/ui/components/nav-user'
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList
} from '@repo/ui/components/ui/navigation-menu'
import { useIsMobile } from '@repo/ui/hooks/use-mobile'
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
@@ -16,6 +22,7 @@ export async function loader({ context }: Route.ActionArgs) {
}
export default function Component({ loaderData }: Route.ComponentProps) {
const isMobile = useIsMobile()
const { user } = loaderData
return (
@@ -25,7 +32,25 @@ export default function Component({ loaderData }: Route.ComponentProps) {
px-4 py-2 lg:py-4 sticky top-0 z-5"
>
<div className="container mx-auto flex items-center">
<ThemedImage />
<div className="flex gap-5">
<Link to="/">
<ThemedImage />
</Link>
<NavigationMenu viewport={isMobile}>
<NavigationMenuList>
<NavigationMenuItem>
<NavMenuLink to="/">Meus cursos</NavMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavMenuLink to="/certs">Certificados</NavMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavMenuLink to="/payments">Histórico de compras</NavMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</div>
<div className="ml-auto flex gap-2.5 items-center">
<ModeToggle />
@@ -42,3 +67,14 @@ export default function Component({ loaderData }: Route.ComponentProps) {
</div>
)
}
function NavMenuLink({ children, ...props }) {
return (
<NavigationMenuLink
className="font-medium aria-[current=page]:bg-muted"
asChild
>
<NavLink {...props}>{children}</NavLink>
</NavigationMenuLink>
)
}

View File

@@ -2,9 +2,10 @@ import type { Route } from './+types'
import { Link } from 'react-router'
import { userContext } from '@/context'
import { request as req } from '@/lib/request'
import type { User } from '@/middleware/auth'
import type { User } from '@repo/auth/auth'
import { userContext } from '@repo/auth/context'
import {
Breadcrumb,
BreadcrumbItem,

84
package-lock.json generated
View File

@@ -663,6 +663,15 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -4678,6 +4687,12 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/js-cookie": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
@@ -4769,6 +4784,28 @@
"resolved": "apps/admin.saladeaula.digital",
"link": true
},
"node_modules/ahooks": {
"version": "3.9.6",
"resolved": "https://registry.npmjs.org/ahooks/-/ahooks-3.9.6.tgz",
"integrity": "sha512-Mr7f05swd5SmKlR9SZo5U6M0LsL4ErweLzpdgXjA1JPmnZ78Vr6wzx0jUtvoxrcqGKYnX0Yjc02iEASVxHFPjQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0",
"@types/js-cookie": "^3.0.6",
"dayjs": "^1.9.1",
"intersection-observer": "^0.12.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"react-fast-compare": "^3.2.2",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.0.0",
"tslib": "^2.4.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -5104,6 +5141,12 @@
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -5469,6 +5512,13 @@
"resolved": "apps/insights.saladeaula.digital",
"link": true
},
"node_modules/intersection-observer": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz",
"integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==",
"deprecated": "The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.",
"license": "Apache-2.0"
},
"node_modules/is-arrayish": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
@@ -5548,6 +5598,15 @@
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6297,6 +6356,12 @@
"react": "^19.2.0"
}
},
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
"license": "MIT"
},
"node_modules/react-hook-form": {
"version": "7.66.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz",
@@ -6502,6 +6567,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
"license": "MIT"
},
"node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@@ -6599,6 +6670,18 @@
"integrity": "sha512-0/A0Z8310GlZ0/Kx54FFeoJ0KDCz9Hwxu3sVvMCQvw37GClwcd6mao5kRio+uivqVJyMTjTWfzc0wsIHZf1l6w==",
"license": "MIT"
},
"node_modules/screenfull": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz",
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -8083,6 +8166,7 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/postcss": "^4.1.16",
"@tailwindcss/vite": "^4.1.16",
"ahooks": "^3.9.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",

View File

@@ -12,5 +12,6 @@ export function createSessionStorage(env) {
maxAge: 86400 * 7 // 7 days
}
})
return sessionStorage
}

View File

@@ -30,6 +30,7 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/postcss": "^4.1.16",
"@tailwindcss/vite": "^4.1.16",
"ahooks": "^3.9.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",

View File

@@ -44,11 +44,11 @@ export function ModeToggle() {
)
}
export function ThemedImage() {
export function ThemedImage({ ...props }) {
return (
<>
<div {...props}>
<img src={light} className="h-8" data-hide-on-theme="dark" />
<img src={dark} className="h-8" data-hide-on-theme="light" />
</>
</div>
)
}

View File

@@ -1,3 +1,4 @@
import { useKeyPress } from 'ahooks'
import { debounce } from 'lodash'
import { SearchIcon, XIcon } from 'lucide-react'
import { useRef } from 'react'
@@ -8,8 +9,6 @@ import {
InputGroupButton,
InputGroupInput
} from '@repo/ui/components/ui/input-group'
import { useKeyPress } from '@repo/ui/hooks/use-keypress'
import { cn } from '@repo/ui/lib/utils'
export function SearchForm({
@@ -26,9 +25,18 @@ export function SearchForm({
} & React.HTMLAttributes<HTMLDivElement>) {
const inputRef = useRef<HTMLInputElement>(null)
useKeyPress('/', () => {
inputRef.current?.focus()
})
useKeyPress(
['forwardslash'],
() => {
inputRef.current?.focus()
},
{
exactMatch: true,
events: ['keyup'],
useCapture: true,
target: () => window
}
)
const debouncedOnChange = debounce((value: string) => {
onChange?.(value)

View File

@@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@@ -1,39 +1,39 @@
"use client"
'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 { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { PanelLeftIcon } from 'lucide-react'
import * as React from '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 { 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"
SheetTitle
} from '@/components/ui/sheet'
import { Skeleton } from '@/components/ui/skeleton'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
TooltipTrigger
} from '@/components/ui/tooltip'
import { useIsMobile } from '@/hooks/use-mobile'
import { cn } from '@/lib/utils'
const SIDEBAR_COOKIE_NAME = "sidebar_state"
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"
const SIDEBAR_WIDTH = '16rem'
const SIDEBAR_WIDTH_MOBILE = '18rem'
const SIDEBAR_WIDTH_ICON = '3rem'
const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
type SidebarContextProps = {
state: "expanded" | "collapsed"
state: 'expanded' | 'collapsed'
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
@@ -47,7 +47,7 @@ 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.")
throw new Error('useSidebar must be used within a SidebarProvider.')
}
return context
@@ -61,7 +61,7 @@ function SidebarProvider({
style,
children,
...props
}: React.ComponentProps<"div"> & {
}: React.ComponentProps<'div'> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
@@ -75,7 +75,7 @@ function SidebarProvider({
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
const openState = typeof value === 'function' ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
@@ -105,13 +105,13 @@ function SidebarProvider({
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
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 state = open ? 'expanded' : 'collapsed'
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
@@ -121,7 +121,7 @@ function SidebarProvider({
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
toggleSidebar
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
@@ -133,13 +133,13 @@ function SidebarProvider({
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...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",
'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
className
)}
{...props}
@@ -152,25 +152,25 @@ function SidebarProvider({
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
side = 'left',
variant = 'sidebar',
collapsible = 'offcanvas',
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}: React.ComponentProps<'div'> & {
side?: 'left' | 'right'
variant?: 'sidebar' | 'floating' | 'inset'
collapsible?: 'offcanvas' | 'icon' | 'none'
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
if (collapsible === 'none') {
return (
<div
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
'bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col',
className
)}
{...props}
@@ -190,7 +190,7 @@ function Sidebar({
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
'--sidebar-width': SIDEBAR_WIDTH_MOBILE
} as React.CSSProperties
}
side={side}
@@ -209,7 +209,7 @@ function Sidebar({
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-collapsible={state === 'collapsed' ? collapsible : ''}
data-variant={variant}
data-side={side}
data-slot="sidebar"
@@ -218,25 +218,25 @@ function Sidebar({
<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)"
'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)]",
'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",
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',
className
)}
{...props}
@@ -266,7 +266,7 @@ function SidebarTrigger({
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn("size-7", className)}
className={cn('size-7', className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
@@ -279,7 +279,7 @@ function SidebarTrigger({
)
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
const { toggleSidebar } = useSidebar()
return (
@@ -291,12 +291,12 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
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",
'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}
@@ -304,13 +304,13 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
)
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
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",
'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}
@@ -326,29 +326,29 @@ function SidebarInput({
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
className={cn('bg-background h-8 w-full shadow-none', className)}
{...props}
/>
)
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
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)}
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
)
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
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)}
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
)
@@ -362,19 +362,19 @@ function SidebarSeparator({
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
className={cn('bg-sidebar-border mx-2 w-auto', className)}
{...props}
/>
)
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
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",
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
className
)}
{...props}
@@ -382,12 +382,12 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
)
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
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)}
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
{...props}
/>
)
@@ -397,16 +397,16 @@ function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
}: 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",
'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}
@@ -418,18 +418,18 @@ function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button"
}: 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",
'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",
'after:absolute after:-inset-2 md:after:hidden',
'group-data-[collapsible=icon]:hidden',
className
)}
{...props}
@@ -440,75 +440,75 @@ function SidebarGroupAction({
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
}: React.ComponentProps<'div'>) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
className={cn('w-full text-sm', className)}
{...props}
/>
)
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
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)}
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
{...props}
/>
)
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
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)}
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",
'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",
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))]",
'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!",
},
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",
},
variant: 'default',
size: 'default'
}
}
)
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
variant = 'default',
size = 'default',
tooltip,
className,
...props
}: React.ComponentProps<"button"> & {
}: React.ComponentProps<'button'> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : 'button'
const { isMobile, state } = useSidebar()
const button = (
@@ -526,9 +526,9 @@ function SidebarMenuButton({
return button
}
if (typeof tooltip === "string") {
if (typeof tooltip === 'string') {
tooltip = {
children: tooltip,
children: tooltip
}
}
@@ -538,7 +538,7 @@ function SidebarMenuButton({
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
hidden={state !== 'collapsed' || isMobile}
{...tooltip}
/>
</Tooltip>
@@ -550,26 +550,26 @@ function SidebarMenuAction({
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
}: React.ComponentProps<'button'> & {
asChild?: boolean
showOnHover?: boolean
}) {
const Comp = asChild ? Slot : "button"
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",
'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",
'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",
'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}
@@ -580,18 +580,18 @@ function SidebarMenuAction({
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
}: 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",
'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}
@@ -603,7 +603,7 @@ function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
}: React.ComponentProps<'div'> & {
showIcon?: boolean
}) {
// Random width between 50 to 90%.
@@ -615,7 +615,7 @@ function SidebarMenuSkeleton({
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
{...props}
>
{showIcon && (
@@ -629,7 +629,7 @@ function SidebarMenuSkeleton({
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
'--skeleton-width': width
} as React.CSSProperties
}
/>
@@ -637,14 +637,14 @@ function SidebarMenuSkeleton({
)
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
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",
'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}
@@ -655,12 +655,12 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
}: React.ComponentProps<'li'>) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
className={cn('group/menu-sub-item relative', className)}
{...props}
/>
)
@@ -668,16 +668,16 @@ function SidebarMenuSubItem({
function SidebarMenuSubButton({
asChild = false,
size = "md",
size = 'md',
isActive = false,
className,
...props
}: React.ComponentProps<"a"> & {
}: React.ComponentProps<'a'> & {
asChild?: boolean
size?: "sm" | "md"
size?: 'sm' | 'md'
isActive?: boolean
}) {
const Comp = asChild ? Slot : "a"
const Comp = asChild ? Slot : 'a'
return (
<Comp
@@ -686,11 +686,11 @@ function SidebarMenuSubButton({
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",
'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}
@@ -722,5 +722,5 @@ export {
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
useSidebar
}

View File

@@ -1,22 +0,0 @@
import { throttle } from 'lodash'
import { useEffect } from 'react'
export function useKeyPress(targetKey: string, callback: CallableFunction) {
useEffect(() => {
const onKeyDown = throttle((event) => {
if (event.key === targetKey) {
event.preventDefault()
callback(event)
}
}, 300)
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
onKeyDown.cancel?.()
}
}, [targetKey, callback])
return null
}