refactor(ui): Improve usage log filter responsiveness and mobile UX

Refactor the usage log filter toolbar into a shared reusable component for common, drawing, and task logs. Optimize desktop filters with a responsive grid, move secondary filters into a mobile drawer, standardize filter typography, remove redundant filter icons, and add the missing i18n translations for the new drawer description.
This commit is contained in:
t0ng7u
2026-05-25 05:35:44 +08:00
parent b302be30e3
commit 583da45296
79 changed files with 1879 additions and 1262 deletions
+11 -5
View File
@@ -53,6 +53,12 @@ import {
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet'
import {
sideDrawerContentClassName,
sideDrawerFooterClassName,
sideDrawerFormClassName,
sideDrawerHeaderClassName,
} from '@/components/drawer-layout'
import { useSidebar } from './ui/sidebar'
const Item = RadioPrimitive.Root
@@ -88,14 +94,14 @@ export function ConfigDrawer() {
>
<Palette className='size-[1.2rem]' aria-hidden='true' />
</SheetTrigger>
<SheetContent className='flex w-full flex-col sm:max-w-md'>
<SheetHeader className='pb-0 text-start'>
<SheetContent className={sideDrawerContentClassName('sm:max-w-md')}>
<SheetHeader className={sideDrawerHeaderClassName()}>
<SheetTitle>{t('Theme Settings')}</SheetTitle>
<SheetDescription id='config-drawer-description'>
{t('Adjust the appearance and layout to suit your preferences.')}
</SheetDescription>
</SheetHeader>
<div className='space-y-6 overflow-y-auto px-4'>
<div className={sideDrawerFormClassName()}>
<ThemeConfig />
<PresetConfig />
<RadiusConfig />
@@ -105,7 +111,7 @@ export function ConfigDrawer() {
<ContentLayoutConfig />
<DirConfig />
</div>
<SheetFooter className='gap-2'>
<SheetFooter className={sideDrawerFooterClassName('grid-cols-1')}>
<Button
variant='destructive'
onClick={handleReset}
@@ -302,7 +308,7 @@ const RADIUS_OPTIONS: {
// CSS border-radius value used to render the visual preview corner.
preview: string
}[] = [
{ value: 'default', label: 'Auto', preview: '999px' },
{ value: 'default', label: 'Auto', preview: '1rem' },
{ value: 'none', label: '0', preview: '0' },
{ value: 'sm', label: '0.3', preview: '0.3rem' },
{ value: 'md', label: '0.5', preview: '0.5rem' },
+105
View File
@@ -0,0 +1,105 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { createElement, type ReactNode } from 'react'
import { cn } from '@/lib/utils'
export const sideDrawerContentClassName = (className?: string) =>
cn(
'bg-background text-foreground flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 shadow-none',
className
)
export const sideDrawerHeaderClassName = (className?: string) =>
cn(
'border-border/70 bg-background/95 border-b px-4 py-3 text-start backdrop-blur supports-[backdrop-filter]:bg-background/80 sm:px-6 sm:py-4',
className
)
export const sideDrawerFormClassName = (className?: string) =>
cn(
'flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto overscroll-contain px-4 py-4 sm:px-6 sm:py-5',
className
)
export const sideDrawerFooterClassName = (className?: string) =>
cn(
'border-border/70 bg-background/95 grid grid-cols-2 gap-2 border-t px-4 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80 sm:flex sm:flex-row sm:justify-end sm:px-6 sm:py-4',
className
)
export const sideDrawerSectionClassName = (className?: string) =>
cn(
'border-border/60 flex flex-col gap-4 border-b pb-6 last:border-b-0 last:pb-0',
className
)
export const sideDrawerSwitchItemClassName = (className?: string) =>
cn(
'border-border/60 flex min-h-16 flex-row items-center justify-between gap-3 border-y py-3',
className
)
export function SideDrawerSection(props: {
children: ReactNode
className?: string
}) {
return createElement(
'section',
{ className: sideDrawerSectionClassName(props.className) },
props.children
)
}
export function SideDrawerSectionHeader(props: {
title: ReactNode
description?: ReactNode
icon?: ReactNode
className?: string
}) {
return createElement(
'div',
{ className: cn('flex items-start gap-3', props.className) },
props.icon
? createElement(
'span',
{
className:
'bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-md',
},
props.icon
)
: null,
createElement(
'div',
{ className: 'min-w-0 flex-1' },
createElement(
'h3',
{ className: 'text-sm leading-none font-semibold tracking-tight' },
props.title
),
props.description
? createElement(
'p',
{ className: 'text-muted-foreground mt-1 text-xs leading-5' },
props.description
)
: null
)
)
}
+4 -5
View File
@@ -31,12 +31,12 @@ type GroupBadgeProps = Omit<
function getGroupRatioClassName(ratio: number): string {
if (ratio > 1) {
return 'border-warning/25 bg-warning/10 text-warning'
return 'bg-warning/10 text-warning'
}
if (ratio < 1) {
return 'border-info/25 bg-info/10 text-info'
return 'bg-info/10 text-info'
}
return 'border-border bg-muted text-muted-foreground'
return 'bg-muted text-muted-foreground'
}
function getGroupLabel(params: {
@@ -94,11 +94,10 @@ export function GroupBadge(props: GroupBadgeProps) {
{badge}
<span
className={cn(
'inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 font-mono text-[11px] leading-none tabular-nums',
'inline-flex h-6 items-center rounded-full px-2 font-mono text-sm leading-none font-medium tabular-nums',
getGroupRatioClassName(ratio)
)}
>
<span className='size-1 rounded-full bg-current opacity-60' />
<span>{ratio}x</span>
</span>
</span>
+9 -18
View File
@@ -20,8 +20,7 @@ import { useNotifications } from '@/hooks/use-notifications'
import { useTopNavLinks } from '@/hooks/use-top-nav-links'
import { ConfigDrawer } from '@/components/config-drawer'
import { LanguageSwitcher } from '@/components/language-switcher'
import { NotificationButton } from '@/components/notification-button'
import { NotificationDialog } from '@/components/notification-dialog'
import { NotificationPopover } from '@/components/notification-popover'
import { ProfileDropdown } from '@/components/profile-dropdown'
import { Search } from '@/components/search'
import { defaultTopNavLinks } from '../config/top-nav.config'
@@ -128,9 +127,15 @@ export function AppHeader({
)}
{showSearch && <Search />}
{showNotifications && (
<NotificationButton
<NotificationPopover
open={notifications.popoverOpen}
onOpenChange={notifications.setPopoverOpen}
unreadCount={notifications.unreadCount}
onClick={() => notifications.openDialog()}
activeTab={notifications.activeTab}
onTabChange={notifications.setActiveTab}
notice={notifications.notice}
announcements={notifications.announcements}
loading={notifications.loading}
/>
)}
<LanguageSwitcher />
@@ -139,20 +144,6 @@ export function AppHeader({
</div>
)}
</Header>
{/* Notification Dialog */}
{showNotifications && (
<NotificationDialog
open={notifications.dialogOpen}
onOpenChange={notifications.setDialogOpen}
activeTab={notifications.activeTab}
onTabChange={notifications.setActiveTab}
notice={notifications.notice}
announcements={notifications.announcements}
loading={notifications.loading}
onCloseToday={notifications.closeToday}
/>
)}
</>
)
}
@@ -35,8 +35,7 @@ import {
} from '@/components/ui/dialog'
import { Skeleton } from '@/components/ui/skeleton'
import { LanguageSwitcher } from '@/components/language-switcher'
import { NotificationButton } from '@/components/notification-button'
import { NotificationDialog } from '@/components/notification-dialog'
import { NotificationPopover } from '@/components/notification-popover'
import { ProfileDropdown } from '@/components/profile-dropdown'
import { ThemeSwitch } from '@/components/theme-switch'
import { defaultTopNavLinks } from '../config/top-nav.config'
@@ -271,9 +270,15 @@ export function PublicHeader(props: PublicHeaderProps) {
{showLanguageSwitcher && <LanguageSwitcher />}
{showThemeSwitch && <ThemeSwitch />}
{showNotifications && (
<NotificationButton
<NotificationPopover
open={notifications.popoverOpen}
onOpenChange={notifications.setPopoverOpen}
unreadCount={notifications.unreadCount}
onClick={() => notifications.openDialog()}
activeTab={notifications.activeTab}
onTabChange={notifications.setActiveTab}
notice={notifications.notice}
announcements={notifications.announcements}
loading={notifications.loading}
/>
)}
@@ -445,20 +450,6 @@ export function PublicHeader(props: PublicHeaderProps) {
</DialogFooter>
</DialogContent>
</Dialog>
{/* Notification Dialog */}
{showNotifications && (
<NotificationDialog
open={notifications.dialogOpen}
onOpenChange={notifications.setDialogOpen}
activeTab={notifications.activeTab}
onTabChange={notifications.setActiveTab}
notice={notifications.notice}
announcements={notifications.announcements}
loading={notifications.loading}
onCloseToday={notifications.closeToday}
/>
)}
</>
)
}
-63
View File
@@ -1,63 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { Bell } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
interface NotificationButtonProps {
unreadCount: number
onClick: () => void
className?: string
}
/**
* Notification bell button with unread badge
* Displays in the app header next to theme switch and profile dropdown
*/
export function NotificationButton({
unreadCount,
onClick,
className,
}: NotificationButtonProps) {
const { t } = useTranslation()
return (
<div className='relative'>
<Button
variant='ghost'
size='icon'
onClick={onClick}
className={cn('h-9 w-9', className)}
aria-label={t('Notifications')}
>
<Bell className='size-[1.2rem]' />
</Button>
{unreadCount > 0 && (
<Badge
variant='destructive'
className='absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center px-1 text-[10px] font-semibold tabular-nums'
>
{unreadCount > 99 ? '99+' : unreadCount}
</Badge>
)}
</div>
)
}
@@ -22,15 +22,23 @@ import { useTranslation } from 'react-i18next'
import { getAnnouncementColorClass } from '@/lib/colors'
import { formatDateTimeObject } from '@/lib/time'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
import { Markdown } from '@/components/ui/markdown'
import {
Popover,
PopoverContent,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from '@/components/ui/popover'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
@@ -42,15 +50,16 @@ interface AnnouncementItem {
publishDate?: string | Date
}
interface NotificationDialogProps {
interface NotificationPopoverProps {
open: boolean
onOpenChange: (open: boolean) => void
unreadCount: number
activeTab: 'notice' | 'announcements'
onTabChange: (tab: 'notice' | 'announcements') => void
notice: string
announcements: AnnouncementItem[]
loading: boolean
onCloseToday: () => void
className?: string
}
/**
@@ -113,7 +122,7 @@ function AnnouncementDot({ type }: { type?: string }) {
return (
<span
className={cn(
'mt-1.5 inline-block h-2 w-2 shrink-0 rounded-full',
'mt-1.5 inline-block size-2 shrink-0 rounded-full',
getAnnouncementColorClass(type)
)}
/>
@@ -123,11 +132,25 @@ function AnnouncementDot({ type }: { type?: string }) {
/**
* Empty state component
*/
function EmptyState({ message }: { message: string }) {
function EmptyState({
icon,
title,
description,
}: {
icon: React.ReactNode
title: string
description?: string
}) {
return (
<div className='flex flex-col items-center justify-center py-12 text-center'>
<p className='text-muted-foreground text-sm'>{message}</p>
</div>
<Empty className='min-h-48 border-0 p-4'>
<EmptyHeader>
<EmptyMedia variant='icon'>{icon}</EmptyMedia>
<EmptyTitle>{title}</EmptyTitle>
{description ? (
<EmptyDescription>{description}</EmptyDescription>
) : null}
</EmptyHeader>
</Empty>
)
}
@@ -144,15 +167,23 @@ function NoticeContent({
t: TFunction
}) {
if (loading) {
return <EmptyState message={t('Loading...')} />
return (
<EmptyState
icon={<Bell />}
title={t('Loading...')}
description={t('Latest platform updates and notices')}
/>
)
}
if (!notice) {
return <EmptyState message={t('No announcements at this time')} />
return (
<EmptyState icon={<Bell />} title={t('No announcements at this time')} />
)
}
return (
<ScrollArea className='h-[50vh] pr-4'>
<ScrollArea className='h-[min(52vh,28rem)] pr-3'>
<Markdown>{notice}</Markdown>
</ScrollArea>
)
@@ -171,16 +202,24 @@ function AnnouncementsContent({
t: TFunction
}) {
if (loading) {
return <EmptyState message={t('Loading...')} />
return (
<EmptyState
icon={<Megaphone />}
title={t('Loading...')}
description={t('Latest platform updates and notices')}
/>
)
}
if (announcements.length === 0) {
return <EmptyState message={t('No system announcements')} />
return (
<EmptyState icon={<Megaphone />} title={t('No system announcements')} />
)
}
return (
<ScrollArea className='h-[50vh] pr-4'>
<div className='space-y-0'>
<ScrollArea className='h-[min(52vh,28rem)] pr-3'>
<div className='flex flex-col'>
{announcements.map((item, idx) => {
const publishDate = item.publishDate
? new Date(item.publishDate)
@@ -197,30 +236,27 @@ function AnnouncementsContent({
<div className='py-3'>
<div className='flex items-start gap-3'>
<AnnouncementDot type={item.type} />
<div className='min-w-0 flex-1 space-y-2'>
{/* Content */}
<div className='flex min-w-0 flex-1 flex-col gap-2'>
<div className='text-sm'>
<Markdown>{item.content || ''}</Markdown>
</div>
{/* Extra info */}
{item.extra && (
{item.extra ? (
<div className='text-muted-foreground text-xs'>
<Markdown>{item.extra}</Markdown>
</div>
)}
) : null}
{/* Time */}
{absoluteTime && (
{absoluteTime ? (
<div className='text-muted-foreground text-xs'>
{relativeTime && `${relativeTime}`}
{relativeTime ? `${relativeTime}` : null}
{absoluteTime}
</div>
)}
) : null}
</div>
</div>
</div>
{idx < announcements.length - 1 && <Separator />}
{idx < announcements.length - 1 ? <Separator /> : null}
</div>
)
})}
@@ -230,25 +266,54 @@ function AnnouncementsContent({
}
/**
* Notification dialog with Notice and Announcements tabs
* Notification popover with Notice and Announcements tabs
*/
export function NotificationDialog({
export function NotificationPopover({
open,
onOpenChange,
unreadCount,
activeTab,
onTabChange,
notice,
announcements,
loading,
onCloseToday,
}: NotificationDialogProps) {
className,
}: NotificationPopoverProps) {
const { t } = useTranslation()
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='max-h-[90vh] sm:max-w-2xl'>
<DialogHeader>
<DialogTitle>{t('System Announcements')}</DialogTitle>
</DialogHeader>
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverTrigger
render={
<Button
variant='ghost'
size='icon'
className={cn('relative size-9', className)}
aria-label={t('Notifications')}
/>
}
>
<Bell className='size-[1.2rem]' />
{unreadCount > 0 ? (
<Badge
variant='destructive'
className='absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center px-1 text-[10px] font-semibold tabular-nums'
>
{unreadCount > 99 ? '99+' : unreadCount}
</Badge>
) : null}
</PopoverTrigger>
<PopoverContent
align='end'
sideOffset={8}
className='w-[min(26rem,calc(100vw-1rem))] gap-3 p-3'
>
<PopoverHeader className='gap-1 px-1'>
<PopoverTitle>{t('System Announcements')}</PopoverTitle>
<p className='text-muted-foreground text-xs'>
{t('Latest platform updates and notices')}
</p>
</PopoverHeader>
<Tabs
value={activeTab}
@@ -256,20 +321,20 @@ export function NotificationDialog({
>
<TabsList className='grid w-full grid-cols-2'>
<TabsTrigger value='notice' className='gap-1.5'>
<Bell className='h-3.5 w-3.5' />
<Bell className='size-3.5' />
{t('Notice')}
</TabsTrigger>
<TabsTrigger value='announcements' className='gap-1.5'>
<Megaphone className='h-3.5 w-3.5' />
<Megaphone className='size-3.5' />
{t('Timeline')}
</TabsTrigger>
</TabsList>
<TabsContent value='notice' className='mt-4'>
<TabsContent value='notice' className='mt-2'>
<NoticeContent notice={notice} loading={loading} t={t} />
</TabsContent>
<TabsContent value='announcements' className='mt-4'>
<TabsContent value='announcements' className='mt-2'>
<AnnouncementsContent
announcements={announcements}
loading={loading}
@@ -278,13 +343,12 @@ export function NotificationDialog({
</TabsContent>
</Tabs>
<DialogFooter className='gap-2'>
<Button variant='outline' onClick={onCloseToday}>
{t('Close Today')}
<div className='flex justify-end'>
<Button size='sm' onClick={() => onOpenChange(false)}>
{t('Close')}
</Button>
<Button onClick={() => onOpenChange(false)}>{t('Close')}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</PopoverContent>
</Popover>
)
}
+91 -23
View File
@@ -74,9 +74,33 @@ export const textColorMap = {
export type StatusVariant = keyof typeof dotColorMap
const sizeMap = {
sm: 'text-xs gap-1.5',
md: 'text-xs gap-1.5',
lg: 'text-sm gap-2',
sm: 'h-6 gap-1 px-2 text-sm leading-none',
md: 'h-6 gap-1 px-2 text-sm leading-none',
lg: 'h-7 gap-1.5 px-2.5 text-sm leading-none',
} as const
const badgeSurfaceMap = {
success: 'bg-success/10 text-success',
warning: 'bg-warning/10 text-warning',
danger: 'bg-destructive/10 text-destructive',
info: 'bg-info/10 text-info',
neutral: 'bg-muted text-muted-foreground',
purple: 'bg-chart-4/10 text-chart-4',
amber: 'bg-warning/10 text-warning',
blue: 'bg-chart-1/10 text-chart-1',
cyan: 'bg-chart-2/10 text-chart-2',
green: 'bg-success/10 text-success',
grey: 'bg-muted text-muted-foreground',
indigo: 'bg-chart-1/10 text-chart-1',
'light-blue': 'bg-info/10 text-info',
'light-green': 'bg-success/10 text-success',
lime: 'bg-chart-3/10 text-chart-3',
orange: 'bg-warning/10 text-warning',
pink: 'bg-chart-5/10 text-chart-5',
red: 'bg-destructive/10 text-destructive',
teal: 'bg-chart-2/10 text-chart-2',
violet: 'bg-chart-4/10 text-chart-4',
yellow: 'bg-warning/10 text-warning',
} as const
export interface StatusBadgeProps extends Omit<
@@ -87,7 +111,7 @@ export interface StatusBadgeProps extends Omit<
children?: React.ReactNode
icon?: LucideIcon
pulse?: boolean
/** When false, hides the leading dot */
/** Kept for compatibility. Badges no longer render leading dots. */
showDot?: boolean
variant?: StatusVariant | null
size?: 'sm' | 'md' | 'lg' | null
@@ -103,7 +127,7 @@ export function StatusBadge({
variant,
size = 'sm',
pulse = false,
showDot = true,
showDot = false,
copyable = true,
copyText,
autoColor,
@@ -112,6 +136,7 @@ export function StatusBadge({
...props
}: StatusBadgeProps) {
const { copyToClipboard } = useCopyToClipboard()
void showDot
const computedVariant: StatusVariant = autoColor
? (stringToColor(autoColor) as StatusVariant)
@@ -131,58 +156,101 @@ export function StatusBadge({
return (
<span
className={cn(
'inline-flex w-fit shrink-0 items-center font-medium whitespace-nowrap',
'inline-flex w-fit max-w-full shrink-0 items-center rounded-full font-medium tracking-normal whitespace-nowrap transition-colors',
sizeMap[size ?? 'sm'],
textColorMap[computedVariant],
badgeSurfaceMap[computedVariant],
pulse && 'animate-pulse',
copyable &&
'cursor-pointer transition-opacity hover:opacity-70 active:scale-95',
'cursor-copy hover:brightness-95 active:scale-95 dark:hover:brightness-110',
className
)}
onClick={handleClick}
title={copyable ? `Click to copy: ${copyText || label || ''}` : undefined}
{...props}
>
{showDot && (
<span
className={cn(
'inline-block size-1.5 shrink-0 rounded-full',
dotColorMap[computedVariant]
)}
aria-hidden='true'
/>
)}
{Icon && <Icon className='size-3 shrink-0' />}
{Icon && <Icon className='size-3.5 shrink-0' />}
{content}
</span>
)
}
export interface StatusBadgeListProps<T> extends Omit<
React.HTMLAttributes<HTMLDivElement>,
'children'
> {
empty?: React.ReactNode
getKey?: (item: T, index: number) => React.Key
items: T[]
max?: number
moreLabel?: (remaining: number) => string
renderItem: (item: T, index: number) => React.ReactNode
}
export function StatusBadgeList<T>(props: StatusBadgeListProps<T>) {
const {
className,
empty = <span className='text-muted-foreground text-xs'>-</span>,
getKey,
items,
max = 2,
moreLabel,
renderItem,
...domProps
} = props
if (items.length === 0) {
return empty
}
const displayed = items.slice(0, max)
const remaining = items.length - max
return (
<div
className={cn(
'flex max-w-full items-center gap-1 overflow-hidden',
className
)}
{...domProps}
>
{displayed.map((item, index) => (
<React.Fragment key={getKey?.(item, index) ?? index}>
{renderItem(item, index)}
</React.Fragment>
))}
{remaining > 0 && (
<StatusBadge
label={moreLabel?.(remaining) ?? `+${remaining}`}
variant='neutral'
size='sm'
copyable={false}
className='shrink-0'
/>
)}
</div>
)
}
export const statusPresets = {
active: {
variant: 'success' as const,
label: 'Active',
showDot: true,
},
inactive: {
variant: 'neutral' as const,
label: 'Inactive',
showDot: true,
},
invited: {
variant: 'info' as const,
label: 'Invited',
showDot: true,
},
suspended: {
variant: 'danger' as const,
label: 'Suspended',
showDot: true,
},
pending: {
variant: 'warning' as const,
label: 'Pending',
showDot: true,
pulse: true,
},
} as const
+37
View File
@@ -0,0 +1,37 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { cn } from '@/lib/utils'
type TableIdProps = {
className?: string
value: number | string
}
export function TableId(props: TableIdProps) {
return (
<span
className={cn(
'text-muted-foreground inline-block font-mono tabular-nums',
props.className
)}
>
{props.value}
</span>
)
}
+1 -1
View File
@@ -73,7 +73,7 @@ function DrawerContent({
<DrawerPrimitive.Content
data-slot='drawer-content'
className={cn(
'group/drawer-content bg-popover text-popover-foreground 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=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 bg-background text-foreground fixed z-50 flex h-auto flex-col overflow-hidden text-sm shadow-none 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=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
)}
{...props}
+1 -1
View File
@@ -76,7 +76,7 @@ function SheetContent({
data-slot='sheet-content'
data-side={side}
className={cn(
'bg-popover text-popover-foreground fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0',
'bg-background text-foreground fixed z-50 flex flex-col gap-4 overflow-hidden bg-clip-padding text-sm shadow-none transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0',
side === 'right' &&
'inset-y-0 right-0 h-full w-3/4 border-l data-ending-style:translate-x-[2.5rem] data-starting-style:translate-x-[2.5rem] sm:max-w-sm',
side === 'left' &&
+23 -9
View File
@@ -26,15 +26,15 @@ import {
Loading03Icon,
} from '@hugeicons/core-free-icons'
import { HugeiconsIcon } from '@hugeicons/react'
import { useTheme } from 'next-themes'
import { Toaster as Sonner, type ToasterProps } from 'sonner'
import { useTheme } from '@/context/theme-provider'
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = 'system' } = useTheme()
const Toaster = (props: ToasterProps) => {
const { resolvedTheme } = useTheme()
return (
<Sonner
theme={theme as ToasterProps['theme']}
theme={resolvedTheme}
className='toaster group'
icons={{
success: (
@@ -78,14 +78,28 @@ const Toaster = ({ ...props }: ToasterProps) => {
'--normal-bg': 'var(--popover)',
'--normal-text': 'var(--popover-foreground)',
'--normal-border': 'var(--border)',
'--success-bg':
'color-mix(in oklch, var(--success) 16%, var(--popover))',
'--success-border':
'color-mix(in oklch, var(--success) 35%, var(--border))',
'--success-text': 'var(--success)',
'--info-bg': 'color-mix(in oklch, var(--info) 16%, var(--popover))',
'--info-border':
'color-mix(in oklch, var(--info) 35%, var(--border))',
'--info-text': 'var(--info)',
'--warning-bg':
'color-mix(in oklch, var(--warning) 18%, var(--popover))',
'--warning-border':
'color-mix(in oklch, var(--warning) 38%, var(--border))',
'--warning-text': 'var(--warning)',
'--error-bg':
'color-mix(in oklch, var(--destructive) 16%, var(--popover))',
'--error-border':
'color-mix(in oklch, var(--destructive) 35%, var(--border))',
'--error-text': 'var(--destructive)',
'--border-radius': 'var(--radius)',
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast: 'cn-toast',
},
}}
{...props}
/>
)
+5 -2
View File
@@ -25,11 +25,14 @@ function Table({ className, ...props }: React.ComponentProps<'table'>) {
return (
<div
data-slot='table-container'
className='relative w-full overflow-x-auto'
className='relative w-full overflow-x-auto overflow-y-hidden'
>
<table
data-slot='table'
className={cn('w-full caption-bottom text-sm', className)}
className={cn(
'w-full caption-bottom text-sm tabular-nums [&_td]:text-sm [&_td_*]:text-sm [&_th]:text-sm [&_th_*]:text-sm',
className
)}
{...props}
/>
</div>