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] = const [columnVisibility, setColumnVisibility] =
useState<VisibilityState>(hiddenColumn_) useState<VisibilityState>(hiddenColumn_)
const [rowSelection, setRowSelection] = useState({})
const table = useReactTable({ const table = useReactTable({
data, data,
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
onRowSelectionChange: setRowSelection,
state: { state: {
rowSelection,
columnVisibility, columnVisibility,
pagination: { pagination: {
pageIndex, pageIndex,

View File

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

View File

@@ -21,7 +21,6 @@ import {
SidebarRail, SidebarRail,
useSidebar useSidebar
} from '@repo/ui/components/ui/sidebar' } from '@repo/ui/components/ui/sidebar'
import { initials } from '@repo/ui/lib/utils' import { initials } from '@repo/ui/lib/utils'
type Org = { 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 <SearchForm
placeholder={ placeholder={
<> <>
Pressione <Kbd className="border font-mono">/</Kbd> para Digite <Kbd className="border font-mono">/</Kbd> para
filtrar... pesquisar
</> </>
} }
defaultValue={term} defaultValue={term}

View File

@@ -13,6 +13,7 @@ import {
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar' import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
import { Badge } from '@repo/ui/components/ui/badge' 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 { Progress } from '@repo/ui/components/ui/progress'
import { cn, initials } from '@repo/ui/lib/utils' import { cn, initials } from '@repo/ui/lib/utils'
@@ -83,6 +84,26 @@ const statusTranslate: Record<string, string> = {
} }
export const columns: ColumnDef<Enrollment>[] = [ 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', header: 'Colaborador',
enableHiding: false, enableHiding: false,
@@ -145,30 +166,35 @@ export const columns: ColumnDef<Enrollment>[] = [
{ {
accessorKey: 'created_at', accessorKey: 'created_at',
header: 'Matriculado em', header: 'Matriculado em',
enableSorting: true,
enableHiding: true, enableHiding: true,
cell: cellDate cell: cellDate
}, },
{ {
accessorKey: 'started_at', accessorKey: 'started_at',
header: 'Iniciado em', header: 'Iniciado em',
enableSorting: true,
enableHiding: true, enableHiding: true,
cell: cellDate cell: cellDate
}, },
{ {
accessorKey: 'completed_at', accessorKey: 'completed_at',
header: 'Aprovado em', header: 'Aprovado em',
enableSorting: true,
enableHiding: true, enableHiding: true,
cell: cellDate cell: cellDate
}, },
{ {
accessorKey: 'failed_at', accessorKey: 'failed_at',
header: 'Reprovado em', header: 'Reprovado em',
enableSorting: true,
enableHiding: true, enableHiding: true,
cell: cellDate cell: cellDate
}, },
{ {
accessorKey: 'canceled_at', accessorKey: 'canceled_at',
header: 'Cancelado em', header: 'Cancelado em',
enableSorting: true,
enableHiding: true, enableHiding: true,
cell: cellDate cell: cellDate
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { useKeyPress } from '@/hooks/use-keypress'
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
InputGroupInput InputGroupInput
} from '@repo/ui/components/ui/input-group' } from '@repo/ui/components/ui/input-group'
import { useKeyPress } from '@/hooks/use-keypress'
import clsx from 'clsx' import clsx from 'clsx'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { SearchIcon } from 'lucide-react' 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' import type { LoaderFunctionArgs } from 'react-router'
enum Method { 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 [ export default [
layout('routes/layout.tsx', [ layout('routes/layout.tsx', [
index('routes/index.tsx'), index('routes/index.tsx'),
route('certs', 'routes/certs.tsx'),
route('payments', 'routes/payments.tsx'), route('payments', 'routes/payments.tsx'),
route('settings', 'routes/settings.tsx'), route('settings', 'routes/settings.tsx'),
route('player/:course', 'routes/player.tsx'), route('player/:course', 'routes/player.tsx'),

View File

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

View File

@@ -1,12 +1,18 @@
import type { Route } from './+types' import type { Route } from './+types'
import { Outlet } from 'react-router' import { Link, NavLink, Outlet } from 'react-router'
import { userContext } from '@repo/auth/context' import { userContext } from '@repo/auth/context'
import { authMiddleware } from '@repo/auth/middleware/auth' import { authMiddleware } from '@repo/auth/middleware/auth'
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode' import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
import { NavUser } from '@repo/ui/components/nav-user' 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] export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
@@ -16,6 +22,7 @@ export async function loader({ context }: Route.ActionArgs) {
} }
export default function Component({ loaderData }: Route.ComponentProps) { export default function Component({ loaderData }: Route.ComponentProps) {
const isMobile = useIsMobile()
const { user } = loaderData const { user } = loaderData
return ( return (
@@ -25,7 +32,25 @@ export default function Component({ loaderData }: Route.ComponentProps) {
px-4 py-2 lg:py-4 sticky top-0 z-5" px-4 py-2 lg:py-4 sticky top-0 z-5"
> >
<div className="container mx-auto flex items-center"> <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"> <div className="ml-auto flex gap-2.5 items-center">
<ModeToggle /> <ModeToggle />
@@ -42,3 +67,14 @@ export default function Component({ loaderData }: Route.ComponentProps) {
</div> </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 { Link } from 'react-router'
import { userContext } from '@/context'
import { request as req } from '@/lib/request' 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 { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,

84
package-lock.json generated
View File

@@ -663,6 +663,15 @@
"@babel/core": "^7.0.0-0" "@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": { "node_modules/@babel/template": {
"version": "7.27.2", "version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -4678,6 +4687,12 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT" "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": { "node_modules/@types/lodash": {
"version": "4.17.20", "version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
@@ -4769,6 +4784,28 @@
"resolved": "apps/admin.saladeaula.digital", "resolved": "apps/admin.saladeaula.digital",
"link": true "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": { "node_modules/ansi-regex": {
"version": "6.2.2", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "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==", "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
"license": "MIT" "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": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -5469,6 +5512,13 @@
"resolved": "apps/insights.saladeaula.digital", "resolved": "apps/insights.saladeaula.digital",
"link": true "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": { "node_modules/is-arrayish": {
"version": "0.3.4", "version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
@@ -5548,6 +5598,15 @@
"url": "https://github.com/sponsors/panva" "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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6297,6 +6356,12 @@
"react": "^19.2.0" "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": { "node_modules/react-hook-form": {
"version": "7.66.0", "version": "7.66.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "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" "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": { "node_modules/retry": {
"version": "0.12.0", "version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@@ -6599,6 +6670,18 @@
"integrity": "sha512-0/A0Z8310GlZ0/Kx54FFeoJ0KDCz9Hwxu3sVvMCQvw37GClwcd6mao5kRio+uivqVJyMTjTWfzc0wsIHZf1l6w==", "integrity": "sha512-0/A0Z8310GlZ0/Kx54FFeoJ0KDCz9Hwxu3sVvMCQvw37GClwcd6mao5kRio+uivqVJyMTjTWfzc0wsIHZf1l6w==",
"license": "MIT" "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": { "node_modules/semver": {
"version": "7.7.3", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -8083,6 +8166,7 @@
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/postcss": "^4.1.16", "@tailwindcss/postcss": "^4.1.16",
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.16",
"ahooks": "^3.9.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",

View File

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

View File

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

View File

@@ -44,11 +44,11 @@ export function ModeToggle() {
) )
} }
export function ThemedImage() { export function ThemedImage({ ...props }) {
return ( return (
<> <div {...props}>
<img src={light} className="h-8" data-hide-on-theme="dark" /> <img src={light} className="h-8" data-hide-on-theme="dark" />
<img src={dark} className="h-8" data-hide-on-theme="light" /> <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 { debounce } from 'lodash'
import { SearchIcon, XIcon } from 'lucide-react' import { SearchIcon, XIcon } from 'lucide-react'
import { useRef } from 'react' import { useRef } from 'react'
@@ -8,8 +9,6 @@ import {
InputGroupButton, InputGroupButton,
InputGroupInput InputGroupInput
} from '@repo/ui/components/ui/input-group' } from '@repo/ui/components/ui/input-group'
import { useKeyPress } from '@repo/ui/hooks/use-keypress'
import { cn } from '@repo/ui/lib/utils' import { cn } from '@repo/ui/lib/utils'
export function SearchForm({ export function SearchForm({
@@ -26,9 +25,18 @@ export function SearchForm({
} & React.HTMLAttributes<HTMLDivElement>) { } & React.HTMLAttributes<HTMLDivElement>) {
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
useKeyPress('/', () => { useKeyPress(
inputRef.current?.focus() ['forwardslash'],
}) () => {
inputRef.current?.focus()
},
{
exactMatch: true,
events: ['keyup'],
useCapture: true,
target: () => window
}
)
const debouncedOnChange = debounce((value: string) => { const debouncedOnChange = debounce((value: string) => {
onChange?.(value) 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 { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from 'class-variance-authority'
import { cva, type VariantProps } from "class-variance-authority" import { PanelLeftIcon } from 'lucide-react'
import { PanelLeftIcon } from "lucide-react" import * as React from 'react'
import { useIsMobile } from "@/hooks/use-mobile" import { Button } from '@/components/ui/button'
import { cn } from "@/lib/utils" import { Input } from '@/components/ui/input'
import { Button } from "@/components/ui/button" import { Separator } from '@/components/ui/separator'
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
SheetDescription, SheetDescription,
SheetHeader, SheetHeader,
SheetTitle, SheetTitle
} from "@/components/ui/sheet" } from '@/components/ui/sheet'
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from '@/components/ui/skeleton'
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger
} from "@/components/ui/tooltip" } 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_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem" const SIDEBAR_WIDTH = '16rem'
const SIDEBAR_WIDTH_MOBILE = "18rem" const SIDEBAR_WIDTH_MOBILE = '18rem'
const SIDEBAR_WIDTH_ICON = "3rem" const SIDEBAR_WIDTH_ICON = '3rem'
const SIDEBAR_KEYBOARD_SHORTCUT = "b" const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
type SidebarContextProps = { type SidebarContextProps = {
state: "expanded" | "collapsed" state: 'expanded' | 'collapsed'
open: boolean open: boolean
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
openMobile: boolean openMobile: boolean
@@ -47,7 +47,7 @@ const SidebarContext = React.createContext<SidebarContextProps | null>(null)
function useSidebar() { function useSidebar() {
const context = React.useContext(SidebarContext) const context = React.useContext(SidebarContext)
if (!context) { if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.") throw new Error('useSidebar must be used within a SidebarProvider.')
} }
return context return context
@@ -61,7 +61,7 @@ function SidebarProvider({
style, style,
children, children,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<'div'> & {
defaultOpen?: boolean defaultOpen?: boolean
open?: boolean open?: boolean
onOpenChange?: (open: boolean) => void onOpenChange?: (open: boolean) => void
@@ -75,7 +75,7 @@ function SidebarProvider({
const open = openProp ?? _open const open = openProp ?? _open
const setOpen = React.useCallback( const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => { (value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value const openState = typeof value === 'function' ? value(open) : value
if (setOpenProp) { if (setOpenProp) {
setOpenProp(openState) setOpenProp(openState)
} else { } else {
@@ -105,13 +105,13 @@ function SidebarProvider({
} }
} }
window.addEventListener("keydown", handleKeyDown) window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown)
}, [toggleSidebar]) }, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed". // 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. // 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>( const contextValue = React.useMemo<SidebarContextProps>(
() => ({ () => ({
@@ -121,7 +121,7 @@ function SidebarProvider({
isMobile, isMobile,
openMobile, openMobile,
setOpenMobile, setOpenMobile,
toggleSidebar, toggleSidebar
}), }),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
) )
@@ -133,13 +133,13 @@ function SidebarProvider({
data-slot="sidebar-wrapper" data-slot="sidebar-wrapper"
style={ style={
{ {
"--sidebar-width": SIDEBAR_WIDTH, '--sidebar-width': SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON, '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
...style, ...style
} as React.CSSProperties } as React.CSSProperties
} }
className={cn( 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 className
)} )}
{...props} {...props}
@@ -152,25 +152,25 @@ function SidebarProvider({
} }
function Sidebar({ function Sidebar({
side = "left", side = 'left',
variant = "sidebar", variant = 'sidebar',
collapsible = "offcanvas", collapsible = 'offcanvas',
className, className,
children, children,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<'div'> & {
side?: "left" | "right" side?: 'left' | 'right'
variant?: "sidebar" | "floating" | "inset" variant?: 'sidebar' | 'floating' | 'inset'
collapsible?: "offcanvas" | "icon" | "none" collapsible?: 'offcanvas' | 'icon' | 'none'
}) { }) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar() const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") { if (collapsible === 'none') {
return ( return (
<div <div
data-slot="sidebar" data-slot="sidebar"
className={cn( 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 className
)} )}
{...props} {...props}
@@ -190,7 +190,7 @@ function Sidebar({
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={ style={
{ {
"--sidebar-width": SIDEBAR_WIDTH_MOBILE, '--sidebar-width': SIDEBAR_WIDTH_MOBILE
} as React.CSSProperties } as React.CSSProperties
} }
side={side} side={side}
@@ -209,7 +209,7 @@ function Sidebar({
<div <div
className="group peer text-sidebar-foreground hidden md:block" className="group peer text-sidebar-foreground hidden md:block"
data-state={state} data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""} data-collapsible={state === 'collapsed' ? collapsible : ''}
data-variant={variant} data-variant={variant}
data-side={side} data-side={side}
data-slot="sidebar" data-slot="sidebar"
@@ -218,25 +218,25 @@ function Sidebar({
<div <div
data-slot="sidebar-gap" data-slot="sidebar-gap"
className={cn( className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear", 'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
"group-data-[collapsible=offcanvas]:w-0", 'group-data-[collapsible=offcanvas]:w-0',
"group-data-[side=right]:rotate-180", 'group-data-[side=right]:rotate-180',
variant === "floating" || variant === "inset" variant === 'floating' || variant === 'inset'
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)" : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)'
)} )}
/> />
<div <div
data-slot="sidebar-container" data-slot="sidebar-container"
className={cn( className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex", 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
side === "left" side === 'left'
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: "right-0 group-data-[collapsible=offcanvas]:right-[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. // Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset" variant === 'floating' || variant === 'inset'
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" ? '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", : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
className className
)} )}
{...props} {...props}
@@ -266,7 +266,7 @@ function SidebarTrigger({
data-slot="sidebar-trigger" data-slot="sidebar-trigger"
variant="ghost" variant="ghost"
size="icon" size="icon"
className={cn("size-7", className)} className={cn('size-7', className)}
onClick={(event) => { onClick={(event) => {
onClick?.(event) onClick?.(event)
toggleSidebar() toggleSidebar()
@@ -279,7 +279,7 @@ function SidebarTrigger({
) )
} }
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
const { toggleSidebar } = useSidebar() const { toggleSidebar } = useSidebar()
return ( return (
@@ -291,12 +291,12 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
onClick={toggleSidebar} onClick={toggleSidebar}
title="Toggle Sidebar" title="Toggle Sidebar"
className={cn( 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", '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", '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", '[[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", '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=left][data-collapsible=offcanvas]_&]:-right-2',
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
className className
)} )}
{...props} {...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 ( return (
<main <main
data-slot="sidebar-inset" data-slot="sidebar-inset"
className={cn( className={cn(
"bg-background relative flex w-full flex-1 flex-col", '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", '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 className
)} )}
{...props} {...props}
@@ -326,29 +326,29 @@ function SidebarInput({
<Input <Input
data-slot="sidebar-input" data-slot="sidebar-input"
data-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} {...props}
/> />
) )
} }
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="sidebar-header" data-slot="sidebar-header"
data-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} {...props}
/> />
) )
} }
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="sidebar-footer" data-slot="sidebar-footer"
data-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} {...props}
/> />
) )
@@ -362,19 +362,19 @@ function SidebarSeparator({
<Separator <Separator
data-slot="sidebar-separator" data-slot="sidebar-separator"
data-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} {...props}
/> />
) )
} }
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="sidebar-content" data-slot="sidebar-content"
data-sidebar="content" data-sidebar="content"
className={cn( 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 className
)} )}
{...props} {...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 ( return (
<div <div
data-slot="sidebar-group" data-slot="sidebar-group"
data-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} {...props}
/> />
) )
@@ -397,16 +397,16 @@ function SidebarGroupLabel({
className, className,
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) { }: React.ComponentProps<'div'> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div" const Comp = asChild ? Slot : 'div'
return ( return (
<Comp <Comp
data-slot="sidebar-group-label" data-slot="sidebar-group-label"
data-sidebar="group-label" data-sidebar="group-label"
className={cn( 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", '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", 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
className className
)} )}
{...props} {...props}
@@ -418,18 +418,18 @@ function SidebarGroupAction({
className, className,
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) { }: React.ComponentProps<'button'> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : 'button'
return ( return (
<Comp <Comp
data-slot="sidebar-group-action" data-slot="sidebar-group-action"
data-sidebar="group-action" data-sidebar="group-action"
className={cn( 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. // Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden", 'after:absolute after:-inset-2 md:after:hidden',
"group-data-[collapsible=icon]:hidden", 'group-data-[collapsible=icon]:hidden',
className className
)} )}
{...props} {...props}
@@ -440,75 +440,75 @@ function SidebarGroupAction({
function SidebarGroupContent({ function SidebarGroupContent({
className, className,
...props ...props
}: React.ComponentProps<"div">) { }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="sidebar-group-content" data-slot="sidebar-group-content"
data-sidebar="group-content" data-sidebar="group-content"
className={cn("w-full text-sm", className)} className={cn('w-full text-sm', className)}
{...props} {...props}
/> />
) )
} }
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
return ( return (
<ul <ul
data-slot="sidebar-menu" data-slot="sidebar-menu"
data-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} {...props}
/> />
) )
} }
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
return ( return (
<li <li
data-slot="sidebar-menu-item" data-slot="sidebar-menu-item"
data-sidebar="menu-item" data-sidebar="menu-item"
className={cn("group/menu-item relative", className)} className={cn('group/menu-item relative', className)}
{...props} {...props}
/> />
) )
} }
const sidebarMenuButtonVariants = cva( 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: { variants: {
variant: { variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
outline: 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: { size: {
default: "h-8 text-sm", default: 'h-8 text-sm',
sm: "h-7 text-xs", sm: 'h-7 text-xs',
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!'
}, }
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default'
}, }
} }
) )
function SidebarMenuButton({ function SidebarMenuButton({
asChild = false, asChild = false,
isActive = false, isActive = false,
variant = "default", variant = 'default',
size = "default", size = 'default',
tooltip, tooltip,
className, className,
...props ...props
}: React.ComponentProps<"button"> & { }: React.ComponentProps<'button'> & {
asChild?: boolean asChild?: boolean
isActive?: boolean isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent> tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>) { } & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : 'button'
const { isMobile, state } = useSidebar() const { isMobile, state } = useSidebar()
const button = ( const button = (
@@ -526,9 +526,9 @@ function SidebarMenuButton({
return button return button
} }
if (typeof tooltip === "string") { if (typeof tooltip === 'string') {
tooltip = { tooltip = {
children: tooltip, children: tooltip
} }
} }
@@ -538,7 +538,7 @@ function SidebarMenuButton({
<TooltipContent <TooltipContent
side="right" side="right"
align="center" align="center"
hidden={state !== "collapsed" || isMobile} hidden={state !== 'collapsed' || isMobile}
{...tooltip} {...tooltip}
/> />
</Tooltip> </Tooltip>
@@ -550,26 +550,26 @@ function SidebarMenuAction({
asChild = false, asChild = false,
showOnHover = false, showOnHover = false,
...props ...props
}: React.ComponentProps<"button"> & { }: React.ComponentProps<'button'> & {
asChild?: boolean asChild?: boolean
showOnHover?: boolean showOnHover?: boolean
}) { }) {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : 'button'
return ( return (
<Comp <Comp
data-slot="sidebar-menu-action" data-slot="sidebar-menu-action"
data-sidebar="menu-action" data-sidebar="menu-action"
className={cn( 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. // Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden", 'after:absolute after:-inset-2 md:after:hidden',
"peer-data-[size=sm]/menu-button:top-1", 'peer-data-[size=sm]/menu-button:top-1',
"peer-data-[size=default]/menu-button:top-1.5", 'peer-data-[size=default]/menu-button:top-1.5',
"peer-data-[size=lg]/menu-button:top-2.5", 'peer-data-[size=lg]/menu-button:top-2.5',
"group-data-[collapsible=icon]:hidden", 'group-data-[collapsible=icon]:hidden',
showOnHover && 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 className
)} )}
{...props} {...props}
@@ -580,18 +580,18 @@ function SidebarMenuAction({
function SidebarMenuBadge({ function SidebarMenuBadge({
className, className,
...props ...props
}: React.ComponentProps<"div">) { }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="sidebar-menu-badge" data-slot="sidebar-menu-badge"
data-sidebar="menu-badge" data-sidebar="menu-badge"
className={cn( 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", '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-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=sm]/menu-button:top-1',
"peer-data-[size=default]/menu-button:top-1.5", 'peer-data-[size=default]/menu-button:top-1.5',
"peer-data-[size=lg]/menu-button:top-2.5", 'peer-data-[size=lg]/menu-button:top-2.5',
"group-data-[collapsible=icon]:hidden", 'group-data-[collapsible=icon]:hidden',
className className
)} )}
{...props} {...props}
@@ -603,7 +603,7 @@ function SidebarMenuSkeleton({
className, className,
showIcon = false, showIcon = false,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<'div'> & {
showIcon?: boolean showIcon?: boolean
}) { }) {
// Random width between 50 to 90%. // Random width between 50 to 90%.
@@ -615,7 +615,7 @@ function SidebarMenuSkeleton({
<div <div
data-slot="sidebar-menu-skeleton" data-slot="sidebar-menu-skeleton"
data-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} {...props}
> >
{showIcon && ( {showIcon && (
@@ -629,7 +629,7 @@ function SidebarMenuSkeleton({
data-sidebar="menu-skeleton-text" data-sidebar="menu-skeleton-text"
style={ style={
{ {
"--skeleton-width": width, '--skeleton-width': width
} as React.CSSProperties } as React.CSSProperties
} }
/> />
@@ -637,14 +637,14 @@ function SidebarMenuSkeleton({
) )
} }
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
return ( return (
<ul <ul
data-slot="sidebar-menu-sub" data-slot="sidebar-menu-sub"
data-sidebar="menu-sub" data-sidebar="menu-sub"
className={cn( 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", '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", 'group-data-[collapsible=icon]:hidden',
className className
)} )}
{...props} {...props}
@@ -655,12 +655,12 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
function SidebarMenuSubItem({ function SidebarMenuSubItem({
className, className,
...props ...props
}: React.ComponentProps<"li">) { }: React.ComponentProps<'li'>) {
return ( return (
<li <li
data-slot="sidebar-menu-sub-item" data-slot="sidebar-menu-sub-item"
data-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} {...props}
/> />
) )
@@ -668,16 +668,16 @@ function SidebarMenuSubItem({
function SidebarMenuSubButton({ function SidebarMenuSubButton({
asChild = false, asChild = false,
size = "md", size = 'md',
isActive = false, isActive = false,
className, className,
...props ...props
}: React.ComponentProps<"a"> & { }: React.ComponentProps<'a'> & {
asChild?: boolean asChild?: boolean
size?: "sm" | "md" size?: 'sm' | 'md'
isActive?: boolean isActive?: boolean
}) { }) {
const Comp = asChild ? Slot : "a" const Comp = asChild ? Slot : 'a'
return ( return (
<Comp <Comp
@@ -686,11 +686,11 @@ function SidebarMenuSubButton({
data-size={size} data-size={size}
data-active={isActive} data-active={isActive}
className={cn( 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", '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", 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === "sm" && "text-xs", size === 'sm' && 'text-xs',
size === "md" && "text-sm", size === 'md' && 'text-sm',
"group-data-[collapsible=icon]:hidden", 'group-data-[collapsible=icon]:hidden',
className className
)} )}
{...props} {...props}
@@ -722,5 +722,5 @@ export {
SidebarRail, SidebarRail,
SidebarSeparator, SidebarSeparator,
SidebarTrigger, 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
}