add list to catalog
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export default function Route() {
|
||||
return <>...</>
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export type Cert = {
|
||||
exp_interval: number
|
||||
}
|
||||
|
||||
export type Course = {
|
||||
id: string
|
||||
name: string
|
||||
access_period: string
|
||||
cert: Cert
|
||||
metadata__unit_price?: number
|
||||
}
|
||||
|
||||
export type CustomPricing = {
|
||||
sk: string
|
||||
unit_price: number
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import Fuse from 'fuse.js'
|
||||
import { AwardIcon, BanIcon, LaptopIcon } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { cn } from '@repo/ui/lib/utils'
|
||||
import { Currency } from '@repo/ui/components/currency'
|
||||
import {
|
||||
Card,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@repo/ui/components/ui/card'
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle
|
||||
} from '@repo/ui/components/ui/empty'
|
||||
|
||||
import type { Course, CustomPricing } from './data'
|
||||
import placeholder from '@/assets/placeholder.webp'
|
||||
|
||||
export function Grid({
|
||||
search,
|
||||
hits = [],
|
||||
customPricing = []
|
||||
}: {
|
||||
search: string
|
||||
hits: Course[]
|
||||
customPricing: CustomPricing[]
|
||||
}) {
|
||||
const fuse = useMemo(() => {
|
||||
return new Fuse(hits, {
|
||||
keys: ['name'],
|
||||
threshold: 0.3,
|
||||
includeMatches: true
|
||||
})
|
||||
}, [hits])
|
||||
|
||||
const hits_ = useMemo(() => {
|
||||
if (!search) {
|
||||
return hits
|
||||
}
|
||||
|
||||
return fuse.search(search).map(({ item }) => item)
|
||||
}, [search, fuse, hits])
|
||||
|
||||
const customPricingMap = new Map(
|
||||
customPricing.map((x) => {
|
||||
const [, courseId] = x.sk.split('#')
|
||||
return [courseId, x.unit_price]
|
||||
})
|
||||
)
|
||||
|
||||
if (hits_.length === 0) {
|
||||
return (
|
||||
<Empty className="border border-dashed">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<BanIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nada encontrado</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Nenhum resultado para <mark>{search}</mark>.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
||||
{hits_.map((props: Course, idx) => {
|
||||
return (
|
||||
<Course
|
||||
key={idx}
|
||||
custom_pricing={customPricingMap.get(props.id)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Course({
|
||||
name,
|
||||
access_period,
|
||||
cert,
|
||||
metadata__unit_price,
|
||||
custom_pricing
|
||||
}: Course & { custom_pricing?: number }) {
|
||||
return (
|
||||
<Card className="overflow-hidden relative h-96">
|
||||
<CardHeader className="z-1 relative">
|
||||
<CardTitle className="text-xl/6">{name}</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardFooter className="text-gray-300 text-sm absolute z-1 bottom-6 w-full flex gap-1.5">
|
||||
<ul className="flex gap-2.5">
|
||||
<li className="flex gap-0.5 items-center">
|
||||
<LaptopIcon className="size-4" />
|
||||
<span>{access_period}d</span>
|
||||
</li>
|
||||
|
||||
{cert?.exp_interval && (
|
||||
<li className="flex gap-0.5 items-center">
|
||||
<AwardIcon className="size-4" />
|
||||
<span>{cert.exp_interval}d</span>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{metadata__unit_price && (
|
||||
<>
|
||||
<li className="flex gap-1.5">
|
||||
<span
|
||||
className={cn({
|
||||
'line-through text-muted-foreground': custom_pricing,
|
||||
'font-bold': !custom_pricing
|
||||
})}
|
||||
>
|
||||
<Currency>{metadata__unit_price}</Currency>
|
||||
</span>
|
||||
|
||||
{custom_pricing && (
|
||||
<span className="font-bold">
|
||||
<Currency>{custom_pricing}</Currency>
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</CardFooter>
|
||||
|
||||
<img
|
||||
src={placeholder}
|
||||
alt={name}
|
||||
className="absolute bottom-0 dark:opacity-75"
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const currency = new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL'
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useMemo } from 'react'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '@repo/ui/components/ui/table'
|
||||
|
||||
import type { Course, CustomPricing } from './data'
|
||||
import { Currency } from '@repo/ui/components/currency'
|
||||
import { Card, CardContent } from '@repo/ui/components/ui/card'
|
||||
|
||||
export function List({
|
||||
search,
|
||||
hits = [],
|
||||
customPricing = []
|
||||
}: {
|
||||
search: string
|
||||
hits: Course[]
|
||||
customPricing: CustomPricing[]
|
||||
}) {
|
||||
const fuse = useMemo(() => {
|
||||
return new Fuse(hits, {
|
||||
keys: ['name'],
|
||||
threshold: 0.3,
|
||||
includeMatches: true
|
||||
})
|
||||
}, [hits])
|
||||
|
||||
const hits_ = useMemo(() => {
|
||||
if (!search) {
|
||||
return hits
|
||||
}
|
||||
|
||||
return fuse.search(search).map(({ item }) => item)
|
||||
}, [search, fuse, hits])
|
||||
|
||||
const customPricingMap = new Map(
|
||||
customPricing.map((x) => {
|
||||
const [, courseId] = x.sk.split('#')
|
||||
return [courseId, x.unit_price]
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="pointer-events-none">
|
||||
<TableHead>Curso</TableHead>
|
||||
<TableHead>Tempo de acesso</TableHead>
|
||||
<TableHead>Validade do cert.</TableHead>
|
||||
<TableHead>Valor</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{hits_.map(
|
||||
(
|
||||
{ id, name, access_period, cert, metadata__unit_price },
|
||||
index
|
||||
) => {
|
||||
const custom_pricing = customPricingMap.get(id)
|
||||
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{name}</TableCell>
|
||||
<TableCell>{access_period} dias</TableCell>
|
||||
<TableCell>
|
||||
{cert?.exp_interval ? (
|
||||
<>{cert.exp_interval} dias</>
|
||||
) : (
|
||||
<>N/A</>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{metadata__unit_price ? (
|
||||
<>
|
||||
{custom_pricing ? (
|
||||
<Currency>{custom_pricing}</Currency>
|
||||
) : (
|
||||
<Currency>{metadata__unit_price}</Currency>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>N/A</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
)}
|
||||
|
||||
{hits_.length === 0 && (
|
||||
<TableRow className="pointer-events-none">
|
||||
<TableCell colSpan={4} className="h-20 text-center">
|
||||
Nenhum resultado para <mark>{search}</mark>.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,50 +1,23 @@
|
||||
import type { Route } from './+types/route'
|
||||
|
||||
import Fuse from 'fuse.js'
|
||||
import { AwardIcon, BanIcon, LaptopIcon } from 'lucide-react'
|
||||
import { Grid2X2Icon, ListIcon } from 'lucide-react'
|
||||
import { Suspense, useMemo } from 'react'
|
||||
import { Await, useSearchParams } from 'react-router'
|
||||
|
||||
import { SearchForm } from '@repo/ui/components/search-form'
|
||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||
import {
|
||||
Card,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@repo/ui/components/ui/card'
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle
|
||||
} from '@repo/ui/components/ui/empty'
|
||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||
import { cn } from '@repo/ui/lib/utils'
|
||||
import { cloudflareContext } from '@repo/auth/context'
|
||||
import { createSearch } from '@repo/util/meili'
|
||||
import { request as req } from '@repo/util/request'
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem
|
||||
} from '@repo/ui/components/ui/toggle-group'
|
||||
|
||||
import placeholder from '@/assets/placeholder.webp'
|
||||
import { Currency } from '@repo/ui/components/currency'
|
||||
|
||||
type Cert = {
|
||||
exp_interval: number
|
||||
}
|
||||
|
||||
type Course = {
|
||||
id: string
|
||||
name: string
|
||||
access_period: string
|
||||
cert: Cert
|
||||
metadata__unit_price?: number
|
||||
}
|
||||
|
||||
type CustomPricing = {
|
||||
sk: string
|
||||
unit_price: number
|
||||
}
|
||||
import type { CustomPricing, Course } from './data'
|
||||
import { List } from './list'
|
||||
import { Grid } from './grid'
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: 'Catálogo de cursos' }]
|
||||
@@ -74,6 +47,7 @@ export async function loader({ context, request, params }: Route.LoaderArgs) {
|
||||
export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const search = searchParams.get('s') as string
|
||||
const view = searchParams.get('view') || 'grid'
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
@@ -88,6 +62,10 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
||||
|
||||
<Await resolve={data}>
|
||||
{([{ hits }, { items }]) => {
|
||||
const hits_ = hits.filter(
|
||||
({ metadata__unit_price = 0 }) => metadata__unit_price > 0
|
||||
) as Course[]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col lg:flex-row justify-between gap-2.5 mb-2.5">
|
||||
@@ -101,17 +79,40 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
||||
}
|
||||
defaultValue={search}
|
||||
onChange={(search) => {
|
||||
setSearchParams({ s: String(search) })
|
||||
setSearchParams((searchParams) => {
|
||||
searchParams.set('s', String(search))
|
||||
return searchParams
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ToggleGroup
|
||||
variant="outline"
|
||||
type="single"
|
||||
value={view}
|
||||
className="**:cursor-pointer"
|
||||
onValueChange={(value) => {
|
||||
setSearchParams((searchParams) => {
|
||||
searchParams.set('view', value)
|
||||
return searchParams
|
||||
})
|
||||
}}
|
||||
>
|
||||
<ToggleGroupItem value="list">
|
||||
<ListIcon />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="grid">
|
||||
<Grid2X2Icon />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
<List
|
||||
search={search}
|
||||
hits={hits as Course[]}
|
||||
customPricing={items}
|
||||
/>
|
||||
{view === 'list' ? (
|
||||
<List search={search} hits={hits_} customPricing={items} />
|
||||
) : (
|
||||
<Grid search={search} hits={hits_} customPricing={items} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
@@ -119,132 +120,3 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
function List({
|
||||
search,
|
||||
hits = [],
|
||||
customPricing = []
|
||||
}: {
|
||||
search: string
|
||||
hits: Course[]
|
||||
customPricing: CustomPricing[]
|
||||
}) {
|
||||
const fuse = useMemo(() => {
|
||||
return new Fuse(hits, {
|
||||
keys: ['name'],
|
||||
threshold: 0.3,
|
||||
includeMatches: true
|
||||
})
|
||||
}, [hits])
|
||||
|
||||
const hits_ = useMemo(() => {
|
||||
if (!search) {
|
||||
return hits
|
||||
}
|
||||
|
||||
return fuse.search(search).map(({ item }) => item)
|
||||
}, [search, fuse, hits])
|
||||
|
||||
const customPricingMap = new Map(
|
||||
customPricing.map((x) => {
|
||||
const [, courseId] = x.sk.split('#')
|
||||
return [courseId, x.unit_price]
|
||||
})
|
||||
)
|
||||
|
||||
if (hits_.length === 0) {
|
||||
return (
|
||||
<Empty className="border border-dashed">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<BanIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nada encontrado</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Nenhum resultado para <mark>{search}</mark>.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
||||
{hits_
|
||||
.filter(({ metadata__unit_price = 0 }) => metadata__unit_price > 0)
|
||||
.map((props: Course, idx) => {
|
||||
return (
|
||||
<Course
|
||||
key={idx}
|
||||
custom_pricing={customPricingMap.get(props.id)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Course({
|
||||
name,
|
||||
access_period,
|
||||
cert,
|
||||
metadata__unit_price,
|
||||
custom_pricing
|
||||
}: Course & { custom_pricing?: number }) {
|
||||
return (
|
||||
<Card className="overflow-hidden relative h-96">
|
||||
<CardHeader className="z-1 relative">
|
||||
<CardTitle className="text-xl/6">{name}</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardFooter className="text-gray-300 text-sm absolute z-1 bottom-6 w-full flex gap-1.5">
|
||||
<ul className="flex gap-2.5">
|
||||
<li className="flex gap-0.5 items-center">
|
||||
<LaptopIcon className="size-4" />
|
||||
<span>{access_period}d</span>
|
||||
</li>
|
||||
|
||||
{cert?.exp_interval && (
|
||||
<li className="flex gap-0.5 items-center">
|
||||
<AwardIcon className="size-4" />
|
||||
<span>{cert.exp_interval}d</span>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{metadata__unit_price && (
|
||||
<>
|
||||
<li className="flex gap-1.5">
|
||||
<span
|
||||
className={cn({
|
||||
'line-through text-muted-foreground': custom_pricing,
|
||||
'font-bold': !custom_pricing
|
||||
})}
|
||||
>
|
||||
<Currency>{metadata__unit_price}</Currency>
|
||||
</span>
|
||||
|
||||
{custom_pricing && (
|
||||
<span className="font-bold">
|
||||
<Currency>{custom_pricing}</Currency>
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</CardFooter>
|
||||
|
||||
<img
|
||||
src={placeholder}
|
||||
alt={name}
|
||||
className="absolute bottom-0 dark:opacity-75"
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const currency = new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL'
|
||||
})
|
||||
|
||||
@@ -72,13 +72,13 @@ export default function Route({}: Route.ComponentProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (fetcher.data?.ok) {
|
||||
toast.success('O colaborador foi atualizado.')
|
||||
toast.success('O colaborador(a) foi atualizado(a).')
|
||||
return
|
||||
}
|
||||
|
||||
switch (fetcher.data?.error?.type) {
|
||||
case 'RateLimitExceededError':
|
||||
toast.error('Seu limite diário de atualizações foi atingido.')
|
||||
toast.error('Limite diário de atualizações atingido.')
|
||||
return
|
||||
case 'CPFConflictError':
|
||||
setError('cpf', { message: 'CPF já está em uso' })
|
||||
|
||||
56
package-lock.json
generated
56
package-lock.json
generated
@@ -3499,6 +3499,60 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
|
||||
"integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle-group": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz",
|
||||
"integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-roving-focus": "1.1.11",
|
||||
"@radix-ui/react-toggle": "1.1.10",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
|
||||
@@ -7441,6 +7495,8 @@
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
|
||||
83
packages/ui/src/components/ui/toggle-group.tsx
Normal file
83
packages/ui/src/components/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||
import { type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { toggleVariants } from "@/components/ui/toggle"
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
spacing?: number
|
||||
}
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 0,
|
||||
})
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
spacing?: number
|
||||
}) {
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
data-spacing={spacing}
|
||||
style={{ "--gap": spacing } as React.CSSProperties}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ToggleGroupItem({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
const context = React.useContext(ToggleGroupContext)
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={context.variant || variant}
|
||||
data-size={context.size || size}
|
||||
data-spacing={context.spacing}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
|
||||
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem }
|
||||
47
packages/ui/src/components/ui/toggle.tsx
Normal file
47
packages/ui/src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2 min-w-9",
|
||||
sm: "h-8 px-1.5 min-w-8",
|
||||
lg: "h-10 px-2.5 min-w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
Reference in New Issue
Block a user