Fix build and formatting

This commit is contained in:
Evan Yang
2026-03-07 18:15:29 +08:00
parent 83a76e86fe
commit c74a877ab5
106 changed files with 7560 additions and 6282 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ import json
import secrets import secrets
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import NoReturn
from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel from pydantic import BaseModel
@@ -46,7 +47,7 @@ def _cleanup_expired_admin_sessions(now_timestamp: int | None = None) -> None:
admin_sessions.pop(token, None) admin_sessions.pop(token, None)
def _raise_unauthorized(detail: str) -> None: def _raise_unauthorized(detail: str) -> NoReturn:
raise HTTPException( raise HTTPException(
status_code=401, status_code=401,
detail=detail, detail=detail,
@@ -264,12 +264,13 @@ async def test_models_endpoint_accept_headers(integration_client: AsyncClient) -
async def test_admin_endpoint_unauthenticated( async def test_admin_endpoint_unauthenticated(
integration_client: AsyncClient, db_snapshot: Any integration_client: AsyncClient, db_snapshot: Any
) -> None: ) -> None:
"""Test GET /admin/ endpoint redirects to /""" """Test unauthenticated access to admin settings endpoint is rejected."""
await db_snapshot.capture() await db_snapshot.capture()
response = await integration_client.get("/admin/api/settings") response = await integration_client.get("/admin/api/settings")
assert response.status_code == 403 assert response.status_code == 401
assert response.headers.get("www-authenticate") == "Bearer"
diff = await db_snapshot.diff() diff = await db_snapshot.diff()
assert len(diff["api_keys"]["added"]) == 0 assert len(diff["api_keys"]["added"]) == 0
+5
View File
@@ -0,0 +1,5 @@
pnpm-lock.yaml
.next
node_modules
out
next-env.d.ts
+10 -10
View File
@@ -1,16 +1,16 @@
@import "tailwindcss"; @import 'tailwindcss';
@import "tw-animate-css"; @import 'tw-animate-css';
@custom-variant dark (&:is(.dark *, .red *)); @custom-variant dark (&:is(.dark *, .red *));
:root { :root {
--radius: 0.625rem; --radius: 0.625rem;
--font-geist-sans: --font-geist-sans:
ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica,
"Apple Color Emoji", "Segoe UI Emoji"; Arial, 'Apple Color Emoji', 'Segoe UI Emoji';
--font-geist-mono: --font-geist-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
"Courier New", monospace; 'Courier New', monospace;
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
@@ -171,10 +171,10 @@ body {
} }
button, button,
[type="button"], [type='button'],
[type="submit"], [type='submit'],
[type="reset"], [type='reset'],
[role="button"] { [role='button'] {
cursor: pointer; cursor: pointer;
} }
} }
+5 -12
View File
@@ -1,19 +1,10 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google'; import { GeistMono } from 'geist/font/mono';
import { GeistSans } from 'geist/font/sans';
import './globals.css'; import './globals.css';
import { Providers } from './providers'; import { Providers } from './providers';
import { SuppressHydrationWarning } from '@/components/suppress-hydration-warning'; import { SuppressHydrationWarning } from '@/components/suppress-hydration-warning';
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Routstr', title: 'Routstr',
description: 'Routstr model management', description: 'Routstr model management',
@@ -29,7 +20,9 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang='en' suppressHydrationWarning> <html lang='en' suppressHydrationWarning>
<body className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}> <body
className={`${GeistSans.variable} ${GeistMono.variable} font-sans antialiased`}
>
<SuppressHydrationWarning> <SuppressHydrationWarning>
<Providers>{children}</Providers> <Providers>{children}</Providers>
</SuppressHydrationWarning> </SuppressHydrationWarning>
+20 -5
View File
@@ -1,4 +1,9 @@
import { useEffect, useState, type ChangeEvent, type KeyboardEvent } from 'react'; import {
useEffect,
useState,
type ChangeEvent,
type KeyboardEvent,
} from 'react';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { CalendarIcon, Filter, X } from 'lucide-react'; import { CalendarIcon, Filter, X } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -88,8 +93,12 @@ const ENDPOINT_OPTIONS = [
'/embeddings/models', '/embeddings/models',
]; ];
const STATUS_4XX_CODES = STATUS_CODE_OPTIONS.filter((code) => code.startsWith('4')); const STATUS_4XX_CODES = STATUS_CODE_OPTIONS.filter((code) =>
const STATUS_5XX_CODES = STATUS_CODE_OPTIONS.filter((code) => code.startsWith('5')); code.startsWith('4')
);
const STATUS_5XX_CODES = STATUS_CODE_OPTIONS.filter((code) =>
code.startsWith('5')
);
export function LogFilters({ export function LogFilters({
selectedDate, selectedDate,
@@ -194,7 +203,9 @@ export function LogFilters({
const handleQuickStatusCode = (range: '4xx' | '5xx') => { const handleQuickStatusCode = (range: '4xx' | '5xx') => {
const rangeCodes = range === '4xx' ? STATUS_4XX_CODES : STATUS_5XX_CODES; const rangeCodes = range === '4xx' ? STATUS_4XX_CODES : STATUS_5XX_CODES;
const nextSelection = new Set(selectedStatusCodes); const nextSelection = new Set(selectedStatusCodes);
const allSelected = rangeCodes.every((code) => selectedStatusCodes.includes(code)); const allSelected = rangeCodes.every((code) =>
selectedStatusCodes.includes(code)
);
if (allSelected) { if (allSelected) {
rangeCodes.forEach((code) => nextSelection.delete(code)); rangeCodes.forEach((code) => nextSelection.delete(code));
@@ -414,7 +425,11 @@ export function LogFilters({
</div> </div>
<div className='flex items-end sm:col-span-2 lg:col-span-1'> <div className='flex items-end sm:col-span-2 lg:col-span-1'>
<Button onClick={onClearFilters} variant='outline' className='w-full'> <Button
onClick={onClearFilters}
variant='outline'
className='w-full'
>
Clear Filters Clear Filters
</Button> </Button>
</div> </div>
+12 -3
View File
@@ -44,7 +44,10 @@ interface MultiSelectCommandFilterProps {
function FilterBadge({ value }: { value: string }) { function FilterBadge({ value }: { value: string }) {
return ( return (
<Badge variant='secondary' className='flex items-center gap-1 px-1 font-normal'> <Badge
variant='secondary'
className='flex items-center gap-1 px-1 font-normal'
>
{value} {value}
<X className='h-3 w-3 opacity-70' aria-hidden='true' /> <X className='h-3 w-3 opacity-70' aria-hidden='true' />
</Badge> </Badge>
@@ -93,7 +96,10 @@ export function MultiSelectCommandFilter({
<Label>{label}</Label> <Label>{label}</Label>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant='outline' className='w-full justify-start text-left font-normal'> <Button
variant='outline'
className='w-full justify-start text-left font-normal'
>
<div className='flex flex-wrap gap-1 overflow-hidden'> <div className='flex flex-wrap gap-1 overflow-hidden'>
{selectedValues.length > 0 ? ( {selectedValues.length > 0 ? (
selectedValues.map((value) => ( selectedValues.map((value) => (
@@ -158,7 +164,10 @@ export function MultiSelectCommandFilter({
{options {options
.filter((option) => !selectedValues.includes(option)) .filter((option) => !selectedValues.includes(option))
.map((option) => ( .map((option) => (
<CommandItem key={option} onSelect={() => toggleSelection(option)}> <CommandItem
key={option}
onSelect={() => toggleSelection(option)}
>
<Checkbox checked={false} className='mr-2' /> <Checkbox checked={false} className='mr-2' />
{option} {option}
</CommandItem> </CommandItem>
+4 -1
View File
@@ -226,7 +226,10 @@ export default function LogsPage() {
{isLoading ? ( {isLoading ? (
<div className='space-y-2'> <div className='space-y-2'>
{Array.from({ length: 8 }).map((_, index) => ( {Array.from({ length: 8 }).map((_, index) => (
<Skeleton key={`logs-loading-${index}`} className='h-16 w-full rounded-lg' /> <Skeleton
key={`logs-loading-${index}`}
className='h-16 w-full rounded-lg'
/>
))} ))}
</div> </div>
) : logsData?.logs && logsData.logs.length > 0 ? ( ) : logsData?.logs && logsData.logs.length > 0 ? (
+52 -28
View File
@@ -16,12 +16,7 @@ import {
type UsageSummary, type UsageSummary,
} from '@/lib/api/services/admin'; } from '@/lib/api/services/admin';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Calendar } from '@/components/ui/calendar'; import { Calendar } from '@/components/ui/calendar';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { import {
@@ -179,8 +174,9 @@ function getAutoIntervalMinutes(hours: number): number {
const allowedIntervals = [5, 15, 30, 60, 120, 180, 240, 360, 480, 720, 1440]; const allowedIntervals = [5, 15, 30, 60, 120, 180, 240, 360, 480, 720, 1440];
return ( return (
allowedIntervals.find((intervalMinutes) => intervalMinutes >= idealInterval) ?? allowedIntervals.find(
allowedIntervals[allowedIntervals.length - 1] (intervalMinutes) => intervalMinutes >= idealInterval
) ?? allowedIntervals[allowedIntervals.length - 1]
); );
} }
@@ -480,7 +476,9 @@ export default function DashboardPage() {
DEFAULT_TIME_RANGE_PRESET; DEFAULT_TIME_RANGE_PRESET;
const customRangeHours = getRangeHours(customRange); const customRangeHours = getRangeHours(customRange);
const queryHours = const queryHours =
isCustomRangeActive && customRangeHours ? customRangeHours : activePreset.hours; isCustomRangeActive && customRangeHours
? customRangeHours
: activePreset.hours;
const autoInterval = getAutoIntervalMinutes(queryHours); const autoInterval = getAutoIntervalMinutes(queryHours);
const { const {
@@ -537,12 +535,14 @@ export default function DashboardPage() {
} }
const metricPoints = metricsData.metrics as ChartDatum[]; const metricPoints = metricsData.metrics as ChartDatum[];
const revenuePoints = metricsData.metrics.map((metric: UsageMetricData) => ({ const revenuePoints = metricsData.metrics.map(
...metric, (metric: UsageMetricData) => ({
revenue_sats: metric.revenue_msats / 1000, ...metric,
refunds_sats: metric.refunds_msats / 1000, revenue_sats: metric.revenue_msats / 1000,
net_revenue_sats: (metric.revenue_msats - metric.refunds_msats) / 1000, refunds_sats: metric.refunds_msats / 1000,
})) as ChartDatum[]; net_revenue_sats: (metric.revenue_msats - metric.refunds_msats) / 1000,
})
) as ChartDatum[];
return [ return [
{ {
@@ -731,7 +731,8 @@ export default function DashboardPage() {
}; };
const activeChartConfig = const activeChartConfig =
chartConfigs.find((config) => config.id === activeChartId) ?? chartConfigs[0]; chartConfigs.find((config) => config.id === activeChartId) ??
chartConfigs[0];
const selectedRangeValue = const selectedRangeValue =
isCustomRangeActive && customRange?.from && customRange?.to isCustomRangeActive && customRange?.from && customRange?.to
? 'custom' ? 'custom'
@@ -746,22 +747,28 @@ export default function DashboardPage() {
<AppPageShell contentClassName='mx-auto w-full max-w-5xl'> <AppPageShell contentClassName='mx-auto w-full max-w-5xl'>
<div className='min-w-0 space-y-4 sm:space-y-6'> <div className='min-w-0 space-y-4 sm:space-y-6'>
<section className='space-y-1 px-1'> <section className='space-y-1 px-1'>
<h1 className='text-lg font-semibold tracking-tight sm:text-2xl'>Dashboard</h1> <h1 className='text-lg font-semibold tracking-tight sm:text-2xl'>
Dashboard
</h1>
<p className='text-muted-foreground text-sm leading-relaxed'> <p className='text-muted-foreground text-sm leading-relaxed'>
Node balances, request health, and revenue trends. Node balances, request health, and revenue trends.
</p> </p>
</section> </section>
<DashboardBalanceSummary displayUnit={displayUnit} usdPerSat={usdPerSat} /> <DashboardBalanceSummary
displayUnit={displayUnit}
usdPerSat={usdPerSat}
/>
<section className='space-y-3 rounded-xl border border-border/60 bg-card/35 p-3 sm:p-4'> <section className='border-border/60 bg-card/35 space-y-3 rounded-xl border p-3 sm:p-4'>
<div className='space-y-1'> <div className='space-y-1'>
<div className='min-w-0'> <div className='min-w-0'>
<h2 className='text-base leading-snug font-semibold tracking-tight sm:text-lg'> <h2 className='text-base leading-snug font-semibold tracking-tight sm:text-lg'>
Usage Analytics Usage Analytics
</h2> </h2>
<p className='text-muted-foreground text-xs sm:text-sm'> <p className='text-muted-foreground text-xs sm:text-sm'>
Select a preset or custom date range to analyze traffic and revenue. Select a preset or custom date range to analyze traffic and
revenue.
</p> </p>
<p className='text-muted-foreground text-[11px] sm:text-xs'> <p className='text-muted-foreground text-[11px] sm:text-xs'>
Showing {activeRangeLabel}. Showing {activeRangeLabel}.
@@ -792,7 +799,10 @@ export default function DashboardPage() {
<CalendarIcon className='h-3.5 w-3.5' /> <CalendarIcon className='h-3.5 w-3.5' />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent align='start' className='w-auto overflow-hidden p-0'> <PopoverContent
align='start'
className='w-auto overflow-hidden p-0'
>
<Calendar <Calendar
mode='range' mode='range'
selected={pendingCustomRange} selected={pendingCustomRange}
@@ -806,13 +816,21 @@ export default function DashboardPage() {
<div className='bg-border/70 w-px' /> <div className='bg-border/70 w-px' />
<Select value={selectedRangeValue} onValueChange={handleRangeSelectChange}> <Select
<SelectTrigger className='h-full w-full min-h-0 rounded-none border-0 !bg-transparent px-3 py-0 text-sm font-normal shadow-none ring-0 hover:!bg-transparent data-[state=open]:!bg-transparent focus-visible:border-transparent focus-visible:ring-0 dark:!bg-transparent dark:hover:!bg-transparent'> value={selectedRangeValue}
<SelectValue placeholder='Select range' className='text-left' /> onValueChange={handleRangeSelectChange}
>
<SelectTrigger className='h-full min-h-0 w-full rounded-none border-0 !bg-transparent px-3 py-0 text-sm font-normal shadow-none ring-0 hover:!bg-transparent focus-visible:border-transparent focus-visible:ring-0 data-[state=open]:!bg-transparent dark:!bg-transparent dark:hover:!bg-transparent'>
<SelectValue
placeholder='Select range'
className='text-left'
/>
</SelectTrigger> </SelectTrigger>
<SelectContent align='start'> <SelectContent align='start'>
{customRange?.from && customRange?.to && ( {customRange?.from && customRange?.to && (
<SelectItem value='custom'>{compactCustomRangeLabel}</SelectItem> <SelectItem value='custom'>
{compactCustomRangeLabel}
</SelectItem>
)} )}
{TIME_RANGE_PRESETS.map((option) => ( {TIME_RANGE_PRESETS.map((option) => (
<SelectItem key={option.value} value={option.value}> <SelectItem key={option.value} value={option.value}>
@@ -831,7 +849,10 @@ export default function DashboardPage() {
className='h-8 w-full px-2.5 text-xs sm:ml-auto sm:w-auto' className='h-8 w-full px-2.5 text-xs sm:ml-auto sm:w-auto'
> >
<RefreshCw <RefreshCw
className={cn('mr-1 h-3 w-3', isManualRefreshing && 'animate-spin')} className={cn(
'mr-1 h-3 w-3',
isManualRefreshing && 'animate-spin'
)}
/> />
{isManualRefreshing ? 'Refreshing...' : 'Refresh'} {isManualRefreshing ? 'Refreshing...' : 'Refresh'}
</Button> </Button>
@@ -850,7 +871,9 @@ export default function DashboardPage() {
metricType={activeChartConfig.metricType} metricType={activeChartConfig.metricType}
tabs={chartConfigs.map((config) => ({ tabs={chartConfigs.map((config) => ({
id: config.id, id: config.id,
label: isMobile ? config.mobileTitle ?? config.title : config.title, label: isMobile
? (config.mobileTitle ?? config.title)
: config.title,
}))} }))}
activeTabId={activeChartId} activeTabId={activeChartId}
onTabChange={setActiveChartId} onTabChange={setActiveChartId}
@@ -865,7 +888,8 @@ export default function DashboardPage() {
</EmptyMedia> </EmptyMedia>
<EmptyTitle>No data available</EmptyTitle> <EmptyTitle>No data available</EmptyTitle>
<EmptyDescription> <EmptyDescription>
No metrics data exists for this range yet. Try a broader range. No metrics data exists for this range yet. Try a broader
range.
</EmptyDescription> </EmptyDescription>
</EmptyHeader> </EmptyHeader>
</Empty> </Empty>
+19 -14
View File
@@ -104,17 +104,16 @@ export default function ProvidersPage() {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const { data: providerModels, isLoading: isLoadingModels } = useQuery< const { data: providerModels, isLoading: isLoadingModels } =
ProviderModels | null useQuery<ProviderModels | null>({
>({ queryKey: ['provider-models', viewingModels],
queryKey: ['provider-models', viewingModels], queryFn: () =>
queryFn: () => viewingModels
viewingModels ? AdminService.getProviderModels(viewingModels)
? AdminService.getProviderModels(viewingModels) : Promise.resolve(null),
: Promise.resolve(null), enabled: !!viewingModels,
enabled: !!viewingModels, refetchOnWindowFocus: false,
refetchOnWindowFocus: false, });
});
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: (data: CreateUpstreamProvider) => mutationFn: (data: CreateUpstreamProvider) =>
@@ -379,7 +378,9 @@ export default function ProvidersPage() {
<Card> <Card>
<CardContent className='flex flex-col items-center justify-center py-12'> <CardContent className='flex flex-col items-center justify-center py-12'>
<Server className='text-muted-foreground mb-4 h-12 w-12' /> <Server className='text-muted-foreground mb-4 h-12 w-12' />
<h3 className='mb-2 text-lg font-semibold'>No providers configured</h3> <h3 className='mb-2 text-lg font-semibold'>
No providers configured
</h3>
<p className='text-muted-foreground mb-4 text-sm'> <p className='text-muted-foreground mb-4 text-sm'>
Get started by adding your first upstream provider Get started by adding your first upstream provider
</p> </p>
@@ -398,9 +399,13 @@ export default function ProvidersPage() {
isExpanded={expandedProviders.has(provider.id)} isExpanded={expandedProviders.has(provider.id)}
canShowBalance={canShowBalance(provider.provider_type)} canShowBalance={canShowBalance(provider.provider_type)}
platformUrl={getPlatformUrl(provider.provider_type)} platformUrl={getPlatformUrl(provider.provider_type)}
isModelsLoading={isLoadingModels && viewingModels === provider.id} isModelsLoading={
isLoadingModels && viewingModels === provider.id
}
providerModels={ providerModels={
viewingModels === provider.id ? providerModels ?? null : null viewingModels === provider.id
? (providerModels ?? null)
: null
} }
isDeletingModel={deleteModelMutation.isPending} isDeletingModel={deleteModelMutation.isPending}
onToggleExpansion={() => toggleProviderExpansion(provider.id)} onToggleExpansion={() => toggleProviderExpansion(provider.id)}
+5 -1
View File
@@ -22,7 +22,11 @@ export default function UnauthorizedPage() {
<Button onClick={() => router.push('/')} className='w-full sm:w-auto'> <Button onClick={() => router.push('/')} className='w-full sm:w-auto'>
Go to Dashboard Go to Dashboard
</Button> </Button>
<Button variant='outline' onClick={() => router.back()} className='w-full sm:w-auto'> <Button
variant='outline'
onClick={() => router.back()}
className='w-full sm:w-auto'
>
Go Back Go Back
</Button> </Button>
</div> </div>
+70 -19
View File
@@ -51,7 +51,9 @@ interface ApiEndpointFormProps {
setImageCount: Dispatch<SetStateAction<number>>; setImageCount: Dispatch<SetStateAction<number>>;
imageSize: '256x256' | '512x512' | '1024x1024' | '1792x1024' | '1024x1792'; imageSize: '256x256' | '512x512' | '1024x1024' | '1792x1024' | '1024x1792';
setImageSize: Dispatch< setImageSize: Dispatch<
SetStateAction<'256x256' | '512x512' | '1024x1024' | '1792x1024' | '1024x1792'> SetStateAction<
'256x256' | '512x512' | '1024x1024' | '1792x1024' | '1024x1792'
>
>; >;
imageQuality: 'standard' | 'hd'; imageQuality: 'standard' | 'hd';
setImageQuality: Dispatch<SetStateAction<'standard' | 'hd'>>; setImageQuality: Dispatch<SetStateAction<'standard' | 'hd'>>;
@@ -167,7 +169,9 @@ export function ApiEndpointForm({
min={1} min={1}
max={4000} max={4000}
value={maxTokens} value={maxTokens}
onChange={(event) => setMaxTokens(parseInt(event.target.value) || 150)} onChange={(event) =>
setMaxTokens(parseInt(event.target.value) || 150)
}
/> />
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
@@ -244,7 +248,9 @@ export function ApiEndpointForm({
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='vision-system-message'>System Message (Optional)</Label> <Label htmlFor='vision-system-message'>
System Message (Optional)
</Label>
<Textarea <Textarea
id='vision-system-message' id='vision-system-message'
placeholder='Enter system message...' placeholder='Enter system message...'
@@ -269,7 +275,9 @@ export function ApiEndpointForm({
<Label htmlFor='image-detail'>Image Detail</Label> <Label htmlFor='image-detail'>Image Detail</Label>
<Select <Select
value={imageDetail} value={imageDetail}
onValueChange={(value: 'low' | 'high' | 'auto') => setImageDetail(value)} onValueChange={(value: 'low' | 'high' | 'auto') =>
setImageDetail(value)
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@@ -284,7 +292,12 @@ export function ApiEndpointForm({
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='image-upload'>Upload Image (Optional)</Label> <Label htmlFor='image-upload'>Upload Image (Optional)</Label>
<Input type='file' accept='image/*' onChange={onImageUpload} className='cursor-pointer' /> <Input
type='file'
accept='image/*'
onChange={onImageUpload}
className='cursor-pointer'
/>
{selectedImage && ( {selectedImage && (
<div className='mt-2 flex flex-wrap items-center gap-2'> <div className='mt-2 flex flex-wrap items-center gap-2'>
<ImageIcon className='text-muted-foreground h-5 w-5' /> <ImageIcon className='text-muted-foreground h-5 w-5' />
@@ -303,7 +316,12 @@ export function ApiEndpointForm({
)} )}
{imagePreviewUrl && ( {imagePreviewUrl && (
<div className='relative mt-2 aspect-square w-32 overflow-hidden rounded-md border'> <div className='relative mt-2 aspect-square w-32 overflow-hidden rounded-md border'>
<Image src={imagePreviewUrl} alt='Image preview' fill className='object-cover' /> <Image
src={imagePreviewUrl}
alt='Image preview'
fill
className='object-cover'
/>
</div> </div>
)} )}
</div> </div>
@@ -317,7 +335,9 @@ export function ApiEndpointForm({
<Label htmlFor='encoding-format'>Encoding Format</Label> <Label htmlFor='encoding-format'>Encoding Format</Label>
<Select <Select
value={encodingFormat} value={encodingFormat}
onValueChange={(value: 'float' | 'base64') => setEncodingFormat(value)} onValueChange={(value: 'float' | 'base64') =>
setEncodingFormat(value)
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@@ -354,7 +374,9 @@ export function ApiEndpointForm({
min={1} min={1}
max={10} max={10}
value={imageCount} value={imageCount}
onChange={(event) => setImageCount(parseInt(event.target.value) || 1)} onChange={(event) =>
setImageCount(parseInt(event.target.value) || 1)
}
/> />
</div> </div>
@@ -388,7 +410,9 @@ export function ApiEndpointForm({
<Label htmlFor='image-quality'>Quality</Label> <Label htmlFor='image-quality'>Quality</Label>
<Select <Select
value={imageQuality} value={imageQuality}
onValueChange={(value: 'standard' | 'hd') => setImageQuality(value)} onValueChange={(value: 'standard' | 'hd') =>
setImageQuality(value)
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@@ -405,7 +429,9 @@ export function ApiEndpointForm({
<Label htmlFor='image-style'>Style</Label> <Label htmlFor='image-style'>Style</Label>
<Select <Select
value={imageStyle} value={imageStyle}
onValueChange={(value: 'vivid' | 'natural') => setImageStyle(value)} onValueChange={(value: 'vivid' | 'natural') =>
setImageStyle(value)
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@@ -439,7 +465,13 @@ export function ApiEndpointForm({
<Select <Select
value={speechVoice} value={speechVoice}
onValueChange={( onValueChange={(
value: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer' value:
| 'alloy'
| 'echo'
| 'fable'
| 'onyx'
| 'nova'
| 'shimmer'
) => setSpeechVoice(value)} ) => setSpeechVoice(value)}
> >
<SelectTrigger> <SelectTrigger>
@@ -487,7 +519,9 @@ export function ApiEndpointForm({
max={4.0} max={4.0}
step={0.25} step={0.25}
value={speechSpeed} value={speechSpeed}
onChange={(event) => setSpeechSpeed(parseFloat(event.target.value) || 1.0)} onChange={(event) =>
setSpeechSpeed(parseFloat(event.target.value) || 1.0)
}
/> />
</div> </div>
</div> </div>
@@ -510,17 +544,23 @@ export function ApiEndpointForm({
<div className='space-y-4'> <div className='space-y-4'>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2'> <div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='audio-transcription-prompt'>Prompt (Optional)</Label> <Label htmlFor='audio-transcription-prompt'>
Prompt (Optional)
</Label>
<Textarea <Textarea
id='audio-transcription-prompt' id='audio-transcription-prompt'
placeholder='Enter a prompt for the transcription...' placeholder='Enter a prompt for the transcription...'
value={audioTranscriptionPrompt} value={audioTranscriptionPrompt}
onChange={(event) => setAudioTranscriptionPrompt(event.target.value)} onChange={(event) =>
setAudioTranscriptionPrompt(event.target.value)
}
rows={2} rows={2}
/> />
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='audio-transcription-temperature'>Temperature</Label> <Label htmlFor='audio-transcription-temperature'>
Temperature
</Label>
<Input <Input
id='audio-transcription-temperature' id='audio-transcription-temperature'
type='number' type='number'
@@ -536,7 +576,9 @@ export function ApiEndpointForm({
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='audio-transcription-language'>Language (Optional)</Label> <Label htmlFor='audio-transcription-language'>
Language (Optional)
</Label>
<Input <Input
id='audio-transcription-language' id='audio-transcription-language'
placeholder='e.g., en-US, fr-FR' placeholder='e.g., en-US, fr-FR'
@@ -546,7 +588,9 @@ export function ApiEndpointForm({
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='audio-transcription-response-format'>Response Format</Label> <Label htmlFor='audio-transcription-response-format'>
Response Format
</Label>
<Select <Select
value={audioResponseFormat} value={audioResponseFormat}
onValueChange={( onValueChange={(
@@ -599,8 +643,15 @@ export function ApiEndpointForm({
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='audio-transcription-upload'>Or Upload Audio File</Label> <Label htmlFor='audio-transcription-upload'>
<Input type='file' accept='audio/*' onChange={onAudioUpload} className='cursor-pointer' /> Or Upload Audio File
</Label>
<Input
type='file'
accept='audio/*'
onChange={onAudioUpload}
className='cursor-pointer'
/>
{recordedAudio && ( {recordedAudio && (
<div className='mt-2 flex flex-wrap items-center gap-2'> <div className='mt-2 flex flex-wrap items-center gap-2'>
<Volume2 className='text-muted-foreground h-5 w-5' /> <Volume2 className='text-muted-foreground h-5 w-5' />
+23 -7
View File
@@ -119,7 +119,11 @@ const isAudioTranscriptionResponse = (
return 'text' in response; return 'text' in response;
}; };
export function ApiEndpointResponse({ response }: { response: ApiResponse | null }) { export function ApiEndpointResponse({
response,
}: {
response: ApiResponse | null;
}) {
if (!response) { if (!response) {
return null; return null;
} }
@@ -164,7 +168,8 @@ export function ApiEndpointResponse({ response }: { response: ApiResponse | null
<Label>Model Response</Label> <Label>Model Response</Label>
<div className='bg-muted rounded-md p-4'> <div className='bg-muted rounded-md p-4'>
<p className='text-sm whitespace-pre-wrap'> <p className='text-sm whitespace-pre-wrap'>
{response.choices?.[0]?.message?.content || 'No content in response'} {response.choices?.[0]?.message?.content ||
'No content in response'}
</p> </p>
</div> </div>
</div> </div>
@@ -174,15 +179,21 @@ export function ApiEndpointResponse({ response }: { response: ApiResponse | null
<Label>Usage Statistics</Label> <Label>Usage Statistics</Label>
<div className='grid grid-cols-1 gap-2 text-sm sm:grid-cols-3 sm:gap-4'> <div className='grid grid-cols-1 gap-2 text-sm sm:grid-cols-3 sm:gap-4'>
<div className='bg-muted rounded p-2 text-center'> <div className='bg-muted rounded p-2 text-center'>
<div className='font-semibold'>{response.usage.prompt_tokens}</div> <div className='font-semibold'>
{response.usage.prompt_tokens}
</div>
<div className='text-muted-foreground'>Prompt Tokens</div> <div className='text-muted-foreground'>Prompt Tokens</div>
</div> </div>
<div className='bg-muted rounded p-2 text-center'> <div className='bg-muted rounded p-2 text-center'>
<div className='font-semibold'>{response.usage.completion_tokens}</div> <div className='font-semibold'>
{response.usage.completion_tokens}
</div>
<div className='text-muted-foreground'>Completion Tokens</div> <div className='text-muted-foreground'>Completion Tokens</div>
</div> </div>
<div className='bg-muted rounded p-2 text-center'> <div className='bg-muted rounded p-2 text-center'>
<div className='font-semibold'>{response.usage.total_tokens}</div> <div className='font-semibold'>
{response.usage.total_tokens}
</div>
<div className='text-muted-foreground'>Total Tokens</div> <div className='text-muted-foreground'>Total Tokens</div>
</div> </div>
</div> </div>
@@ -207,7 +218,11 @@ export function ApiEndpointResponse({ response }: { response: ApiResponse | null
Show first 10 values Show first 10 values
</summary> </summary>
<pre className='mt-2 text-xs'> <pre className='mt-2 text-xs'>
{JSON.stringify(response.data?.[0]?.embedding?.slice(0, 10), null, 2)} {JSON.stringify(
response.data?.[0]?.embedding?.slice(0, 10),
null,
2
)}
... ...
</pre> </pre>
</details> </details>
@@ -274,7 +289,8 @@ export function ApiEndpointResponse({ response }: { response: ApiResponse | null
)} )}
{model.created && ( {model.created && (
<div className='text-muted-foreground text-sm'> <div className='text-muted-foreground text-sm'>
Created: {new Date(model.created * 1000).toLocaleDateString()} Created:{' '}
{new Date(model.created * 1000).toLocaleDateString()}
</div> </div>
)} )}
</div> </div>
+16 -12
View File
@@ -79,7 +79,7 @@ export function AppPageShell({
return ( return (
<div className='bg-background text-foreground min-h-dvh overflow-x-clip md:h-screen md:overflow-hidden'> <div className='bg-background text-foreground min-h-dvh overflow-x-clip md:h-screen md:overflow-hidden'>
<div className='flex min-h-dvh min-w-0 w-full overflow-x-clip md:h-full'> <div className='flex min-h-dvh w-full min-w-0 overflow-x-clip md:h-full'>
<aside <aside
className={cn( className={cn(
'border-border/60 hidden shrink-0 border-r py-5 transition-[width,padding] duration-200 md:flex md:h-full md:flex-col md:overflow-y-auto', 'border-border/60 hidden shrink-0 border-r py-5 transition-[width,padding] duration-200 md:flex md:h-full md:flex-col md:overflow-y-auto',
@@ -102,7 +102,9 @@ export function AppPageShell({
height={24} height={24}
className='rounded-sm' className='rounded-sm'
/> />
<h1 className='text-lg font-semibold tracking-tight'>Routstr Node</h1> <h1 className='text-lg font-semibold tracking-tight'>
Routstr Node
</h1>
</div> </div>
)} )}
<Button <Button
@@ -110,7 +112,7 @@ export function AppPageShell({
size='icon' size='icon'
className={cn( className={cn(
'text-muted-foreground hover:text-foreground h-8 w-8', 'text-muted-foreground hover:text-foreground h-8 w-8',
isSidebarCollapsed ? 'mx-auto' : 'ml-auto -mr-1' isSidebarCollapsed ? 'mx-auto' : '-mr-1 ml-auto'
)} )}
onClick={() => setIsSidebarCollapsed((current) => !current)} onClick={() => setIsSidebarCollapsed((current) => !current)}
> >
@@ -196,16 +198,16 @@ export function AppPageShell({
</Button> </Button>
</> </>
) : ( ) : (
<div className='space-y-1 rounded-lg border border-border/60 bg-card/30 p-1'> <div className='border-border/60 bg-card/30 space-y-1 rounded-lg border p-1'>
<CurrencyToggle <CurrencyToggle
menuSide='right' menuSide='right'
menuAlign='start' menuAlign='start'
className='text-foreground/90 hover:bg-accent/35 h-8 w-full justify-between rounded-md border-border/60 bg-background/25 px-2.5 text-[11px]' className='text-foreground/90 hover:bg-accent/35 border-border/60 bg-background/25 h-8 w-full justify-between rounded-md px-2.5 text-[11px]'
/> />
<ThemeToggle <ThemeToggle
menuSide='right' menuSide='right'
menuAlign='start' menuAlign='start'
className='text-foreground/90 hover:bg-accent/35 h-8 w-full justify-between rounded-md border-border/60 bg-background/25 px-2.5 text-[11px]' className='text-foreground/90 hover:bg-accent/35 border-border/60 bg-background/25 h-8 w-full justify-between rounded-md px-2.5 text-[11px]'
/> />
<Button <Button
variant='ghost' variant='ghost'
@@ -221,10 +223,10 @@ export function AppPageShell({
</div> </div>
</aside> </aside>
<section className='relative flex min-w-0 w-full flex-1 flex-col overflow-x-clip md:h-full md:min-h-0'> <section className='relative flex w-full min-w-0 flex-1 flex-col overflow-x-clip md:h-full md:min-h-0'>
<main <main
className={cn( className={cn(
'pb-mobile-nav min-w-0 w-full flex-1 overflow-x-clip p-3 sm:p-4 md:min-h-0 md:overflow-y-auto md:p-6 md:pb-6', 'pb-mobile-nav w-full min-w-0 flex-1 overflow-x-clip p-3 sm:p-4 md:min-h-0 md:overflow-y-auto md:p-6 md:pb-6',
contentClassName, contentClassName,
className className
)} )}
@@ -235,7 +237,7 @@ export function AppPageShell({
</div> </div>
<div className='pointer-events-none fixed inset-x-0 bottom-0 z-40 px-4 pb-[calc(0.35rem+env(safe-area-inset-bottom))] md:hidden'> <div className='pointer-events-none fixed inset-x-0 bottom-0 z-40 px-4 pb-[calc(0.35rem+env(safe-area-inset-bottom))] md:hidden'>
<nav className='pointer-events-auto mx-auto w-full max-w-[34rem] overflow-x-auto overscroll-x-contain rounded-[1.5rem] border border-border/65 bg-background/80 shadow-[0_-16px_36px_-22px_rgba(0,0,0,0.9)] backdrop-blur-2xl [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden supports-[backdrop-filter]:bg-background/72'> <nav className='border-border/65 bg-background/80 supports-[backdrop-filter]:bg-background/72 pointer-events-auto mx-auto w-full max-w-[34rem] overflow-x-auto overscroll-x-contain rounded-[1.5rem] border shadow-[0_-16px_36px_-22px_rgba(0,0,0,0.9)] backdrop-blur-2xl [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'>
<div <div
className={cn( className={cn(
'grid min-w-full snap-x snap-mandatory grid-flow-col gap-2 p-2.5', 'grid min-w-full snap-x snap-mandatory grid-flow-col gap-2 p-2.5',
@@ -280,12 +282,14 @@ export function AppPageShell({
<DrawerContent className='md:hidden'> <DrawerContent className='md:hidden'>
<DrawerHeader className='px-4 pb-2 text-left'> <DrawerHeader className='px-4 pb-2 text-left'>
<DrawerTitle>More</DrawerTitle> <DrawerTitle>More</DrawerTitle>
<DrawerDescription>Account and appearance settings.</DrawerDescription> <DrawerDescription>
Account and appearance settings.
</DrawerDescription>
</DrawerHeader> </DrawerHeader>
<div className='space-y-4 px-4 pb-4'> <div className='space-y-4 px-4 pb-4'>
<div className='space-y-2'> <div className='space-y-2'>
<p className='text-muted-foreground text-xs font-semibold uppercase tracking-[0.08em]'> <p className='text-muted-foreground text-xs font-semibold tracking-[0.08em] uppercase'>
Preferences Preferences
</p> </p>
<div className='grid grid-cols-2 gap-2'> <div className='grid grid-cols-2 gap-2'>
@@ -303,7 +307,7 @@ export function AppPageShell({
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
<p className='text-muted-foreground text-xs font-semibold uppercase tracking-[0.08em]'> <p className='text-muted-foreground text-xs font-semibold tracking-[0.08em] uppercase'>
Account Account
</p> </p>
<Button <Button
+15 -5
View File
@@ -72,7 +72,7 @@ const data = {
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) { export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return ( return (
<Sidebar collapsible='offcanvas' {...props}> <Sidebar collapsible='offcanvas' {...props}>
<SidebarHeader className='px-3 pb-3 pt-4'> <SidebarHeader className='px-3 pt-4 pb-3'>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
<div className='flex items-center gap-2 px-2 py-1'> <div className='flex items-center gap-2 px-2 py-1'>
@@ -84,7 +84,9 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
className='rounded' className='rounded'
/> />
<div className='space-y-0.5'> <div className='space-y-0.5'>
<p className='text-sm font-semibold tracking-tight'>Routstr Node</p> <p className='text-sm font-semibold tracking-tight'>
Routstr Node
</p>
<p className='text-muted-foreground text-[11px]'> <p className='text-muted-foreground text-[11px]'>
Admin dashboard Admin dashboard
</p> </p>
@@ -95,12 +97,16 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarHeader> </SidebarHeader>
<SidebarContent className='flex-1 overflow-y-auto px-2 pb-2'> <SidebarContent className='flex-1 overflow-y-auto px-2 pb-2'>
<NavSecondary items={data.navSecondary} /> <NavSecondary items={data.navSecondary} />
<SidebarGroup className='mt-auto px-0 pb-0 pt-2'> <SidebarGroup className='mt-auto px-0 pt-2 pb-0'>
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton asChild className='h-10 rounded-lg px-3'> <SidebarMenuButton asChild className='h-10 rounded-lg px-3'>
<Link href='https://docs.routstr.com' target='_blank' rel='noreferrer'> <Link
href='https://docs.routstr.com'
target='_blank'
rel='noreferrer'
>
<span>Docs</span> <span>Docs</span>
<ExternalLinkIcon className='ml-auto h-3.5 w-3.5' /> <ExternalLinkIcon className='ml-auto h-3.5 w-3.5' />
</Link> </Link>
@@ -108,7 +114,11 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarMenuItem> </SidebarMenuItem>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton asChild className='h-10 rounded-lg px-3'> <SidebarMenuButton asChild className='h-10 rounded-lg px-3'>
<Link href='https://chat.routstr.com' target='_blank' rel='noreferrer'> <Link
href='https://chat.routstr.com'
target='_blank'
rel='noreferrer'
>
<span>Chat App</span> <span>Chat App</span>
<ExternalLinkIcon className='ml-auto h-3.5 w-3.5' /> <ExternalLinkIcon className='ml-auto h-3.5 w-3.5' />
</Link> </Link>
+6 -2
View File
@@ -24,8 +24,12 @@ export function AuthPageShell({
<div className='bg-background text-foreground flex min-h-dvh items-center justify-center px-4 py-10 sm:py-12'> <div className='bg-background text-foreground flex min-h-dvh items-center justify-center px-4 py-10 sm:py-12'>
<Card className='w-full max-w-md'> <Card className='w-full max-w-md'>
<CardHeader className='space-y-1 pb-4'> <CardHeader className='space-y-1 pb-4'>
<CardTitle className='text-center text-2xl font-bold'>{title}</CardTitle> <CardTitle className='text-center text-2xl font-bold'>
<CardDescription className='text-center'>{description}</CardDescription> {title}
</CardTitle>
<CardDescription className='text-center'>
{description}
</CardDescription>
</CardHeader> </CardHeader>
<CardContent>{children}</CardContent> <CardContent>{children}</CardContent>
</Card> </Card>
+3 -1
View File
@@ -109,7 +109,9 @@ export function CostCalculator({ model }: CostCalculatorProps) {
{/* Minimum Cost Alert */} {/* Minimum Cost Alert */}
{hasMinimumCost && ( {hasMinimumCost && (
<Alert variant={costCalculation.isMinimumApplied ? 'destructive' : 'default'}> <Alert
variant={costCalculation.isMinimumApplied ? 'destructive' : 'default'}
>
{costCalculation.isMinimumApplied ? ( {costCalculation.isMinimumApplied ? (
<AlertTriangle className='h-4 w-4' /> <AlertTriangle className='h-4 w-4' />
) : ( ) : (
+3 -1
View File
@@ -65,7 +65,9 @@ export function CurrencyToggle({
size='sm' size='sm'
className={cn( className={cn(
'border-border/60 bg-background/65 text-muted-foreground hover:text-foreground rounded-md', 'border-border/60 bg-background/65 text-muted-foreground hover:text-foreground rounded-md',
compact ? 'h-8 w-10 justify-center px-0' : 'h-8 justify-between gap-2', compact
? 'h-8 w-10 justify-center px-0'
: 'h-8 justify-between gap-2',
className className
)} )}
> >
+9 -3
View File
@@ -78,17 +78,23 @@ export function DashboardBalanceSummary({
return ( return (
<div className='grid grid-cols-2 gap-2.5 max-[359px]:grid-cols-1 sm:gap-3 lg:grid-cols-3'> <div className='grid grid-cols-2 gap-2.5 max-[359px]:grid-cols-1 sm:gap-3 lg:grid-cols-3'>
{cards.map((card) => ( {cards.map((card) => (
<Card key={card.title} size='sm' className='min-h-[6.25rem] sm:min-h-[7rem]'> <Card
key={card.title}
size='sm'
className='min-h-[6.25rem] sm:min-h-[7rem]'
>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-1'> <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-1'>
<CardTitle className='text-muted-foreground text-[11px] font-medium sm:text-sm'> <CardTitle className='text-muted-foreground text-[11px] font-medium sm:text-sm'>
{card.title} {card.title}
</CardTitle> </CardTitle>
<div className='flex size-6 items-center justify-center sm:size-7'> <div className='flex size-6 items-center justify-center sm:size-7'>
<card.icon className={`h-3.5 w-3.5 sm:h-4 sm:w-4 ${card.color}`} /> <card.icon
className={`h-3.5 w-3.5 sm:h-4 sm:w-4 ${card.color}`}
/>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className='pt-0'> <CardContent className='pt-0'>
<div className='break-words text-base font-semibold sm:text-xl'> <div className='text-base font-semibold break-words sm:text-xl'>
{card.value} {card.value}
</div> </div>
</CardContent> </CardContent>
+1 -5
View File
@@ -190,11 +190,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
variant='outline' variant='outline'
className='text-muted-foreground flex gap-1 px-1.5 [&_svg]:size-3' className='text-muted-foreground flex gap-1 px-1.5 [&_svg]:size-3'
> >
{row.original.status === 'Done' ? ( {row.original.status === 'Done' ? <CheckCircle2Icon /> : <LoaderIcon />}
<CheckCircle2Icon />
) : (
<LoaderIcon />
)}
{row.original.status} {row.original.status}
</Badge> </Badge>
), ),
+110 -70
View File
@@ -85,7 +85,8 @@ export function DetailedWalletBalance({
const rows = (data ?? []) const rows = (data ?? [])
.filter( .filter(
(detail) => (detail.wallet_balance && detail.wallet_balance > 0) || detail.error (detail) =>
(detail.wallet_balance && detail.wallet_balance > 0) || detail.error
) )
.map((detail, index) => { .map((detail, index) => {
const walletMsat = convertToMsat(detail.wallet_balance || 0, detail.unit); const walletMsat = convertToMsat(detail.wallet_balance || 0, detail.unit);
@@ -147,7 +148,10 @@ export function DetailedWalletBalance({
<div className='space-y-4'> <div className='space-y-4'>
<div className='grid gap-3 md:grid-cols-3'> <div className='grid gap-3 md:grid-cols-3'>
{Array.from({ length: 3 }).map((_, index) => ( {Array.from({ length: 3 }).map((_, index) => (
<Card key={`wallet-stat-skeleton-${index}`} className='shadow-none'> <Card
key={`wallet-stat-skeleton-${index}`}
className='shadow-none'
>
<CardHeader className='space-y-2 pb-1'> <CardHeader className='space-y-2 pb-1'>
<Skeleton className='h-3.5 w-28' /> <Skeleton className='h-3.5 w-28' />
<Skeleton className='h-3 w-8' /> <Skeleton className='h-3 w-8' />
@@ -161,7 +165,10 @@ export function DetailedWalletBalance({
<Skeleton className='h-3 w-52' /> <Skeleton className='h-3 w-52' />
<div className='space-y-2'> <div className='space-y-2'>
{Array.from({ length: 5 }).map((_, index) => ( {Array.from({ length: 5 }).map((_, index) => (
<Skeleton key={`wallet-row-skeleton-${index}`} className='h-11 w-full' /> <Skeleton
key={`wallet-row-skeleton-${index}`}
className='h-11 w-full'
/>
))} ))}
</div> </div>
</div> </div>
@@ -177,7 +184,9 @@ export function DetailedWalletBalance({
<div className='grid gap-3 md:grid-cols-3'> <div className='grid gap-3 md:grid-cols-3'>
<Card className='shadow-none'> <Card className='shadow-none'>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'> <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium'>Your Balance (Total)</CardTitle> <CardTitle className='text-sm font-medium'>
Your Balance (Total)
</CardTitle>
<span className='inline-flex size-8 items-center justify-center text-green-600 dark:text-green-300'> <span className='inline-flex size-8 items-center justify-center text-green-600 dark:text-green-300'>
<Coins className='h-4 w-4' /> <Coins className='h-4 w-4' />
</span> </span>
@@ -190,7 +199,9 @@ export function DetailedWalletBalance({
</Card> </Card>
<Card className='shadow-none'> <Card className='shadow-none'>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'> <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium'>Total Wallet</CardTitle> <CardTitle className='text-sm font-medium'>
Total Wallet
</CardTitle>
<span className='inline-flex size-8 items-center justify-center text-blue-600 dark:text-blue-300'> <span className='inline-flex size-8 items-center justify-center text-blue-600 dark:text-blue-300'>
<Wallet className='h-4 w-4' /> <Wallet className='h-4 w-4' />
</span> </span>
@@ -203,7 +214,9 @@ export function DetailedWalletBalance({
</Card> </Card>
<Card className='shadow-none'> <Card className='shadow-none'>
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'> <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
<CardTitle className='text-sm font-medium'>User Balance</CardTitle> <CardTitle className='text-sm font-medium'>
User Balance
</CardTitle>
<span className='inline-flex size-8 items-center justify-center text-purple-600 dark:text-purple-300'> <span className='inline-flex size-8 items-center justify-center text-purple-600 dark:text-purple-300'>
<User className='h-4 w-4' /> <User className='h-4 w-4' />
</span> </span>
@@ -233,76 +246,102 @@ export function DetailedWalletBalance({
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{rows.map(({ key, detail, walletMsat, userMsat, ownerMsat }) => ( {rows.map(
<TableRow ({
key={key} key,
className={cn( detail,
detail.error && 'bg-destructive/10 text-destructive' walletMsat,
)} userMsat,
> ownerMsat,
<TableCell className='max-w-md font-mono text-xs break-all whitespace-normal'> }) => (
{formatMintLabel(detail)} <TableRow
</TableCell> key={key}
<TableCell className='text-right font-mono'>
{detail.error ? 'error' : formatAmount(walletMsat)}
</TableCell>
<TableCell className='text-right font-mono'>
{detail.error ? '-' : formatAmount(userMsat)}
</TableCell>
<TableCell
className={cn( className={cn(
'text-right font-mono', detail.error &&
!detail.error && ownerMsat > 0 && 'text-primary font-semibold' 'bg-destructive/10 text-destructive'
)} )}
> >
{detail.error ? '-' : formatAmount(ownerMsat)} <TableCell className='max-w-md font-mono text-xs break-all whitespace-normal'>
</TableCell> {formatMintLabel(detail)}
</TableRow> </TableCell>
))} <TableCell className='text-right font-mono'>
{detail.error
? 'error'
: formatAmount(walletMsat)}
</TableCell>
<TableCell className='text-right font-mono'>
{detail.error ? '-' : formatAmount(userMsat)}
</TableCell>
<TableCell
className={cn(
'text-right font-mono',
!detail.error &&
ownerMsat > 0 &&
'text-primary font-semibold'
)}
>
{detail.error ? '-' : formatAmount(ownerMsat)}
</TableCell>
</TableRow>
)
)}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
<div className='space-y-2 md:hidden'> <div className='space-y-2 md:hidden'>
{rows.map(({ key, detail, walletMsat, userMsat, ownerMsat }) => ( {rows.map(
<Card ({ key, detail, walletMsat, userMsat, ownerMsat }) => (
key={`${key}-mobile`} <Card
className={cn( key={`${key}-mobile`}
'shadow-none', className={cn(
detail.error && 'border-destructive/40 bg-destructive/5' 'shadow-none',
)} detail.error &&
> 'border-destructive/40 bg-destructive/5'
<CardHeader className='p-4 pb-2'> )}
<CardDescription className='font-mono text-xs break-all'> >
{formatMintLabel(detail)} <CardHeader className='p-4 pb-2'>
</CardDescription> <CardDescription className='font-mono text-xs break-all'>
</CardHeader> {formatMintLabel(detail)}
<CardContent className='grid grid-cols-1 gap-2 p-4 pt-0 sm:grid-cols-3 sm:gap-3'> </CardDescription>
<div> </CardHeader>
<p className='text-muted-foreground text-xs'>Wallet</p> <CardContent className='grid grid-cols-1 gap-2 p-4 pt-0 sm:grid-cols-3 sm:gap-3'>
<p className='font-mono text-sm'> <div>
{detail.error ? 'error' : formatAmount(walletMsat)} <p className='text-muted-foreground text-xs'>
</p> Wallet
</div> </p>
<div> <p className='font-mono text-sm'>
<p className='text-muted-foreground text-xs'>Users</p> {detail.error
<p className='font-mono text-sm'> ? 'error'
{detail.error ? '-' : formatAmount(userMsat)} : formatAmount(walletMsat)}
</p> </p>
</div> </div>
<div> <div>
<p className='text-muted-foreground text-xs'>Owner</p> <p className='text-muted-foreground text-xs'>
<p Users
className={cn( </p>
'font-mono text-sm', <p className='font-mono text-sm'>
!detail.error && ownerMsat > 0 && 'text-primary font-semibold' {detail.error ? '-' : formatAmount(userMsat)}
)} </p>
> </div>
{detail.error ? '-' : formatAmount(ownerMsat)} <div>
</p> <p className='text-muted-foreground text-xs'>
</div> Owner
</CardContent> </p>
</Card> <p
))} className={cn(
'font-mono text-sm',
!detail.error &&
ownerMsat > 0 &&
'text-primary font-semibold'
)}
>
{detail.error ? '-' : formatAmount(ownerMsat)}
</p>
</div>
</CardContent>
</Card>
)
)}
</div> </div>
</> </>
) : ( ) : (
@@ -313,7 +352,8 @@ export function DetailedWalletBalance({
</EmptyMedia> </EmptyMedia>
<EmptyTitle>No balances to display</EmptyTitle> <EmptyTitle>No balances to display</EmptyTitle>
<EmptyDescription> <EmptyDescription>
Wallet balances will appear here after funds are available. Wallet balances will appear here after funds are
available.
</EmptyDescription> </EmptyDescription>
</EmptyHeader> </EmptyHeader>
</Empty> </Empty>
+3 -3
View File
@@ -176,9 +176,9 @@ export function EditGroupForm({
> >
<span className='truncate'>{model.name}</span> <span className='truncate'>{model.name}</span>
{model.api_key && <Key className='ml-1 h-3 w-3' />} {model.api_key && <Key className='ml-1 h-3 w-3' />}
{model.url && !model.url.startsWith('/') && model.api_key && ( {model.url &&
<Globe className='ml-1 h-3 w-3' /> !model.url.startsWith('/') &&
)} model.api_key && <Globe className='ml-1 h-3 w-3' />}
</Badge> </Badge>
))} ))}
</div> </div>
+9 -3
View File
@@ -64,7 +64,9 @@ const toStringArray = (value: unknown, fallback: string[]): string[] => {
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
return fallback; return fallback;
} }
const filtered = value.filter((item): item is string => typeof item === 'string'); const filtered = value.filter(
(item): item is string => typeof item === 'string'
);
return filtered.length > 0 ? filtered : fallback; return filtered.length > 0 ? filtered : fallback;
}; };
@@ -125,7 +127,9 @@ const normalizeAdminModelData = (
created: toNumber(adminModel.created, Math.floor(Date.now() / 1000)), created: toNumber(adminModel.created, Math.floor(Date.now() / 1000)),
context_length: Math.max( context_length: Math.max(
0, 0,
Math.trunc(toNumber(adminModel.context_length, fallbackModel.contextLength || 4096)) Math.trunc(
toNumber(adminModel.context_length, fallbackModel.contextLength || 4096)
)
), ),
architecture: { architecture: {
modality: modality:
@@ -148,7 +152,9 @@ const normalizeAdminModelData = (
: null, : null,
}, },
pricing: { pricing: {
prompt: roundToFiveDecimals(toNumber(pricingRecord.prompt, fallbackModel.input_cost)), prompt: roundToFiveDecimals(
toNumber(pricingRecord.prompt, fallbackModel.input_cost)
),
completion: roundToFiveDecimals( completion: roundToFiveDecimals(
toNumber(pricingRecord.completion, fallbackModel.output_cost) toNumber(pricingRecord.completion, fallbackModel.output_cost)
), ),
+2 -2
View File
@@ -167,7 +167,7 @@ export function CheatSheet(): JSX.Element {
const refundToken = refundReceipt?.token ?? null; const refundToken = refundReceipt?.token ?? null;
return ( return (
<div className='min-h-screen bg-background'> <div className='bg-background min-h-screen'>
<main className='mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8'> <main className='mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8'>
<section className='relative space-y-3 text-center md:text-left'> <section className='relative space-y-3 text-center md:text-left'>
<div className='absolute top-0 right-0 hidden md:block'> <div className='absolute top-0 right-0 hidden md:block'>
@@ -235,7 +235,7 @@ export function CheatSheet(): JSX.Element {
<div className='space-y-2 sm:col-span-2'> <div className='space-y-2 sm:col-span-2'>
<Skeleton className='h-3 w-10' /> <Skeleton className='h-3 w-10' />
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<Skeleton className='h-5 flex-1 max-w-[28rem]' /> <Skeleton className='h-5 max-w-[28rem] flex-1' />
<Skeleton className='h-7 w-7 rounded-md' /> <Skeleton className='h-7 w-7 rounded-md' />
</div> </div>
</div> </div>
@@ -91,7 +91,12 @@ function InvoiceDetailsCard({
<span> <span>
{label} ({amountSats} sats) {label} ({amountSats} sats)
</span> </span>
<Button variant='outline' size='sm' className='gap-1 text-xs' onClick={onCopy}> <Button
variant='outline'
size='sm'
className='gap-1 text-xs'
onClick={onCopy}
>
<Copy className='h-3 w-3' /> <Copy className='h-3 w-3' />
Copy Copy
</Button> </Button>
@@ -117,9 +122,16 @@ function InvoiceDetailsCard({
</div> </div>
)} )}
<Textarea value={bolt11} readOnly rows={4} className='font-mono text-xs' /> <Textarea
value={bolt11}
readOnly
rows={4}
className='font-mono text-xs'
/>
<p className='text-muted-foreground text-center text-xs'>{helperText}</p> <p className='text-muted-foreground text-center text-xs'>
{helperText}
</p>
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -137,16 +149,31 @@ function ApiKeyResultAlert({
<CheckCircle className='h-4 w-4' /> <CheckCircle className='h-4 w-4' />
<AlertTitle className='flex items-center justify-between gap-2'> <AlertTitle className='flex items-center justify-between gap-2'>
<span>{title}</span> <span>{title}</span>
<Button variant='outline' size='sm' className='gap-1 text-xs' onClick={onCopy}> <Button
variant='outline'
size='sm'
className='gap-1 text-xs'
onClick={onCopy}
>
<Copy className='h-3 w-3' /> <Copy className='h-3 w-3' />
Copy Copy
</Button> </Button>
</AlertTitle> </AlertTitle>
<AlertDescription className='space-y-3'> <AlertDescription className='space-y-3'>
<Textarea value={apiKey} readOnly rows={2} className='font-mono text-xs' /> <Textarea
value={apiKey}
readOnly
rows={2}
className='font-mono text-xs'
/>
<div className='flex items-center justify-between gap-2'> <div className='flex items-center justify-between gap-2'>
<span>{description}</span> <span>{description}</span>
<Button variant='ghost' size='sm' className='h-6 px-2 text-xs' onClick={onDismiss}> <Button
variant='ghost'
size='sm'
className='h-6 px-2 text-xs'
onClick={onDismiss}
>
Dismiss Dismiss
</Button> </Button>
</div> </div>
@@ -1,6 +1,11 @@
'use client'; 'use client';
import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card'; import {
Card,
CardContent,
CardDescription,
CardHeader,
} from '@/components/ui/card';
interface WalletBalanceStatsProps { interface WalletBalanceStatsProps {
balanceMsats?: number; balanceMsats?: number;
@@ -31,7 +36,7 @@ function WalletMetricCard({
{label} {label}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className='px-3 pb-3 pt-0'> <CardContent className='px-3 pt-0 pb-3'>
<p className='text-xl font-semibold tabular-nums'> <p className='text-xl font-semibold tabular-nums'>
{hasValue ? `${formatSats(valueMsats)} sats` : '-'} {hasValue ? `${formatSats(valueMsats)} sats` : '-'}
</p> </p>
+2 -1
View File
@@ -36,7 +36,8 @@ export function NavMain({
isActive={ isActive={
item.url === '/' item.url === '/'
? pathname === '/' ? pathname === '/'
: pathname === item.url || pathname.startsWith(`${item.url}/`) : pathname === item.url ||
pathname.startsWith(`${item.url}/`)
} }
> >
<Link href={item.url}> <Link href={item.url}>
+2 -1
View File
@@ -40,7 +40,8 @@ export function NavSecondary({
isActive={ isActive={
item.url === '/' item.url === '/'
? pathname === '/' ? pathname === '/'
: pathname === item.url || pathname.startsWith(`${item.url}/`) : pathname === item.url ||
pathname.startsWith(`${item.url}/`)
} }
> >
<Link href={item.url}> <Link href={item.url}>
+9 -3
View File
@@ -27,11 +27,17 @@ export function PageHeader({
> >
<div className='min-w-0 space-y-1'> <div className='min-w-0 space-y-1'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
{icon ? <span className='text-muted-foreground shrink-0'>{icon}</span> : null} {icon ? (
<h2 className='text-xl font-semibold tracking-tight sm:text-2xl'>{title}</h2> <span className='text-muted-foreground shrink-0'>{icon}</span>
) : null}
<h2 className='text-xl font-semibold tracking-tight sm:text-2xl'>
{title}
</h2>
</div> </div>
{description ? ( {description ? (
<p className='text-muted-foreground text-sm leading-relaxed'>{description}</p> <p className='text-muted-foreground text-sm leading-relaxed'>
{description}
</p>
) : null} ) : null}
</div> </div>
{actions ? ( {actions ? (
+32 -9
View File
@@ -25,7 +25,10 @@ interface ProviderBalanceProps {
platformUrl?: string | null; platformUrl?: string | null;
} }
export function ProviderBalance({ providerId, platformUrl }: ProviderBalanceProps) { export function ProviderBalance({
providerId,
platformUrl,
}: ProviderBalanceProps) {
const [isTopupDialogOpen, setIsTopupDialogOpen] = useState(false); const [isTopupDialogOpen, setIsTopupDialogOpen] = useState(false);
const [topupAmount, setTopupAmount] = useState(''); const [topupAmount, setTopupAmount] = useState('');
const [topupError, setTopupError] = useState(''); const [topupError, setTopupError] = useState('');
@@ -73,7 +76,10 @@ export function ProviderBalance({ providerId, platformUrl }: ProviderBalanceProp
const topupMutation = useMutation({ const topupMutation = useMutation({
mutationFn: async (amount: number) => { mutationFn: async (amount: number) => {
const result = await AdminService.initiateProviderTopup(providerId, amount); const result = await AdminService.initiateProviderTopup(
providerId,
amount
);
return result; return result;
}, },
onSuccess: (data) => { onSuccess: (data) => {
@@ -112,7 +118,8 @@ export function ProviderBalance({ providerId, platformUrl }: ProviderBalanceProp
const handleTopUpClick = () => { const handleTopUpClick = () => {
if ( if (
platformUrl && platformUrl &&
(platformUrl.includes('openrouter.ai') || platformUrl.includes('openai.com')) (platformUrl.includes('openrouter.ai') ||
platformUrl.includes('openai.com'))
) { ) {
window.open(platformUrl, '_blank'); window.open(platformUrl, '_blank');
return; return;
@@ -175,7 +182,9 @@ export function ProviderBalance({ providerId, platformUrl }: ProviderBalanceProp
<DialogContent className='sm:max-w-md'> <DialogContent className='sm:max-w-md'>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{paymentStatus === 'paid' ? 'Payment Confirmed!' : 'Top Up Balance'} {paymentStatus === 'paid'
? 'Payment Confirmed!'
: 'Top Up Balance'}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{paymentStatus === 'paid' {paymentStatus === 'paid'
@@ -223,7 +232,9 @@ export function ProviderBalance({ providerId, platformUrl }: ProviderBalanceProp
size='sm' size='sm'
variant='outline' variant='outline'
onClick={() => { onClick={() => {
navigator.clipboard.writeText(invoiceData.payment_request); navigator.clipboard.writeText(
invoiceData.payment_request
);
toast.success('Invoice copied to clipboard!'); toast.success('Invoice copied to clipboard!');
}} }}
className='w-full sm:w-auto' className='w-full sm:w-auto'
@@ -255,7 +266,9 @@ export function ProviderBalance({ providerId, platformUrl }: ProviderBalanceProp
max='500' max='500'
step='0.01' step='0.01'
/> />
{topupError && <p className='text-destructive text-sm'>{topupError}</p>} {topupError && (
<p className='text-destructive text-sm'>{topupError}</p>
)}
</div> </div>
</div> </div>
)} )}
@@ -266,12 +279,20 @@ export function ProviderBalance({ providerId, platformUrl }: ProviderBalanceProp
Done Done
</Button> </Button>
) : invoiceData ? ( ) : invoiceData ? (
<Button variant='outline' onClick={handleCloseDialog} className='w-full'> <Button
variant='outline'
onClick={handleCloseDialog}
className='w-full'
>
Cancel Cancel
</Button> </Button>
) : ( ) : (
<> <>
<Button variant='outline' onClick={handleCloseDialog} className='w-full sm:w-auto'> <Button
variant='outline'
onClick={handleCloseDialog}
className='w-full sm:w-auto'
>
Cancel Cancel
</Button> </Button>
<Button <Button
@@ -279,7 +300,9 @@ export function ProviderBalance({ providerId, platformUrl }: ProviderBalanceProp
disabled={topupMutation.isPending || !topupAmount} disabled={topupMutation.isPending || !topupAmount}
className='w-full sm:w-auto' className='w-full sm:w-auto'
> >
{topupMutation.isPending ? 'Processing...' : 'Generate Invoice'} {topupMutation.isPending
? 'Processing...'
: 'Generate Invoice'}
</Button> </Button>
</> </>
)} )}
+8 -3
View File
@@ -69,7 +69,7 @@ export function ProviderCard({
{provider.enabled ? 'Enabled' : 'Disabled'} {provider.enabled ? 'Enabled' : 'Disabled'}
</Badge> </Badge>
</div> </div>
<CardDescription className='mt-1 break-all text-xs sm:text-sm'> <CardDescription className='mt-1 text-xs break-all sm:text-sm'>
{provider.base_url} {provider.base_url}
</CardDescription> </CardDescription>
</div> </div>
@@ -77,7 +77,10 @@ export function ProviderCard({
<div className='grid w-full grid-cols-2 gap-2 sm:flex sm:w-auto sm:flex-wrap sm:items-center sm:justify-end'> <div className='grid w-full grid-cols-2 gap-2 sm:flex sm:w-auto sm:flex-wrap sm:items-center sm:justify-end'>
{canShowBalance && provider.api_key && ( {canShowBalance && provider.api_key && (
<div className='col-span-2 sm:col-auto'> <div className='col-span-2 sm:col-auto'>
<ProviderBalance providerId={provider.id} platformUrl={platformUrl} /> <ProviderBalance
providerId={provider.id}
platformUrl={platformUrl}
/>
</div> </div>
)} )}
@@ -127,7 +130,9 @@ export function ProviderCard({
{provider.api_version && ( {provider.api_version && (
<div className='flex flex-col gap-1 text-sm sm:flex-row sm:items-center sm:justify-between'> <div className='flex flex-col gap-1 text-sm sm:flex-row sm:items-center sm:justify-between'>
<span className='text-muted-foreground'>API Version:</span> <span className='text-muted-foreground'>API Version:</span>
<span className='font-mono break-all'>{provider.api_version}</span> <span className='font-mono break-all'>
{provider.api_version}
</span>
</div> </div>
)} )}
+14 -3
View File
@@ -1,5 +1,8 @@
import type { Dispatch, SetStateAction } from 'react'; import type { Dispatch, SetStateAction } from 'react';
import type { CreateUpstreamProvider, ProviderType } from '@/lib/api/services/admin'; import type {
CreateUpstreamProvider,
ProviderType,
} from '@/lib/api/services/admin';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
DialogContent, DialogContent,
@@ -65,10 +68,18 @@ export function ProviderFormDialogContent({
onCreateAccount={onCreateAccount} onCreateAccount={onCreateAccount}
/> />
<DialogFooter> <DialogFooter>
<Button variant='outline' onClick={onCancel} className='w-full sm:w-auto'> <Button
variant='outline'
onClick={onCancel}
className='w-full sm:w-auto'
>
Cancel Cancel
</Button> </Button>
<Button onClick={onSubmit} disabled={isSubmitting} className='w-full sm:w-auto'> <Button
onClick={onSubmit}
disabled={isSubmitting}
className='w-full sm:w-auto'
>
{isSubmitting ? submittingLabel : submitLabel} {isSubmitting ? submittingLabel : submitLabel}
</Button> </Button>
</DialogFooter> </DialogFooter>
+15 -5
View File
@@ -1,7 +1,10 @@
'use client'; 'use client';
import type { Dispatch, SetStateAction } from 'react'; import type { Dispatch, SetStateAction } from 'react';
import type { CreateUpstreamProvider, ProviderType } from '@/lib/api/services/admin'; import type {
CreateUpstreamProvider,
ProviderType,
} from '@/lib/api/services/admin';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
@@ -39,10 +42,13 @@ export function ProviderFormFields({
onCreateAccount, onCreateAccount,
}: ProviderFormFieldsProps) { }: ProviderFormFieldsProps) {
const idPrefix = mode === 'edit' ? 'edit_' : ''; const idPrefix = mode === 'edit' ? 'edit_' : '';
const providerType = providerTypes.find((pt) => pt.id === formData.provider_type); const providerType = providerTypes.find(
(pt) => pt.id === formData.provider_type
);
const hasFixedBaseUrl = providerType?.fixed_base_url || false; const hasFixedBaseUrl = providerType?.fixed_base_url || false;
const platformUrl = providerType?.platform_url || null; const platformUrl = providerType?.platform_url || null;
const isGenericType = (type: ProviderType) => type.id.toLowerCase() === 'generic'; const isGenericType = (type: ProviderType) =>
type.id.toLowerCase() === 'generic';
const nonGenericTypes = providerTypes.filter((type) => !isGenericType(type)); const nonGenericTypes = providerTypes.filter((type) => !isGenericType(type));
const genericType = providerTypes.find((type) => isGenericType(type)); const genericType = providerTypes.find((type) => isGenericType(type));
const apiKeyLabel = const apiKeyLabel =
@@ -170,7 +176,9 @@ export function ProviderFormFields({
</div> </div>
<div className='grid gap-2'> <div className='grid gap-2'>
<Label htmlFor={`${idPrefix}provider_fee`}>Provider Fee (Multiplier)</Label> <Label htmlFor={`${idPrefix}provider_fee`}>
Provider Fee (Multiplier)
</Label>
<Input <Input
id={`${idPrefix}provider_fee`} id={`${idPrefix}provider_fee`}
type='number' type='number'
@@ -180,7 +188,9 @@ export function ProviderFormFields({
onChange={(e) => onChange={(e) =>
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
provider_fee: e.target.value ? parseFloat(e.target.value) : undefined, provider_fee: e.target.value
? parseFloat(e.target.value)
: undefined,
})) }))
} }
placeholder={providerFeePlaceholder} placeholder={providerFeePlaceholder}
+3 -1
View File
@@ -42,7 +42,9 @@ export function ProviderModelsPanel({
return ( return (
<Tabs <Tabs
defaultValue={providerModels.remote_models.length > 0 ? 'provided' : 'custom'} defaultValue={
providerModels.remote_models.length > 0 ? 'provided' : 'custom'
}
className='w-full' className='w-full'
> >
<TabsList className='grid w-full grid-cols-2'> <TabsList className='grid w-full grid-cols-2'>
+3 -1
View File
@@ -76,7 +76,9 @@ export function RevenueByModelTable({
) : ( ) : (
models.map((model) => { models.map((model) => {
const share = const share =
totalRevenue > 0 ? (model.revenue_sats / totalRevenue) * 100 : 0; totalRevenue > 0
? (model.revenue_sats / totalRevenue) * 100
: 0;
return ( return (
<TableRow key={model.model}> <TableRow key={model.model}>
<TableCell className='font-medium'>{model.model}</TableCell> <TableCell className='font-medium'>{model.model}</TableCell>
+18 -7
View File
@@ -357,9 +357,12 @@ export function AdminSettings() {
const hasFieldChanged = (key: string): boolean => const hasFieldChanged = (key: string): boolean =>
!areValuesEqual(settings[key], initialSettings[key]); !areValuesEqual(settings[key], initialSettings[key]);
const basicInfoChanged = ['name', 'description', 'http_url', 'onion_url'].some( const basicInfoChanged = [
hasFieldChanged 'name',
); 'description',
'http_url',
'onion_url',
].some(hasFieldChanged);
const nostrChanged = ['npub', 'nsec'].some(hasFieldChanged); const nostrChanged = ['npub', 'nsec'].some(hasFieldChanged);
const cashuMintsChanged = hasFieldChanged('cashu_mints'); const cashuMintsChanged = hasFieldChanged('cashu_mints');
const relaysChanged = hasFieldChanged('relays'); const relaysChanged = hasFieldChanged('relays');
@@ -369,8 +372,8 @@ export function AdminSettings() {
const advancedChanged = advancedKeys.some(hasFieldChanged); const advancedChanged = advancedKeys.some(hasFieldChanged);
const hasPasswordChanges = Boolean( const hasPasswordChanges = Boolean(
passwordData.current_password || passwordData.current_password ||
passwordData.new_password || passwordData.new_password ||
passwordData.confirm_password passwordData.confirm_password
); );
const resetFields = (keys: string[]) => { const resetFields = (keys: string[]) => {
@@ -561,7 +564,11 @@ export function AdminSettings() {
onChange={(e) => setNewMint(e.target.value)} onChange={(e) => setNewMint(e.target.value)}
placeholder='https://mint.example.com' placeholder='https://mint.example.com'
/> />
<Button onClick={addMint} disabled={!newMint.trim()} className='sm:w-auto'> <Button
onClick={addMint}
disabled={!newMint.trim()}
className='sm:w-auto'
>
Add Mint Add Mint
</Button> </Button>
</div> </div>
@@ -627,7 +634,11 @@ export function AdminSettings() {
onChange={(e) => setNewRelay(e.target.value)} onChange={(e) => setNewRelay(e.target.value)}
placeholder='wss://relay.example.com' placeholder='wss://relay.example.com'
/> />
<Button onClick={addRelay} disabled={!newRelay.trim()} className='sm:w-auto'> <Button
onClick={addRelay}
disabled={!newRelay.trim()}
className='sm:w-auto'
>
Add Relay Add Relay
</Button> </Button>
</div> </div>
+66 -21
View File
@@ -68,7 +68,9 @@ function TemporaryBalanceStat({
</span> </span>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className='text-2xl font-bold tracking-tight tabular-nums'>{value}</p> <p className='text-2xl font-bold tracking-tight tabular-nums'>
{value}
</p>
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -111,7 +113,8 @@ function buildHierarchicalData(
const orphans = filteredBalances.filter( const orphans = filteredBalances.filter(
(item) => (item) =>
item.parent_key_hash && !result.some((r) => r.hashed_key === item.hashed_key) item.parent_key_hash &&
!result.some((r) => r.hashed_key === item.hashed_key)
); );
result.push(...orphans.map((item) => ({ ...item, isChild: true }))); result.push(...orphans.map((item) => ({ ...item, isChild: true })));
@@ -182,7 +185,10 @@ export function TemporaryBalances({
className='h-8 w-full sm:w-8' className='h-8 w-full sm:w-8'
> >
<RefreshCw <RefreshCw
className={cn('h-4 w-4', (isFetching || isLoading) && 'animate-spin')} className={cn(
'h-4 w-4',
(isFetching || isLoading) && 'animate-spin'
)}
/> />
<span className='sr-only'>Refresh temporary balances</span> <span className='sr-only'>Refresh temporary balances</span>
</Button> </Button>
@@ -197,7 +203,10 @@ export function TemporaryBalances({
<div className='space-y-4'> <div className='space-y-4'>
<div className='grid gap-3 md:grid-cols-3'> <div className='grid gap-3 md:grid-cols-3'>
{Array.from({ length: 3 }).map((_, index) => ( {Array.from({ length: 3 }).map((_, index) => (
<Card key={`temp-stat-skeleton-${index}`} className='shadow-none'> <Card
key={`temp-stat-skeleton-${index}`}
className='shadow-none'
>
<CardHeader className='space-y-2 pb-1'> <CardHeader className='space-y-2 pb-1'>
<Skeleton className='h-3.5 w-24' /> <Skeleton className='h-3.5 w-24' />
<Skeleton className='h-3 w-8' /> <Skeleton className='h-3 w-8' />
@@ -210,7 +219,10 @@ export function TemporaryBalances({
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
{Array.from({ length: 6 }).map((_, index) => ( {Array.from({ length: 6 }).map((_, index) => (
<Skeleton key={`temp-row-skeleton-${index}`} className='h-11 w-full' /> <Skeleton
key={`temp-row-skeleton-${index}`}
className='h-11 w-full'
/>
))} ))}
</div> </div>
</div> </div>
@@ -252,10 +264,16 @@ export function TemporaryBalances({
<TableRow> <TableRow>
<TableHead>Hashed Key</TableHead> <TableHead>Hashed Key</TableHead>
<TableHead className='text-right'>Balance</TableHead> <TableHead className='text-right'>Balance</TableHead>
<TableHead className='text-right'>Total Spent</TableHead> <TableHead className='text-right'>
<TableHead className='text-right'>Total Requests</TableHead> Total Spent
</TableHead>
<TableHead className='text-right'>
Total Requests
</TableHead>
<TableHead>Refund Address</TableHead> <TableHead>Refund Address</TableHead>
<TableHead className='text-right'>Expiry Time</TableHead> <TableHead className='text-right'>
Expiry Time
</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@@ -263,14 +281,19 @@ export function TemporaryBalances({
<TableRow <TableRow
key={`${balance.hashed_key}-${balance.parent_key_hash ?? 'root'}-${index}`} key={`${balance.hashed_key}-${balance.parent_key_hash ?? 'root'}-${index}`}
className={cn( className={cn(
balance.balance === 0 && !balance.isChild && 'opacity-60', balance.balance === 0 &&
!balance.isChild &&
'opacity-60',
balance.isChild && 'bg-muted/30' balance.isChild && 'bg-muted/30'
)} )}
> >
<TableCell className='max-w-[16rem] font-mono text-xs break-all whitespace-normal'> <TableCell className='max-w-[16rem] font-mono text-xs break-all whitespace-normal'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
{balance.isChild && ( {balance.isChild && (
<Badge variant='outline' className='h-4 px-1 text-[10px] uppercase'> <Badge
variant='outline'
className='h-4 px-1 text-[10px] uppercase'
>
Child Child
</Badge> </Badge>
)} )}
@@ -279,7 +302,9 @@ export function TemporaryBalances({
</TableCell> </TableCell>
<TableCell className='text-right font-mono'> <TableCell className='text-right font-mono'>
{balance.isChild ? ( {balance.isChild ? (
<span className='text-muted-foreground italic'>(Parent)</span> <span className='text-muted-foreground italic'>
(Parent)
</span>
) : ( ) : (
formatBalance(balance.balance) formatBalance(balance.balance)
)} )}
@@ -298,7 +323,9 @@ export function TemporaryBalances({
<div className='inline-flex items-center justify-end gap-1'> <div className='inline-flex items-center justify-end gap-1'>
<Clock className='h-3 w-3' /> <Clock className='h-3 w-3' />
<span> <span>
{new Date(balance.key_expiry_time * 1000).toLocaleDateString()} {new Date(
balance.key_expiry_time * 1000
).toLocaleDateString()}
</span> </span>
</div> </div>
) : ( ) : (
@@ -317,7 +344,9 @@ export function TemporaryBalances({
key={`${balance.hashed_key}-${balance.parent_key_hash ?? 'root'}-mobile-${index}`} key={`${balance.hashed_key}-${balance.parent_key_hash ?? 'root'}-mobile-${index}`}
className={cn( className={cn(
'shadow-none', 'shadow-none',
balance.balance === 0 && !balance.isChild && 'opacity-80', balance.balance === 0 &&
!balance.isChild &&
'opacity-80',
balance.isChild && 'bg-muted/30' balance.isChild && 'bg-muted/30'
)} )}
> >
@@ -327,7 +356,10 @@ export function TemporaryBalances({
{balance.hashed_key} {balance.hashed_key}
</CardDescription> </CardDescription>
{balance.isChild && ( {balance.isChild && (
<Badge variant='outline' className='h-4 px-1.5 text-[10px] uppercase'> <Badge
variant='outline'
className='h-4 px-1.5 text-[10px] uppercase'
>
Child Child
</Badge> </Badge>
)} )}
@@ -335,9 +367,13 @@ export function TemporaryBalances({
</CardHeader> </CardHeader>
<CardContent className='grid grid-cols-2 gap-3 p-4 pt-0'> <CardContent className='grid grid-cols-2 gap-3 p-4 pt-0'>
<div> <div>
<p className='text-muted-foreground text-xs'>Balance</p> <p className='text-muted-foreground text-xs'>
Balance
</p>
<p className='font-mono text-sm'> <p className='font-mono text-sm'>
{balance.isChild ? '(Uses Parent)' : formatBalance(balance.balance)} {balance.isChild
? '(Uses Parent)'
: formatBalance(balance.balance)}
</p> </p>
</div> </div>
<div> <div>
@@ -347,18 +383,24 @@ export function TemporaryBalances({
</p> </p>
</div> </div>
<div> <div>
<p className='text-muted-foreground text-xs'>Requests</p> <p className='text-muted-foreground text-xs'>
Requests
</p>
<p className='font-mono text-sm'> <p className='font-mono text-sm'>
{balance.total_requests.toLocaleString()} {balance.total_requests.toLocaleString()}
</p> </p>
</div> </div>
<div> <div>
<p className='text-muted-foreground text-xs'>Expires</p> <p className='text-muted-foreground text-xs'>
Expires
</p>
<p className='font-mono text-xs'> <p className='font-mono text-xs'>
{balance.key_expiry_time ? ( {balance.key_expiry_time ? (
<span className='inline-flex items-center gap-1'> <span className='inline-flex items-center gap-1'>
<Clock className='h-3 w-3' /> <Clock className='h-3 w-3' />
{new Date(balance.key_expiry_time * 1000).toLocaleDateString()} {new Date(
balance.key_expiry_time * 1000
).toLocaleDateString()}
</span> </span>
) : ( ) : (
'-' '-'
@@ -367,7 +409,9 @@ export function TemporaryBalances({
</div> </div>
{balance.refund_address && ( {balance.refund_address && (
<div className='col-span-2'> <div className='col-span-2'>
<p className='text-muted-foreground text-xs'>Refund Address</p> <p className='text-muted-foreground text-xs'>
Refund Address
</p>
<p className='font-mono text-xs break-all'> <p className='font-mono text-xs break-all'>
{balance.refund_address} {balance.refund_address}
</p> </p>
@@ -404,7 +448,8 @@ export function TemporaryBalances({
{data && data.length > 0 && ( {data && data.length > 0 && (
<p className='text-muted-foreground text-xs'> <p className='text-muted-foreground text-xs'>
Showing {filteredData.length} of {data.length} temporary balances Showing {filteredData.length} of {data.length} temporary
balances
</p> </p>
)} )}
</div> </div>
+14 -4
View File
@@ -22,7 +22,11 @@ interface ThemeToggleProps {
type ThemeMode = 'light' | 'dark' | 'system'; type ThemeMode = 'light' | 'dark' | 'system';
const THEME_OPTIONS: Array<{ value: ThemeMode; label: string; icon: typeof Sun }> = [ const THEME_OPTIONS: Array<{
value: ThemeMode;
label: string;
icon: typeof Sun;
}> = [
{ value: 'dark', label: 'Dark', icon: Moon }, { value: 'dark', label: 'Dark', icon: Moon },
{ value: 'light', label: 'Light', icon: Sun }, { value: 'light', label: 'Light', icon: Sun },
{ value: 'system', label: 'System', icon: Monitor }, { value: 'system', label: 'System', icon: Monitor },
@@ -33,7 +37,9 @@ function isThemeMode(value: string | undefined): value is ThemeMode {
} }
function getThemeOption(theme: ThemeMode) { function getThemeOption(theme: ThemeMode) {
return THEME_OPTIONS.find((option) => option.value === theme) ?? THEME_OPTIONS[2]; return (
THEME_OPTIONS.find((option) => option.value === theme) ?? THEME_OPTIONS[2]
);
} }
export function ThemeToggle({ export function ThemeToggle({
@@ -68,7 +74,9 @@ export function ThemeToggle({
size='sm' size='sm'
className={cn( className={cn(
'border-border/60 bg-background/65 text-muted-foreground hover:text-foreground rounded-md', 'border-border/60 bg-background/65 text-muted-foreground hover:text-foreground rounded-md',
compact ? 'h-8 w-10 justify-center px-0' : 'h-8 justify-between gap-2', compact
? 'h-8 w-10 justify-center px-0'
: 'h-8 justify-between gap-2',
className className
)} )}
disabled={!mounted} disabled={!mounted}
@@ -76,7 +84,9 @@ export function ThemeToggle({
<span className='inline-flex min-w-0 items-center gap-1.5'> <span className='inline-flex min-w-0 items-center gap-1.5'>
<ActiveIcon className='h-3.5 w-3.5 shrink-0' /> <ActiveIcon className='h-3.5 w-3.5 shrink-0' />
{compact ? null : ( {compact ? null : (
<span className='truncate text-[11px] font-medium'>{activeOption.label}</span> <span className='truncate text-[11px] font-medium'>
{activeOption.label}
</span>
)} )}
</span> </span>
{compact ? ( {compact ? (
+9 -6
View File
@@ -1,7 +1,7 @@
"use client"; 'use client';
import React from "react"; import React from 'react';
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils';
interface ModalShellProps { interface ModalShellProps {
open: boolean; open: boolean;
@@ -27,7 +27,7 @@ export const ModalShell: React.FC<ModalShellProps> = ({
closeOnOverlayClick = false, closeOnOverlayClick = false,
closeOnAnyClick = false, closeOnAnyClick = false,
stopPropagation, stopPropagation,
contentRole = "dialog", contentRole = 'dialog',
contentAriaLabel, contentAriaLabel,
}) => { }) => {
if (!open) return null; if (!open) return null;
@@ -47,13 +47,16 @@ export const ModalShell: React.FC<ModalShellProps> = ({
return ( return (
<div <div
className={cn("fixed inset-0 flex items-center justify-center", overlayClassName)} className={cn(
'fixed inset-0 flex items-center justify-center',
overlayClassName
)}
onMouseDown={handleOverlayMouseDown} onMouseDown={handleOverlayMouseDown}
> >
<div <div
className={contentClassName} className={contentClassName}
role={contentRole} role={contentRole}
aria-modal="true" aria-modal='true'
aria-label={contentAriaLabel} aria-label={contentAriaLabel}
style={contentStyle} style={contentStyle}
onMouseDown={ onMouseDown={
+11 -11
View File
@@ -1,9 +1,9 @@
"use client"; 'use client';
import React from "react"; import React from 'react';
import { Drawer, DrawerContent, DrawerTitle } from "@/components/ui/drawer"; import { Drawer, DrawerContent, DrawerTitle } from '@/components/ui/drawer';
import { ModalShell } from "@/components/ui/ModalShell"; import { ModalShell } from '@/components/ui/ModalShell';
import { useMediaQuery } from "@/hooks/useMediaQuery"; import { useMediaQuery } from '@/hooks/useMediaQuery';
interface SettingsDialogProps { interface SettingsDialogProps {
open: boolean; open: boolean;
@@ -18,16 +18,16 @@ export default function SettingsDialog({
onOpenChange, onOpenChange,
children, children,
isMobile: propIsMobile, isMobile: propIsMobile,
title = "Dialog", title = 'Dialog',
}: SettingsDialogProps) { }: SettingsDialogProps) {
const mediaQueryIsMobile = useMediaQuery("(max-width: 640px)"); const mediaQueryIsMobile = useMediaQuery('(max-width: 640px)');
const isMobile = propIsMobile ?? mediaQueryIsMobile; const isMobile = propIsMobile ?? mediaQueryIsMobile;
if (isMobile) { if (isMobile) {
return ( return (
<Drawer open={open} onOpenChange={onOpenChange}> <Drawer open={open} onOpenChange={onOpenChange}>
<DrawerContent className="p-4"> <DrawerContent className='p-4'>
<DrawerTitle className="sr-only">{title}</DrawerTitle> <DrawerTitle className='sr-only'>{title}</DrawerTitle>
{children} {children}
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
@@ -38,8 +38,8 @@ export default function SettingsDialog({
<ModalShell <ModalShell
open={open} open={open}
onClose={() => onOpenChange(false)} onClose={() => onOpenChange(false)}
overlayClassName="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4" overlayClassName='fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4'
contentClassName="bg-card border border-border rounded-lg p-6 w-full max-w-md" contentClassName='bg-card border border-border rounded-lg p-6 w-full max-w-md'
closeOnOverlayClick closeOnOverlayClick
contentAriaLabel={title} contentAriaLabel={title}
> >
+28 -22
View File
@@ -1,10 +1,10 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Accordion as AccordionPrimitive } from "radix-ui" import { Accordion as AccordionPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react" import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
function Accordion({ function Accordion({
className, className,
@@ -12,11 +12,11 @@ function Accordion({
}: React.ComponentProps<typeof AccordionPrimitive.Root>) { }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return ( return (
<AccordionPrimitive.Root <AccordionPrimitive.Root
data-slot="accordion" data-slot='accordion'
className={cn("flex w-full flex-col", className)} className={cn('flex w-full flex-col', className)}
{...props} {...props}
/> />
) );
} }
function AccordionItem({ function AccordionItem({
@@ -25,11 +25,11 @@ function AccordionItem({
}: React.ComponentProps<typeof AccordionPrimitive.Item>) { }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return ( return (
<AccordionPrimitive.Item <AccordionPrimitive.Item
data-slot="accordion-item" data-slot='accordion-item'
className={cn("not-last:border-b", className)} className={cn('not-last:border-b', className)}
{...props} {...props}
/> />
) );
} }
function AccordionTrigger({ function AccordionTrigger({
@@ -38,21 +38,27 @@ function AccordionTrigger({
...props ...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) { }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return ( return (
<AccordionPrimitive.Header className="flex"> <AccordionPrimitive.Header className='flex'>
<AccordionPrimitive.Trigger <AccordionPrimitive.Trigger
data-slot="accordion-trigger" data-slot='accordion-trigger'
className={cn( className={cn(
"focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:after:border-ring **:data-[slot=accordion-trigger-icon]:text-muted-foreground rounded-lg py-2.5 text-left text-sm font-medium hover:underline focus-visible:ring-3 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 group/accordion-trigger relative flex flex-1 items-start justify-between border border-transparent transition-all outline-none disabled:pointer-events-none disabled:opacity-50", 'focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:after:border-ring **:data-[slot=accordion-trigger-icon]:text-muted-foreground group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4',
className className
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronDownIcon data-slot="accordion-trigger-icon" className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" /> <ChevronDownIcon
<ChevronUpIcon data-slot="accordion-trigger-icon" className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" /> data-slot='accordion-trigger-icon'
className='pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden'
/>
<ChevronUpIcon
data-slot='accordion-trigger-icon'
className='pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline'
/>
</AccordionPrimitive.Trigger> </AccordionPrimitive.Trigger>
</AccordionPrimitive.Header> </AccordionPrimitive.Header>
) );
} }
function AccordionContent({ function AccordionContent({
@@ -62,20 +68,20 @@ function AccordionContent({
}: React.ComponentProps<typeof AccordionPrimitive.Content>) { }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return ( return (
<AccordionPrimitive.Content <AccordionPrimitive.Content
data-slot="accordion-content" data-slot='accordion-content'
className="data-open:animate-accordion-down data-closed:animate-accordion-up text-sm overflow-hidden" className='data-open:animate-accordion-down data-closed:animate-accordion-up overflow-hidden text-sm'
{...props} {...props}
> >
<div <div
className={cn( className={cn(
"pt-0 pb-2.5 [&_a]:hover:text-foreground h-(--radix-accordion-content-height) [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4", '[&_a]:hover:text-foreground h-(--radix-accordion-content-height) pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4',
className className
)} )}
> >
{children} {children}
</div> </div>
</AccordionPrimitive.Content> </AccordionPrimitive.Content>
) );
} }
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
+62 -47
View File
@@ -1,31 +1,31 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { AlertDialog as AlertDialogPrimitive } from "radix-ui" import { AlertDialog as AlertDialogPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button';
function AlertDialog({ function AlertDialog({
...props ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} /> return <AlertDialogPrimitive.Root data-slot='alert-dialog' {...props} />;
} }
function AlertDialogTrigger({ function AlertDialogTrigger({
...props ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return ( return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> <AlertDialogPrimitive.Trigger data-slot='alert-dialog-trigger' {...props} />
) );
} }
function AlertDialogPortal({ function AlertDialogPortal({
...props ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return ( return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} /> <AlertDialogPrimitive.Portal data-slot='alert-dialog-portal' {...props} />
) );
} }
function AlertDialogOverlay({ function AlertDialogOverlay({
@@ -34,76 +34,85 @@ function AlertDialogOverlay({
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return ( return (
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay" data-slot='alert-dialog-overlay'
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)} className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs',
className
)}
{...props} {...props}
/> />
) );
} }
function AlertDialogContent({ function AlertDialogContent({
className, className,
size = "default", size = 'default',
...props ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & { }: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
size?: "default" | "sm" size?: 'default' | 'sm';
}) { }) {
return ( return (
<AlertDialogPortal> <AlertDialogPortal>
<AlertDialogOverlay /> <AlertDialogOverlay />
<AlertDialogPrimitive.Content <AlertDialogPrimitive.Content
data-slot="alert-dialog-content" data-slot='alert-dialog-content'
data-size={size} data-size={size}
className={cn( className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 gap-4 rounded-xl p-4 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none", 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl p-4 ring-1 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm',
className className
)} )}
{...props} {...props}
/> />
</AlertDialogPortal> </AlertDialogPortal>
) );
} }
function AlertDialogHeader({ function AlertDialogHeader({
className, className,
...props ...props
}: React.ComponentProps<"div">) { }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="alert-dialog-header" data-slot='alert-dialog-header'
className={cn("grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]", className)} className={cn(
'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]',
className
)}
{...props} {...props}
/> />
) );
} }
function AlertDialogFooter({ function AlertDialogFooter({
className, className,
...props ...props
}: React.ComponentProps<"div">) { }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="alert-dialog-footer" data-slot='alert-dialog-footer'
className={cn( className={cn(
"bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end", 'bg-muted/50 -mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function AlertDialogMedia({ function AlertDialogMedia({
className, className,
...props ...props
}: React.ComponentProps<"div">) { }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="alert-dialog-media" data-slot='alert-dialog-media'
className={cn("bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6", className)} className={cn(
"bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
className
)}
{...props} {...props}
/> />
) );
} }
function AlertDialogTitle({ function AlertDialogTitle({
@@ -112,11 +121,14 @@ function AlertDialogTitle({
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return ( return (
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
data-slot="alert-dialog-title" data-slot='alert-dialog-title'
className={cn("text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2", className)} className={cn(
'text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2',
className
)}
{...props} {...props}
/> />
) );
} }
function AlertDialogDescription({ function AlertDialogDescription({
@@ -125,47 +137,50 @@ function AlertDialogDescription({
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return ( return (
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
data-slot="alert-dialog-description" data-slot='alert-dialog-description'
className={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3", className)} className={cn(
'text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3',
className
)}
{...props} {...props}
/> />
) );
} }
function AlertDialogAction({ function AlertDialogAction({
className, className,
variant = "default", variant = 'default',
size = "default", size = 'default',
...props ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> & }: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) { Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
return ( return (
<Button variant={variant} size={size} asChild> <Button variant={variant} size={size} asChild>
<AlertDialogPrimitive.Action <AlertDialogPrimitive.Action
data-slot="alert-dialog-action" data-slot='alert-dialog-action'
className={cn(className)} className={cn(className)}
{...props} {...props}
/> />
</Button> </Button>
) );
} }
function AlertDialogCancel({ function AlertDialogCancel({
className, className,
variant = "outline", variant = 'outline',
size = "default", size = 'default',
...props ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> & }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) { Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
return ( return (
<Button variant={variant} size={size} asChild> <Button variant={variant} size={size} asChild>
<AlertDialogPrimitive.Cancel <AlertDialogPrimitive.Cancel
data-slot="alert-dialog-cancel" data-slot='alert-dialog-cancel'
className={cn(className)} className={cn(className)}
{...props} {...props}
/> />
</Button> </Button>
) );
} }
export { export {
@@ -181,4 +196,4 @@ export {
AlertDialogPortal, AlertDialogPortal,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} };
+34 -30
View File
@@ -1,72 +1,76 @@
import * as React from "react" import * as React from 'react';
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
const alertVariants = cva("grid gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 w-full relative group/alert", { const alertVariants = cva(
variants: { "grid gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 w-full relative group/alert",
variant: { {
default: "bg-card text-card-foreground", variants: {
destructive: "text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", variant: {
default: 'bg-card text-card-foreground',
destructive:
'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current',
},
}, },
}, defaultVariants: {
defaultVariants: { variant: 'default',
variant: "default", },
}, }
}) );
function Alert({ function Alert({
className, className,
variant, variant,
...props ...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) { }: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
return ( return (
<div <div
data-slot="alert" data-slot='alert'
role="alert" role='alert'
className={cn(alertVariants({ variant }), className)} className={cn(alertVariants({ variant }), className)}
{...props} {...props}
/> />
) );
} }
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="alert-title" data-slot='alert-title'
className={cn( className={cn(
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3", '[&_a]:hover:text-foreground font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function AlertDescription({ function AlertDescription({
className, className,
...props ...props
}: React.ComponentProps<"div">) { }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="alert-description" data-slot='alert-description'
className={cn( className={cn(
"text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3", 'text-muted-foreground [&_a]:hover:text-foreground text-sm text-balance md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function AlertAction({ className, ...props }: React.ComponentProps<"div">) { function AlertAction({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="alert-action" data-slot='alert-action'
className={cn("absolute top-2 right-2", className)} className={cn('absolute top-2 right-2', className)}
{...props} {...props}
/> />
) );
} }
export { Alert, AlertTitle, AlertDescription, AlertAction } export { Alert, AlertTitle, AlertDescription, AlertAction };
+4 -4
View File
@@ -1,11 +1,11 @@
"use client" 'use client';
import { AspectRatio as AspectRatioPrimitive } from "radix-ui" import { AspectRatio as AspectRatioPrimitive } from 'radix-ui';
function AspectRatio({ function AspectRatio({
...props ...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) { }: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} /> return <AspectRatioPrimitive.Root data-slot='aspect-ratio' {...props} />;
} }
export { AspectRatio } export { AspectRatio };
+34 -31
View File
@@ -1,28 +1,28 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Avatar as AvatarPrimitive } from "radix-ui" import { Avatar as AvatarPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Avatar({ function Avatar({
className, className,
size = "default", size = 'default',
...props ...props
}: React.ComponentProps<typeof AvatarPrimitive.Root> & { }: React.ComponentProps<typeof AvatarPrimitive.Root> & {
size?: "default" | "sm" | "lg" size?: 'default' | 'sm' | 'lg';
}) { }) {
return ( return (
<AvatarPrimitive.Root <AvatarPrimitive.Root
data-slot="avatar" data-slot='avatar'
data-size={size} data-size={size}
className={cn( className={cn(
"size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 after:border-border group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten", 'after:border-border group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function AvatarImage({ function AvatarImage({
@@ -31,14 +31,14 @@ function AvatarImage({
}: React.ComponentProps<typeof AvatarPrimitive.Image>) { }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return ( return (
<AvatarPrimitive.Image <AvatarPrimitive.Image
data-slot="avatar-image" data-slot='avatar-image'
className={cn( className={cn(
"rounded-full aspect-square size-full object-cover", 'aspect-square size-full rounded-full object-cover',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function AvatarFallback({ function AvatarFallback({
@@ -47,56 +47,59 @@ function AvatarFallback({
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return ( return (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
data-slot="avatar-fallback" data-slot='avatar-fallback'
className={cn( className={cn(
"bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs", 'bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
return ( return (
<span <span
data-slot="avatar-badge" data-slot='avatar-badge'
className={cn( className={cn(
"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none", 'bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none',
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden", 'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", 'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", 'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="avatar-group" data-slot='avatar-group'
className={cn( className={cn(
"*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2", '*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function AvatarGroupCount({ function AvatarGroupCount({
className, className,
...props ...props
}: React.ComponentProps<"div">) { }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="avatar-group-count" data-slot='avatar-group-count'
className={cn("bg-muted text-muted-foreground size-8 rounded-full text-sm group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", className)} className={cn(
'bg-muted text-muted-foreground ring-background relative flex size-8 shrink-0 items-center justify-center rounded-full text-sm ring-2 group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
className
)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -106,4 +109,4 @@ export {
AvatarGroup, AvatarGroup,
AvatarGroupCount, AvatarGroupCount,
AvatarBadge, AvatarBadge,
} };
+23 -19
View File
@@ -1,45 +1,49 @@
import * as React from "react" import * as React from 'react';
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from "radix-ui" import { Slot } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
const badgeVariants = cva( const badgeVariants = cva(
"h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive overflow-hidden group/badge", 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive overflow-hidden group/badge',
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", secondary:
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20", 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", destructive:
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", 'bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20',
link: "text-primary underline-offset-4 hover:underline", outline:
'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
ghost:
'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
link: 'text-primary underline-offset-4 hover:underline',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
}, },
} }
) );
function Badge({ function Badge({
className, className,
variant = "default", variant = 'default',
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"span"> & }: React.ComponentProps<'span'> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) { VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span" const Comp = asChild ? Slot.Root : 'span';
return ( return (
<Comp <Comp
data-slot="badge" data-slot='badge'
data-variant={variant} data-variant={variant}
className={cn(badgeVariants({ variant }), className)} className={cn(badgeVariants({ variant }), className)}
{...props} {...props}
/> />
) );
} }
export { Badge, badgeVariants } export { Badge, badgeVariants };
+45 -48
View File
@@ -1,114 +1,111 @@
import * as React from "react" import * as React from 'react';
import { Slot } from "radix-ui" import { Slot } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react" import { ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react';
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) { function Breadcrumb({ className, ...props }: React.ComponentProps<'nav'>) {
return ( return (
<nav <nav
aria-label="breadcrumb" aria-label='breadcrumb'
data-slot="breadcrumb" data-slot='breadcrumb'
className={cn(className)} className={cn(className)}
{...props} {...props}
/> />
) );
} }
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) { function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
return ( return (
<ol <ol
data-slot="breadcrumb-list" data-slot='breadcrumb-list'
className={cn( className={cn(
"text-muted-foreground gap-1.5 text-sm flex flex-wrap items-center wrap-break-word", 'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm wrap-break-word',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) { function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
return ( return (
<li <li
data-slot="breadcrumb-item" data-slot='breadcrumb-item'
className={cn("gap-1 inline-flex items-center", className)} className={cn('inline-flex items-center gap-1', className)}
{...props} {...props}
/> />
) );
} }
function BreadcrumbLink({ function BreadcrumbLink({
asChild, asChild,
className, className,
...props ...props
}: React.ComponentProps<"a"> & { }: React.ComponentProps<'a'> & {
asChild?: boolean asChild?: boolean;
}) { }) {
const Comp = asChild ? Slot.Root : "a" const Comp = asChild ? Slot.Root : 'a';
return ( return (
<Comp <Comp
data-slot="breadcrumb-link" data-slot='breadcrumb-link'
className={cn("hover:text-foreground transition-colors", className)} className={cn('hover:text-foreground transition-colors', className)}
{...props} {...props}
/> />
) );
} }
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) { function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
return ( return (
<span <span
data-slot="breadcrumb-page" data-slot='breadcrumb-page'
role="link" role='link'
aria-disabled="true" aria-disabled='true'
aria-current="page" aria-current='page'
className={cn("text-foreground font-normal", className)} className={cn('text-foreground font-normal', className)}
{...props} {...props}
/> />
) );
} }
function BreadcrumbSeparator({ function BreadcrumbSeparator({
children, children,
className, className,
...props ...props
}: React.ComponentProps<"li">) { }: React.ComponentProps<'li'>) {
return ( return (
<li <li
data-slot="breadcrumb-separator" data-slot='breadcrumb-separator'
role="presentation" role='presentation'
aria-hidden="true" aria-hidden='true'
className={cn("[&>svg]:size-3.5", className)} className={cn('[&>svg]:size-3.5', className)}
{...props} {...props}
> >
{children ?? ( {children ?? <ChevronRightIcon />}
<ChevronRightIcon />
)}
</li> </li>
) );
} }
function BreadcrumbEllipsis({ function BreadcrumbEllipsis({
className, className,
...props ...props
}: React.ComponentProps<"span">) { }: React.ComponentProps<'span'>) {
return ( return (
<span <span
data-slot="breadcrumb-ellipsis" data-slot='breadcrumb-ellipsis'
role="presentation" role='presentation'
aria-hidden="true" aria-hidden='true'
className={cn( className={cn(
"size-5 [&>svg]:size-4 flex items-center justify-center", 'flex size-5 items-center justify-center [&>svg]:size-4',
className className
)} )}
{...props} {...props}
> >
<MoreHorizontalIcon <MoreHorizontalIcon />
/> <span className='sr-only'>More</span>
<span className="sr-only">More</span>
</span> </span>
) );
} }
export { export {
@@ -119,4 +116,4 @@ export {
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
BreadcrumbEllipsis, BreadcrumbEllipsis,
} };
+22 -22
View File
@@ -1,8 +1,8 @@
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from "radix-ui" import { Slot } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Separator } from "@/components/ui/separator" import { Separator } from '@/components/ui/separator';
const buttonGroupVariants = cva( const buttonGroupVariants = cva(
"has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg flex w-fit items-stretch *:focus-visible:z-10 *:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", "has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg flex w-fit items-stretch *:focus-visible:z-10 *:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
@@ -10,69 +10,69 @@ const buttonGroupVariants = cva(
variants: { variants: {
orientation: { orientation: {
horizontal: horizontal:
"[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg! [&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none", '[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg! [&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none',
vertical: vertical:
"[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none", '[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
}, },
}, },
defaultVariants: { defaultVariants: {
orientation: "horizontal", orientation: 'horizontal',
}, },
} }
) );
function ButtonGroup({ function ButtonGroup({
className, className,
orientation, orientation,
...props ...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) { }: React.ComponentProps<'div'> & VariantProps<typeof buttonGroupVariants>) {
return ( return (
<div <div
role="group" role='group'
data-slot="button-group" data-slot='button-group'
data-orientation={orientation} data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)} className={cn(buttonGroupVariants({ orientation }), className)}
{...props} {...props}
/> />
) );
} }
function ButtonGroupText({ function ButtonGroupText({
className, className,
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<'div'> & {
asChild?: boolean asChild?: boolean;
}) { }) {
const Comp = asChild ? Slot.Root : "div" const Comp = asChild ? Slot.Root : 'div';
return ( return (
<Comp <Comp
className={cn( className={cn(
"bg-muted gap-2 rounded-lg border px-2.5 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none", "bg-muted flex items-center gap-2 rounded-lg border px-2.5 text-sm font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
/> />
) );
} }
function ButtonGroupSeparator({ function ButtonGroupSeparator({
className, className,
orientation = "vertical", orientation = 'vertical',
...props ...props
}: React.ComponentProps<typeof Separator>) { }: React.ComponentProps<typeof Separator>) {
return ( return (
<Separator <Separator
data-slot="button-group-separator" data-slot='button-group-separator'
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"bg-input relative self-stretch data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto", 'bg-input relative self-stretch data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto',
className className
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -80,4 +80,4 @@ export {
ButtonGroupSeparator, ButtonGroupSeparator,
ButtonGroupText, ButtonGroupText,
buttonGroupVariants, buttonGroupVariants,
} };
+34 -27
View File
@@ -1,60 +1,67 @@
import * as React from "react" import * as React from 'react';
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from "radix-ui" import { Slot } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
const buttonVariants = cva( const buttonVariants = cva(
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground", outline:
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", 'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground',
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", secondary:
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
link: "text-primary underline-offset-4 hover:underline", ghost:
'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',
destructive:
'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',
link: 'text-primary underline-offset-4 hover:underline',
}, },
size: { size: {
default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", default:
'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3',
icon: "size-8", icon: 'size-8',
"icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", 'icon-xs':
"icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-lg": "size-9", 'icon-sm':
'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
'icon-lg': 'size-9',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default',
}, },
} }
) );
function Button({ function Button({
className, className,
variant = "default", variant = 'default',
size = "default", size = 'default',
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"button"> & }: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean asChild?: boolean;
}) { }) {
const Comp = asChild ? Slot.Root : "button" const Comp = asChild ? Slot.Root : 'button';
return ( return (
<Comp <Comp
data-slot="button" data-slot='button'
data-variant={variant} data-variant={variant}
data-size={size} data-size={size}
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
{...props} {...props}
/> />
) );
} }
export { Button, buttonVariants } export { Button, buttonVariants };
+71 -64
View File
@@ -1,37 +1,41 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { import {
DayPicker, DayPicker,
getDefaultClassNames, getDefaultClassNames,
type DayButton, type DayButton,
type Locale, type Locale,
} from "react-day-picker" } from 'react-day-picker';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Button, buttonVariants } from "@/components/ui/button" import { Button, buttonVariants } from '@/components/ui/button';
import { ChevronLeftIcon, ChevronRightIcon, ChevronDownIcon } from "lucide-react" import {
ChevronLeftIcon,
ChevronRightIcon,
ChevronDownIcon,
} from 'lucide-react';
function Calendar({ function Calendar({
className, className,
classNames, classNames,
showOutsideDays = true, showOutsideDays = true,
captionLayout = "label", captionLayout = 'label',
buttonVariant = "ghost", buttonVariant = 'ghost',
locale, locale,
formatters, formatters,
components, components,
...props ...props
}: React.ComponentProps<typeof DayPicker> & { }: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"] buttonVariant?: React.ComponentProps<typeof Button>['variant'];
}) { }) {
const defaultClassNames = getDefaultClassNames() const defaultClassNames = getDefaultClassNames();
return ( return (
<DayPicker <DayPicker
showOutsideDays={showOutsideDays} showOutsideDays={showOutsideDays}
className={cn( className={cn(
"p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)] bg-background group/calendar in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent", 'bg-background group/calendar p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent',
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className className
@@ -40,126 +44,129 @@ function Calendar({
locale={locale} locale={locale}
formatters={{ formatters={{
formatMonthDropdown: (date) => formatMonthDropdown: (date) =>
date.toLocaleString(locale?.code, { month: "short" }), date.toLocaleString(locale?.code, { month: 'short' }),
...formatters, ...formatters,
}} }}
classNames={{ classNames={{
root: cn("w-fit", defaultClassNames.root), root: cn('w-fit', defaultClassNames.root),
months: cn( months: cn(
"flex gap-4 flex-col md:flex-row relative", 'flex gap-4 flex-col md:flex-row relative',
defaultClassNames.months defaultClassNames.months
), ),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month), month: cn('flex flex-col w-full gap-4', defaultClassNames.month),
nav: cn( nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", 'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between',
defaultClassNames.nav defaultClassNames.nav
), ),
button_previous: cn( button_previous: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
defaultClassNames.button_previous defaultClassNames.button_previous
), ),
button_next: cn( button_next: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
defaultClassNames.button_next defaultClassNames.button_next
), ),
month_caption: cn( month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", 'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)',
defaultClassNames.month_caption defaultClassNames.month_caption
), ),
dropdowns: cn( dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", 'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5',
defaultClassNames.dropdowns defaultClassNames.dropdowns
), ),
dropdown_root: cn( dropdown_root: cn(
"relative rounded-(--cell-radius)", 'relative rounded-(--cell-radius)',
defaultClassNames.dropdown_root defaultClassNames.dropdown_root
), ),
dropdown: cn( dropdown: cn(
"absolute bg-popover inset-0 opacity-0", 'absolute bg-popover inset-0 opacity-0',
defaultClassNames.dropdown defaultClassNames.dropdown
), ),
caption_label: cn( caption_label: cn(
"select-none font-medium", 'select-none font-medium',
captionLayout === "label" captionLayout === 'label'
? "text-sm" ? 'text-sm'
: "rounded-(--cell-radius) flex items-center gap-1 text-sm [&>svg]:text-muted-foreground [&>svg]:size-3.5", : 'rounded-(--cell-radius) flex items-center gap-1 text-sm [&>svg]:text-muted-foreground [&>svg]:size-3.5',
defaultClassNames.caption_label defaultClassNames.caption_label
), ),
table: "w-full border-collapse", table: 'w-full border-collapse',
weekdays: cn("flex", defaultClassNames.weekdays), weekdays: cn('flex', defaultClassNames.weekdays),
weekday: cn( weekday: cn(
"text-muted-foreground rounded-(--cell-radius) flex-1 font-normal text-[0.8rem] select-none", 'text-muted-foreground rounded-(--cell-radius) flex-1 font-normal text-[0.8rem] select-none',
defaultClassNames.weekday defaultClassNames.weekday
), ),
week: cn("flex w-full mt-2", defaultClassNames.week), week: cn('flex w-full mt-2', defaultClassNames.week),
week_number_header: cn( week_number_header: cn(
"select-none w-(--cell-size)", 'select-none w-(--cell-size)',
defaultClassNames.week_number_header defaultClassNames.week_number_header
), ),
week_number: cn( week_number: cn(
"text-[0.8rem] select-none text-muted-foreground", 'text-[0.8rem] select-none text-muted-foreground',
defaultClassNames.week_number defaultClassNames.week_number
), ),
day: cn( day: cn(
"relative w-full rounded-(--cell-radius) h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius) group/day aspect-square select-none", 'relative w-full rounded-(--cell-radius) h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius) group/day aspect-square select-none',
props.showWeekNumber props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)" ? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)'
: "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)", : '[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)',
defaultClassNames.day defaultClassNames.day
), ),
range_start: cn( range_start: cn(
"rounded-l-(--cell-radius) bg-muted relative after:bg-muted after:absolute after:inset-y-0 after:w-4 after:right-0 z-0 isolate", 'rounded-l-(--cell-radius) bg-muted relative after:bg-muted after:absolute after:inset-y-0 after:w-4 after:right-0 z-0 isolate',
defaultClassNames.range_start defaultClassNames.range_start
), ),
range_middle: cn("rounded-none", defaultClassNames.range_middle), range_middle: cn('rounded-none', defaultClassNames.range_middle),
range_end: cn( range_end: cn(
"rounded-r-(--cell-radius) bg-muted relative after:bg-muted after:absolute after:inset-y-0 after:w-4 after:left-0 z-0 isolate", 'rounded-r-(--cell-radius) bg-muted relative after:bg-muted after:absolute after:inset-y-0 after:w-4 after:left-0 z-0 isolate',
defaultClassNames.range_end defaultClassNames.range_end
), ),
today: cn( today: cn(
"bg-muted text-foreground rounded-(--cell-radius) data-[selected=true]:rounded-none", 'bg-muted text-foreground rounded-(--cell-radius) data-[selected=true]:rounded-none',
defaultClassNames.today defaultClassNames.today
), ),
outside: cn( outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground", 'text-muted-foreground aria-selected:text-muted-foreground',
defaultClassNames.outside defaultClassNames.outside
), ),
disabled: cn( disabled: cn(
"text-muted-foreground opacity-50", 'text-muted-foreground opacity-50',
defaultClassNames.disabled defaultClassNames.disabled
), ),
hidden: cn("invisible", defaultClassNames.hidden), hidden: cn('invisible', defaultClassNames.hidden),
...classNames, ...classNames,
}} }}
components={{ components={{
Root: ({ className, rootRef, ...props }) => { Root: ({ className, rootRef, ...props }) => {
return ( return (
<div <div
data-slot="calendar" data-slot='calendar'
ref={rootRef} ref={rootRef}
className={cn(className)} className={cn(className)}
{...props} {...props}
/> />
) );
}, },
Chevron: ({ className, orientation, ...props }) => { Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") { if (orientation === 'left') {
return ( return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} /> <ChevronLeftIcon className={cn('size-4', className)} {...props} />
) );
} }
if (orientation === "right") { if (orientation === 'right') {
return ( return (
<ChevronRightIcon className={cn("size-4", className)} {...props} /> <ChevronRightIcon
) className={cn('size-4', className)}
{...props}
/>
);
} }
return ( return (
<ChevronDownIcon className={cn("size-4", className)} {...props} /> <ChevronDownIcon className={cn('size-4', className)} {...props} />
) );
}, },
DayButton: ({ ...props }) => ( DayButton: ({ ...props }) => (
<CalendarDayButton locale={locale} {...props} /> <CalendarDayButton locale={locale} {...props} />
@@ -167,17 +174,17 @@ function Calendar({
WeekNumber: ({ children, ...props }) => { WeekNumber: ({ children, ...props }) => {
return ( return (
<td {...props}> <td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center"> <div className='flex size-(--cell-size) items-center justify-center text-center'>
{children} {children}
</div> </div>
</td> </td>
) );
}, },
...components, ...components,
}} }}
{...props} {...props}
/> />
) );
} }
function CalendarDayButton({ function CalendarDayButton({
@@ -187,18 +194,18 @@ function CalendarDayButton({
locale, locale,
...props ...props
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) { }: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
const defaultClassNames = getDefaultClassNames() const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null) const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => { React.useEffect(() => {
if (modifiers.focused) ref.current?.focus() if (modifiers.focused) ref.current?.focus();
}, [modifiers.focused]) }, [modifiers.focused]);
return ( return (
<Button <Button
ref={ref} ref={ref}
variant="ghost" variant='ghost'
size="icon" size='icon'
data-day={day.date.toLocaleDateString(locale?.code)} data-day={day.date.toLocaleDateString(locale?.code)}
data-selected-single={ data-selected-single={
modifiers.selected && modifiers.selected &&
@@ -210,13 +217,13 @@ function CalendarDayButton({
data-range-end={modifiers.range_end} data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle} data-range-middle={modifiers.range_middle}
className={cn( className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-foreground relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) [&>span]:text-xs [&>span]:opacity-70", 'data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-foreground relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) [&>span]:text-xs [&>span]:opacity-70',
defaultClassNames.day, defaultClassNames.day,
className className
)} )}
{...props} {...props}
/> />
) );
} }
export { Calendar, CalendarDayButton } export { Calendar, CalendarDayButton };
+59 -50
View File
@@ -1,86 +1,95 @@
import * as React from "react" import * as React from 'react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Card({ function Card({
className, className,
size = "default", size = 'default',
...props ...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { }: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
return ( return (
<div <div
data-slot="card" data-slot='card'
data-size={size} data-size={size}
className={cn("ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn( className={cn(
"gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]", 'ring-foreground/10 bg-card text-card-foreground group/card flex flex-col gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function CardTitle({ className, ...props }: React.ComponentProps<"div">) { function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-title" data-slot='card-header'
className={cn("text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn( className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end", 'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function CardContent({ className, ...props }: React.ComponentProps<"div">) { function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-content" data-slot='card-title'
className={cn("px-4 group-data-[size=sm]/card:px-3", className)} className={cn(
'text-base leading-snug font-medium group-data-[size=sm]/card:text-sm',
className
)}
{...props} {...props}
/> />
) );
} }
function CardFooter({ className, ...props }: React.ComponentProps<"div">) { function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-footer" data-slot='card-description'
className={cn("bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center", className)} className={cn('text-muted-foreground text-sm', className)}
{...props} {...props}
/> />
) );
}
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-action'
className={cn(
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-content'
className={cn('px-4 group-data-[size=sm]/card:px-3', className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-footer'
className={cn(
'bg-muted/50 flex items-center rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3',
className
)}
{...props}
/>
);
} }
export { export {
@@ -91,4 +100,4 @@ export {
CardAction, CardAction,
CardDescription, CardDescription,
CardContent, CardContent,
} };
+99 -99
View File
@@ -1,108 +1,108 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import useEmblaCarousel, { import useEmblaCarousel, {
type UseEmblaCarouselType, type UseEmblaCarouselType,
} from "embla-carousel-react" } from 'embla-carousel-react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button';
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react" import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
type CarouselApi = UseEmblaCarouselType[1] type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel> type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0] type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1] type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = { type CarouselProps = {
opts?: CarouselOptions opts?: CarouselOptions;
plugins?: CarouselPlugin plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical" orientation?: 'horizontal' | 'vertical';
setApi?: (api: CarouselApi) => void setApi?: (api: CarouselApi) => void;
} };
type CarouselContextProps = { type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0] carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1] api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void scrollPrev: () => void;
scrollNext: () => void scrollNext: () => void;
canScrollPrev: boolean canScrollPrev: boolean;
canScrollNext: boolean canScrollNext: boolean;
} & CarouselProps } & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null) const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() { function useCarousel() {
const context = React.useContext(CarouselContext) const context = React.useContext(CarouselContext);
if (!context) { if (!context) {
throw new Error("useCarousel must be used within a <Carousel />") throw new Error('useCarousel must be used within a <Carousel />');
} }
return context return context;
} }
function Carousel({ function Carousel({
orientation = "horizontal", orientation = 'horizontal',
opts, opts,
setApi, setApi,
plugins, plugins,
className, className,
children, children,
...props ...props
}: React.ComponentProps<"div"> & CarouselProps) { }: React.ComponentProps<'div'> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel( const [carouselRef, api] = useEmblaCarousel(
{ {
...opts, ...opts,
axis: orientation === "horizontal" ? "x" : "y", axis: orientation === 'horizontal' ? 'x' : 'y',
}, },
plugins plugins
) );
const [canScrollPrev, setCanScrollPrev] = React.useState(false) const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false) const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => { const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return if (!api) return;
setCanScrollPrev(api.canScrollPrev()) setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext()) setCanScrollNext(api.canScrollNext());
}, []) }, []);
const scrollPrev = React.useCallback(() => { const scrollPrev = React.useCallback(() => {
api?.scrollPrev() api?.scrollPrev();
}, [api]) }, [api]);
const scrollNext = React.useCallback(() => { const scrollNext = React.useCallback(() => {
api?.scrollNext() api?.scrollNext();
}, [api]) }, [api]);
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") { if (event.key === 'ArrowLeft') {
event.preventDefault() event.preventDefault();
scrollPrev() scrollPrev();
} else if (event.key === "ArrowRight") { } else if (event.key === 'ArrowRight') {
event.preventDefault() event.preventDefault();
scrollNext() scrollNext();
} }
}, },
[scrollPrev, scrollNext] [scrollPrev, scrollNext]
) );
React.useEffect(() => { React.useEffect(() => {
if (!api || !setApi) return if (!api || !setApi) return;
setApi(api) setApi(api);
}, [api, setApi]) }, [api, setApi]);
React.useEffect(() => { React.useEffect(() => {
if (!api) return if (!api) return;
onSelect(api) onSelect(api);
api.on("reInit", onSelect) api.on('reInit', onSelect);
api.on("select", onSelect) api.on('select', onSelect);
return () => { return () => {
api?.off("select", onSelect) api?.off('select', onSelect);
} };
}, [api, onSelect]) }, [api, onSelect]);
return ( return (
<CarouselContext.Provider <CarouselContext.Provider
@@ -111,7 +111,7 @@ function Carousel({
api: api, api: api,
opts, opts,
orientation: orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
scrollPrev, scrollPrev,
scrollNext, scrollNext,
canScrollPrev, canScrollPrev,
@@ -120,75 +120,75 @@ function Carousel({
> >
<div <div
onKeyDownCapture={handleKeyDown} onKeyDownCapture={handleKeyDown}
className={cn("relative", className)} className={cn('relative', className)}
role="region" role='region'
aria-roledescription="carousel" aria-roledescription='carousel'
data-slot="carousel" data-slot='carousel'
{...props} {...props}
> >
{children} {children}
</div> </div>
</CarouselContext.Provider> </CarouselContext.Provider>
) );
} }
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) { function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
const { carouselRef, orientation } = useCarousel() const { carouselRef, orientation } = useCarousel();
return ( return (
<div <div
ref={carouselRef} ref={carouselRef}
className="overflow-hidden" className='overflow-hidden'
data-slot="carousel-content" data-slot='carousel-content'
> >
<div <div
className={cn( className={cn(
"flex", 'flex',
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
className className
)} )}
{...props} {...props}
/> />
</div> </div>
) );
} }
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) { function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
const { orientation } = useCarousel() const { orientation } = useCarousel();
return ( return (
<div <div
role="group" role='group'
aria-roledescription="slide" aria-roledescription='slide'
data-slot="carousel-item" data-slot='carousel-item'
className={cn( className={cn(
"min-w-0 shrink-0 grow-0 basis-full", 'min-w-0 shrink-0 grow-0 basis-full',
orientation === "horizontal" ? "pl-4" : "pt-4", orientation === 'horizontal' ? 'pl-4' : 'pt-4',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function CarouselPrevious({ function CarouselPrevious({
className, className,
variant = "outline", variant = 'outline',
size = "icon-sm", size = 'icon-sm',
...props ...props
}: React.ComponentProps<typeof Button>) { }: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel() const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return ( return (
<Button <Button
data-slot="carousel-previous" data-slot='carousel-previous'
variant={variant} variant={variant}
size={size} size={size}
className={cn( className={cn(
"rounded-full absolute touch-manipulation", 'absolute touch-manipulation rounded-full',
orientation === "horizontal" orientation === 'horizontal'
? "top-1/2 -left-12 -translate-y-1/2" ? 'top-1/2 -left-12 -translate-y-1/2'
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", : '-top-12 left-1/2 -translate-x-1/2 rotate-90',
className className
)} )}
disabled={!canScrollPrev} disabled={!canScrollPrev}
@@ -196,29 +196,29 @@ function CarouselPrevious({
{...props} {...props}
> >
<ChevronLeftIcon /> <ChevronLeftIcon />
<span className="sr-only">Previous slide</span> <span className='sr-only'>Previous slide</span>
</Button> </Button>
) );
} }
function CarouselNext({ function CarouselNext({
className, className,
variant = "outline", variant = 'outline',
size = "icon-sm", size = 'icon-sm',
...props ...props
}: React.ComponentProps<typeof Button>) { }: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel() const { orientation, scrollNext, canScrollNext } = useCarousel();
return ( return (
<Button <Button
data-slot="carousel-next" data-slot='carousel-next'
variant={variant} variant={variant}
size={size} size={size}
className={cn( className={cn(
"rounded-full absolute touch-manipulation", 'absolute touch-manipulation rounded-full',
orientation === "horizontal" orientation === 'horizontal'
? "top-1/2 -right-12 -translate-y-1/2" ? 'top-1/2 -right-12 -translate-y-1/2'
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
className className
)} )}
disabled={!canScrollNext} disabled={!canScrollNext}
@@ -226,9 +226,9 @@ function CarouselNext({
{...props} {...props}
> >
<ChevronRightIcon /> <ChevronRightIcon />
<span className="sr-only">Next slide</span> <span className='sr-only'>Next slide</span>
</Button> </Button>
) );
} }
export { export {
@@ -239,4 +239,4 @@ export {
CarouselPrevious, CarouselPrevious,
CarouselNext, CarouselNext,
useCarousel, useCarousel,
} };
+115 -110
View File
@@ -1,37 +1,37 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as RechartsPrimitive from "recharts" import * as RechartsPrimitive from 'recharts';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
// Format: { THEME_NAME: CSS_SELECTOR } // Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const const THEMES = { light: '', dark: '.dark' } as const;
export type ChartConfig = { export type ChartConfig = {
[k in string]: { [k in string]: {
label?: React.ReactNode label?: React.ReactNode;
icon?: React.ComponentType icon?: React.ComponentType;
} & ( } & (
| { color?: string; theme?: never } | { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> } | { color?: never; theme: Record<keyof typeof THEMES, string> }
) );
} };
type ChartContextProps = { type ChartContextProps = {
config: ChartConfig config: ChartConfig;
} };
const ChartContext = React.createContext<ChartContextProps | null>(null) const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() { function useChart() {
const context = React.useContext(ChartContext) const context = React.useContext(ChartContext);
if (!context) { if (!context) {
throw new Error("useChart must be used within a <ChartContainer />") throw new Error('useChart must be used within a <ChartContainer />');
} }
return context return context;
} }
function ChartContainer({ function ChartContainer({
@@ -40,19 +40,19 @@ function ChartContainer({
children, children,
config, config,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<'div'> & {
config: ChartConfig config: ChartConfig;
children: React.ComponentProps< children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer typeof RechartsPrimitive.ResponsiveContainer
>["children"] >['children'];
}) { }) {
const uniqueId = React.useId() const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
return ( return (
<ChartContext.Provider value={{ config }}> <ChartContext.Provider value={{ config }}>
<div <div
data-slot="chart" data-slot='chart'
data-chart={chartId} data-chart={chartId}
className={cn( className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden", "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
@@ -66,16 +66,16 @@ function ChartContainer({
</RechartsPrimitive.ResponsiveContainer> </RechartsPrimitive.ResponsiveContainer>
</div> </div>
</ChartContext.Provider> </ChartContext.Provider>
) );
} }
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter( const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color ([, config]) => config.theme || config.color
) );
if (!colorConfig.length) { if (!colorConfig.length) {
return null return null;
} }
return ( return (
@@ -89,28 +89,28 @@ ${colorConfig
.map(([key, itemConfig]) => { .map(([key, itemConfig]) => {
const color = const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color itemConfig.color;
return color ? ` --color-${key}: ${color};` : null return color ? ` --color-${key}: ${color};` : null;
}) })
.join("\n")} .join('\n')}
} }
` `
) )
.join("\n"), .join('\n'),
}} }}
/> />
) );
} };
const ChartTooltip = RechartsPrimitive.Tooltip const ChartTooltip = RechartsPrimitive.Tooltip;
type ChartValueType = number | string | ReadonlyArray<number | string> type ChartValueType = number | string | ReadonlyArray<number | string>;
type ChartNameType = number | string type ChartNameType = number | string;
function ChartTooltipContent({ function ChartTooltipContent({
active, active,
payload, payload,
className, className,
indicator = "dot", indicator = 'dot',
hideLabel = false, hideLabel = false,
hideIndicator = false, hideIndicator = false,
label, label,
@@ -120,42 +120,44 @@ function ChartTooltipContent({
color, color,
nameKey, nameKey,
labelKey, labelKey,
}: Partial<RechartsPrimitive.TooltipContentProps<ChartValueType, ChartNameType>> & }: Partial<
React.ComponentProps<"div"> & { RechartsPrimitive.TooltipContentProps<ChartValueType, ChartNameType>
hideLabel?: boolean > &
hideIndicator?: boolean React.ComponentProps<'div'> & {
indicator?: "line" | "dot" | "dashed" hideLabel?: boolean;
nameKey?: string hideIndicator?: boolean;
labelKey?: string indicator?: 'line' | 'dot' | 'dashed';
nameKey?: string;
labelKey?: string;
}) { }) {
const { config } = useChart() const { config } = useChart();
const tooltipLabel = React.useMemo(() => { const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) { if (hideLabel || !payload?.length) {
return null return null;
} }
const [item] = payload const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}` const key = `${labelKey || item?.dataKey || item?.name || 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value = const value =
!labelKey && typeof label === "string" !labelKey && typeof label === 'string'
? config[label as keyof typeof config]?.label || label ? config[label as keyof typeof config]?.label || label
: itemConfig?.label : itemConfig?.label;
if (labelFormatter) { if (labelFormatter) {
return ( return (
<div className={cn("font-medium", labelClassName)}> <div className={cn('font-medium', labelClassName)}>
{labelFormatter(value, payload)} {labelFormatter(value, payload)}
</div> </div>
) );
} }
if (!value) { if (!value) {
return null return null;
} }
return <div className={cn("font-medium", labelClassName)}>{value}</div> return <div className={cn('font-medium', labelClassName)}>{value}</div>;
}, [ }, [
label, label,
labelFormatter, labelFormatter,
@@ -164,39 +166,42 @@ function ChartTooltipContent({
labelClassName, labelClassName,
config, config,
labelKey, labelKey,
]) ]);
if (!active || !payload?.length) { if (!active || !payload?.length) {
return null return null;
} }
const nestLabel = payload.length === 1 && indicator !== "dot" const nestLabel = payload.length === 1 && indicator !== 'dot';
return ( return (
<div <div
className={cn("border-border/50 bg-background gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl grid min-w-32 items-start", className)} className={cn(
'border-border/50 bg-background grid min-w-32 items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl',
className
)}
> >
{!nestLabel ? tooltipLabel : null} {!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5"> <div className='grid gap-1.5'>
{payload {payload
.filter((item) => item.type !== "none") .filter((item) => item.type !== 'none')
.map((item, index) => { .map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}` const key = `${nameKey || item.name || item.dataKey || 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color const indicatorColor = color || item.payload.fill || item.color;
const rawValue = item.value const rawValue = item.value;
const numericValue = const numericValue =
typeof rawValue === "number" ? rawValue : Number(rawValue) typeof rawValue === 'number' ? rawValue : Number(rawValue);
const displayValue = Number.isFinite(numericValue) const displayValue = Number.isFinite(numericValue)
? numericValue.toLocaleString() ? numericValue.toLocaleString()
: "-" : '-';
return ( return (
<div <div
key={item.dataKey} key={item.dataKey}
className={cn( className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5", '[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
indicator === "dot" && "items-center" indicator === 'dot' && 'items-center'
)} )}
> >
{formatter && item?.value !== undefined && item.name ? ( {formatter && item?.value !== undefined && item.name ? (
@@ -209,19 +214,19 @@ function ChartTooltipContent({
!hideIndicator && ( !hideIndicator && (
<div <div
className={cn( className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)", 'shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)',
{ {
"h-2.5 w-2.5": indicator === "dot", 'h-2.5 w-2.5': indicator === 'dot',
"w-1": indicator === "line", 'w-1': indicator === 'line',
"w-0 border-[1.5px] border-dashed bg-transparent": 'w-0 border-[1.5px] border-dashed bg-transparent':
indicator === "dashed", indicator === 'dashed',
"my-0.5": nestLabel && indicator === "dashed", 'my-0.5': nestLabel && indicator === 'dashed',
} }
)} )}
style={ style={
{ {
"--color-bg": indicatorColor, '--color-bg': indicatorColor,
"--color-border": indicatorColor, '--color-border': indicatorColor,
} as React.CSSProperties } as React.CSSProperties
} }
/> />
@@ -229,78 +234,78 @@ function ChartTooltipContent({
)} )}
<div <div
className={cn( className={cn(
"grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)_auto] gap-x-4 leading-none", 'grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)_auto] gap-x-4 leading-none',
nestLabel ? "items-end" : "items-center" nestLabel ? 'items-end' : 'items-center'
)} )}
> >
<div className="grid min-w-0 gap-1.5"> <div className='grid min-w-0 gap-1.5'>
{nestLabel ? tooltipLabel : null} {nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground truncate pr-1"> <span className='text-muted-foreground truncate pr-1'>
{itemConfig?.label || item.name} {itemConfig?.label || item.name}
</span> </span>
</div> </div>
<span className="text-foreground ml-1 text-right font-mono font-medium tabular-nums"> <span className='text-foreground ml-1 text-right font-mono font-medium tabular-nums'>
{displayValue} {displayValue}
</span> </span>
</div> </div>
</> </>
)} )}
</div> </div>
) );
})} })}
</div> </div>
</div> </div>
) );
} }
const ChartLegend = RechartsPrimitive.Legend const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({ function ChartLegendContent({
className, className,
hideIcon = false, hideIcon = false,
payload, payload,
verticalAlign = "bottom", verticalAlign = 'bottom',
nameKey, nameKey,
}: React.ComponentProps<"div"> & }: React.ComponentProps<'div'> &
Pick< Pick<
RechartsPrimitive.DefaultLegendContentProps, RechartsPrimitive.DefaultLegendContentProps,
"payload" | "verticalAlign" 'payload' | 'verticalAlign'
> & { > & {
hideIcon?: boolean hideIcon?: boolean;
nameKey?: string nameKey?: string;
}) { }) {
const { config } = useChart() const { config } = useChart();
if (!payload?.length) { if (!payload?.length) {
return null return null;
} }
return ( return (
<div <div
className={cn( className={cn(
"flex items-center justify-center gap-4", 'flex items-center justify-center gap-4',
verticalAlign === "top" ? "pb-3" : "pt-3", verticalAlign === 'top' ? 'pb-3' : 'pt-3',
className className
)} )}
> >
{payload {payload
.filter((item) => item.type !== "none") .filter((item) => item.type !== 'none')
.map((item) => { .map((item) => {
const key = `${nameKey || item.dataKey || "value"}` const key = `${nameKey || item.dataKey || 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key);
return ( return (
<div <div
key={item.value} key={item.value}
className={cn( className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3" '[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3'
)} )}
> >
{itemConfig?.icon && !hideIcon ? ( {itemConfig?.icon && !hideIcon ? (
<itemConfig.icon /> <itemConfig.icon />
) : ( ) : (
<div <div
className="h-2 w-2 shrink-0 rounded-[2px]" className='h-2 w-2 shrink-0 rounded-[2px]'
style={{ style={{
backgroundColor: item.color, backgroundColor: item.color,
}} }}
@@ -308,10 +313,10 @@ function ChartLegendContent({
)} )}
{itemConfig?.label} {itemConfig?.label}
</div> </div>
) );
})} })}
</div> </div>
) );
} }
function getPayloadConfigFromPayload( function getPayloadConfigFromPayload(
@@ -319,37 +324,37 @@ function getPayloadConfigFromPayload(
payload: unknown, payload: unknown,
key: string key: string
) { ) {
if (typeof payload !== "object" || payload === null) { if (typeof payload !== 'object' || payload === null) {
return undefined return undefined;
} }
const payloadPayload = const payloadPayload =
"payload" in payload && 'payload' in payload &&
typeof payload.payload === "object" && typeof payload.payload === 'object' &&
payload.payload !== null payload.payload !== null
? payload.payload ? payload.payload
: undefined : undefined;
let configLabelKey: string = key let configLabelKey: string = key;
if ( if (
key in payload && key in payload &&
typeof payload[key as keyof typeof payload] === "string" typeof payload[key as keyof typeof payload] === 'string'
) { ) {
configLabelKey = payload[key as keyof typeof payload] as string configLabelKey = payload[key as keyof typeof payload] as string;
} else if ( } else if (
payloadPayload && payloadPayload &&
key in payloadPayload && key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string" typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
) { ) {
configLabelKey = payloadPayload[ configLabelKey = payloadPayload[
key as keyof typeof payloadPayload key as keyof typeof payloadPayload
] as string ] as string;
} }
return configLabelKey in config return configLabelKey in config
? config[configLabelKey] ? config[configLabelKey]
: config[key as keyof typeof config] : config[key as keyof typeof config];
} }
export { export {
@@ -359,4 +364,4 @@ export {
ChartLegend, ChartLegend,
ChartLegendContent, ChartLegendContent,
ChartStyle, ChartStyle,
} };
+12 -13
View File
@@ -1,10 +1,10 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Checkbox as CheckboxPrimitive } from "radix-ui" import { Checkbox as CheckboxPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { CheckIcon } from "lucide-react" import { CheckIcon } from 'lucide-react';
function Checkbox({ function Checkbox({
className, className,
@@ -12,22 +12,21 @@ function Checkbox({
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) { }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return ( return (
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
data-slot="checkbox" data-slot='checkbox'
className={cn( className={cn(
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50", 'border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3',
className className
)} )}
{...props} {...props}
> >
<CheckboxPrimitive.Indicator <CheckboxPrimitive.Indicator
data-slot="checkbox-indicator" data-slot='checkbox-indicator'
className="[&>svg]:size-3.5 grid place-content-center text-current transition-none" className='grid place-content-center text-current transition-none [&>svg]:size-3.5'
> >
<CheckIcon <CheckIcon />
/>
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
) );
} }
export { Checkbox } export { Checkbox };
+8 -8
View File
@@ -1,11 +1,11 @@
"use client" 'use client';
import { Collapsible as CollapsiblePrimitive } from "radix-ui" import { Collapsible as CollapsiblePrimitive } from 'radix-ui';
function Collapsible({ function Collapsible({
...props ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) { }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} /> return <CollapsiblePrimitive.Root data-slot='collapsible' {...props} />;
} }
function CollapsibleTrigger({ function CollapsibleTrigger({
@@ -13,10 +13,10 @@ function CollapsibleTrigger({
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) { }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return ( return (
<CollapsiblePrimitive.CollapsibleTrigger <CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger" data-slot='collapsible-trigger'
{...props} {...props}
/> />
) );
} }
function CollapsibleContent({ function CollapsibleContent({
@@ -24,10 +24,10 @@ function CollapsibleContent({
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) { }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return ( return (
<CollapsiblePrimitive.CollapsibleContent <CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content" data-slot='collapsible-content'
{...props} {...props}
/> />
) );
} }
export { Collapsible, CollapsibleTrigger, CollapsibleContent } export { Collapsible, CollapsibleTrigger, CollapsibleContent };
+80 -72
View File
@@ -1,22 +1,22 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Combobox as ComboboxPrimitive } from "@base-ui/react" import { Combobox as ComboboxPrimitive } from '@base-ui/react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button';
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
InputGroupButton, InputGroupButton,
InputGroupInput, InputGroupInput,
} from "@/components/ui/input-group" } from '@/components/ui/input-group';
import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react" import { ChevronDownIcon, XIcon, CheckIcon } from 'lucide-react';
const Combobox = ComboboxPrimitive.Root const Combobox = ComboboxPrimitive.Root;
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} /> return <ComboboxPrimitive.Value data-slot='combobox-value' {...props} />;
} }
function ComboboxTrigger({ function ComboboxTrigger({
@@ -26,27 +26,27 @@ function ComboboxTrigger({
}: ComboboxPrimitive.Trigger.Props) { }: ComboboxPrimitive.Trigger.Props) {
return ( return (
<ComboboxPrimitive.Trigger <ComboboxPrimitive.Trigger
data-slot="combobox-trigger" data-slot='combobox-trigger'
className={cn("[&_svg:not([class*='size-'])]:size-4", className)} className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
{...props} {...props}
> >
{children} {children}
<ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" /> <ChevronDownIcon className='text-muted-foreground pointer-events-none size-4' />
</ComboboxPrimitive.Trigger> </ComboboxPrimitive.Trigger>
) );
} }
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
return ( return (
<ComboboxPrimitive.Clear <ComboboxPrimitive.Clear
data-slot="combobox-clear" data-slot='combobox-clear'
render={<InputGroupButton variant="ghost" size="icon-xs" />} render={<InputGroupButton variant='ghost' size='icon-xs' />}
className={cn(className)} className={cn(className)}
{...props} {...props}
> >
<XIcon className="pointer-events-none" /> <XIcon className='pointer-events-none' />
</ComboboxPrimitive.Clear> </ComboboxPrimitive.Clear>
) );
} }
function ComboboxInput({ function ComboboxInput({
@@ -57,23 +57,23 @@ function ComboboxInput({
showClear = false, showClear = false,
...props ...props
}: ComboboxPrimitive.Input.Props & { }: ComboboxPrimitive.Input.Props & {
showTrigger?: boolean showTrigger?: boolean;
showClear?: boolean showClear?: boolean;
}) { }) {
return ( return (
<InputGroup className={cn("w-auto", className)}> <InputGroup className={cn('w-auto', className)}>
<ComboboxPrimitive.Input <ComboboxPrimitive.Input
render={<InputGroupInput disabled={disabled} />} render={<InputGroupInput disabled={disabled} />}
{...props} {...props}
/> />
<InputGroupAddon align="inline-end"> <InputGroupAddon align='inline-end'>
{showTrigger && ( {showTrigger && (
<InputGroupButton <InputGroupButton
size="icon-xs" size='icon-xs'
variant="ghost" variant='ghost'
asChild asChild
data-slot="input-group-button" data-slot='input-group-button'
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent" className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent'
disabled={disabled} disabled={disabled}
> >
<ComboboxTrigger /> <ComboboxTrigger />
@@ -83,21 +83,21 @@ function ComboboxInput({
</InputGroupAddon> </InputGroupAddon>
{children} {children}
</InputGroup> </InputGroup>
) );
} }
function ComboboxContent({ function ComboboxContent({
className, className,
side = "bottom", side = 'bottom',
sideOffset = 6, sideOffset = 6,
align = "start", align = 'start',
alignOffset = 0, alignOffset = 0,
anchor, anchor,
...props ...props
}: ComboboxPrimitive.Popup.Props & }: ComboboxPrimitive.Popup.Props &
Pick< Pick<
ComboboxPrimitive.Positioner.Props, ComboboxPrimitive.Positioner.Props,
"side" | "align" | "sideOffset" | "alignOffset" | "anchor" 'side' | 'align' | 'sideOffset' | 'alignOffset' | 'anchor'
>) { >) {
return ( return (
<ComboboxPrimitive.Portal> <ComboboxPrimitive.Portal>
@@ -107,30 +107,33 @@ function ComboboxContent({
align={align} align={align}
alignOffset={alignOffset} alignOffset={alignOffset}
anchor={anchor} anchor={anchor}
className="isolate z-50" className='isolate z-50'
> >
<ComboboxPrimitive.Popup <ComboboxPrimitive.Popup
data-slot="combobox-content" data-slot='combobox-content'
data-chips={!!anchor} data-chips={!!anchor}
className={cn("bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:border-input/30 overflow-hidden rounded-lg shadow-md ring-1 duration-100 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) data-[chips=true]:min-w-(--anchor-width)", className )} className={cn(
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:border-input/30 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg shadow-md ring-1 duration-100 data-[chips=true]:min-w-(--anchor-width) *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none',
className
)}
{...props} {...props}
/> />
</ComboboxPrimitive.Positioner> </ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal> </ComboboxPrimitive.Portal>
) );
} }
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
return ( return (
<ComboboxPrimitive.List <ComboboxPrimitive.List
data-slot="combobox-list" data-slot='combobox-list'
className={cn( className={cn(
"no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 p-1 data-empty:p-0 overflow-y-auto overscroll-contain", 'no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function ComboboxItem({ function ComboboxItem({
@@ -140,31 +143,33 @@ function ComboboxItem({
}: ComboboxPrimitive.Item.Props) { }: ComboboxPrimitive.Item.Props) {
return ( return (
<ComboboxPrimitive.Item <ComboboxPrimitive.Item
data-slot="combobox-item" data-slot='combobox-item'
className={cn( className={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<ComboboxPrimitive.ItemIndicator <ComboboxPrimitive.ItemIndicator
render={<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />} render={
<span className='pointer-events-none absolute right-2 flex size-4 items-center justify-center' />
}
> >
<CheckIcon className="pointer-events-none" /> <CheckIcon className='pointer-events-none' />
</ComboboxPrimitive.ItemIndicator> </ComboboxPrimitive.ItemIndicator>
</ComboboxPrimitive.Item> </ComboboxPrimitive.Item>
) );
} }
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
return ( return (
<ComboboxPrimitive.Group <ComboboxPrimitive.Group
data-slot="combobox-group" data-slot='combobox-group'
className={cn(className)} className={cn(className)}
{...props} {...props}
/> />
) );
} }
function ComboboxLabel({ function ComboboxLabel({
@@ -173,27 +178,30 @@ function ComboboxLabel({
}: ComboboxPrimitive.GroupLabel.Props) { }: ComboboxPrimitive.GroupLabel.Props) {
return ( return (
<ComboboxPrimitive.GroupLabel <ComboboxPrimitive.GroupLabel
data-slot="combobox-label" data-slot='combobox-label'
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)} className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
{...props} {...props}
/> />
) );
} }
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
return ( return (
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} /> <ComboboxPrimitive.Collection data-slot='combobox-collection' {...props} />
) );
} }
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
return ( return (
<ComboboxPrimitive.Empty <ComboboxPrimitive.Empty
data-slot="combobox-empty" data-slot='combobox-empty'
className={cn("text-muted-foreground hidden w-full justify-center py-2 text-center text-sm group-data-empty/combobox-content:flex", className)} className={cn(
'text-muted-foreground hidden w-full justify-center py-2 text-center text-sm group-data-empty/combobox-content:flex',
className
)}
{...props} {...props}
/> />
) );
} }
function ComboboxSeparator({ function ComboboxSeparator({
@@ -202,11 +210,11 @@ function ComboboxSeparator({
}: ComboboxPrimitive.Separator.Props) { }: ComboboxPrimitive.Separator.Props) {
return ( return (
<ComboboxPrimitive.Separator <ComboboxPrimitive.Separator
data-slot="combobox-separator" data-slot='combobox-separator'
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props} {...props}
/> />
) );
} }
function ComboboxChips({ function ComboboxChips({
@@ -216,11 +224,14 @@ function ComboboxChips({
ComboboxPrimitive.Chips.Props) { ComboboxPrimitive.Chips.Props) {
return ( return (
<ComboboxPrimitive.Chips <ComboboxPrimitive.Chips
data-slot="combobox-chips" data-slot='combobox-chips'
className={cn("dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-8 flex-wrap items-center gap-1 rounded-lg border bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:ring-3 has-aria-invalid:ring-3 has-data-[slot=combobox-chip]:px-1", className)} className={cn(
'dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-8 flex-wrap items-center gap-1 rounded-lg border bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:ring-3 has-aria-invalid:ring-3 has-data-[slot=combobox-chip]:px-1',
className
)}
{...props} {...props}
/> />
) );
} }
function ComboboxChip({ function ComboboxChip({
@@ -229,13 +240,13 @@ function ComboboxChip({
showRemove = true, showRemove = true,
...props ...props
}: ComboboxPrimitive.Chip.Props & { }: ComboboxPrimitive.Chip.Props & {
showRemove?: boolean showRemove?: boolean;
}) { }) {
return ( return (
<ComboboxPrimitive.Chip <ComboboxPrimitive.Chip
data-slot="combobox-chip" data-slot='combobox-chip'
className={cn( className={cn(
"bg-muted text-foreground flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm px-1.5 text-xs font-medium whitespace-nowrap has-data-[slot=combobox-chip-remove]:pr-0 has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50", 'bg-muted text-foreground flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm px-1.5 text-xs font-medium whitespace-nowrap has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0',
className className
)} )}
{...props} {...props}
@@ -243,15 +254,15 @@ function ComboboxChip({
{children} {children}
{showRemove && ( {showRemove && (
<ComboboxPrimitive.ChipRemove <ComboboxPrimitive.ChipRemove
render={<Button variant="ghost" size="icon-xs" />} render={<Button variant='ghost' size='icon-xs' />}
className="-ml-1 opacity-50 hover:opacity-100" className='-ml-1 opacity-50 hover:opacity-100'
data-slot="combobox-chip-remove" data-slot='combobox-chip-remove'
> >
<XIcon className="pointer-events-none" /> <XIcon className='pointer-events-none' />
</ComboboxPrimitive.ChipRemove> </ComboboxPrimitive.ChipRemove>
)} )}
</ComboboxPrimitive.Chip> </ComboboxPrimitive.Chip>
) );
} }
function ComboboxChipsInput({ function ComboboxChipsInput({
@@ -260,18 +271,15 @@ function ComboboxChipsInput({
}: ComboboxPrimitive.Input.Props) { }: ComboboxPrimitive.Input.Props) {
return ( return (
<ComboboxPrimitive.Input <ComboboxPrimitive.Input
data-slot="combobox-chip-input" data-slot='combobox-chip-input'
className={cn( className={cn('min-w-16 flex-1 outline-none', className)}
"min-w-16 flex-1 outline-none",
className
)}
{...props} {...props}
/> />
) );
} }
function useComboboxAnchor() { function useComboboxAnchor() {
return React.useRef<HTMLDivElement | null>(null) return React.useRef<HTMLDivElement | null>(null);
} }
export { export {
@@ -291,4 +299,4 @@ export {
ComboboxTrigger, ComboboxTrigger,
ComboboxValue, ComboboxValue,
useComboboxAnchor, useComboboxAnchor,
} };
+22 -22
View File
@@ -1,9 +1,9 @@
"use client"; 'use client';
import * as React from "react"; import * as React from 'react';
import { Command as CommandPrimitive } from "cmdk"; import { Command as CommandPrimitive } from 'cmdk';
import { Search } from "lucide-react"; import { Search } from 'lucide-react';
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils';
function Command({ function Command({
className, className,
@@ -11,9 +11,9 @@ function Command({
}: React.ComponentProps<typeof CommandPrimitive>) { }: React.ComponentProps<typeof CommandPrimitive>) {
return ( return (
<CommandPrimitive <CommandPrimitive
data-slot="command" data-slot='command'
className={cn( className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md", 'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
className className
)} )}
{...props} {...props}
@@ -27,14 +27,14 @@ function CommandInput({
}: React.ComponentProps<typeof CommandPrimitive.Input>) { }: React.ComponentProps<typeof CommandPrimitive.Input>) {
return ( return (
<div <div
data-slot="command-input-wrapper" data-slot='command-input-wrapper'
className="flex items-center border-b border-border px-2.5" className='border-border flex items-center border-b px-2.5'
> >
<Search className="h-4 w-4 shrink-0 text-muted-foreground" /> <Search className='text-muted-foreground h-4 w-4 shrink-0' />
<CommandPrimitive.Input <CommandPrimitive.Input
data-slot="command-input" data-slot='command-input'
className={cn( className={cn(
"flex h-9 w-full rounded-md bg-transparent py-2 pl-2 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", 'placeholder:text-muted-foreground flex h-9 w-full rounded-md bg-transparent py-2 pl-2 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50',
className className
)} )}
{...props} {...props}
@@ -49,8 +49,8 @@ function CommandList({
}: React.ComponentProps<typeof CommandPrimitive.List>) { }: React.ComponentProps<typeof CommandPrimitive.List>) {
return ( return (
<CommandPrimitive.List <CommandPrimitive.List
data-slot="command-list" data-slot='command-list'
className={cn("max-h-72 overflow-y-auto overflow-x-hidden", className)} className={cn('max-h-72 overflow-x-hidden overflow-y-auto', className)}
{...props} {...props}
/> />
); );
@@ -61,8 +61,8 @@ function CommandEmpty({
}: React.ComponentProps<typeof CommandPrimitive.Empty>) { }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return ( return (
<CommandPrimitive.Empty <CommandPrimitive.Empty
data-slot="command-empty" data-slot='command-empty'
className="py-6 text-center text-sm text-muted-foreground" className='text-muted-foreground py-6 text-center text-sm'
{...props} {...props}
/> />
); );
@@ -74,9 +74,9 @@ function CommandGroup({
}: React.ComponentProps<typeof CommandPrimitive.Group>) { }: React.ComponentProps<typeof CommandPrimitive.Group>) {
return ( return (
<CommandPrimitive.Group <CommandPrimitive.Group
data-slot="command-group" data-slot='command-group'
className={cn( className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1 [&_[cmdk-group-heading]]:text-xs", 'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1 [&_[cmdk-group-heading]]:text-xs',
className className
)} )}
{...props} {...props}
@@ -90,9 +90,9 @@ function CommandItem({
}: React.ComponentProps<typeof CommandPrimitive.Item>) { }: React.ComponentProps<typeof CommandPrimitive.Item>) {
return ( return (
<CommandPrimitive.Item <CommandPrimitive.Item
data-slot="command-item" data-slot='command-item'
className={cn( className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none select-none", 'data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none select-none',
className className
)} )}
{...props} {...props}
@@ -106,8 +106,8 @@ function CommandSeparator({
}: React.ComponentProps<typeof CommandPrimitive.Separator>) { }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return ( return (
<CommandPrimitive.Separator <CommandPrimitive.Separator
data-slot="command-separator" data-slot='command-separator'
className={cn("bg-border -mx-1 h-px", className)} className={cn('bg-border -mx-1 h-px', className)}
{...props} {...props}
/> />
); );
+70 -60
View File
@@ -1,15 +1,15 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { ContextMenu as ContextMenuPrimitive } from "radix-ui" import { ContextMenu as ContextMenuPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { ChevronRightIcon, CheckIcon } from "lucide-react" import { ChevronRightIcon, CheckIcon } from 'lucide-react';
function ContextMenu({ function ContextMenu({
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} /> return <ContextMenuPrimitive.Root data-slot='context-menu' {...props} />;
} }
function ContextMenuTrigger({ function ContextMenuTrigger({
@@ -18,33 +18,33 @@ function ContextMenuTrigger({
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return ( return (
<ContextMenuPrimitive.Trigger <ContextMenuPrimitive.Trigger
data-slot="context-menu-trigger" data-slot='context-menu-trigger'
className={cn("select-none", className)} className={cn('select-none', className)}
{...props} {...props}
/> />
) );
} }
function ContextMenuGroup({ function ContextMenuGroup({
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return ( return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} /> <ContextMenuPrimitive.Group data-slot='context-menu-group' {...props} />
) );
} }
function ContextMenuPortal({ function ContextMenuPortal({
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return ( return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} /> <ContextMenuPrimitive.Portal data-slot='context-menu-portal' {...props} />
) );
} }
function ContextMenuSub({ function ContextMenuSub({
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} /> return <ContextMenuPrimitive.Sub data-slot='context-menu-sub' {...props} />;
} }
function ContextMenuRadioGroup({ function ContextMenuRadioGroup({
@@ -52,50 +52,53 @@ function ContextMenuRadioGroup({
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) { }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return ( return (
<ContextMenuPrimitive.RadioGroup <ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group" data-slot='context-menu-radio-group'
{...props} {...props}
/> />
) );
} }
function ContextMenuContent({ function ContextMenuContent({
className, className,
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content> & { }: React.ComponentProps<typeof ContextMenuPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left" side?: 'top' | 'right' | 'bottom' | 'left';
}) { }) {
return ( return (
<ContextMenuPrimitive.Portal> <ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content <ContextMenuPrimitive.Content
data-slot="context-menu-content" data-slot='context-menu-content'
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-36 rounded-lg p-1 shadow-md ring-1 duration-100 z-50 max-h-(--radix-context-menu-content-available-height) origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto", className )} className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground z-50 max-h-(--radix-context-menu-content-available-height) min-w-36 origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100',
className
)}
{...props} {...props}
/> />
</ContextMenuPrimitive.Portal> </ContextMenuPrimitive.Portal>
) );
} }
function ContextMenuItem({ function ContextMenuItem({
className, className,
inset, inset,
variant = "default", variant = 'default',
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & { }: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
variant?: "default" | "destructive" variant?: 'default' | 'destructive';
}) { }) {
return ( return (
<ContextMenuPrimitive.Item <ContextMenuPrimitive.Item
data-slot="context-menu-item" data-slot='context-menu-item'
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/context-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive focus:*:[svg]:text-accent-foreground group/context-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
/> />
) );
} }
function ContextMenuSubTrigger({ function ContextMenuSubTrigger({
@@ -104,22 +107,22 @@ function ContextMenuSubTrigger({
children, children,
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & { }: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<ContextMenuPrimitive.SubTrigger <ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger" data-slot='context-menu-sub-trigger'
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0", "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto" /> <ChevronRightIcon className='ml-auto' />
</ContextMenuPrimitive.SubTrigger> </ContextMenuPrimitive.SubTrigger>
) );
} }
function ContextMenuSubContent({ function ContextMenuSubContent({
@@ -128,11 +131,14 @@ function ContextMenuSubContent({
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) { }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return ( return (
<ContextMenuPrimitive.SubContent <ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content" data-slot='context-menu-sub-content'
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground min-w-32 rounded-lg border p-1 shadow-lg duration-100 z-50 origin-(--radix-context-menu-content-transform-origin) overflow-hidden", className )} className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground z-50 min-w-32 origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-lg border p-1 shadow-lg duration-100',
className
)}
{...props} {...props}
/> />
) );
} }
function ContextMenuCheckboxItem({ function ContextMenuCheckboxItem({
@@ -142,28 +148,27 @@ function ContextMenuCheckboxItem({
inset, inset,
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem> & { }: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<ContextMenuPrimitive.CheckboxItem <ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item" data-slot='context-menu-checkbox-item'
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="absolute right-2 pointer-events-none"> <span className='pointer-events-none absolute right-2'>
<ContextMenuPrimitive.ItemIndicator> <ContextMenuPrimitive.ItemIndicator>
<CheckIcon <CheckIcon />
/>
</ContextMenuPrimitive.ItemIndicator> </ContextMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</ContextMenuPrimitive.CheckboxItem> </ContextMenuPrimitive.CheckboxItem>
) );
} }
function ContextMenuRadioItem({ function ContextMenuRadioItem({
@@ -172,27 +177,26 @@ function ContextMenuRadioItem({
inset, inset,
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem> & { }: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<ContextMenuPrimitive.RadioItem <ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item" data-slot='context-menu-radio-item'
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
> >
<span className="absolute right-2 pointer-events-none"> <span className='pointer-events-none absolute right-2'>
<ContextMenuPrimitive.ItemIndicator> <ContextMenuPrimitive.ItemIndicator>
<CheckIcon <CheckIcon />
/>
</ContextMenuPrimitive.ItemIndicator> </ContextMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</ContextMenuPrimitive.RadioItem> </ContextMenuPrimitive.RadioItem>
) );
} }
function ContextMenuLabel({ function ContextMenuLabel({
@@ -200,16 +204,19 @@ function ContextMenuLabel({
inset, inset,
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & { }: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<ContextMenuPrimitive.Label <ContextMenuPrimitive.Label
data-slot="context-menu-label" data-slot='context-menu-label'
data-inset={inset} data-inset={inset}
className={cn("text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7", className)} className={cn(
'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7',
className
)}
{...props} {...props}
/> />
) );
} }
function ContextMenuSeparator({ function ContextMenuSeparator({
@@ -218,24 +225,27 @@ function ContextMenuSeparator({
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return ( return (
<ContextMenuPrimitive.Separator <ContextMenuPrimitive.Separator
data-slot="context-menu-separator" data-slot='context-menu-separator'
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props} {...props}
/> />
) );
} }
function ContextMenuShortcut({ function ContextMenuShortcut({
className, className,
...props ...props
}: React.ComponentProps<"span">) { }: React.ComponentProps<'span'>) {
return ( return (
<span <span
data-slot="context-menu-shortcut" data-slot='context-menu-shortcut'
className={cn("text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)} className={cn(
'text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
className
)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -254,4 +264,4 @@ export {
ContextMenuSubContent, ContextMenuSubContent,
ContextMenuSubTrigger, ContextMenuSubTrigger,
ContextMenuRadioGroup, ContextMenuRadioGroup,
} };
+54 -48
View File
@@ -1,13 +1,13 @@
"use client"; 'use client';
import * as React from "react"; import * as React from 'react';
import * as DialogPrimitive from "@radix-ui/react-dialog"; import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Drawer as DrawerPrimitive } from "vaul"; import { Drawer as DrawerPrimitive } from 'vaul';
import { X } from "lucide-react"; import { X } from 'lucide-react';
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils';
import { useMediaQuery } from "@/hooks/useMediaQuery"; import { useMediaQuery } from '@/hooks/useMediaQuery';
const MOBILE_DIALOG_QUERY = "(max-width: 767px)"; const MOBILE_DIALOG_QUERY = '(max-width: 767px)';
const DialogMobileContext = React.createContext(false); const DialogMobileContext = React.createContext(false);
function useDialogMobileMode(): boolean { function useDialogMobileMode(): boolean {
@@ -24,13 +24,13 @@ function Dialog({
<DialogMobileContext.Provider value={isMobile}> <DialogMobileContext.Provider value={isMobile}>
{isMobile ? ( {isMobile ? (
<DrawerPrimitive.Root <DrawerPrimitive.Root
data-slot="dialog" data-slot='dialog'
{...(rootProps as React.ComponentProps<typeof DrawerPrimitive.Root>)} {...(rootProps as React.ComponentProps<typeof DrawerPrimitive.Root>)}
> >
{children} {children}
</DrawerPrimitive.Root> </DrawerPrimitive.Root>
) : ( ) : (
<DialogPrimitive.Root data-slot="dialog" {...rootProps}> <DialogPrimitive.Root data-slot='dialog' {...rootProps}>
{children} {children}
</DialogPrimitive.Root> </DialogPrimitive.Root>
)} )}
@@ -45,12 +45,12 @@ function DialogTrigger({
if (isMobile) { if (isMobile) {
return ( return (
<DrawerPrimitive.Trigger <DrawerPrimitive.Trigger
data-slot="dialog-trigger" data-slot='dialog-trigger'
{...(props as React.ComponentProps<typeof DrawerPrimitive.Trigger>)} {...(props as React.ComponentProps<typeof DrawerPrimitive.Trigger>)}
/> />
); );
} }
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; return <DialogPrimitive.Trigger data-slot='dialog-trigger' {...props} />;
} }
function DialogPortal({ function DialogPortal({
@@ -60,12 +60,12 @@ function DialogPortal({
if (isMobile) { if (isMobile) {
return ( return (
<DrawerPrimitive.Portal <DrawerPrimitive.Portal
data-slot="dialog-portal" data-slot='dialog-portal'
{...(props as React.ComponentProps<typeof DrawerPrimitive.Portal>)} {...(props as React.ComponentProps<typeof DrawerPrimitive.Portal>)}
/> />
); );
} }
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; return <DialogPrimitive.Portal data-slot='dialog-portal' {...props} />;
} }
function DialogClose({ function DialogClose({
@@ -75,12 +75,12 @@ function DialogClose({
if (isMobile) { if (isMobile) {
return ( return (
<DrawerPrimitive.Close <DrawerPrimitive.Close
data-slot="dialog-close" data-slot='dialog-close'
{...(props as React.ComponentProps<typeof DrawerPrimitive.Close>)} {...(props as React.ComponentProps<typeof DrawerPrimitive.Close>)}
/> />
); );
} }
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; return <DialogPrimitive.Close data-slot='dialog-close' {...props} />;
} }
function DialogOverlay({ function DialogOverlay({
@@ -91,9 +91,9 @@ function DialogOverlay({
if (isMobile) { if (isMobile) {
return ( return (
<DrawerPrimitive.Overlay <DrawerPrimitive.Overlay
data-slot="dialog-overlay" data-slot='dialog-overlay'
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/70 data-closed:animate-out data-open:animate-in data-closed:fade-out-0 data-open:fade-in-0", 'data-closed:animate-out data-open:animate-in data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/70',
className className
)} )}
{...(props as React.ComponentProps<typeof DrawerPrimitive.Overlay>)} {...(props as React.ComponentProps<typeof DrawerPrimitive.Overlay>)}
@@ -103,9 +103,9 @@ function DialogOverlay({
return ( return (
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
data-slot="dialog-overlay" data-slot='dialog-overlay'
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/70 data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", 'data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/70',
className className
)} )}
{...props} {...props}
@@ -125,27 +125,27 @@ function DialogContent({
if (isMobile) { if (isMobile) {
return ( return (
<DialogPortal data-slot="dialog-portal"> <DialogPortal data-slot='dialog-portal'>
<DialogOverlay /> <DialogOverlay />
<DrawerPrimitive.Content <DrawerPrimitive.Content
data-slot="dialog-content" data-slot='dialog-content'
className={cn( className={cn(
"fixed inset-x-0 bottom-0 z-50 grid max-h-[90vh] w-full gap-4 rounded-t-xl border-t border-border bg-card p-4 pb-[calc(1rem+env(safe-area-inset-bottom))] shadow-lg data-closed:animate-out data-open:animate-in data-closed:slide-out-to-bottom data-open:slide-in-from-bottom", 'border-border bg-card data-closed:animate-out data-open:animate-in data-closed:slide-out-to-bottom data-open:slide-in-from-bottom fixed inset-x-0 bottom-0 z-50 grid max-h-[90vh] w-full gap-4 rounded-t-xl border-t p-4 pb-[calc(1rem+env(safe-area-inset-bottom))] shadow-lg',
className className
)} )}
{...(props as React.ComponentProps<typeof DrawerPrimitive.Content>)} {...(props as React.ComponentProps<typeof DrawerPrimitive.Content>)}
> >
<div <div
className="bg-muted mx-auto -mt-1 mb-1 h-1 w-12 rounded-full" className='bg-muted mx-auto -mt-1 mb-1 h-1 w-12 rounded-full'
aria-hidden="true" aria-hidden='true'
/> />
{children} {children}
{showCloseButton ? ( {showCloseButton ? (
<DrawerPrimitive.Close <DrawerPrimitive.Close
className="absolute top-3 right-3 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-1 focus:ring-ring disabled:pointer-events-none" className='focus:ring-ring absolute top-3 right-3 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-1 focus:outline-hidden disabled:pointer-events-none'
aria-label="Close" aria-label='Close'
> >
<X className="h-4 w-4" /> <X className='h-4 w-4' />
</DrawerPrimitive.Close> </DrawerPrimitive.Close>
) : null} ) : null}
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
@@ -154,12 +154,12 @@ function DialogContent({
} }
return ( return (
<DialogPortal data-slot="dialog-portal"> <DialogPortal data-slot='dialog-portal'>
<DialogOverlay /> <DialogOverlay />
<DialogPrimitive.Content <DialogPrimitive.Content
data-slot="dialog-content" data-slot='dialog-content'
className={cn( className={cn(
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-card p-6 shadow-lg duration-200 rounded-lg data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", 'border-border bg-card data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200',
className className
)} )}
{...props} {...props}
@@ -167,10 +167,10 @@ function DialogContent({
{children} {children}
{showCloseButton ? ( {showCloseButton ? (
<DialogPrimitive.Close <DialogPrimitive.Close
className="absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-1 focus:ring-ring disabled:pointer-events-none" className='focus:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-1 focus:outline-hidden disabled:pointer-events-none'
aria-label="Close" aria-label='Close'
> >
<X className="h-4 w-4" /> <X className='h-4 w-4' />
</DialogPrimitive.Close> </DialogPrimitive.Close>
) : null} ) : null}
</DialogPrimitive.Content> </DialogPrimitive.Content>
@@ -178,21 +178,27 @@ function DialogContent({
); );
} }
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="dialog-header" data-slot='dialog-header'
className={cn("flex flex-col gap-1.5 text-center sm:text-left", className)} className={cn(
'flex flex-col gap-1.5 text-center sm:text-left',
className
)}
{...props} {...props}
/> />
); );
} }
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="dialog-footer" data-slot='dialog-footer'
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)} className={cn(
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
className
)}
{...props} {...props}
/> />
); );
@@ -206,8 +212,8 @@ function DialogTitle({
if (isMobile) { if (isMobile) {
return ( return (
<DrawerPrimitive.Title <DrawerPrimitive.Title
data-slot="dialog-title" data-slot='dialog-title'
className={cn("text-lg leading-none font-semibold", className)} className={cn('text-lg leading-none font-semibold', className)}
{...(props as React.ComponentProps<typeof DrawerPrimitive.Title>)} {...(props as React.ComponentProps<typeof DrawerPrimitive.Title>)}
/> />
); );
@@ -215,8 +221,8 @@ function DialogTitle({
return ( return (
<DialogPrimitive.Title <DialogPrimitive.Title
data-slot="dialog-title" data-slot='dialog-title'
className={cn("text-lg leading-none font-semibold", className)} className={cn('text-lg leading-none font-semibold', className)}
{...props} {...props}
/> />
); );
@@ -230,8 +236,8 @@ function DialogDescription({
if (isMobile) { if (isMobile) {
return ( return (
<DrawerPrimitive.Description <DrawerPrimitive.Description
data-slot="dialog-description" data-slot='dialog-description'
className={cn("text-sm text-muted-foreground", className)} className={cn('text-muted-foreground text-sm', className)}
{...(props as React.ComponentProps<typeof DrawerPrimitive.Description>)} {...(props as React.ComponentProps<typeof DrawerPrimitive.Description>)}
/> />
); );
@@ -239,8 +245,8 @@ function DialogDescription({
return ( return (
<DialogPrimitive.Description <DialogPrimitive.Description
data-slot="dialog-description" data-slot='dialog-description'
className={cn("text-sm text-muted-foreground", className)} className={cn('text-muted-foreground text-sm', className)}
{...props} {...props}
/> />
); );
+7 -7
View File
@@ -1,22 +1,22 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Direction } from "radix-ui" import { Direction } from 'radix-ui';
function DirectionProvider({ function DirectionProvider({
dir, dir,
direction, direction,
children, children,
}: React.ComponentProps<typeof Direction.DirectionProvider> & { }: React.ComponentProps<typeof Direction.DirectionProvider> & {
direction?: React.ComponentProps<typeof Direction.DirectionProvider>["dir"] direction?: React.ComponentProps<typeof Direction.DirectionProvider>['dir'];
}) { }) {
return ( return (
<Direction.DirectionProvider dir={direction ?? dir}> <Direction.DirectionProvider dir={direction ?? dir}>
{children} {children}
</Direction.DirectionProvider> </Direction.DirectionProvider>
) );
} }
const useDirection = Direction.useDirection const useDirection = Direction.useDirection;
export { DirectionProvider, useDirection } export { DirectionProvider, useDirection };
+37 -31
View File
@@ -1,32 +1,32 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Drawer as DrawerPrimitive } from "vaul" import { Drawer as DrawerPrimitive } from 'vaul';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Drawer({ function Drawer({
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) { }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} /> return <DrawerPrimitive.Root data-slot='drawer' {...props} />;
} }
function DrawerTrigger({ function DrawerTrigger({
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) { }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} /> return <DrawerPrimitive.Trigger data-slot='drawer-trigger' {...props} />;
} }
function DrawerPortal({ function DrawerPortal({
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) { }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} /> return <DrawerPrimitive.Portal data-slot='drawer-portal' {...props} />;
} }
function DrawerClose({ function DrawerClose({
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) { }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} /> return <DrawerPrimitive.Close data-slot='drawer-close' {...props} />;
} }
function DrawerOverlay({ function DrawerOverlay({
@@ -35,11 +35,14 @@ function DrawerOverlay({
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) { }: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return ( return (
<DrawerPrimitive.Overlay <DrawerPrimitive.Overlay
data-slot="drawer-overlay" data-slot='drawer-overlay'
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)} className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/10 supports-backdrop-filter:backdrop-blur-xs',
className
)}
{...props} {...props}
/> />
) );
} }
function DrawerContent({ function DrawerContent({
@@ -48,41 +51,44 @@ function DrawerContent({
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) { }: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return ( return (
<DrawerPortal data-slot="drawer-portal"> <DrawerPortal data-slot='drawer-portal'>
<DrawerOverlay /> <DrawerOverlay />
<DrawerPrimitive.Content <DrawerPrimitive.Content
data-slot="drawer-content" data-slot='drawer-content'
className={cn( className={cn(
"bg-background flex h-auto flex-col text-sm data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-xl data-[vaul-drawer-direction=bottom]:border-t data-[vaul-drawer-direction=bottom]:pb-[calc(0.5rem+env(safe-area-inset-bottom))] data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:rounded-r-xl data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:rounded-l-xl data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-xl data-[vaul-drawer-direction=top]:border-b data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm group/drawer-content fixed z-50", 'bg-background group/drawer-content fixed z-50 flex h-auto flex-col text-sm data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-xl data-[vaul-drawer-direction=bottom]:border-t data-[vaul-drawer-direction=bottom]:pb-[calc(0.5rem+env(safe-area-inset-bottom))] data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:rounded-r-xl data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:rounded-l-xl data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-xl data-[vaul-drawer-direction=top]:border-b data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm',
className className
)} )}
{...props} {...props}
> >
<div className="bg-muted mt-4 h-1 w-[100px] rounded-full mx-auto hidden shrink-0 group-data-[vaul-drawer-direction=bottom]/drawer-content:block" /> <div className='bg-muted mx-auto mt-4 hidden h-1 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block' />
{children} {children}
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
</DrawerPortal> </DrawerPortal>
) );
} }
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="drawer-header" data-slot='drawer-header'
className={cn("gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-0.5 md:text-left flex flex-col", className)} className={cn(
'flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-0.5 md:text-left',
className
)}
{...props} {...props}
/> />
) );
} }
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="drawer-footer" data-slot='drawer-footer'
className={cn("gap-2 p-4 mt-auto flex flex-col", className)} className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props} {...props}
/> />
) );
} }
function DrawerTitle({ function DrawerTitle({
@@ -91,11 +97,11 @@ function DrawerTitle({
}: React.ComponentProps<typeof DrawerPrimitive.Title>) { }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return ( return (
<DrawerPrimitive.Title <DrawerPrimitive.Title
data-slot="drawer-title" data-slot='drawer-title'
className={cn("text-foreground text-base font-medium", className)} className={cn('text-foreground text-base font-medium', className)}
{...props} {...props}
/> />
) );
} }
function DrawerDescription({ function DrawerDescription({
@@ -104,11 +110,11 @@ function DrawerDescription({
}: React.ComponentProps<typeof DrawerPrimitive.Description>) { }: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return ( return (
<DrawerPrimitive.Description <DrawerPrimitive.Description
data-slot="drawer-description" data-slot='drawer-description'
className={cn("text-muted-foreground text-sm", className)} className={cn('text-muted-foreground text-sm', className)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -122,4 +128,4 @@ export {
DrawerFooter, DrawerFooter,
DrawerTitle, DrawerTitle,
DrawerDescription, DrawerDescription,
} };
+71 -61
View File
@@ -1,23 +1,23 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui" import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { CheckIcon, ChevronRightIcon } from "lucide-react" import { CheckIcon, ChevronRightIcon } from 'lucide-react';
function DropdownMenu({ function DropdownMenu({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> return <DropdownMenuPrimitive.Root data-slot='dropdown-menu' {...props} />;
} }
function DropdownMenuPortal({ function DropdownMenuPortal({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return ( return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> <DropdownMenuPrimitive.Portal data-slot='dropdown-menu-portal' {...props} />
) );
} }
function DropdownMenuTrigger({ function DropdownMenuTrigger({
@@ -25,60 +25,63 @@ function DropdownMenuTrigger({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return ( return (
<DropdownMenuPrimitive.Trigger <DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger" data-slot='dropdown-menu-trigger'
{...props} {...props}
/> />
) );
} }
function DropdownMenuContent({ function DropdownMenuContent({
className, className,
align = "start", align = 'start',
sideOffset = 4, sideOffset = 4,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return ( return (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content" data-slot='dropdown-menu-content'
sideOffset={sideOffset} sideOffset={sideOffset}
align={align} align={align}
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto data-[state=closed]:overflow-hidden", className )} className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 data-[state=closed]:overflow-hidden',
className
)}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
) );
} }
function DropdownMenuGroup({ function DropdownMenuGroup({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return ( return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> <DropdownMenuPrimitive.Group data-slot='dropdown-menu-group' {...props} />
) );
} }
function DropdownMenuItem({ function DropdownMenuItem({
className, className,
inset, inset,
variant = "default", variant = 'default',
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
variant?: "default" | "destructive" variant?: 'default' | 'destructive';
}) { }) {
return ( return (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item" data-slot='dropdown-menu-item'
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
/> />
) );
} }
function DropdownMenuCheckboxItem({ function DropdownMenuCheckboxItem({
@@ -88,31 +91,30 @@ function DropdownMenuCheckboxItem({
inset, inset,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item" data-slot='dropdown-menu-checkbox-item'
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span <span
className="absolute right-2 flex items-center justify-center pointer-events-none" className='pointer-events-none absolute right-2 flex items-center justify-center'
data-slot="dropdown-menu-checkbox-item-indicator" data-slot='dropdown-menu-checkbox-item-indicator'
> >
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<CheckIcon <CheckIcon />
/>
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
) );
} }
function DropdownMenuRadioGroup({ function DropdownMenuRadioGroup({
@@ -120,10 +122,10 @@ function DropdownMenuRadioGroup({
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return ( return (
<DropdownMenuPrimitive.RadioGroup <DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group" data-slot='dropdown-menu-radio-group'
{...props} {...props}
/> />
) );
} }
function DropdownMenuRadioItem({ function DropdownMenuRadioItem({
@@ -132,30 +134,29 @@ function DropdownMenuRadioItem({
inset, inset,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item" data-slot='dropdown-menu-radio-item'
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
> >
<span <span
className="absolute right-2 flex items-center justify-center pointer-events-none" className='pointer-events-none absolute right-2 flex items-center justify-center'
data-slot="dropdown-menu-radio-item-indicator" data-slot='dropdown-menu-radio-item-indicator'
> >
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<CheckIcon <CheckIcon />
/>
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
) );
} }
function DropdownMenuLabel({ function DropdownMenuLabel({
@@ -163,16 +164,19 @@ function DropdownMenuLabel({
inset, inset,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label" data-slot='dropdown-menu-label'
data-inset={inset} data-inset={inset}
className={cn("text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7", className)} className={cn(
'text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7',
className
)}
{...props} {...props}
/> />
) );
} }
function DropdownMenuSeparator({ function DropdownMenuSeparator({
@@ -181,30 +185,33 @@ function DropdownMenuSeparator({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return ( return (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator" data-slot='dropdown-menu-separator'
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props} {...props}
/> />
) );
} }
function DropdownMenuShortcut({ function DropdownMenuShortcut({
className, className,
...props ...props
}: React.ComponentProps<"span">) { }: React.ComponentProps<'span'>) {
return ( return (
<span <span
data-slot="dropdown-menu-shortcut" data-slot='dropdown-menu-shortcut'
className={cn("text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)} className={cn(
'text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest',
className
)}
{...props} {...props}
/> />
) );
} }
function DropdownMenuSub({ function DropdownMenuSub({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} /> return <DropdownMenuPrimitive.Sub data-slot='dropdown-menu-sub' {...props} />;
} }
function DropdownMenuSubTrigger({ function DropdownMenuSubTrigger({
@@ -213,22 +220,22 @@ function DropdownMenuSubTrigger({
children, children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger" data-slot='dropdown-menu-sub-trigger'
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0", "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto" /> <ChevronRightIcon className='ml-auto' />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
) );
} }
function DropdownMenuSubContent({ function DropdownMenuSubContent({
@@ -237,11 +244,14 @@ function DropdownMenuSubContent({
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return ( return (
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content" data-slot='dropdown-menu-sub-content'
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-lg p-1 shadow-lg ring-1 duration-100 z-50 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden", className )} className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg p-1 shadow-lg ring-1 duration-100',
className
)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -260,4 +270,4 @@ export {
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuSubContent, DropdownMenuSubContent,
} };
+31 -34
View File
@@ -1,97 +1,94 @@
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Empty({ className, ...props }: React.ComponentProps<"div">) { function Empty({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="empty" data-slot='empty'
className={cn( className={cn(
"gap-4 rounded-xl border-dashed p-6 flex w-full min-w-0 flex-1 flex-col items-center justify-center text-center text-balance", 'flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { function EmptyHeader({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="empty-header" data-slot='empty-header'
className={cn( className={cn('flex max-w-sm flex-col items-center gap-2', className)}
"gap-2 flex max-w-sm flex-col items-center",
className
)}
{...props} {...props}
/> />
) );
} }
const emptyMediaVariants = cva( const emptyMediaVariants = cva(
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", 'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
{ {
variants: { variants: {
variant: { variant: {
default: "bg-transparent", default: 'bg-transparent',
icon: "bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-4", icon: "bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-4",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
}, },
} }
) );
function EmptyMedia({ function EmptyMedia({
className, className,
variant = "default", variant = 'default',
...props ...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) { }: React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>) {
return ( return (
<div <div
data-slot="empty-icon" data-slot='empty-icon'
data-variant={variant} data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))} className={cn(emptyMediaVariants({ variant, className }))}
{...props} {...props}
/> />
) );
} }
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { function EmptyTitle({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="empty-title" data-slot='empty-title'
className={cn("text-sm font-medium tracking-tight", className)} className={cn('text-sm font-medium tracking-tight', className)}
{...props} {...props}
/> />
) );
} }
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
return ( return (
<div <div
data-slot="empty-description" data-slot='empty-description'
className={cn( className={cn(
"text-sm/relaxed text-muted-foreground [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", 'text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="empty-content" data-slot='empty-content'
className={cn( className={cn(
"gap-2.5 text-sm flex w-full max-w-sm min-w-0 flex-col items-center text-balance", 'flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance',
className className
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -101,4 +98,4 @@ export {
EmptyDescription, EmptyDescription,
EmptyContent, EmptyContent,
EmptyMedia, EmptyMedia,
} };
+96 -85
View File
@@ -1,93 +1,101 @@
"use client" 'use client';
import { useMemo } from "react" import { useMemo } from 'react';
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Label } from "@/components/ui/label" import { Label } from '@/components/ui/label';
import { Separator } from "@/components/ui/separator" import { Separator } from '@/components/ui/separator';
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
return ( return (
<fieldset <fieldset
data-slot="field-set" data-slot='field-set'
className={cn("gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 flex flex-col", className)} className={cn(
'flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
className
)}
{...props} {...props}
/> />
) );
} }
function FieldLegend({ function FieldLegend({
className, className,
variant = "legend", variant = 'legend',
...props ...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { }: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
return ( return (
<legend <legend
data-slot="field-legend" data-slot='field-legend'
data-variant={variant} data-variant={variant}
className={cn("mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base", className)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-group"
className={cn( className={cn(
"gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4 group/field-group @container/field-group flex w-full flex-col", 'mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base',
className className
)} )}
{...props} {...props}
/> />
) );
} }
const fieldVariants = cva("data-[invalid=true]:text-destructive gap-2 group/field flex w-full", { function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
variants: { return (
orientation: { <div
vertical: data-slot='field-group'
"flex-col *:w-full [&>.sr-only]:w-auto", className={cn(
horizontal: 'group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4',
"flex-row items-center *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", className
responsive: )}
"flex-col *:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:*:data-[slot=field-label]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", {...props}
/>
);
}
const fieldVariants = cva(
'data-[invalid=true]:text-destructive gap-2 group/field flex w-full',
{
variants: {
orientation: {
vertical: 'flex-col *:w-full [&>.sr-only]:w-auto',
horizontal:
'flex-row items-center *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
responsive:
'flex-col *:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:*:data-[slot=field-label]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
},
}, },
}, defaultVariants: {
defaultVariants: { orientation: 'vertical',
orientation: "vertical", },
}, }
}) );
function Field({ function Field({
className, className,
orientation = "vertical", orientation = 'vertical',
...props ...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) { }: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
return ( return (
<div <div
role="group" role='group'
data-slot="field" data-slot='field'
data-orientation={orientation} data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)} className={cn(fieldVariants({ orientation }), className)}
{...props} {...props}
/> />
) );
} }
function FieldContent({ className, ...props }: React.ComponentProps<"div">) { function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="field-content" data-slot='field-content'
className={cn( className={cn(
"gap-0.5 group/field-content flex flex-1 flex-col leading-snug", 'group/field-content flex flex-1 flex-col gap-0.5 leading-snug',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function FieldLabel({ function FieldLabel({
@@ -96,70 +104,73 @@ function FieldLabel({
}: React.ComponentProps<typeof Label>) { }: React.ComponentProps<typeof Label>) {
return ( return (
<Label <Label
data-slot="field-label" data-slot='field-label'
className={cn( className={cn(
"has-data-checked:bg-primary/5 has-data-checked:border-primary/30 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 gap-2 group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 group/field-label peer/field-label flex w-fit leading-snug", 'has-data-checked:bg-primary/5 has-data-checked:border-primary/30 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5',
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col", 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="field-label" data-slot='field-label'
className={cn( className={cn(
"gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50 flex w-fit items-center leading-snug", 'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
return ( return (
<p <p
data-slot="field-description" data-slot='field-description'
className={cn( className={cn(
"text-muted-foreground text-left text-sm [[data-variant=legend]+&]:-mt-1.5 leading-normal font-normal group-has-data-horizontal/field:text-balance", 'text-muted-foreground text-left text-sm leading-normal font-normal group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5',
"last:mt-0 nth-last-2:-mt-1", 'last:mt-0 nth-last-2:-mt-1',
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", '[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function FieldSeparator({ function FieldSeparator({
children, children,
className, className,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<'div'> & {
children?: React.ReactNode children?: React.ReactNode;
}) { }) {
return ( return (
<div <div
data-slot="field-separator" data-slot='field-separator'
data-content={!!children} data-content={!!children}
className={cn("-my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2 relative", className)} className={cn(
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
className
)}
{...props} {...props}
> >
<Separator className="absolute inset-0 top-1/2" /> <Separator className='absolute inset-0 top-1/2' />
{children && ( {children && (
<span <span
className="text-muted-foreground px-2 bg-background relative mx-auto block w-fit" className='text-muted-foreground bg-background relative mx-auto block w-fit px-2'
data-slot="field-separator-content" data-slot='field-separator-content'
> >
{children} {children}
</span> </span>
)} )}
</div> </div>
) );
} }
function FieldError({ function FieldError({
@@ -167,50 +178,50 @@ function FieldError({
children, children,
errors, errors,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<'div'> & {
errors?: Array<{ message?: string } | undefined> errors?: Array<{ message?: string } | undefined>;
}) { }) {
const content = useMemo(() => { const content = useMemo(() => {
if (children) { if (children) {
return children return children;
} }
if (!errors?.length) { if (!errors?.length) {
return null return null;
} }
const uniqueErrors = [ const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(), ...new Map(errors.map((error) => [error?.message, error])).values(),
] ];
if (uniqueErrors?.length == 1) { if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message return uniqueErrors[0]?.message;
} }
return ( return (
<ul className="ml-4 flex list-disc flex-col gap-1"> <ul className='ml-4 flex list-disc flex-col gap-1'>
{uniqueErrors.map( {uniqueErrors.map(
(error, index) => (error, index) =>
error?.message && <li key={index}>{error.message}</li> error?.message && <li key={index}>{error.message}</li>
)} )}
</ul> </ul>
) );
}, [children, errors]) }, [children, errors]);
if (!content) { if (!content) {
return null return null;
} }
return ( return (
<div <div
role="alert" role='alert'
data-slot="field-error" data-slot='field-error'
className={cn("text-destructive text-sm font-normal", className)} className={cn('text-destructive text-sm font-normal', className)}
{...props} {...props}
> >
{content} {content}
</div> </div>
) );
} }
export { export {
@@ -224,4 +235,4 @@ export {
FieldSet, FieldSet,
FieldContent, FieldContent,
FieldTitle, FieldTitle,
} };
+13 -13
View File
@@ -1,44 +1,44 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { HoverCard as HoverCardPrimitive } from "radix-ui" import { HoverCard as HoverCardPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function HoverCard({ function HoverCard({
...props ...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) { }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} /> return <HoverCardPrimitive.Root data-slot='hover-card' {...props} />;
} }
function HoverCardTrigger({ function HoverCardTrigger({
...props ...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) { }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return ( return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} /> <HoverCardPrimitive.Trigger data-slot='hover-card-trigger' {...props} />
) );
} }
function HoverCardContent({ function HoverCardContent({
className, className,
align = "center", align = 'center',
sideOffset = 4, sideOffset = 4,
...props ...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) { }: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return ( return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal"> <HoverCardPrimitive.Portal data-slot='hover-card-portal'>
<HoverCardPrimitive.Content <HoverCardPrimitive.Content
data-slot="hover-card-content" data-slot='hover-card-content'
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground w-64 rounded-lg p-2.5 text-sm shadow-md ring-1 duration-100 z-50 origin-(--radix-hover-card-content-transform-origin) outline-hidden", 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-lg p-2.5 text-sm shadow-md ring-1 outline-hidden duration-100',
className className
)} )}
{...props} {...props}
/> />
</HoverCardPrimitive.Portal> </HoverCardPrimitive.Portal>
) );
} }
export { HoverCard, HoverCardTrigger, HoverCardContent } export { HoverCard, HoverCardTrigger, HoverCardContent };
+60 -51
View File
@@ -1,25 +1,25 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button';
import { Input } from "@/components/ui/input" import { Input } from '@/components/ui/input';
import { Textarea } from "@/components/ui/textarea" import { Textarea } from '@/components/ui/textarea';
function InputGroup({ className, ...props }: React.ComponentProps<"div">) { function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="input-group" data-slot='input-group'
role="group" role='group'
className={cn( className={cn(
"border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-disabled:bg-input/50 dark:has-disabled:bg-input/80 h-8 rounded-lg border transition-colors in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto", 'border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-disabled:bg-input/50 dark:has-disabled:bg-input/80 group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',
className className
)} )}
{...props} {...props}
/> />
) );
} }
const inputGroupAddonVariants = cva( const inputGroupAddonVariants = cva(
@@ -27,66 +27,69 @@ const inputGroupAddonVariants = cva(
{ {
variants: { variants: {
align: { align: {
"inline-start": "pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem] order-first", 'inline-start':
"inline-end": "pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem] order-last", 'pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem] order-first',
"block-start": 'inline-end':
"px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start", 'pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem] order-last',
"block-end": 'block-start':
"px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start", 'px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start',
'block-end':
'px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start',
}, },
}, },
defaultVariants: { defaultVariants: {
align: "inline-start", align: 'inline-start',
}, },
} }
) );
function InputGroupAddon({ function InputGroupAddon({
className, className,
align = "inline-start", align = 'inline-start',
...props ...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) { }: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
return ( return (
<div <div
role="group" role='group'
data-slot="input-group-addon" data-slot='input-group-addon'
data-align={align} data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)} className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => { onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) { if ((e.target as HTMLElement).closest('button')) {
return return;
} }
e.currentTarget.parentElement?.querySelector("input")?.focus() e.currentTarget.parentElement?.querySelector('input')?.focus();
}} }}
{...props} {...props}
/> />
) );
} }
const inputGroupButtonVariants = cva( const inputGroupButtonVariants = cva(
"gap-2 text-sm shadow-none flex items-center", 'gap-2 text-sm shadow-none flex items-center',
{ {
variants: { variants: {
size: { size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5", xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "", sm: '',
"icon-xs": "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0", 'icon-xs':
"icon-sm": "size-8 p-0 has-[>svg]:p-0", 'size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0',
'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
}, },
}, },
defaultVariants: { defaultVariants: {
size: "xs", size: 'xs',
}, },
} }
) );
function InputGroupButton({ function InputGroupButton({
className, className,
type = "button", type = 'button',
variant = "ghost", variant = 'ghost',
size = "xs", size = 'xs',
...props ...props
}: Omit<React.ComponentProps<typeof Button>, "size"> & }: Omit<React.ComponentProps<typeof Button>, 'size'> &
VariantProps<typeof inputGroupButtonVariants>) { VariantProps<typeof inputGroupButtonVariants>) {
return ( return (
<Button <Button
@@ -96,45 +99,51 @@ function InputGroupButton({
className={cn(inputGroupButtonVariants({ size }), className)} className={cn(inputGroupButtonVariants({ size }), className)}
{...props} {...props}
/> />
) );
} }
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) { function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
return ( return (
<span <span
className={cn( className={cn(
"text-muted-foreground gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none", "text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
/> />
) );
} }
function InputGroupInput({ function InputGroupInput({
className, className,
...props ...props
}: React.ComponentProps<"input">) { }: React.ComponentProps<'input'>) {
return ( return (
<Input <Input
data-slot="input-group-control" data-slot='input-group-control'
className={cn("rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent flex-1", className)} className={cn(
'flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent',
className
)}
{...props} {...props}
/> />
) );
} }
function InputGroupTextarea({ function InputGroupTextarea({
className, className,
...props ...props
}: React.ComponentProps<"textarea">) { }: React.ComponentProps<'textarea'>) {
return ( return (
<Textarea <Textarea
data-slot="input-group-control" data-slot='input-group-control'
className={cn("rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent flex-1 resize-none", className)} className={cn(
'flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent',
className
)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -144,4 +153,4 @@ export {
InputGroupText, InputGroupText,
InputGroupInput, InputGroupInput,
InputGroupTextarea, InputGroupTextarea,
} };
+36 -37
View File
@@ -1,87 +1,86 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { OTPInput, OTPInputContext } from "input-otp" import { OTPInput, OTPInputContext } from 'input-otp';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { MinusIcon } from "lucide-react" import { MinusIcon } from 'lucide-react';
function InputOTP({ function InputOTP({
className, className,
containerClassName, containerClassName,
...props ...props
}: React.ComponentProps<typeof OTPInput> & { }: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string containerClassName?: string;
}) { }) {
return ( return (
<OTPInput <OTPInput
data-slot="input-otp" data-slot='input-otp'
containerClassName={cn( containerClassName={cn(
"cn-input-otp flex items-center has-disabled:opacity-50", 'cn-input-otp flex items-center has-disabled:opacity-50',
containerClassName containerClassName
)} )}
spellCheck={false} spellCheck={false}
className={cn('disabled:cursor-not-allowed', className)}
{...props}
/>
);
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='input-otp-group'
className={cn( className={cn(
"disabled:cursor-not-allowed", 'has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive flex items-center rounded-lg has-aria-invalid:ring-3',
className className
)} )}
{...props} {...props}
/> />
) );
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-group"
className={cn("has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive rounded-lg has-aria-invalid:ring-3 flex items-center", className)}
{...props}
/>
)
} }
function InputOTPSlot({ function InputOTPSlot({
index, index,
className, className,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<'div'> & {
index: number index: number;
}) { }) {
const inputOTPContext = React.useContext(OTPInputContext) const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {} const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
return ( return (
<div <div
data-slot="input-otp-slot" data-slot='input-otp-slot'
data-active={isActive} data-active={isActive}
className={cn( className={cn(
"dark:bg-input/30 border-input data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive size-8 border-y border-r text-sm transition-all outline-none first:rounded-l-lg first:border-l last:rounded-r-lg data-[active=true]:ring-3 relative flex items-center justify-center data-[active=true]:z-10", 'dark:bg-input/30 border-input data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive relative flex size-8 items-center justify-center border-y border-r text-sm transition-all outline-none first:rounded-l-lg first:border-l last:rounded-r-lg data-[active=true]:z-10 data-[active=true]:ring-3',
className className
)} )}
{...props} {...props}
> >
{char} {char}
{hasFakeCaret && ( {hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center"> <div className='pointer-events-none absolute inset-0 flex items-center justify-center'>
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" /> <div className='animate-caret-blink bg-foreground h-4 w-px duration-1000' />
</div> </div>
)} )}
</div> </div>
) );
} }
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) { function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="input-otp-separator" data-slot='input-otp-separator'
className="[&_svg:not([class*='size-'])]:size-4 flex items-center" className="flex items-center [&_svg:not([class*='size-'])]:size-4"
role="separator" role='separator'
{...props} {...props}
> >
<MinusIcon <MinusIcon />
/>
</div> </div>
) );
} }
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
+7 -7
View File
@@ -1,19 +1,19 @@
import * as React from "react" import * as React from 'react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Input({ className, type, ...props }: React.ComponentProps<"input">) { function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return ( return (
<input <input
type={type} type={type}
data-slot="input" data-slot='input'
className={cn( className={cn(
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", 'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 file:text-foreground placeholder:text-muted-foreground h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
className className
)} )}
{...props} {...props}
/> />
) );
} }
export { Input } export { Input };
+65 -64
View File
@@ -1,22 +1,22 @@
import * as React from "react" import * as React from 'react';
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from "radix-ui" import { Slot } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Separator } from "@/components/ui/separator" import { Separator } from '@/components/ui/separator';
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) { function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
role="list" role='list'
data-slot="item-group" data-slot='item-group'
className={cn( className={cn(
"gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2 group/item-group flex w-full flex-col", 'group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function ItemSeparator({ function ItemSeparator({
@@ -25,160 +25,161 @@ function ItemSeparator({
}: React.ComponentProps<typeof Separator>) { }: React.ComponentProps<typeof Separator>) {
return ( return (
<Separator <Separator
data-slot="item-separator" data-slot='item-separator'
orientation="horizontal" orientation='horizontal'
className={cn("my-2", className)} className={cn('my-2', className)}
{...props} {...props}
/> />
) );
} }
const itemVariants = cva( const itemVariants = cva(
"[a]:hover:bg-muted rounded-lg border text-sm w-full group/item focus-visible:border-ring focus-visible:ring-ring/50 flex items-center flex-wrap outline-none transition-colors duration-100 focus-visible:ring-[3px] [a]:transition-colors", '[a]:hover:bg-muted rounded-lg border text-sm w-full group/item focus-visible:border-ring focus-visible:ring-ring/50 flex items-center flex-wrap outline-none transition-colors duration-100 focus-visible:ring-[3px] [a]:transition-colors',
{ {
variants: { variants: {
variant: { variant: {
default: "border-transparent", default: 'border-transparent',
outline: "border-border", outline: 'border-border',
muted: "bg-muted/50 border-transparent", muted: 'bg-muted/50 border-transparent',
}, },
size: { size: {
default: "gap-2.5 px-3 py-2.5", default: 'gap-2.5 px-3 py-2.5',
sm: "gap-2.5 px-3 py-2.5", sm: 'gap-2.5 px-3 py-2.5',
xs: "gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0", xs: 'gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default',
}, },
} }
) );
function Item({ function Item({
className, className,
variant = "default", variant = 'default',
size = "default", size = 'default',
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"div"> & }: React.ComponentProps<'div'> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) { VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "div" const Comp = asChild ? Slot.Root : 'div';
return ( return (
<Comp <Comp
data-slot="item" data-slot='item'
data-variant={variant} data-variant={variant}
data-size={size} data-size={size}
className={cn(itemVariants({ variant, size, className }))} className={cn(itemVariants({ variant, size, className }))}
{...props} {...props}
/> />
) );
} }
const itemMediaVariants = cva( const itemMediaVariants = cva(
"gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none", 'gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none',
{ {
variants: { variants: {
variant: { variant: {
default: "bg-transparent", default: 'bg-transparent',
icon: "[&_svg:not([class*='size-'])]:size-4", icon: "[&_svg:not([class*='size-'])]:size-4",
image: "size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover", image:
'size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
}, },
} }
) );
function ItemMedia({ function ItemMedia({
className, className,
variant = "default", variant = 'default',
...props ...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) { }: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
return ( return (
<div <div
data-slot="item-media" data-slot='item-media'
data-variant={variant} data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))} className={cn(itemMediaVariants({ variant, className }))}
{...props} {...props}
/> />
) );
} }
function ItemContent({ className, ...props }: React.ComponentProps<"div">) { function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="item-content" data-slot='item-content'
className={cn( className={cn(
"gap-1 group-data-[size=xs]/item:gap-0 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none", 'flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) { function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="item-title" data-slot='item-title'
className={cn( className={cn(
"gap-2 text-sm leading-snug font-medium underline-offset-4 line-clamp-1 flex w-fit items-center", 'line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) { function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
return ( return (
<p <p
data-slot="item-description" data-slot='item-description'
className={cn( className={cn(
"text-muted-foreground text-left text-sm leading-normal group-data-[size=xs]/item:text-xs [&>a:hover]:text-primary line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4", 'text-muted-foreground [&>a:hover]:text-primary line-clamp-2 text-left text-sm leading-normal font-normal group-data-[size=xs]/item:text-xs [&>a]:underline [&>a]:underline-offset-4',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function ItemActions({ className, ...props }: React.ComponentProps<"div">) { function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="item-actions" data-slot='item-actions'
className={cn("gap-2 flex items-center", className)} className={cn('flex items-center gap-2', className)}
{...props} {...props}
/> />
) );
} }
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) { function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="item-header" data-slot='item-header'
className={cn( className={cn(
"gap-2 flex basis-full items-center justify-between", 'flex basis-full items-center justify-between gap-2',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) { function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="item-footer" data-slot='item-footer'
className={cn( className={cn(
"gap-2 flex basis-full items-center justify-between", 'flex basis-full items-center justify-between gap-2',
className className
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -192,4 +193,4 @@ export {
ItemDescription, ItemDescription,
ItemHeader, ItemHeader,
ItemFooter, ItemFooter,
} };
+10 -10
View File
@@ -1,26 +1,26 @@
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) { function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
return ( return (
<kbd <kbd
data-slot="kbd" data-slot='kbd'
className={cn( className={cn(
"bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 h-5 w-fit min-w-5 gap-1 rounded-sm px-1 font-sans text-xs font-medium [&_svg:not([class*='size-'])]:size-3 pointer-events-none inline-flex items-center justify-center select-none", "bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none [&_svg:not([class*='size-'])]:size-3",
className className
)} )}
{...props} {...props}
/> />
) );
} }
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) { function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<kbd <kbd
data-slot="kbd-group" data-slot='kbd-group'
className={cn("gap-1 inline-flex items-center", className)} className={cn('inline-flex items-center gap-1', className)}
{...props} {...props}
/> />
) );
} }
export { Kbd, KbdGroup } export { Kbd, KbdGroup };
+8 -8
View File
@@ -1,9 +1,9 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Label as LabelPrimitive } from "radix-ui" import { Label as LabelPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Label({ function Label({
className, className,
@@ -11,14 +11,14 @@ function Label({
}: React.ComponentProps<typeof LabelPrimitive.Root>) { }: React.ComponentProps<typeof LabelPrimitive.Root>) {
return ( return (
<LabelPrimitive.Root <LabelPrimitive.Root
data-slot="label" data-slot='label'
className={cn( className={cn(
"gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed", 'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className className
)} )}
{...props} {...props}
/> />
) );
} }
export { Label } export { Label };
+74 -61
View File
@@ -1,10 +1,10 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Menubar as MenubarPrimitive } from "radix-ui" import { Menubar as MenubarPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { CheckIcon, ChevronRightIcon } from "lucide-react" import { CheckIcon, ChevronRightIcon } from 'lucide-react';
function Menubar({ function Menubar({
className, className,
@@ -12,37 +12,40 @@ function Menubar({
}: React.ComponentProps<typeof MenubarPrimitive.Root>) { }: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return ( return (
<MenubarPrimitive.Root <MenubarPrimitive.Root
data-slot="menubar" data-slot='menubar'
className={cn("bg-background h-8 gap-0.5 rounded-lg border p-[3px] flex items-center", className)} className={cn(
'bg-background flex h-8 items-center gap-0.5 rounded-lg border p-[3px]',
className
)}
{...props} {...props}
/> />
) );
} }
function MenubarMenu({ function MenubarMenu({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) { }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} /> return <MenubarPrimitive.Menu data-slot='menubar-menu' {...props} />;
} }
function MenubarGroup({ function MenubarGroup({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) { }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} /> return <MenubarPrimitive.Group data-slot='menubar-group' {...props} />;
} }
function MenubarPortal({ function MenubarPortal({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) { }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} /> return <MenubarPrimitive.Portal data-slot='menubar-portal' {...props} />;
} }
function MenubarRadioGroup({ function MenubarRadioGroup({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) { }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return ( return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} /> <MenubarPrimitive.RadioGroup data-slot='menubar-radio-group' {...props} />
) );
} }
function MenubarTrigger({ function MenubarTrigger({
@@ -51,19 +54,19 @@ function MenubarTrigger({
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) { }: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return ( return (
<MenubarPrimitive.Trigger <MenubarPrimitive.Trigger
data-slot="menubar-trigger" data-slot='menubar-trigger'
className={cn( className={cn(
"hover:bg-muted aria-expanded:bg-muted rounded-sm px-1.5 py-[2px] text-sm font-medium flex items-center outline-hidden select-none", 'hover:bg-muted aria-expanded:bg-muted flex items-center rounded-sm px-1.5 py-[2px] text-sm font-medium outline-hidden select-none',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function MenubarContent({ function MenubarContent({
className, className,
align = "start", align = 'start',
alignOffset = -4, alignOffset = -4,
sideOffset = 8, sideOffset = 8,
...props ...props
@@ -71,38 +74,41 @@ function MenubarContent({
return ( return (
<MenubarPortal> <MenubarPortal>
<MenubarPrimitive.Content <MenubarPrimitive.Content
data-slot="menubar-content" data-slot='menubar-content'
align={align} align={align}
alignOffset={alignOffset} alignOffset={alignOffset}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn("bg-popover text-popover-foreground data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-lg p-1 shadow-md ring-1 duration-100 z-50 origin-(--radix-menubar-content-transform-origin) overflow-hidden", className )} className={cn(
'bg-popover text-popover-foreground data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 z-50 min-w-36 origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-lg p-1 shadow-md ring-1 duration-100',
className
)}
{...props} {...props}
/> />
</MenubarPortal> </MenubarPortal>
) );
} }
function MenubarItem({ function MenubarItem({
className, className,
inset, inset,
variant = "default", variant = 'default',
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & { }: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean inset?: boolean;
variant?: "default" | "destructive" variant?: 'default' | 'destructive';
}) { }) {
return ( return (
<MenubarPrimitive.Item <MenubarPrimitive.Item
data-slot="menubar-item" data-slot='menubar-item'
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive! not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-disabled:opacity-50 data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/menubar-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0", "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive! not-data-[variant=destructive]:focus:**:text-accent-foreground group/menubar-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
/> />
) );
} }
function MenubarCheckboxItem({ function MenubarCheckboxItem({
@@ -112,28 +118,27 @@ function MenubarCheckboxItem({
inset, inset,
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem> & { }: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<MenubarPrimitive.CheckboxItem <MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item" data-slot='menubar-checkbox-item'
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-1.5 pl-7 text-sm data-inset:pl-7 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0", 'focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-1.5 pl-7 text-sm outline-hidden select-none data-disabled:pointer-events-none data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0',
className className
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="left-1.5 size-4 [&_svg:not([class*='size-'])]:size-4 pointer-events-none absolute flex items-center justify-center"> <span className="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
<MenubarPrimitive.ItemIndicator> <MenubarPrimitive.ItemIndicator>
<CheckIcon <CheckIcon />
/>
</MenubarPrimitive.ItemIndicator> </MenubarPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</MenubarPrimitive.CheckboxItem> </MenubarPrimitive.CheckboxItem>
) );
} }
function MenubarRadioItem({ function MenubarRadioItem({
@@ -142,27 +147,26 @@ function MenubarRadioItem({
inset, inset,
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem> & { }: React.ComponentProps<typeof MenubarPrimitive.RadioItem> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<MenubarPrimitive.RadioItem <MenubarPrimitive.RadioItem
data-slot="menubar-radio-item" data-slot='menubar-radio-item'
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-1.5 pl-7 text-sm data-disabled:opacity-50 data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0", "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-1.5 pl-7 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
> >
<span className="left-1.5 size-4 [&_svg:not([class*='size-'])]:size-4 pointer-events-none absolute flex items-center justify-center"> <span className="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
<MenubarPrimitive.ItemIndicator> <MenubarPrimitive.ItemIndicator>
<CheckIcon <CheckIcon />
/>
</MenubarPrimitive.ItemIndicator> </MenubarPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</MenubarPrimitive.RadioItem> </MenubarPrimitive.RadioItem>
) );
} }
function MenubarLabel({ function MenubarLabel({
@@ -170,16 +174,19 @@ function MenubarLabel({
inset, inset,
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & { }: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<MenubarPrimitive.Label <MenubarPrimitive.Label
data-slot="menubar-label" data-slot='menubar-label'
data-inset={inset} data-inset={inset}
className={cn("px-1.5 py-1 text-sm font-medium data-inset:pl-7", className)} className={cn(
'px-1.5 py-1 text-sm font-medium data-inset:pl-7',
className
)}
{...props} {...props}
/> />
) );
} }
function MenubarSeparator({ function MenubarSeparator({
@@ -188,30 +195,33 @@ function MenubarSeparator({
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) { }: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return ( return (
<MenubarPrimitive.Separator <MenubarPrimitive.Separator
data-slot="menubar-separator" data-slot='menubar-separator'
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props} {...props}
/> />
) );
} }
function MenubarShortcut({ function MenubarShortcut({
className, className,
...props ...props
}: React.ComponentProps<"span">) { }: React.ComponentProps<'span'>) {
return ( return (
<span <span
data-slot="menubar-shortcut" data-slot='menubar-shortcut'
className={cn("text-muted-foreground group-focus/menubar-item:text-accent-foreground text-xs tracking-widest ml-auto", className)} className={cn(
'text-muted-foreground group-focus/menubar-item:text-accent-foreground ml-auto text-xs tracking-widest',
className
)}
{...props} {...props}
/> />
) );
} }
function MenubarSub({ function MenubarSub({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) { }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} /> return <MenubarPrimitive.Sub data-slot='menubar-sub' {...props} />;
} }
function MenubarSubTrigger({ function MenubarSubTrigger({
@@ -220,22 +230,22 @@ function MenubarSubTrigger({
children, children,
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & { }: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<MenubarPrimitive.SubTrigger <MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger" data-slot='menubar-sub-trigger'
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-none select-none", "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-none select-none data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto size-4" /> <ChevronRightIcon className='ml-auto size-4' />
</MenubarPrimitive.SubTrigger> </MenubarPrimitive.SubTrigger>
) );
} }
function MenubarSubContent({ function MenubarSubContent({
@@ -244,11 +254,14 @@ function MenubarSubContent({
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) { }: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return ( return (
<MenubarPrimitive.SubContent <MenubarPrimitive.SubContent
data-slot="menubar-sub-content" data-slot='menubar-sub-content'
className={cn("bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-32 rounded-lg p-1 shadow-lg ring-1 duration-100 z-50 origin-(--radix-menubar-content-transform-origin) overflow-hidden", className )} className={cn(
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 z-50 min-w-32 origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-lg p-1 shadow-lg ring-1 duration-100',
className
)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -268,4 +281,4 @@ export {
MenubarSub, MenubarSub,
MenubarSubTrigger, MenubarSubTrigger,
MenubarSubContent, MenubarSubContent,
} };
+23 -19
View File
@@ -1,52 +1,56 @@
import * as React from "react" import * as React from 'react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { ChevronDownIcon } from "lucide-react" import { ChevronDownIcon } from 'lucide-react';
type NativeSelectProps = Omit<React.ComponentProps<"select">, "size"> & { type NativeSelectProps = Omit<React.ComponentProps<'select'>, 'size'> & {
size?: "sm" | "default" size?: 'sm' | 'default';
} };
function NativeSelect({ function NativeSelect({
className, className,
size = "default", size = 'default',
...props ...props
}: NativeSelectProps) { }: NativeSelectProps) {
return ( return (
<div <div
className={cn( className={cn(
"group/native-select relative w-fit has-[select:disabled]:opacity-50", 'group/native-select relative w-fit has-[select:disabled]:opacity-50',
className className
)} )}
data-slot="native-select-wrapper" data-slot='native-select-wrapper'
data-size={size} data-size={size}
> >
<select <select
data-slot="native-select" data-slot='native-select'
data-size={size} data-size={size}
className="border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-8 w-full min-w-0 appearance-none rounded-lg border bg-transparent py-1 pr-8 pl-2.5 text-sm transition-colors select-none focus-visible:ring-3 aria-invalid:ring-3 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5 outline-none disabled:pointer-events-none disabled:cursor-not-allowed" className='border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-8 w-full min-w-0 appearance-none rounded-lg border bg-transparent py-1 pr-8 pl-2.5 text-sm transition-colors outline-none select-none focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed aria-invalid:ring-3 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5'
{...props} {...props}
/> />
<ChevronDownIcon className="text-muted-foreground top-1/2 right-2.5 size-4 -translate-y-1/2 pointer-events-none absolute select-none" aria-hidden="true" data-slot="native-select-icon" /> <ChevronDownIcon
className='text-muted-foreground pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 select-none'
aria-hidden='true'
data-slot='native-select-icon'
/>
</div> </div>
) );
} }
function NativeSelectOption({ ...props }: React.ComponentProps<"option">) { function NativeSelectOption({ ...props }: React.ComponentProps<'option'>) {
return <option data-slot="native-select-option" {...props} /> return <option data-slot='native-select-option' {...props} />;
} }
function NativeSelectOptGroup({ function NativeSelectOptGroup({
className, className,
...props ...props
}: React.ComponentProps<"optgroup">) { }: React.ComponentProps<'optgroup'>) {
return ( return (
<optgroup <optgroup
data-slot="native-select-optgroup" data-slot='native-select-optgroup'
className={cn(className)} className={cn(className)}
{...props} {...props}
/> />
) );
} }
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption } export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };
+43 -37
View File
@@ -1,9 +1,9 @@
import * as React from "react" import * as React from 'react';
import { cva } from "class-variance-authority" import { cva } from 'class-variance-authority';
import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui" import { NavigationMenu as NavigationMenuPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { ChevronDownIcon } from "lucide-react" import { ChevronDownIcon } from 'lucide-react';
function NavigationMenu({ function NavigationMenu({
className, className,
@@ -11,14 +11,14 @@ function NavigationMenu({
viewport = true, viewport = true,
...props ...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & { }: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean viewport?: boolean;
}) { }) {
return ( return (
<NavigationMenuPrimitive.Root <NavigationMenuPrimitive.Root
data-slot="navigation-menu" data-slot='navigation-menu'
data-viewport={viewport} data-viewport={viewport}
className={cn( className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center", 'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
className className
)} )}
{...props} {...props}
@@ -26,7 +26,7 @@ function NavigationMenu({
{children} {children}
{viewport && <NavigationMenuViewport />} {viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root> </NavigationMenuPrimitive.Root>
) );
} }
function NavigationMenuList({ function NavigationMenuList({
@@ -35,14 +35,14 @@ function NavigationMenuList({
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return ( return (
<NavigationMenuPrimitive.List <NavigationMenuPrimitive.List
data-slot="navigation-menu-list" data-slot='navigation-menu-list'
className={cn( className={cn(
"gap-0 group flex flex-1 list-none items-center justify-center", 'group flex flex-1 list-none items-center justify-center gap-0',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function NavigationMenuItem({ function NavigationMenuItem({
@@ -51,16 +51,16 @@ function NavigationMenuItem({
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return ( return (
<NavigationMenuPrimitive.Item <NavigationMenuPrimitive.Item
data-slot="navigation-menu-item" data-slot='navigation-menu-item'
className={cn("relative", className)} className={cn('relative', className)}
{...props} {...props}
/> />
) );
} }
const navigationMenuTriggerStyle = cva( const navigationMenuTriggerStyle = cva(
"bg-background hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all focus-visible:ring-3 focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center disabled:pointer-events-none outline-none" 'bg-background hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all focus-visible:ring-3 focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center disabled:pointer-events-none outline-none'
) );
function NavigationMenuTrigger({ function NavigationMenuTrigger({
className, className,
@@ -69,14 +69,17 @@ function NavigationMenuTrigger({
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return ( return (
<NavigationMenuPrimitive.Trigger <NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger" data-slot='navigation-menu-trigger'
className={cn(navigationMenuTriggerStyle(), "group", className)} className={cn(navigationMenuTriggerStyle(), 'group', className)}
{...props} {...props}
> >
{children}{" "} {children}{' '}
<ChevronDownIcon className="relative top-px ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180 group-data-popup-open/navigation-menu-trigger:rotate-180" aria-hidden="true" /> <ChevronDownIcon
className='relative top-px ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180 group-data-popup-open/navigation-menu-trigger:rotate-180'
aria-hidden='true'
/>
</NavigationMenuPrimitive.Trigger> </NavigationMenuPrimitive.Trigger>
) );
} }
function NavigationMenuContent({ function NavigationMenuContent({
@@ -85,14 +88,14 @@ function NavigationMenuContent({
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return ( return (
<NavigationMenuPrimitive.Content <NavigationMenuPrimitive.Content
data-slot="navigation-menu-content" data-slot='navigation-menu-content'
className={cn( className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:ring-foreground/10 p-1 ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:duration-300 top-0 left-0 w-full group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none md:absolute md:w-auto", 'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:ring-foreground/10 top-0 left-0 w-full p-1 ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:duration-300 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none md:absolute md:w-auto',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function NavigationMenuViewport({ function NavigationMenuViewport({
@@ -102,19 +105,19 @@ function NavigationMenuViewport({
return ( return (
<div <div
className={cn( className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center" 'absolute top-full left-0 isolate z-50 flex justify-center'
)} )}
> >
<NavigationMenuPrimitive.Viewport <NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport" data-slot='navigation-menu-viewport'
className={cn( className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:zoom-out-95 data-open:zoom-in-90 ring-foreground/10 rounded-lg shadow ring-1 duration-100 origin-top-center relative mt-1.5 h-(--radix-navigation-menu-viewport-height) w-full overflow-hidden md:w-(--radix-navigation-menu-viewport-width)", 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:zoom-out-95 data-open:zoom-in-90 ring-foreground/10 origin-top-center relative mt-1.5 h-(--radix-navigation-menu-viewport-height) w-full overflow-hidden rounded-lg shadow ring-1 duration-100 md:w-(--radix-navigation-menu-viewport-width)',
className className
)} )}
{...props} {...props}
/> />
</div> </div>
) );
} }
function NavigationMenuLink({ function NavigationMenuLink({
@@ -123,11 +126,14 @@ function NavigationMenuLink({
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return ( return (
<NavigationMenuPrimitive.Link <NavigationMenuPrimitive.Link
data-slot="navigation-menu-link" data-slot='navigation-menu-link'
className={cn("data-active:focus:bg-muted data-active:hover:bg-muted data-active:bg-muted/50 focus-visible:ring-ring/50 hover:bg-muted focus:bg-muted flex items-center gap-2 rounded-lg p-2 text-sm transition-all outline-none focus-visible:ring-3 focus-visible:outline-1 in-data-[slot=navigation-menu-content]:rounded-md [&_svg:not([class*='size-'])]:size-4", className)} className={cn(
"data-active:focus:bg-muted data-active:hover:bg-muted data-active:bg-muted/50 focus-visible:ring-ring/50 hover:bg-muted focus:bg-muted flex items-center gap-2 rounded-lg p-2 text-sm transition-all outline-none focus-visible:ring-3 focus-visible:outline-1 in-data-[slot=navigation-menu-content]:rounded-md [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props} {...props}
/> />
) );
} }
function NavigationMenuIndicator({ function NavigationMenuIndicator({
@@ -136,16 +142,16 @@ function NavigationMenuIndicator({
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return ( return (
<NavigationMenuPrimitive.Indicator <NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator" data-slot='navigation-menu-indicator'
className={cn( className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden", 'data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden',
className className
)} )}
{...props} {...props}
> >
<div className="bg-border rounded-tl-sm shadow-md relative top-[60%] h-2 w-2 rotate-45" /> <div className='bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md' />
</NavigationMenuPrimitive.Indicator> </NavigationMenuPrimitive.Indicator>
) );
} }
export { export {
@@ -158,4 +164,4 @@ export {
NavigationMenuIndicator, NavigationMenuIndicator,
NavigationMenuViewport, NavigationMenuViewport,
navigationMenuTriggerStyle, navigationMenuTriggerStyle,
} };
+49 -51
View File
@@ -1,126 +1,124 @@
import * as React from "react" import * as React from 'react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button';
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react" import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from 'lucide-react';
function Pagination({ className, ...props }: React.ComponentProps<"nav">) { function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
return ( return (
<nav <nav
role="navigation" role='navigation'
aria-label="pagination" aria-label='pagination'
data-slot="pagination" data-slot='pagination'
className={cn( className={cn('mx-auto flex w-full justify-center', className)}
"mx-auto flex w-full justify-center",
className
)}
{...props} {...props}
/> />
) );
} }
function PaginationContent({ function PaginationContent({
className, className,
...props ...props
}: React.ComponentProps<"ul">) { }: React.ComponentProps<'ul'>) {
return ( return (
<ul <ul
data-slot="pagination-content" data-slot='pagination-content'
className={cn("gap-0.5 flex items-center", className)} className={cn('flex items-center gap-0.5', className)}
{...props} {...props}
/> />
) );
} }
function PaginationItem({ ...props }: React.ComponentProps<"li">) { function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
return <li data-slot="pagination-item" {...props} /> return <li data-slot='pagination-item' {...props} />;
} }
type PaginationLinkProps = { type PaginationLinkProps = {
isActive?: boolean isActive?: boolean;
} & Pick<React.ComponentProps<typeof Button>, "size"> & } & Pick<React.ComponentProps<typeof Button>, 'size'> &
React.ComponentProps<"a"> React.ComponentProps<'a'>;
function PaginationLink({ function PaginationLink({
className, className,
isActive, isActive,
size = "icon", size = 'icon',
...props ...props
}: PaginationLinkProps) { }: PaginationLinkProps) {
return ( return (
<Button <Button
asChild asChild
variant={isActive ? "outline" : "ghost"} variant={isActive ? 'outline' : 'ghost'}
size={size} size={size}
className={cn(className)} className={cn(className)}
> >
<a <a
aria-current={isActive ? "page" : undefined} aria-current={isActive ? 'page' : undefined}
data-slot="pagination-link" data-slot='pagination-link'
data-active={isActive} data-active={isActive}
{...props} {...props}
/> />
</Button> </Button>
) );
} }
function PaginationPrevious({ function PaginationPrevious({
className, className,
text = "Previous", text = 'Previous',
...props ...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) { }: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
return ( return (
<PaginationLink <PaginationLink
aria-label="Go to previous page" aria-label='Go to previous page'
size="default" size='default'
className={cn("pl-1.5!", className)} className={cn('pl-1.5!', className)}
{...props} {...props}
> >
<ChevronLeftIcon data-icon="inline-start" /> <ChevronLeftIcon data-icon='inline-start' />
<span className="hidden sm:block"> <span className='hidden sm:block'>{text}</span>
{text}
</span>
</PaginationLink> </PaginationLink>
) );
} }
function PaginationNext({ function PaginationNext({
className, className,
text = "Next", text = 'Next',
...props ...props
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) { }: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
return ( return (
<PaginationLink <PaginationLink
aria-label="Go to next page" aria-label='Go to next page'
size="default" size='default'
className={cn("pr-1.5!", className)} className={cn('pr-1.5!', className)}
{...props} {...props}
> >
<span className="hidden sm:block">{text}</span> <span className='hidden sm:block'>{text}</span>
<ChevronRightIcon data-icon="inline-end" /> <ChevronRightIcon data-icon='inline-end' />
</PaginationLink> </PaginationLink>
) );
} }
function PaginationEllipsis({ function PaginationEllipsis({
className, className,
...props ...props
}: React.ComponentProps<"span">) { }: React.ComponentProps<'span'>) {
return ( return (
<span <span
aria-hidden aria-hidden
data-slot="pagination-ellipsis" data-slot='pagination-ellipsis'
className={cn( className={cn(
"size-8 [&_svg:not([class*='size-'])]:size-4 flex items-center justify-center", "flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
> >
<MoreHorizontalIcon <MoreHorizontalIcon />
/> <span className='sr-only'>More pages</span>
<span className="sr-only">More pages</span>
</span> </span>
) );
} }
export { export {
@@ -131,4 +129,4 @@ export {
PaginationLink, PaginationLink,
PaginationNext, PaginationNext,
PaginationPrevious, PaginationPrevious,
} };
+9 -9
View File
@@ -1,35 +1,35 @@
"use client"; 'use client';
import * as React from "react"; import * as React from 'react';
import { Popover as PopoverPrimitive } from "radix-ui"; import { Popover as PopoverPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils';
function Popover({ function Popover({
...props ...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) { }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />; return <PopoverPrimitive.Root data-slot='popover' {...props} />;
} }
function PopoverTrigger({ function PopoverTrigger({
...props ...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) { }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />; return <PopoverPrimitive.Trigger data-slot='popover-trigger' {...props} />;
} }
function PopoverContent({ function PopoverContent({
className, className,
align = "center", align = 'center',
sideOffset = 4, sideOffset = 4,
...props ...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) { }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return ( return (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
<PopoverPrimitive.Content <PopoverPrimitive.Content
data-slot="popover-content" data-slot='popover-content'
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-popover text-popover-foreground ring-foreground/10 border-border data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 rounded-lg border p-4 shadow-md ring-1 outline-none", 'bg-popover text-popover-foreground ring-foreground/10 border-border data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 rounded-lg border p-4 shadow-md ring-1 outline-none',
className className
)} )}
{...props} {...props}
+10 -10
View File
@@ -1,9 +1,9 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Progress as ProgressPrimitive } from "radix-ui" import { Progress as ProgressPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Progress({ function Progress({
className, className,
@@ -12,20 +12,20 @@ function Progress({
}: React.ComponentProps<typeof ProgressPrimitive.Root>) { }: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return ( return (
<ProgressPrimitive.Root <ProgressPrimitive.Root
data-slot="progress" data-slot='progress'
className={cn( className={cn(
"bg-muted h-1 rounded-full relative flex w-full items-center overflow-x-hidden", 'bg-muted relative flex h-1 w-full items-center overflow-x-hidden rounded-full',
className className
)} )}
{...props} {...props}
> >
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
data-slot="progress-indicator" data-slot='progress-indicator'
className="bg-primary size-full flex-1 transition-all" className='bg-primary size-full flex-1 transition-all'
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>
) );
} }
export { Progress } export { Progress };
+13 -13
View File
@@ -1,10 +1,10 @@
"use client"; 'use client';
import * as React from "react"; import * as React from 'react';
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"; import { RadioGroup as RadioGroupPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils';
import { CircleIcon } from "lucide-react"; import { CircleIcon } from 'lucide-react';
function RadioGroup({ function RadioGroup({
className, className,
@@ -12,8 +12,8 @@ function RadioGroup({
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) { }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return ( return (
<RadioGroupPrimitive.Root <RadioGroupPrimitive.Root
data-slot="radio-group" data-slot='radio-group'
className={cn("grid gap-2 w-full", className)} className={cn('grid w-full gap-2', className)}
{...props} {...props}
/> />
); );
@@ -25,18 +25,18 @@ function RadioGroupItem({
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) { }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return ( return (
<RadioGroupPrimitive.Item <RadioGroupPrimitive.Item
data-slot="radio-group-item" data-slot='radio-group-item'
className={cn( className={cn(
"border-input text-primary dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 flex size-4 rounded-full focus-visible:ring-3 aria-invalid:ring-3 group/radio-group-item peer relative aspect-square shrink-0 border outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50", 'border-input text-primary dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3',
className, className
)} )}
{...props} {...props}
> >
<RadioGroupPrimitive.Indicator <RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator" data-slot='radio-group-indicator'
className="group-aria-invalid/radio-group-item:text-destructive text-primary flex size-4 items-center justify-center" className='group-aria-invalid/radio-group-item:text-destructive text-primary flex size-4 items-center justify-center'
> >
<CircleIcon className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-current" /> <CircleIcon className='absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-current' />
</RadioGroupPrimitive.Indicator> </RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item> </RadioGroupPrimitive.Item>
); );
+17 -15
View File
@@ -1,9 +1,9 @@
"use client" 'use client';
import type { ComponentProps } from "react" import type { ComponentProps } from 'react';
import * as ResizablePrimitive from "react-resizable-panels" import * as ResizablePrimitive from 'react-resizable-panels';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function ResizablePanelGroup({ function ResizablePanelGroup({
className, className,
@@ -11,18 +11,20 @@ function ResizablePanelGroup({
}: ComponentProps<typeof ResizablePrimitive.Group>) { }: ComponentProps<typeof ResizablePrimitive.Group>) {
return ( return (
<ResizablePrimitive.Group <ResizablePrimitive.Group
data-slot="resizable-panel-group" data-slot='resizable-panel-group'
className={cn( className={cn(
"flex h-full w-full aria-[orientation=vertical]:flex-col", 'flex h-full w-full aria-[orientation=vertical]:flex-col',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function ResizablePanel({ ...props }: ComponentProps<typeof ResizablePrimitive.Panel>) { function ResizablePanel({
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} /> ...props
}: ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot='resizable-panel' {...props} />;
} }
function ResizableHandle({ function ResizableHandle({
@@ -30,22 +32,22 @@ function ResizableHandle({
className, className,
...props ...props
}: ComponentProps<typeof ResizablePrimitive.Separator> & { }: ComponentProps<typeof ResizablePrimitive.Separator> & {
withHandle?: boolean withHandle?: boolean;
}) { }) {
return ( return (
<ResizablePrimitive.Separator <ResizablePrimitive.Separator
data-slot="resizable-handle" data-slot='resizable-handle'
className={cn( className={cn(
"bg-border focus-visible:ring-ring ring-offset-background relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90", 'bg-border focus-visible:ring-ring ring-offset-background relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90',
className className
)} )}
{...props} {...props}
> >
{withHandle && ( {withHandle && (
<div className="bg-border h-6 w-1 rounded-lg z-10 flex shrink-0" /> <div className='bg-border z-10 flex h-6 w-1 shrink-0 rounded-lg' />
)} )}
</ResizablePrimitive.Separator> </ResizablePrimitive.Separator>
) );
} }
export { ResizableHandle, ResizablePanel, ResizablePanelGroup } export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
+16 -16
View File
@@ -1,9 +1,9 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui" import { ScrollArea as ScrollAreaPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function ScrollArea({ function ScrollArea({
className, className,
@@ -12,44 +12,44 @@ function ScrollArea({
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) { }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return ( return (
<ScrollAreaPrimitive.Root <ScrollAreaPrimitive.Root
data-slot="scroll-area" data-slot='scroll-area'
className={cn("relative", className)} className={cn('relative', className)}
{...props} {...props}
> >
<ScrollAreaPrimitive.Viewport <ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport" data-slot='scroll-area-viewport'
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1" className='focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1'
> >
{children} {children}
</ScrollAreaPrimitive.Viewport> </ScrollAreaPrimitive.Viewport>
<ScrollBar /> <ScrollBar />
<ScrollAreaPrimitive.Corner /> <ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root> </ScrollAreaPrimitive.Root>
) );
} }
function ScrollBar({ function ScrollBar({
className, className,
orientation = "vertical", orientation = 'vertical',
...props ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) { }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return ( return (
<ScrollAreaPrimitive.ScrollAreaScrollbar <ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar" data-slot='scroll-area-scrollbar'
data-orientation={orientation} data-orientation={orientation}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent flex touch-none p-px transition-colors select-none", 'flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent',
className className
)} )}
{...props} {...props}
> >
<ScrollAreaPrimitive.ScrollAreaThumb <ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb" data-slot='scroll-area-thumb'
className="rounded-full bg-border relative flex-1" className='bg-border relative flex-1 rounded-full'
/> />
</ScrollAreaPrimitive.ScrollAreaScrollbar> </ScrollAreaPrimitive.ScrollAreaScrollbar>
) );
} }
export { ScrollArea, ScrollBar } export { ScrollArea, ScrollBar };
+25 -21
View File
@@ -1,9 +1,9 @@
"use client"; 'use client';
import { useMemo, useState } from "react"; import { useMemo, useState } from 'react';
import { Check, ChevronsUpDown } from "lucide-react"; import { Check, ChevronsUpDown } from 'lucide-react';
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils';
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button';
import { import {
Command, Command,
CommandEmpty, CommandEmpty,
@@ -11,8 +11,12 @@ import {
CommandInput, CommandInput,
CommandItem, CommandItem,
CommandList, CommandList,
} from "@/components/ui/command"; } from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
export type SearchableSelectOption = { export type SearchableSelectOption = {
value: string; value: string;
@@ -35,9 +39,9 @@ export default function SearchableSelect({
value, value,
onValueChange, onValueChange,
options, options,
placeholder = "Select option", placeholder = 'Select option',
searchPlaceholder = "Search...", searchPlaceholder = 'Search...',
emptyMessage = "No option found.", emptyMessage = 'No option found.',
disabled = false, disabled = false,
className, className,
}: SearchableSelectProps) { }: SearchableSelectProps) {
@@ -53,22 +57,22 @@ export default function SearchableSelect({
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant='outline'
role="combobox" role='combobox'
aria-expanded={open} aria-expanded={open}
disabled={disabled} disabled={disabled}
className={cn( className={cn(
"w-full justify-between font-normal", 'w-full justify-between font-normal',
!selectedOption && !value ? "text-muted-foreground" : "", !selectedOption && !value ? 'text-muted-foreground' : '',
className className
)} )}
type="button" type='button'
> >
<span className="truncate">{displayLabel}</span> <span className='truncate'>{displayLabel}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-(--radix-popover-trigger-width) p-0"> <PopoverContent className='w-(--radix-popover-trigger-width) p-0'>
<Command> <Command>
<CommandInput placeholder={searchPlaceholder} /> <CommandInput placeholder={searchPlaceholder} />
<CommandList> <CommandList>
@@ -84,11 +88,11 @@ export default function SearchableSelect({
setOpen(false); setOpen(false);
}} }}
> >
<span className="truncate">{option.label}</span> <span className='truncate'>{option.label}</span>
<Check <Check
className={cn( className={cn(
"ml-auto h-4 w-4", 'ml-auto h-4 w-4',
value === option.value ? "opacity-100" : "opacity-0" value === option.value ? 'opacity-100' : 'opacity-0'
)} )}
/> />
</CommandItem> </CommandItem>
+55 -46
View File
@@ -1,15 +1,15 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Select as SelectPrimitive } from "radix-ui" import { Select as SelectPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react" import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from 'lucide-react';
function Select({ function Select({
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) { }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} /> return <SelectPrimitive.Root data-slot='select' {...props} />;
} }
function SelectGroup({ function SelectGroup({
@@ -18,58 +18,63 @@ function SelectGroup({
}: React.ComponentProps<typeof SelectPrimitive.Group>) { }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return ( return (
<SelectPrimitive.Group <SelectPrimitive.Group
data-slot="select-group" data-slot='select-group'
className={cn("scroll-my-1 p-1", className)} className={cn('scroll-my-1 p-1', className)}
{...props} {...props}
/> />
) );
} }
function SelectValue({ function SelectValue({
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) { }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} /> return <SelectPrimitive.Value data-slot='select-value' {...props} />;
} }
function SelectTrigger({ function SelectTrigger({
className, className,
size = "default", size = 'default',
children, children,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & { }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default" size?: 'sm' | 'default';
}) { }) {
return ( return (
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
data-slot="select-trigger" data-slot='select-trigger'
data-size={size} data-size={size}
className={cn( className={cn(
"border-input data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-lg border bg-transparent py-2 pr-2 pl-2.5 text-sm transition-colors select-none focus-visible:ring-3 aria-invalid:ring-3 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0", "border-input data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 flex w-fit items-center justify-between gap-1.5 rounded-lg border bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<SelectPrimitive.Icon asChild> <SelectPrimitive.Icon asChild>
<ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" /> <ChevronDownIcon className='text-muted-foreground pointer-events-none size-4' />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
) );
} }
function SelectContent({ function SelectContent({
className, className,
children, children,
position = "item-aligned", position = 'item-aligned',
align = "center", align = 'center',
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) { }: React.ComponentProps<typeof SelectPrimitive.Content>) {
return ( return (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
data-slot="select-content" data-slot='select-content'
data-align-trigger={position === "item-aligned"} data-align-trigger={position === 'item-aligned'}
className={cn("bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-lg shadow-md ring-1 duration-100 relative z-50 max-h-(--radix-select-content-available-height) origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto data-[align-trigger=true]:animate-none", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )} className={cn(
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg shadow-md ring-1 duration-100 data-[align-trigger=true]:animate-none',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
)}
position={position} position={position}
align={align} align={align}
{...props} {...props}
@@ -78,8 +83,8 @@ function SelectContent({
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
data-position={position} data-position={position}
className={cn( className={cn(
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)", 'data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)',
position === "popper" && "" position === 'popper' && ''
)} )}
> >
{children} {children}
@@ -87,7 +92,7 @@ function SelectContent({
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
) );
} }
function SelectLabel({ function SelectLabel({
@@ -96,11 +101,11 @@ function SelectLabel({
}: React.ComponentProps<typeof SelectPrimitive.Label>) { }: React.ComponentProps<typeof SelectPrimitive.Label>) {
return ( return (
<SelectPrimitive.Label <SelectPrimitive.Label
data-slot="select-label" data-slot='select-label'
className={cn("text-muted-foreground px-1.5 py-1 text-xs", className)} className={cn('text-muted-foreground px-1.5 py-1 text-xs', className)}
{...props} {...props}
/> />
) );
} }
function SelectItem({ function SelectItem({
@@ -110,21 +115,21 @@ function SelectItem({
}: React.ComponentProps<typeof SelectPrimitive.Item>) { }: React.ComponentProps<typeof SelectPrimitive.Item>) {
return ( return (
<SelectPrimitive.Item <SelectPrimitive.Item
data-slot="select-item" data-slot='select-item'
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className className
)} )}
{...props} {...props}
> >
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center"> <span className='pointer-events-none absolute right-2 flex size-4 items-center justify-center'>
<SelectPrimitive.ItemIndicator> <SelectPrimitive.ItemIndicator>
<CheckIcon className="pointer-events-none" /> <CheckIcon className='pointer-events-none' />
</SelectPrimitive.ItemIndicator> </SelectPrimitive.ItemIndicator>
</span> </span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
) );
} }
function SelectSeparator({ function SelectSeparator({
@@ -133,11 +138,11 @@ function SelectSeparator({
}: React.ComponentProps<typeof SelectPrimitive.Separator>) { }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return ( return (
<SelectPrimitive.Separator <SelectPrimitive.Separator
data-slot="select-separator" data-slot='select-separator'
className={cn("bg-border -mx-1 my-1 h-px pointer-events-none", className)} className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
{...props} {...props}
/> />
) );
} }
function SelectScrollUpButton({ function SelectScrollUpButton({
@@ -146,14 +151,16 @@ function SelectScrollUpButton({
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) { }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return ( return (
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button" data-slot='select-scroll-up-button'
className={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4", className)} className={cn(
"bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props} {...props}
> >
<ChevronUpIcon <ChevronUpIcon />
/>
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpButton>
) );
} }
function SelectScrollDownButton({ function SelectScrollDownButton({
@@ -162,14 +169,16 @@ function SelectScrollDownButton({
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) { }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return ( return (
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button" data-slot='select-scroll-down-button'
className={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4", className)} className={cn(
"bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props} {...props}
> >
<ChevronDownIcon <ChevronDownIcon />
/>
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
) );
} }
export { export {
@@ -183,4 +192,4 @@ export {
SelectSeparator, SelectSeparator,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} };
+7 -7
View File
@@ -1,22 +1,22 @@
"use client"; 'use client';
import * as React from "react"; import * as React from 'react';
import { Separator as SeparatorPrimitive } from "radix-ui"; import { Separator as SeparatorPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils';
function Separator({ function Separator({
className, className,
orientation = "horizontal", orientation = 'horizontal',
decorative = true, decorative = true,
...props ...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) { }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return ( return (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
data-slot="separator-root" data-slot='separator-root'
decorative={decorative} decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
className className
)} )}
{...props} {...props}
+48 -39
View File
@@ -1,32 +1,32 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Dialog as SheetPrimitive } from "radix-ui" import { Dialog as SheetPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button';
import { XIcon } from "lucide-react" import { XIcon } from 'lucide-react';
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} /> return <SheetPrimitive.Root data-slot='sheet' {...props} />;
} }
function SheetTrigger({ function SheetTrigger({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) { }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> return <SheetPrimitive.Trigger data-slot='sheet-trigger' {...props} />;
} }
function SheetClose({ function SheetClose({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) { }: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> return <SheetPrimitive.Close data-slot='sheet-close' {...props} />;
} }
function SheetPortal({ function SheetPortal({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) { }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> return <SheetPrimitive.Portal data-slot='sheet-portal' {...props} />;
} }
function SheetOverlay({ function SheetOverlay({
@@ -35,65 +35,74 @@ function SheetOverlay({
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) { }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return ( return (
<SheetPrimitive.Overlay <SheetPrimitive.Overlay
data-slot="sheet-overlay" data-slot='sheet-overlay'
className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)} className={cn(
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/10 duration-100 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs',
className
)}
{...props} {...props}
/> />
) );
} }
function SheetContent({ function SheetContent({
className, className,
children, children,
side = "right", side = 'right',
showCloseButton = true, showCloseButton = true,
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & { }: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left" side?: 'top' | 'right' | 'bottom' | 'left';
showCloseButton?: boolean showCloseButton?: boolean;
}) { }) {
return ( return (
<SheetPortal> <SheetPortal>
<SheetOverlay /> <SheetOverlay />
<SheetPrimitive.Content <SheetPrimitive.Content
data-slot="sheet-content" data-slot='sheet-content'
data-side={side} data-side={side}
className={cn("bg-background data-open:animate-in data-closed:animate-out data-[side=right]:data-closed:slide-out-to-right-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm", className)} className={cn(
'bg-background data-open:animate-in data-closed:animate-out data-[side=right]:data-closed:slide-out-to-right-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm',
className
)}
{...props} {...props}
> >
{children} {children}
{showCloseButton && ( {showCloseButton && (
<SheetPrimitive.Close data-slot="sheet-close" asChild> <SheetPrimitive.Close data-slot='sheet-close' asChild>
<Button variant="ghost" className="absolute top-3 right-3" size="icon-sm"> <Button
<XIcon variant='ghost'
/> className='absolute top-3 right-3'
<span className="sr-only">Close</span> size='icon-sm'
>
<XIcon />
<span className='sr-only'>Close</span>
</Button> </Button>
</SheetPrimitive.Close> </SheetPrimitive.Close>
)} )}
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPortal>
) );
} }
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="sheet-header" data-slot='sheet-header'
className={cn("gap-0.5 p-4 flex flex-col", className)} className={cn('flex flex-col gap-0.5 p-4', className)}
{...props} {...props}
/> />
) );
} }
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="sheet-footer" data-slot='sheet-footer'
className={cn("gap-2 p-4 mt-auto flex flex-col", className)} className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props} {...props}
/> />
) );
} }
function SheetTitle({ function SheetTitle({
@@ -102,11 +111,11 @@ function SheetTitle({
}: React.ComponentProps<typeof SheetPrimitive.Title>) { }: React.ComponentProps<typeof SheetPrimitive.Title>) {
return ( return (
<SheetPrimitive.Title <SheetPrimitive.Title
data-slot="sheet-title" data-slot='sheet-title'
className={cn("text-foreground text-base font-medium", className)} className={cn('text-foreground text-base font-medium', className)}
{...props} {...props}
/> />
) );
} }
function SheetDescription({ function SheetDescription({
@@ -115,11 +124,11 @@ function SheetDescription({
}: React.ComponentProps<typeof SheetPrimitive.Description>) { }: React.ComponentProps<typeof SheetPrimitive.Description>) {
return ( return (
<SheetPrimitive.Description <SheetPrimitive.Description
data-slot="sheet-description" data-slot='sheet-description'
className={cn("text-muted-foreground text-sm", className)} className={cn('text-muted-foreground text-sm', className)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -131,4 +140,4 @@ export {
SheetFooter, SheetFooter,
SheetTitle, SheetTitle,
SheetDescription, SheetDescription,
} };
+260 -259
View File
@@ -1,55 +1,55 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { Slot } from "radix-ui" import { Slot } from 'radix-ui';
import { useIsMobile } from "@/hooks/use-mobile" import { useIsMobile } from '@/hooks/use-mobile';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button';
import { Input } from "@/components/ui/input" import { Input } from '@/components/ui/input';
import { Separator } from "@/components/ui/separator" 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,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip" } from '@/components/ui/tooltip';
import { PanelLeftIcon } from "lucide-react" import { PanelLeftIcon } from 'lucide-react';
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;
setOpenMobile: (open: boolean) => void setOpenMobile: (open: boolean) => void;
isMobile: boolean isMobile: boolean;
toggleSidebar: () => void toggleSidebar: () => void;
} };
const SidebarContext = React.createContext<SidebarContextProps | null>(null) 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;
} }
function SidebarProvider({ function SidebarProvider({
@@ -60,37 +60,37 @@ 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;
}) { }) {
const isMobile = useIsMobile() const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false) const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar. // This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component. // We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen) const [_open, _setOpen] = React.useState(defaultOpen);
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 {
_setOpen(openState) _setOpen(openState);
} }
// This sets the cookie to keep the sidebar state. // This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
}, },
[setOpenProp, open] [setOpenProp, open]
) );
// Helper to toggle the sidebar. // Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => { const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]) }, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar. // Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => { React.useEffect(() => {
@@ -99,18 +99,18 @@ function SidebarProvider({
event.key === SIDEBAR_KEYBOARD_SHORTCUT && event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey) (event.metaKey || event.ctrlKey)
) { ) {
event.preventDefault() event.preventDefault();
toggleSidebar() toggleSidebar();
} }
} };
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>(
() => ({ () => ({
@@ -123,21 +123,21 @@ function SidebarProvider({
toggleSidebar, toggleSidebar,
}), }),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
) );
return ( return (
<SidebarContext.Provider value={contextValue}> <SidebarContext.Provider value={contextValue}>
<div <div
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}
@@ -145,37 +145,37 @@ function SidebarProvider({
{children} {children}
</div> </div>
</SidebarContext.Provider> </SidebarContext.Provider>
) );
} }
function Sidebar({ function Sidebar({
side = "left", side = 'left',
variant = "sidebar", variant = 'sidebar',
collapsible = "offcanvas", collapsible = 'offcanvas',
className, className,
children, children,
dir, dir,
...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}
> >
{children} {children}
</div> </div>
) );
} }
if (isMobile) { if (isMobile) {
@@ -183,71 +183,71 @@ function Sidebar({
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent <SheetContent
dir={dir} dir={dir}
data-sidebar="sidebar" data-sidebar='sidebar'
data-slot="sidebar" data-slot='sidebar'
data-mobile="true" data-mobile='true'
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}
> >
<SheetHeader className="sr-only"> <SheetHeader className='sr-only'>
<SheetTitle>Sidebar</SheetTitle> <SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription> <SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader> </SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div> <div className='flex h-full w-full flex-col'>{children}</div>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
) );
} }
return ( return (
<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'
> >
{/* This is what handles the sidebar gap on desktop */} {/* This is what handles the sidebar gap on desktop */}
<div <div
data-slot="sidebar-gap" data-slot='sidebar-gap'
className={cn( className={cn(
"transition-[width] duration-200 ease-linear relative w-(--sidebar-width) bg-transparent", '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'
data-side={side} data-side={side}
className={cn( className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex", 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex',
// 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}
> >
<div <div
data-sidebar="sidebar" data-sidebar='sidebar'
data-slot="sidebar-inner" data-slot='sidebar-inner'
className="bg-sidebar group-data-[variant=floating]:ring-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 flex size-full flex-col" className='bg-sidebar group-data-[variant=floating]:ring-sidebar-border flex size-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1'
> >
{children} {children}
</div> </div>
</div> </div>
</div> </div>
) );
} }
function SidebarTrigger({ function SidebarTrigger({
@@ -255,63 +255,63 @@ function SidebarTrigger({
onClick, onClick,
...props ...props
}: React.ComponentProps<typeof Button>) { }: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar() const { toggleSidebar } = useSidebar();
return ( return (
<Button <Button
data-sidebar="trigger" data-sidebar='trigger'
data-slot="sidebar-trigger" data-slot='sidebar-trigger'
variant="ghost" variant='ghost'
size="icon-sm" size='icon-sm'
className={cn(className)} className={cn(className)}
onClick={(event) => { onClick={(event) => {
onClick?.(event) onClick?.(event);
toggleSidebar() toggleSidebar();
}} }}
{...props} {...props}
> >
<PanelLeftIcon /> <PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span> <span className='sr-only'>Toggle Sidebar</span>
</Button> </Button>
) );
} }
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
const { toggleSidebar } = useSidebar() const { toggleSidebar } = useSidebar();
return ( return (
<button <button
data-sidebar="rail" data-sidebar='rail'
data-slot="sidebar-rail" data-slot='sidebar-rail'
aria-label="Toggle Sidebar" aria-label='Toggle Sidebar'
tabIndex={-1} tabIndex={-1}
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 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2", 'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2',
"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}
/> />
) );
} }
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 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 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',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function SidebarInput({ function SidebarInput({
@@ -320,34 +320,34 @@ function SidebarInput({
}: React.ComponentProps<typeof Input>) { }: React.ComponentProps<typeof Input>) {
return ( return (
<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("gap-2 p-2 flex flex-col", 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("gap-2 p-2 flex flex-col", className)} className={cn('flex flex-col gap-2 p-2', className)}
{...props} {...props}
/> />
) );
} }
function SidebarSeparator({ function SidebarSeparator({
@@ -356,187 +356,185 @@ function SidebarSeparator({
}: React.ComponentProps<typeof Separator>) { }: React.ComponentProps<typeof Separator>) {
return ( return (
<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(
"no-scrollbar gap-0 flex min-h-0 flex-1 flex-col overflow-auto group-data-[collapsible=icon]:overflow-hidden", 'no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
className className
)} )}
{...props} {...props}
/> />
) );
} }
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( className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
"p-2 relative flex w-full min-w-0 flex-col",
className
)}
{...props} {...props}
/> />
) );
} }
function SidebarGroupLabel({ function SidebarGroupLabel({
className, className,
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) { }: React.ComponentProps<'div'> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "div" const Comp = asChild ? Slot.Root : '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 h-8 rounded-md px-2 text-xs font-medium transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 flex shrink-0 items-center outline-hidden [&>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 group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function SidebarGroupAction({ function SidebarGroupAction({
className, className,
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) { }: React.ComponentProps<'button'> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "button" const Comp = asChild ? Slot.Root : '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 w-5 rounded-md p-0 focus-visible:ring-2 [&>svg]:size-4 flex aspect-square items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>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 group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0',
className className
)} )}
{...props} {...props}
/> />
) );
} }
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("text-sm w-full", 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("gap-0 flex w-full min-w-0 flex-col", className)} className={cn('flex w-full min-w-0 flex-col gap-0', 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(
"ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-left text-sm transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-[active=true]:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden group/menu-button 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", 'ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-left text-sm transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-[active=true]:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden group/menu-button 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',
{ {
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: "bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", outline:
'bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] 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.Root : "button" const Comp = asChild ? Slot.Root : 'button';
const { isMobile, state } = useSidebar() const { isMobile, state } = useSidebar();
const button = ( const button = (
<Comp <Comp
data-slot="sidebar-menu-button" data-slot='sidebar-menu-button'
data-sidebar="menu-button" data-sidebar='menu-button'
data-size={size} data-size={size}
data-active={isActive ? "true" : undefined} data-active={isActive ? 'true' : undefined}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)} className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props} {...props}
/> />
) );
if (!tooltip) { if (!tooltip) {
return button return button;
} }
if (typeof tooltip === "string") { if (typeof tooltip === 'string') {
tooltip = { tooltip = {
children: tooltip, children: tooltip,
} };
} }
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger> <TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent <TooltipContent
side="right" side='right'
align="center" align='center'
hidden={state !== "collapsed" || isMobile} hidden={state !== 'collapsed' || isMobile}
{...tooltip} {...tooltip}
/> />
</Tooltip> </Tooltip>
) );
} }
function SidebarMenuAction({ function SidebarMenuAction({
@@ -544,133 +542,136 @@ 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.Root : "button" const Comp = asChild ? Slot.Root : '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 aspect-square w-5 rounded-md p-0 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 focus-visible:ring-2 [&>svg]:size-4 flex items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>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 group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0',
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 aria-expanded: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 aria-expanded:opacity-100 md:opacity-0',
className className
)} )}
{...props} {...props}
/> />
) );
} }
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 peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 h-5 min-w-5 rounded-md px-1 text-xs font-medium peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 flex items-center justify-center tabular-nums select-none group-data-[collapsible=icon]:hidden", 'text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-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 group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function SidebarMenuSkeleton({ 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%.
const [width] = React.useState(() => { const [width] = React.useState(() => {
return `${Math.floor(Math.random() * 40) + 50}%` return `${Math.floor(Math.random() * 40) + 50}%`;
}) });
return ( return (
<div <div
data-slot="sidebar-menu-skeleton" data-slot='sidebar-menu-skeleton'
data-sidebar="menu-skeleton" data-sidebar='menu-skeleton'
className={cn("h-8 gap-2 rounded-md px-2 flex items-center", className)} className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
{...props} {...props}
> >
{showIcon && ( {showIcon && (
<Skeleton <Skeleton
className="size-4 rounded-md" className='size-4 rounded-md'
data-sidebar="menu-skeleton-icon" data-sidebar='menu-skeleton-icon'
/> />
)} )}
<Skeleton <Skeleton
className="h-4 max-w-(--skeleton-width) flex-1" className='h-4 max-w-(--skeleton-width) flex-1'
data-sidebar="menu-skeleton-text" data-sidebar='menu-skeleton-text'
style={ style={
{ {
"--skeleton-width": width, '--skeleton-width': width,
} as React.CSSProperties } as React.CSSProperties
} }
/> />
</div> </div>
) );
} }
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("border-sidebar-border mx-3.5 translate-x-px gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden flex min-w-0 flex-col", className)} className={cn(
'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden',
className
)}
{...props} {...props}
/> />
) );
} }
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}
/> />
) );
} }
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.Root : "a" const Comp = asChild ? Slot.Root : 'a';
return ( return (
<Comp <Comp
data-slot="sidebar-menu-sub-button" data-slot='sidebar-menu-sub-button'
data-sidebar="menu-sub-button" data-sidebar='menu-sub-button'
data-size={size} data-size={size}
data-active={isActive ? "true" : undefined} data-active={isActive ? 'true' : undefined}
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 data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 focus-visible:ring-2 data-[size=md]:text-sm data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>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 data-[active=true]:bg-sidebar-accent data-[active=true]: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 group-data-[collapsible=icon]:hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
className className
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -698,4 +699,4 @@ export {
SidebarSeparator, SidebarSeparator,
SidebarTrigger, SidebarTrigger,
useSidebar, useSidebar,
} };
+6 -6
View File
@@ -1,13 +1,13 @@
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Skeleton({ className, ...props }: React.ComponentProps<"div">) { function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="skeleton" data-slot='skeleton'
className={cn("bg-muted rounded-md animate-pulse", className)} className={cn('bg-muted animate-pulse rounded-md', className)}
{...props} {...props}
/> />
) );
} }
export { Skeleton } export { Skeleton };
+15 -15
View File
@@ -1,9 +1,9 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Slider as SliderPrimitive } from "radix-ui" import { Slider as SliderPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Slider({ function Slider({
className, className,
@@ -21,39 +21,39 @@ function Slider({
? defaultValue ? defaultValue
: [min, max], : [min, max],
[value, defaultValue, min, max] [value, defaultValue, min, max]
) );
return ( return (
<SliderPrimitive.Root <SliderPrimitive.Root
data-slot="slider" data-slot='slider'
defaultValue={defaultValue} defaultValue={defaultValue}
value={value} value={value}
min={min} min={min}
max={max} max={max}
className={cn( className={cn(
"data-vertical:min-h-40 relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:w-auto data-vertical:flex-col", 'relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col',
className className
)} )}
{...props} {...props}
> >
<SliderPrimitive.Track <SliderPrimitive.Track
data-slot="slider-track" data-slot='slider-track'
className="bg-muted rounded-full data-horizontal:h-1 data-vertical:w-1 relative grow overflow-hidden data-horizontal:w-full data-vertical:h-full" className='bg-muted relative grow overflow-hidden rounded-full data-horizontal:h-1 data-horizontal:w-full data-vertical:h-full data-vertical:w-1'
> >
<SliderPrimitive.Range <SliderPrimitive.Range
data-slot="slider-range" data-slot='slider-range'
className="bg-primary absolute select-none data-horizontal:h-full data-vertical:w-full" className='bg-primary absolute select-none data-horizontal:h-full data-vertical:w-full'
/> />
</SliderPrimitive.Track> </SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => ( {Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb <SliderPrimitive.Thumb
data-slot="slider-thumb" data-slot='slider-thumb'
key={index} key={index}
className="border-ring ring-ring/50 relative size-3 rounded-full border bg-white transition-[color,box-shadow] after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-hidden active:ring-3 block shrink-0 select-none disabled:pointer-events-none disabled:opacity-50" className='border-ring ring-ring/50 relative block size-3 shrink-0 rounded-full border bg-white transition-[color,box-shadow] select-none after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-hidden active:ring-3 disabled:pointer-events-none disabled:opacity-50'
/> />
))} ))}
</SliderPrimitive.Root> </SliderPrimitive.Root>
) );
} }
export { Slider } export { Slider };
+38 -40
View File
@@ -1,68 +1,66 @@
"use client" 'use client';
import type { CSSProperties } from "react" import type { CSSProperties } from 'react';
import { useTheme } from "next-themes" import { useTheme } from 'next-themes';
import { Toaster as Sonner, type ToasterProps } from "sonner" import { Toaster as Sonner, type ToasterProps } from 'sonner';
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react" import {
CircleCheckIcon,
InfoIcon,
TriangleAlertIcon,
OctagonXIcon,
Loader2Icon,
} from 'lucide-react';
type SonnerTheme = NonNullable<ToasterProps["theme"]> type SonnerTheme = NonNullable<ToasterProps['theme']>;
function normalizeTheme(theme: string | undefined): SonnerTheme { function normalizeTheme(theme: string | undefined): SonnerTheme {
if (theme === "light" || theme === "dark" || theme === "system") { if (theme === 'light' || theme === 'dark' || theme === 'system') {
return theme return theme;
} }
return "system" return 'system';
} }
function resolveSystemTheme(theme: string | undefined): Exclude<SonnerTheme, "system"> { function resolveSystemTheme(
return theme === "light" ? "light" : "dark" theme: string | undefined
): Exclude<SonnerTheme, 'system'> {
return theme === 'light' ? 'light' : 'dark';
} }
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system", resolvedTheme } = useTheme() const { theme = 'system', resolvedTheme } = useTheme();
const normalizedTheme = normalizeTheme(theme) const normalizedTheme = normalizeTheme(theme);
const toasterTheme: ToasterProps["theme"] = const toasterTheme: ToasterProps['theme'] =
normalizedTheme === "system" normalizedTheme === 'system'
? resolveSystemTheme(resolvedTheme) ? resolveSystemTheme(resolvedTheme)
: normalizedTheme : normalizedTheme;
return ( return (
<Sonner <Sonner
theme={toasterTheme} theme={toasterTheme}
className="toaster group" className='toaster group'
icons={{ icons={{
success: ( success: <CircleCheckIcon className='size-4' />,
<CircleCheckIcon className="size-4" /> info: <InfoIcon className='size-4' />,
), warning: <TriangleAlertIcon className='size-4' />,
info: ( error: <OctagonXIcon className='size-4' />,
<InfoIcon className="size-4" /> loading: <Loader2Icon className='size-4 animate-spin' />,
),
warning: (
<TriangleAlertIcon className="size-4" />
),
error: (
<OctagonXIcon className="size-4" />
),
loading: (
<Loader2Icon className="size-4 animate-spin" />
),
}} }}
style={ style={
{ {
"--normal-bg": "var(--popover)", '--normal-bg': 'var(--popover)',
"--normal-text": "var(--popover-foreground)", '--normal-text': 'var(--popover-foreground)',
"--normal-border": "var(--border)", '--normal-border': 'var(--border)',
"--border-radius": "var(--radius)", '--border-radius': 'var(--radius)',
} as CSSProperties } as CSSProperties
} }
toastOptions={{ toastOptions={{
classNames: { classNames: {
toast: "cn-toast", toast: 'cn-toast',
}, },
}} }}
{...props} {...props}
/> />
) );
} };
export { Toaster } export { Toaster };
+11 -6
View File
@@ -1,10 +1,15 @@
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Loader2Icon } from "lucide-react" import { Loader2Icon } from 'lucide-react';
function Spinner({ className, ...props }: React.ComponentProps<"svg">) { function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
return ( return (
<Loader2Icon role="status" aria-label="Loading" className={cn("size-4 animate-spin", className)} {...props} /> <Loader2Icon
) role='status'
aria-label='Loading'
className={cn('size-4 animate-spin', className)}
{...props}
/>
);
} }
export { Spinner } export { Spinner };
+12 -12
View File
@@ -1,33 +1,33 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Switch as SwitchPrimitive } from "radix-ui" import { Switch as SwitchPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Switch({ function Switch({
className, className,
size = "default", size = 'default',
...props ...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & { }: React.ComponentProps<typeof SwitchPrimitive.Root> & {
size?: "sm" | "default" size?: 'sm' | 'default';
}) { }) {
return ( return (
<SwitchPrimitive.Root <SwitchPrimitive.Root
data-slot="switch" data-slot='switch'
data-size={size} data-size={size}
className={cn( className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 shrink-0 rounded-full border border-border/70 bg-muted/85 data-[state=checked]:border-primary/35 data-[state=checked]:bg-primary focus-visible:ring-3 aria-invalid:ring-3 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer group/switch relative inline-flex items-center transition-colors outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50", 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 border-border/70 bg-muted/85 data-[state=checked]:border-primary/35 data-[state=checked]:bg-primary peer group/switch relative inline-flex shrink-0 items-center rounded-full border transition-colors outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-3 aria-invalid:ring-3 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px]',
className className
)} )}
{...props} {...props}
> >
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
data-slot="switch-thumb" data-slot='switch-thumb'
className="rounded-full border border-border/45 bg-background data-[state=checked]:bg-primary-foreground group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-[state=checked]:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-[state=checked]:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-[state=unchecked]:translate-x-0 group-data-[size=sm]/switch:data-[state=unchecked]:translate-x-0 pointer-events-none block ring-0 shadow-[0_1px_2px_rgba(0,0,0,0.28)] transition-[transform,background-color,border-color]" className='border-border/45 bg-background data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full border shadow-[0_1px_2px_rgba(0,0,0,0.28)] ring-0 transition-[transform,background-color,border-color] group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-[state=checked]:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-[state=checked]:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-[state=unchecked]:translate-x-0 group-data-[size=sm]/switch:data-[state=unchecked]:translate-x-0'
/> />
</SwitchPrimitive.Root> </SwitchPrimitive.Root>
) );
} }
export { Switch } export { Switch };
+52 -37
View File
@@ -1,92 +1,107 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Table({ className, ...props }: React.ComponentProps<"table">) { function Table({ className, ...props }: React.ComponentProps<'table'>) {
return ( return (
<div data-slot="table-container" className="relative w-full overflow-x-auto"> <div
data-slot='table-container'
className='relative w-full overflow-x-auto'
>
<table <table
data-slot="table" data-slot='table'
className={cn("w-full caption-bottom text-sm", className)} className={cn('w-full caption-bottom text-sm', className)}
{...props} {...props}
/> />
</div> </div>
) );
} }
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
return ( return (
<thead <thead
data-slot="table-header" data-slot='table-header'
className={cn("[&_tr]:border-b", className)} className={cn('[&_tr]:border-b', className)}
{...props} {...props}
/> />
) );
} }
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
return ( return (
<tbody <tbody
data-slot="table-body" data-slot='table-body'
className={cn("[&_tr:last-child]:border-0", className)} className={cn('[&_tr:last-child]:border-0', className)}
{...props} {...props}
/> />
) );
} }
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
return ( return (
<tfoot <tfoot
data-slot="table-footer" data-slot='table-footer'
className={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)} className={cn(
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
className
)}
{...props} {...props}
/> />
) );
} }
function TableRow({ className, ...props }: React.ComponentProps<"tr">) { function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
return ( return (
<tr <tr
data-slot="table-row" data-slot='table-row'
className={cn("hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", className)} className={cn(
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
className
)}
{...props} {...props}
/> />
) );
} }
function TableHead({ className, ...props }: React.ComponentProps<"th">) { function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
return ( return (
<th <th
data-slot="table-head" data-slot='table-head'
className={cn("text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)} className={cn(
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0',
className
)}
{...props} {...props}
/> />
) );
} }
function TableCell({ className, ...props }: React.ComponentProps<"td">) { function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
return ( return (
<td <td
data-slot="table-cell" data-slot='table-cell'
className={cn("p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", className)} className={cn(
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0',
className
)}
{...props} {...props}
/> />
) );
} }
function TableCaption({ function TableCaption({
className, className,
...props ...props
}: React.ComponentProps<"caption">) { }: React.ComponentProps<'caption'>) {
return ( return (
<caption <caption
data-slot="table-caption" data-slot='table-caption'
className={cn("text-muted-foreground mt-4 text-sm", className)} className={cn('text-muted-foreground mt-4 text-sm', className)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -98,4 +113,4 @@ export {
TableRow, TableRow,
TableCell, TableCell,
TableCaption, TableCaption,
} };
+26 -26
View File
@@ -1,58 +1,58 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { Tabs as TabsPrimitive } from "radix-ui" import { Tabs as TabsPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Tabs({ function Tabs({
className, className,
orientation = "horizontal", orientation = 'horizontal',
...props ...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) { }: React.ComponentProps<typeof TabsPrimitive.Root>) {
return ( return (
<TabsPrimitive.Root <TabsPrimitive.Root
data-slot="tabs" data-slot='tabs'
data-orientation={orientation} data-orientation={orientation}
className={cn( className={cn(
"group/tabs flex w-full flex-col gap-2 data-[orientation=vertical]:flex-row", 'group/tabs flex w-full flex-col gap-2 data-[orientation=vertical]:flex-row',
className className
)} )}
{...props} {...props}
/> />
) );
} }
const tabsListVariants = cva( const tabsListVariants = cva(
"rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col", 'rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col',
{ {
variants: { variants: {
variant: { variant: {
default: "bg-muted", default: 'bg-muted',
line: "h-auto w-full justify-start gap-1 border-b bg-transparent p-0", line: 'h-auto w-full justify-start gap-1 border-b bg-transparent p-0',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
}, },
} }
) );
function TabsList({ function TabsList({
className, className,
variant = "default", variant = 'default',
...props ...props
}: React.ComponentProps<typeof TabsPrimitive.List> & }: React.ComponentProps<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>) { VariantProps<typeof tabsListVariants>) {
return ( return (
<TabsPrimitive.List <TabsPrimitive.List
data-slot="tabs-list" data-slot='tabs-list'
data-variant={variant} data-variant={variant}
className={cn(tabsListVariants({ variant }), className)} className={cn(tabsListVariants({ variant }), className)}
{...props} {...props}
/> />
) );
} }
function TabsTrigger({ function TabsTrigger({
@@ -61,16 +61,16 @@ function TabsTrigger({
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) { }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return ( return (
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
data-slot="tabs-trigger" data-slot='tabs-trigger'
className={cn( className={cn(
"gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium group-data-[variant=default]/tabs-list:data-[state=active]:bg-background group-data-[variant=default]/tabs-list:data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=default]/tabs-list:dark:data-[state=active]:border-input group-data-[variant=default]/tabs-list:dark:data-[state=active]:bg-input/30 group-data-[variant=default]/tabs-list:dark:data-[state=active]:text-foreground group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] items-center justify-center whitespace-nowrap transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "group-data-[variant=default]/tabs-list:data-[state=active]:bg-background group-data-[variant=default]/tabs-list:data-[state=active]:text-foreground group-data-[variant=default]/tabs-list:dark:data-[state=active]:border-input group-data-[variant=default]/tabs-list:dark:data-[state=active]:bg-input/30 group-data-[variant=default]/tabs-list:dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:h-10 group-data-[variant=line]/tabs-list:flex-none group-data-[variant=line]/tabs-list:rounded-none group-data-[variant=line]/tabs-list:border-x-0 group-data-[variant=line]/tabs-list:border-t-0 group-data-[variant=line]/tabs-list:border-b-2 group-data-[variant=line]/tabs-list:border-b-transparent group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:px-3 group-data-[variant=line]/tabs-list:pb-2.5 group-data-[variant=line]/tabs-list:pt-2 group-data-[variant=line]/tabs-list:data-[state=active]:border-b-foreground group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:dark:data-[state=active]:border-b-foreground", 'group-data-[variant=line]/tabs-list:data-[state=active]:border-b-foreground group-data-[variant=line]/tabs-list:dark:data-[state=active]:border-b-foreground group-data-[variant=line]/tabs-list:h-10 group-data-[variant=line]/tabs-list:flex-none group-data-[variant=line]/tabs-list:rounded-none group-data-[variant=line]/tabs-list:border-x-0 group-data-[variant=line]/tabs-list:border-t-0 group-data-[variant=line]/tabs-list:border-b-2 group-data-[variant=line]/tabs-list:border-b-transparent group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:px-3 group-data-[variant=line]/tabs-list:pt-2 group-data-[variant=line]/tabs-list:pb-2.5 group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent',
"group-data-[variant=line]/tabs-list:data-[state=active]:text-foreground", 'group-data-[variant=line]/tabs-list:data-[state=active]:text-foreground',
className className
)} )}
{...props} {...props}
/> />
) );
} }
function TabsContent({ function TabsContent({
@@ -79,11 +79,11 @@ function TabsContent({
}: React.ComponentProps<typeof TabsPrimitive.Content>) { }: React.ComponentProps<typeof TabsPrimitive.Content>) {
return ( return (
<TabsPrimitive.Content <TabsPrimitive.Content
data-slot="tabs-content" data-slot='tabs-content'
className={cn("text-sm flex-1 outline-none", className)} className={cn('flex-1 text-sm outline-none', className)}
{...props} {...props}
/> />
) );
} }
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
+7 -7
View File
@@ -1,18 +1,18 @@
import * as React from "react" import * as React from 'react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return ( return (
<textarea <textarea
data-slot="textarea" data-slot='textarea'
className={cn( className={cn(
"dark:bg-input/30 border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 min-h-16 w-full rounded-lg border bg-transparent px-2.5 py-2 text-base transition-colors outline-none focus-visible:ring-2 focus-visible:ring-inset aria-invalid:ring-2 aria-invalid:ring-inset md:text-sm box-border disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", 'dark:bg-input/30 border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 box-border min-h-16 w-full rounded-lg border bg-transparent px-2.5 py-2 text-base transition-colors outline-none focus-visible:ring-2 focus-visible:ring-inset disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-2 aria-invalid:ring-inset md:text-sm',
className className
)} )}
{...props} {...props}
/> />
) );
} }
export { Textarea } export { Textarea };
+26 -26
View File
@@ -1,47 +1,47 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { type VariantProps } from "class-variance-authority" import { type VariantProps } from 'class-variance-authority';
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui" import { ToggleGroup as ToggleGroupPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { toggleVariants } from "@/components/ui/toggle" import { toggleVariants } from '@/components/ui/toggle';
const ToggleGroupContext = React.createContext< const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & { VariantProps<typeof toggleVariants> & {
spacing?: number spacing?: number;
orientation?: "horizontal" | "vertical" orientation?: 'horizontal' | 'vertical';
} }
>({ >({
size: "default", size: 'default',
variant: "default", variant: 'default',
spacing: 0, spacing: 0,
orientation: "horizontal", orientation: 'horizontal',
}) });
function ToggleGroup({ function ToggleGroup({
className, className,
variant, variant,
size, size,
spacing = 0, spacing = 0,
orientation = "horizontal", orientation = 'horizontal',
children, children,
...props ...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> & }: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants> & { VariantProps<typeof toggleVariants> & {
spacing?: number spacing?: number;
orientation?: "horizontal" | "vertical" orientation?: 'horizontal' | 'vertical';
}) { }) {
return ( return (
<ToggleGroupPrimitive.Root <ToggleGroupPrimitive.Root
data-slot="toggle-group" data-slot='toggle-group'
data-variant={variant} data-variant={variant}
data-size={size} data-size={size}
data-spacing={spacing} data-spacing={spacing}
data-orientation={orientation} data-orientation={orientation}
style={{ "--gap": spacing } as React.CSSProperties} style={{ '--gap': spacing } as React.CSSProperties}
className={cn( className={cn(
"rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-vertical:flex-col data-vertical:items-stretch", 'group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-vertical:flex-col data-vertical:items-stretch data-[size=sm]:rounded-[min(var(--radius-md),10px)]',
className className
)} )}
{...props} {...props}
@@ -52,27 +52,27 @@ function ToggleGroup({
{children} {children}
</ToggleGroupContext.Provider> </ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root> </ToggleGroupPrimitive.Root>
) );
} }
function ToggleGroupItem({ function ToggleGroupItem({
className, className,
children, children,
variant = "default", variant = 'default',
size = "default", size = 'default',
...props ...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) { VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext) const context = React.useContext(ToggleGroupContext);
return ( return (
<ToggleGroupPrimitive.Item <ToggleGroupPrimitive.Item
data-slot="toggle-group-item" data-slot='toggle-group-item'
data-variant={context.variant || variant} data-variant={context.variant || variant}
data-size={context.size || size} data-size={context.size || size}
data-spacing={context.spacing} data-spacing={context.spacing}
className={cn( className={cn(
"group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg shrink-0 focus:z-10 focus-visible:z-10 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t", 'shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t',
toggleVariants({ toggleVariants({
variant: context.variant || variant, variant: context.variant || variant,
size: context.size || size, size: context.size || size,
@@ -83,7 +83,7 @@ function ToggleGroupItem({
> >
{children} {children}
</ToggleGroupPrimitive.Item> </ToggleGroupPrimitive.Item>
) );
} }
export { ToggleGroup, ToggleGroupItem } export { ToggleGroup, ToggleGroupItem };
+18 -18
View File
@@ -1,46 +1,46 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from 'class-variance-authority';
import { Toggle as TogglePrimitive } from "radix-ui" import { Toggle as TogglePrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
const toggleVariants = cva( const toggleVariants = cva(
"hover:text-foreground aria-pressed:bg-muted focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[state=on]:bg-muted gap-1 rounded-lg text-sm font-medium transition-all [&_svg:not([class*='size-'])]:size-4 group/toggle hover:bg-muted inline-flex items-center justify-center whitespace-nowrap outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "hover:text-foreground aria-pressed:bg-muted focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[state=on]:bg-muted gap-1 rounded-lg text-sm font-medium transition-all [&_svg:not([class*='size-'])]:size-4 group/toggle hover:bg-muted inline-flex items-center justify-center whitespace-nowrap outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-transparent", default: 'bg-transparent',
outline: "border-input hover:bg-muted border bg-transparent", outline: 'border-input hover:bg-muted border bg-transparent',
}, },
size: { size: {
default: "h-8 min-w-8 px-2", default: 'h-8 min-w-8 px-2',
sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-1.5 text-[0.8rem]", sm: 'h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-1.5 text-[0.8rem]',
lg: "h-9 min-w-9 px-2.5", lg: 'h-9 min-w-9 px-2.5',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default',
}, },
} }
) );
function Toggle({ function Toggle({
className, className,
variant = "default", variant = 'default',
size = "default", size = 'default',
...props ...props
}: React.ComponentProps<typeof TogglePrimitive.Root> & }: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) { VariantProps<typeof toggleVariants>) {
return ( return (
<TogglePrimitive.Root <TogglePrimitive.Root
data-slot="toggle" data-slot='toggle'
className={cn(toggleVariants({ variant, size, className }))} className={cn(toggleVariants({ variant, size, className }))}
{...props} {...props}
/> />
) );
} }
export { Toggle, toggleVariants } export { Toggle, toggleVariants };
+13 -13
View File
@@ -1,9 +1,9 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import { Tooltip as TooltipPrimitive } from "radix-ui" import { Tooltip as TooltipPrimitive } from 'radix-ui';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function TooltipProvider({ function TooltipProvider({
delayDuration = 0, delayDuration = 0,
@@ -11,23 +11,23 @@ function TooltipProvider({
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) { }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return ( return (
<TooltipPrimitive.Provider <TooltipPrimitive.Provider
data-slot="tooltip-provider" data-slot='tooltip-provider'
delayDuration={delayDuration} delayDuration={delayDuration}
{...props} {...props}
/> />
) );
} }
function Tooltip({ function Tooltip({
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) { }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} /> return <TooltipPrimitive.Root data-slot='tooltip' {...props} />;
} }
function TooltipTrigger({ function TooltipTrigger({
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} /> return <TooltipPrimitive.Trigger data-slot='tooltip-trigger' {...props} />;
} }
function TooltipContent({ function TooltipContent({
@@ -39,19 +39,19 @@ function TooltipContent({
return ( return (
<TooltipPrimitive.Portal> <TooltipPrimitive.Portal>
<TooltipPrimitive.Content <TooltipPrimitive.Content
data-slot="tooltip-content" data-slot='tooltip-content'
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 rounded-md px-3 py-1.5 text-xs bg-foreground text-background z-50 w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin)", 'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-foreground text-background z-50 w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs',
className className
)} )}
{...props} {...props}
> >
{children} {children}
<TooltipPrimitive.Arrow className="size-2.5 rotate-45 rounded-[2px] bg-foreground fill-foreground z-50 translate-y-[calc(-50%_-_2px)]" /> <TooltipPrimitive.Arrow className='bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]' />
</TooltipPrimitive.Content> </TooltipPrimitive.Content>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
) );
} }
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
+22 -14
View File
@@ -5,13 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ExpandIcon, Minimize2Icon } from 'lucide-react'; import { ExpandIcon, Minimize2Icon } from 'lucide-react';
import { import { Area, AreaChart, XAxis, YAxis, CartesianGrid } from 'recharts';
Area,
AreaChart,
XAxis,
YAxis,
CartesianGrid,
} from 'recharts';
import { import {
ChartConfig, ChartConfig,
ChartContainer, ChartContainer,
@@ -95,7 +89,8 @@ export function UsageMetricsChart({
const metricChips = dataKeys.map((dataKey) => { const metricChips = dataKeys.map((dataKey) => {
const numericValue = data.reduce((sum, point) => { const numericValue = data.reduce((sum, point) => {
const rawValue = point?.[dataKey.key]; const rawValue = point?.[dataKey.key];
const value = typeof rawValue === 'number' ? rawValue : Number(rawValue || 0); const value =
typeof rawValue === 'number' ? rawValue : Number(rawValue || 0);
return Number.isFinite(value) ? sum + value : sum; return Number.isFinite(value) ? sum + value : sum;
}, 0); }, 0);
@@ -159,13 +154,19 @@ export function UsageMetricsChart({
return ( return (
<div ref={containerRef}> <div ref={containerRef}>
<Card className={cn(isFullscreen && 'h-full rounded-none border-0 ring-0')}> <Card
className={cn(isFullscreen && 'h-full rounded-none border-0 ring-0')}
>
<CardHeader className='space-y-3 sm:space-y-4'> <CardHeader className='space-y-3 sm:space-y-4'>
{tabs && activeTabId && onTabChange ? ( {tabs && activeTabId && onTabChange ? (
<Tabs value={activeTabId} onValueChange={onTabChange} className='w-full'> <Tabs
value={activeTabId}
onValueChange={onTabChange}
className='w-full'
>
<TabsList <TabsList
variant='line' variant='line'
className='max-w-full overflow-x-auto whitespace-nowrap border-b-0 pb-1' className='max-w-full overflow-x-auto border-b-0 pb-1 whitespace-nowrap'
> >
{tabs.map((tab) => ( {tabs.map((tab) => (
<TabsTrigger key={tab.id} value={tab.id}> <TabsTrigger key={tab.id} value={tab.id}>
@@ -197,7 +198,9 @@ export function UsageMetricsChart({
<ExpandIcon className='h-4 w-4' /> <ExpandIcon className='h-4 w-4' />
)} )}
<span className='sr-only'> <span className='sr-only'>
{isFullscreen ? 'Exit fullscreen chart' : 'Enter fullscreen chart'} {isFullscreen
? 'Exit fullscreen chart'
: 'Enter fullscreen chart'}
</span> </span>
</Button> </Button>
</div> </div>
@@ -222,7 +225,10 @@ export function UsageMetricsChart({
onClick={() => toggleSeries(metric.key)} onClick={() => toggleSeries(metric.key)}
> >
<span <span
className={cn('size-2 rounded-full', hidden && 'opacity-35')} className={cn(
'size-2 rounded-full',
hidden && 'opacity-35'
)}
style={{ backgroundColor: metric.color }} style={{ backgroundColor: metric.color }}
/> />
<span className='max-w-[9rem] truncate sm:max-w-none'> <span className='max-w-[9rem] truncate sm:max-w-none'>
@@ -231,7 +237,9 @@ export function UsageMetricsChart({
<span <span
className={cn( className={cn(
'text-xs', 'text-xs',
hidden ? 'text-muted-foreground/45' : 'text-muted-foreground' hidden
? 'text-muted-foreground/45'
: 'text-muted-foreground'
)} )}
> >
{formatMetricValue(metric.value)} {formatMetricValue(metric.value)}
+1 -1
View File
@@ -126,7 +126,7 @@ export function UsageSummaryCards({ summary }: UsageSummaryCardsProps) {
</span> </span>
</CardHeader> </CardHeader>
<CardContent className='pt-0'> <CardContent className='pt-0'>
<div className='break-words text-lg font-semibold tracking-tight sm:text-2xl'> <div className='text-lg font-semibold tracking-tight break-words sm:text-2xl'>
{card.value} {card.value}
</div> </div>
</CardContent> </CardContent>
+6 -3
View File
@@ -142,7 +142,11 @@ export function WithdrawModal({
</> </>
)} )}
</Button> </Button>
<Button onClick={() => onOpenChange(false)} variant='outline' className='w-full sm:w-auto'> <Button
onClick={() => onOpenChange(false)}
variant='outline'
className='w-full sm:w-auto'
>
Close Close
</Button> </Button>
</div> </div>
@@ -220,8 +224,7 @@ export function WithdrawModal({
<Alert variant='destructive'> <Alert variant='destructive'>
<AlertCircle className='h-5 w-5' /> <AlertCircle className='h-5 w-5' />
<AlertDescription> <AlertDescription>
Warning: Withdrawing more than your balance will use user Warning: Withdrawing more than your balance will use user funds.
funds.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
+7 -7
View File
@@ -1,20 +1,20 @@
import nextCoreWebVitals from "eslint-config-next/core-web-vitals" import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
import nextTypescript from "eslint-config-next/typescript" import nextTypescript from 'eslint-config-next/typescript';
const eslintConfig = [ const eslintConfig = [
...nextCoreWebVitals, ...nextCoreWebVitals,
...nextTypescript, ...nextTypescript,
{ {
rules: { rules: {
"react-hooks/set-state-in-effect": "off", 'react-hooks/set-state-in-effect': 'off',
"react-hooks/incompatible-library": "off", 'react-hooks/incompatible-library': 'off',
}, },
settings: { settings: {
react: { react: {
version: "19.2", version: '19.2',
}, },
}, },
}, },
] ];
export default eslintConfig export default eslintConfig;

Some files were not shown because too many files have changed in this diff Show More