update
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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') || ''}
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
<div className="flex gap-5">
|
||||||
|
<Link to="/">
|
||||||
<ThemedImage />
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
84
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ export function createSessionStorage(env) {
|
|||||||
maxAge: 86400 * 7 // 7 days
|
maxAge: 86400 * 7 // 7 days
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return sessionStorage
|
return sessionStorage
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
['forwardslash'],
|
||||||
|
() => {
|
||||||
inputRef.current?.focus()
|
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)
|
||||||
|
|||||||
32
packages/ui/src/components/ui/checkbox.tsx
Normal file
32
packages/ui/src/components/ui/checkbox.tsx
Normal 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 }
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user