Commit eb863116 by t0ng7u

style(ui): align wallet and profile interactions

parent b36888ba
...@@ -16,7 +16,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,7 +16,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { flexRender, type Header, type Table as TanstackTable } from '@tanstack/react-table' import {
flexRender,
type Header,
type Table as TanstackTable,
} from '@tanstack/react-table'
import { TableHead, TableHeader, TableRow } from '@/components/ui/table' import { TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { DataTableColumnHeader } from './column-header' import { DataTableColumnHeader } from './column-header'
import type { DataTableColumnClassName } from './types' import type { DataTableColumnClassName } from './types'
......
...@@ -344,7 +344,8 @@ function renderDesktop<TData>( ...@@ -344,7 +344,8 @@ function renderDesktop<TData>(
splitHeader={fixedHeight} splitHeader={fixedHeight}
tableContainerClassName={fixedHeight ? 'h-full min-h-0' : undefined} tableContainerClassName={fixedHeight ? 'h-full min-h-0' : undefined}
tableHeaderClassName={cn( tableHeaderClassName={cn(
fixedHeight && '[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))]', fixedHeight &&
'[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))]',
props.tableHeaderClassName props.tableHeaderClassName
)} )}
getColumnClassName={props.getColumnClassName} getColumnClassName={props.getColumnClassName}
......
...@@ -25,7 +25,6 @@ import { ...@@ -25,7 +25,6 @@ import {
} from '@tanstack/react-table' } from '@tanstack/react-table'
import { Database } from 'lucide-react' import { Database } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StatusBadgeTypeContext } from '@/components/status-badge'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { import {
Empty, Empty,
...@@ -35,6 +34,7 @@ import { ...@@ -35,6 +34,7 @@ import {
EmptyTitle, EmptyTitle,
} from '@/components/ui/empty' } from '@/components/ui/empty'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { StatusBadgeTypeContext } from '@/components/status-badge'
interface MobileCardListProps<TData> { interface MobileCardListProps<TData> {
table: Table<TData> table: Table<TData>
...@@ -211,7 +211,10 @@ function FallbackRow<TData>({ row }: { row: Row<TData> }) { ...@@ -211,7 +211,10 @@ function FallbackRow<TData>({ row }: { row: Row<TData> }) {
if (!label) { if (!label) {
return ( return (
<div key={cell.id} className='flex justify-end overflow-hidden [&_[data-slot=provider-badge]]:ml-0 [&_[data-slot=status-badge]]:ml-0'> <div
key={cell.id}
className='flex justify-end overflow-hidden [&_[data-slot=provider-badge]]:ml-0 [&_[data-slot=status-badge]]:ml-0'
>
<StatusBadgeTypeContext.Provider value='text'> <StatusBadgeTypeContext.Provider value='text'>
{renderCellContent(cell)} {renderCellContent(cell)}
</StatusBadgeTypeContext.Provider> </StatusBadgeTypeContext.Provider>
......
...@@ -22,7 +22,8 @@ export const staticDataTableClassNames = { ...@@ -22,7 +22,8 @@ export const staticDataTableClassNames = {
embeddedContainer: 'rounded-none border-0', embeddedContainer: 'rounded-none border-0',
compactTable: 'text-sm', compactTable: 'text-sm',
compactHeaderRow: 'hover:bg-transparent', compactHeaderRow: 'hover:bg-transparent',
mutedHeaderRow: '[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))] hover:[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))]', mutedHeaderRow:
'[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))] hover:[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))]',
compactHeaderCell: compactHeaderCell:
'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase', 'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase',
compactHeaderCellRight: compactHeaderCellRight:
......
...@@ -38,20 +38,14 @@ export function ProviderBadge({ ...@@ -38,20 +38,14 @@ export function ProviderBadge({
return ( return (
<div <div
data-slot='provider-badge' data-slot='provider-badge'
className={cn( className={cn('flex max-w-full min-w-0 items-center gap-1.5', className)}
'flex min-w-0 max-w-full items-center gap-1.5',
className
)}
> >
{icon && <span className='flex shrink-0 items-center'>{icon}</span>} {icon && <span className='flex shrink-0 items-center'>{icon}</span>}
<StatusBadge <StatusBadge
label={label} label={label}
autoColor={label} autoColor={label}
size='sm' size='sm'
className={cn( className={cn('min-w-0 shrink overflow-hidden', !icon && 'pl-0')}
'min-w-0 shrink overflow-hidden',
!icon && 'pl-0'
)}
{...badgeProps} {...badgeProps}
/> />
</div> </div>
......
...@@ -77,7 +77,7 @@ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) { ...@@ -77,7 +77,7 @@ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
<tr <tr
data-slot='table-row' data-slot='table-row'
className={cn( className={cn(
'group hover:[background-color:color-mix(in_oklch,var(--muted)_50%,var(--background))] has-aria-expanded:[background-color:color-mix(in_oklch,var(--muted)_50%,var(--background))] data-[state=selected]:bg-muted border-b transition-colors', 'group data-[state=selected]:bg-muted border-b transition-colors hover:[background-color:color-mix(in_oklch,var(--muted)_50%,var(--background))] has-aria-expanded:[background-color:color-mix(in_oklch,var(--muted)_50%,var(--background))]',
className className
)} )}
{...props} {...props}
......
...@@ -32,6 +32,7 @@ type TitledCardProps = { ...@@ -32,6 +32,7 @@ type TitledCardProps = {
icon?: ReactNode icon?: ReactNode
action?: ReactNode action?: ReactNode
children?: ReactNode children?: ReactNode
disableHoverEffect?: boolean
className?: string className?: string
headerClassName?: string headerClassName?: string
contentClassName?: string contentClassName?: string
...@@ -46,6 +47,7 @@ export function TitledCard({ ...@@ -46,6 +47,7 @@ export function TitledCard({
icon, icon,
action, action,
children, children,
disableHoverEffect,
className, className,
headerClassName, headerClassName,
contentClassName, contentClassName,
...@@ -54,7 +56,10 @@ export function TitledCard({ ...@@ -54,7 +56,10 @@ export function TitledCard({
descriptionClassName, descriptionClassName,
}: TitledCardProps) { }: TitledCardProps) {
return ( return (
<Card className={cn('gap-0 overflow-hidden py-0', className)}> <Card
data-card-hover={disableHoverEffect ? 'false' : undefined}
className={cn('gap-0 overflow-hidden py-0', className)}
>
<CardHeader <CardHeader
className={cn('border-b p-3 !pb-3 sm:p-5 sm:!pb-5', headerClassName)} className={cn('border-b p-3 !pb-3 sm:p-5 sm:!pb-5', headerClassName)}
> >
......
...@@ -622,7 +622,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] { ...@@ -622,7 +622,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
: undefined : undefined
return ( return (
<div className='flex min-w-0 max-w-full items-center gap-2 overflow-hidden'> <div className='flex max-w-full min-w-0 items-center gap-2 overflow-hidden'>
{isMultiKey && ( {isMultiKey && (
<TooltipProvider delay={100}> <TooltipProvider delay={100}>
<Tooltip> <Tooltip>
...@@ -641,7 +641,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] { ...@@ -641,7 +641,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger
render={ render={
<div className='min-w-0 max-w-full overflow-hidden' /> <div className='max-w-full min-w-0 overflow-hidden' />
} }
> >
<ProviderBadge <ProviderBadge
...@@ -649,7 +649,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] { ...@@ -649,7 +649,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
label={typeName} label={typeName}
copyable={false} copyable={false}
showDot={false} showDot={false}
className='min-w-0 max-w-full overflow-hidden' className='max-w-full min-w-0 overflow-hidden'
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side='top'>{typeName}</TooltipContent> <TooltipContent side='top'>{typeName}</TooltipContent>
...@@ -895,7 +895,14 @@ export function useChannelsColumns(): ColumnDef<Channel>[] { ...@@ -895,7 +895,14 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
if (!tag) if (!tag)
return <span className='text-muted-foreground text-xs'>-</span> return <span className='text-muted-foreground text-xs'>-</span>
return <StatusBadge label={tag} autoColor={tag} size='sm' className='-ml-1.5' /> return (
<StatusBadge
label={tag}
autoColor={tag}
size='sm'
className='-ml-1.5'
/>
)
}, },
size: 120, size: 120,
enableSorting: false, enableSorting: false,
......
...@@ -190,7 +190,7 @@ function SetupGuideBackdrop(props: { compact?: boolean }) { ...@@ -190,7 +190,7 @@ function SetupGuideBackdrop(props: { compact?: boolean }) {
/> />
<div <div
className={cn( className={cn(
'text-foreground/5 pointer-events-none absolute inset-y-0 right-0 hidden overflow-hidden font-mono sm:block dark:text-foreground/8', 'text-foreground/5 dark:text-foreground/8 pointer-events-none absolute inset-y-0 right-0 hidden overflow-hidden font-mono sm:block',
props.compact ? 'w-1/2 opacity-45' : 'w-[58%] opacity-75' props.compact ? 'w-1/2 opacity-45' : 'w-[58%] opacity-75'
)} )}
aria-hidden='true' aria-hidden='true'
...@@ -589,7 +589,9 @@ export function OverviewDashboard() { ...@@ -589,7 +589,9 @@ export function OverviewDashboard() {
model, model,
keyName, keyName,
keyId: preferredKey?.id, keyId: preferredKey?.id,
displayKey: preferredKey ? formatDisplayKey(`sk-${preferredKey.key}`) : 'sk-...', displayKey: preferredKey
? formatDisplayKey(`sk-${preferredKey.key}`)
: 'sk-...',
ready, ready,
} }
}, [apiInfoItems, modelsQuery.data, preferredKey, t]) }, [apiInfoItems, modelsQuery.data, preferredKey, t])
......
...@@ -64,77 +64,77 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) { ...@@ -64,77 +64,77 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
return ( return (
<div className='-ml-2'> <div className='-ml-2'>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
render={ render={
<Button <Button
variant='ghost' variant='ghost'
className='data-popup-open:bg-muted flex h-8 w-8 p-0' className='data-popup-open:bg-muted flex h-8 w-8 p-0'
/> />
} }
> >
<MoreHorizontal className='h-4 w-4' /> <MoreHorizontal className='h-4 w-4' />
<span className='sr-only'>{t('Open menu')}</span> <span className='sr-only'>{t('Open menu')}</span>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-48'> <DropdownMenuContent align='end' className='w-48'>
{/* Edit */} {/* Edit */}
<DropdownMenuItem onClick={handleEdit}> <DropdownMenuItem onClick={handleEdit}>
{t('Edit')} {t('Edit')}
<DropdownMenuShortcut> <DropdownMenuShortcut>
<Pencil size={16} /> <Pencil size={16} />
</DropdownMenuShortcut> </DropdownMenuShortcut>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{/* Enable/Disable */} {/* Enable/Disable */}
<DropdownMenuItem onClick={handleToggleStatus}> <DropdownMenuItem onClick={handleToggleStatus}>
{isEnabled ? ( {isEnabled ? (
<> <>
{t('Disable')} {t('Disable')}
<DropdownMenuShortcut> <DropdownMenuShortcut>
<PowerOff size={16} /> <PowerOff size={16} />
</DropdownMenuShortcut> </DropdownMenuShortcut>
</> </>
) : ( ) : (
<> <>
{t('Enable')} {t('Enable')}
<DropdownMenuShortcut> <DropdownMenuShortcut>
<Power size={16} /> <Power size={16} />
</DropdownMenuShortcut> </DropdownMenuShortcut>
</> </>
)} )}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{/* Delete */} {/* Delete */}
<DropdownMenuItem <DropdownMenuItem
onSelect={(e) => { onSelect={(e) => {
e.preventDefault() e.preventDefault()
setDeleteConfirmOpen(true) setDeleteConfirmOpen(true)
}} }}
className='text-destructive focus:text-destructive' className='text-destructive focus:text-destructive'
> >
{t('Delete')} {t('Delete')}
<DropdownMenuShortcut> <DropdownMenuShortcut>
<Trash2 size={16} /> <Trash2 size={16} />
</DropdownMenuShortcut> </DropdownMenuShortcut>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
<ConfirmDialog <ConfirmDialog
open={deleteConfirmOpen} open={deleteConfirmOpen}
onOpenChange={setDeleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}
title={t('Delete Model')} title={t('Delete Model')}
desc={`Are you sure you want to delete "${model.model_name}"? This action cannot be undone.`} desc={`Are you sure you want to delete "${model.model_name}"? This action cannot be undone.`}
confirmText='Delete' confirmText='Delete'
destructive destructive
handleConfirm={() => { handleConfirm={() => {
handleDeleteModel(model.id, queryClient) handleDeleteModel(model.id, queryClient)
setDeleteConfirmOpen(false) setDeleteConfirmOpen(false)
}} }}
/> />
</DropdownMenu> </DropdownMenu>
</div> </div>
) )
} }
...@@ -322,13 +322,11 @@ export function DynamicPricingBreakdown({ ...@@ -322,13 +322,11 @@ export function DynamicPricingBreakdown({
className: 'text-muted-foreground py-2 font-medium', className: 'text-muted-foreground py-2 font-medium',
cellClassName: 'py-2.5 align-top', cellClassName: 'py-2.5 align-top',
cell: (tier) => { cell: (tier) => {
const condSummary = formatConditionSummary( const condSummary = formatConditionSummary(tier.conditions, t)
tier.conditions,
t
)
const isMatched = const isMatched =
normalizedMatchedTierLabel !== '' && normalizedMatchedTierLabel !== '' &&
normalizeTierLabel(tier.label) === normalizedMatchedTierLabel normalizeTierLabel(tier.label) ===
normalizedMatchedTierLabel
return ( return (
<> <>
<div className='flex flex-wrap items-center gap-1.5'> <div className='flex flex-wrap items-center gap-1.5'>
......
...@@ -57,9 +57,7 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { ...@@ -57,9 +57,7 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
const groups = props.model.enable_groups || [] const groups = props.model.enable_groups || []
const endpoints = props.model.supported_endpoint_types || [] const endpoints = props.model.supported_endpoint_types || []
const modelIconKey = props.model.icon || props.model.vendor_icon const modelIconKey = props.model.icon || props.model.vendor_icon
const modelIcon = modelIconKey const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 28) : null
? getLobeIcon(modelIconKey, 28)
: null
const initial = props.model.model_name?.charAt(0).toUpperCase() || '?' const initial = props.model.model_name?.charAt(0).toUpperCase() || '?'
const isDynamicPricing = const isDynamicPricing =
props.model.billing_mode === 'tiered_expr' && props.model.billing_mode === 'tiered_expr' &&
......
...@@ -32,6 +32,7 @@ import { formatQuotaWithCurrency } from '@/lib/currency' ...@@ -32,6 +32,7 @@ import { formatQuotaWithCurrency } from '@/lib/currency'
import dayjs from '@/lib/dayjs' import dayjs from '@/lib/dayjs'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { import {
Tooltip, Tooltip,
...@@ -221,7 +222,7 @@ export function CheckinCalendarCard({ ...@@ -221,7 +222,7 @@ export function CheckinCalendarCard({
if (isLoading) { if (isLoading) {
return ( return (
<div className='bg-card overflow-hidden rounded-2xl border'> <Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
<div className='p-6'> <div className='p-6'>
<div className='flex items-start justify-between gap-4'> <div className='flex items-start justify-between gap-4'>
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
...@@ -234,7 +235,7 @@ export function CheckinCalendarCard({ ...@@ -234,7 +235,7 @@ export function CheckinCalendarCard({
<Skeleton className='h-9 w-28 rounded-md' /> <Skeleton className='h-9 w-28 rounded-md' />
</div> </div>
</div> </div>
</div> </Card>
) )
} }
...@@ -270,14 +271,13 @@ export function CheckinCalendarCard({ ...@@ -270,14 +271,13 @@ export function CheckinCalendarCard({
</div> </div>
</Dialog> </Dialog>
<div className='bg-card overflow-hidden rounded-2xl border'> <Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
{/* Header */} {/* Header */}
<div className='border-b p-4 sm:p-6'> <div className='border-b p-4 sm:p-6'>
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4'> <div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4'>
<Button <button
type='button' type='button'
variant='ghost' className='flex min-w-0 flex-1 items-start gap-3 rounded-lg text-left whitespace-normal outline-none'
className='flex h-auto min-w-0 flex-1 items-start gap-3 p-0 text-left whitespace-normal hover:bg-transparent'
onClick={() => setCollapsed((v) => !v)} onClick={() => setCollapsed((v) => !v)}
> >
<div className='bg-primary/10 text-primary flex h-10 w-10 shrink-0 items-center justify-center rounded-xl sm:h-11 sm:w-11'> <div className='bg-primary/10 text-primary flex h-10 w-10 shrink-0 items-center justify-center rounded-xl sm:h-11 sm:w-11'>
...@@ -311,7 +311,7 @@ export function CheckinCalendarCard({ ...@@ -311,7 +311,7 @@ export function CheckinCalendarCard({
: t('Check in daily to receive random quota rewards')} : t('Check in daily to receive random quota rewards')}
</p> </p>
</div> </div>
</Button> </button>
<Button <Button
onClick={() => doCheckin()} onClick={() => doCheckin()}
disabled={checkinLoading || checkedToday} disabled={checkinLoading || checkedToday}
...@@ -423,7 +423,6 @@ export function CheckinCalendarCard({ ...@@ -423,7 +423,6 @@ export function CheckinCalendarCard({
'relative flex h-9 w-full flex-col items-center justify-center rounded-lg px-0 text-xs font-medium sm:h-10 sm:text-sm', 'relative flex h-9 w-full flex-col items-center justify-center rounded-lg px-0 text-xs font-medium sm:h-10 sm:text-sm',
!dayObj.isCurrentMonth && !dayObj.isCurrentMonth &&
'text-muted-foreground/40 cursor-default', 'text-muted-foreground/40 cursor-default',
isToday && 'hover:bg-primary/90',
!isToday && isCheckedIn && 'font-semibold' !isToday && isCheckedIn && 'font-semibold'
)} )}
> >
...@@ -476,7 +475,7 @@ export function CheckinCalendarCard({ ...@@ -476,7 +475,7 @@ export function CheckinCalendarCard({
</div> </div>
</> </>
) : null} ) : null}
</div> </Card>
</TooltipProvider> </TooltipProvider>
) )
} }
...@@ -105,6 +105,7 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) { ...@@ -105,6 +105,7 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
title={t('Language Preferences')} title={t('Language Preferences')}
description={t('Set the language used across the interface')} description={t('Set the language used across the interface')}
icon={<Languages className='h-4 w-4' />} icon={<Languages className='h-4 w-4' />}
disableHoverEffect
> >
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4'> <div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4'>
<div className='space-y-1'> <div className='space-y-1'>
......
...@@ -187,7 +187,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) { ...@@ -187,7 +187,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
if (pageLoading || loading) { if (pageLoading || loading) {
return ( return (
<Card className='gap-0 overflow-hidden py-0'> <Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
<CardHeader className='p-3 sm:p-5'> <CardHeader className='p-3 sm:p-5'>
<Skeleton className='h-6 w-48' /> <Skeleton className='h-6 w-48' />
<Skeleton className='mt-2 h-4 w-64' /> <Skeleton className='mt-2 h-4 w-64' />
...@@ -208,7 +208,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) { ...@@ -208,7 +208,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
return ( return (
<> <>
<Card className='gap-0 overflow-hidden py-0'> <Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
<CardHeader className='p-3 sm:p-5'> <CardHeader className='p-3 sm:p-5'>
<CardTitle className='text-lg tracking-tight sm:text-xl'> <CardTitle className='text-lg tracking-tight sm:text-xl'>
{t('Passkey Login')} {t('Passkey Login')}
...@@ -310,7 +310,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) { ...@@ -310,7 +310,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
{t('Cancel')} {t('Cancel')}
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className='bg-destructive text-destructive-foreground hover:bg-destructive/90' className='bg-destructive text-destructive-foreground'
disabled={removing} disabled={removing}
onClick={(event) => { onClick={(event) => {
event.preventDefault() event.preventDefault()
......
...@@ -18,12 +18,14 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -18,12 +18,14 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import { Activity, BarChart3, WalletCards } from 'lucide-react' import { Activity, BarChart3, WalletCards } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { getUserAvatarFallback, getUserAvatarStyle } from '@/lib/avatar'
import { formatCompactNumber, formatQuota } from '@/lib/format' import { formatCompactNumber, formatQuota } from '@/lib/format'
import { getRoleLabel } from '@/lib/roles' import { getRoleLabel } from '@/lib/roles'
import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Card, CardContent } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { getUserInitials, getDisplayName } from '../lib' import { getDisplayName } from '../lib'
import type { UserProfile } from '../types' import type { UserProfile } from '../types'
// ============================================================================ // ============================================================================
...@@ -40,8 +42,8 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) { ...@@ -40,8 +42,8 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
if (loading) { if (loading) {
return ( return (
<div className='bg-card overflow-hidden rounded-lg border'> <Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
<div className='p-4 sm:p-5'> <CardContent className='p-4 sm:p-5'>
<div className='flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'> <div className='flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'>
<Skeleton className='h-16 w-16 rounded-2xl' /> <Skeleton className='h-16 w-16 rounded-2xl' />
<div className='space-y-3'> <div className='space-y-3'>
...@@ -56,7 +58,7 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) { ...@@ -56,7 +58,7 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
</div> </div>
</div> </div>
</div> </div>
</div> </CardContent>
<div className='border-t'> <div className='border-t'>
<div className='divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'> <div className='divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'>
{Array.from({ length: 3 }).map((_, i) => ( {Array.from({ length: 3 }).map((_, i) => (
...@@ -68,14 +70,16 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) { ...@@ -68,14 +70,16 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
))} ))}
</div> </div>
</div> </div>
</div> </Card>
) )
} }
if (!profile) return null if (!profile) return null
const displayName = getDisplayName(profile) const displayName = getDisplayName(profile)
const initials = getUserInitials(profile) const avatarName = profile.username || displayName
const avatarFallback = getUserAvatarFallback(avatarName)
const avatarFallbackStyle = getUserAvatarStyle(avatarName)
const roleLabel = getRoleLabel(profile.role) const roleLabel = getRoleLabel(profile.role)
const stats = [ const stats = [
{ {
...@@ -99,12 +103,15 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) { ...@@ -99,12 +103,15 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
] ]
return ( return (
<div className='bg-card overflow-hidden rounded-lg border'> <Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
<div className='p-3 sm:p-5'> <CardContent className='p-3 sm:p-5'>
<div className='flex items-center gap-3 text-left sm:gap-4'> <div className='flex items-center gap-3 text-left sm:gap-4'>
<Avatar className='ring-background h-12 w-12 rounded-xl text-sm ring-2 sm:h-16 sm:w-16 sm:rounded-2xl sm:text-lg sm:ring-4'> <Avatar className='ring-background h-12 w-12 rounded-xl text-sm ring-2 sm:h-16 sm:w-16 sm:rounded-2xl sm:text-lg sm:ring-4'>
<AvatarFallback className='bg-primary/10 text-primary rounded-xl sm:rounded-2xl'> <AvatarFallback
{initials} className='rounded-xl font-semibold text-white sm:rounded-2xl'
style={avatarFallbackStyle}
>
{avatarFallback}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
...@@ -142,7 +149,7 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) { ...@@ -142,7 +149,7 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
</div> </div>
</div> </div>
</div> </div>
</div> </CardContent>
<div className='border-t'> <div className='border-t'>
<div className='divide-border/60 grid grid-cols-3 divide-x'> <div className='divide-border/60 grid grid-cols-3 divide-x'>
{stats.map((item) => ( {stats.map((item) => (
...@@ -164,6 +171,6 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) { ...@@ -164,6 +171,6 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
))} ))}
</div> </div>
</div> </div>
</div> </Card>
) )
} }
...@@ -47,7 +47,7 @@ export function ProfileSecurityCard({ ...@@ -47,7 +47,7 @@ export function ProfileSecurityCard({
if (loading) { if (loading) {
return ( return (
<Card className='gap-0 overflow-hidden py-0'> <Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
<CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'> <CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
<Skeleton className='h-6 w-32' /> <Skeleton className='h-6 w-32' />
<Skeleton className='mt-2 h-4 w-48' /> <Skeleton className='mt-2 h-4 w-48' />
...@@ -93,6 +93,7 @@ export function ProfileSecurityCard({ ...@@ -93,6 +93,7 @@ export function ProfileSecurityCard({
title={t('Security')} title={t('Security')}
description={t('Manage your security settings and account access')} description={t('Manage your security settings and account access')}
icon={<Shield className='h-4 w-4' />} icon={<Shield className='h-4 w-4' />}
disableHoverEffect
> >
<div className='grid grid-cols-1 gap-2.5 sm:gap-3 md:grid-cols-3'> <div className='grid grid-cols-1 gap-2.5 sm:gap-3 md:grid-cols-3'>
{securityActions.map((item) => ( {securityActions.map((item) => (
...@@ -100,10 +101,8 @@ export function ProfileSecurityCard({ ...@@ -100,10 +101,8 @@ export function ProfileSecurityCard({
key={item.title} key={item.title}
type='button' type='button'
onClick={item.action} onClick={item.action}
className={`hover:bg-muted/50 flex items-center gap-3 rounded-lg border p-3 text-left transition-colors md:flex-col md:gap-2 md:p-4 md:text-center ${ className={`flex items-center gap-3 rounded-lg border p-3 text-left md:flex-col md:gap-2 md:p-4 md:text-center ${
item.variant === 'destructive' item.variant === 'destructive' ? 'border-destructive/30' : ''
? 'border-destructive/30 hover:border-destructive/50 hover:bg-destructive/5'
: ''
}`} }`}
> >
<div <div
......
...@@ -47,7 +47,7 @@ export function ProfileSettingsCard({ ...@@ -47,7 +47,7 @@ export function ProfileSettingsCard({
if (loading) { if (loading) {
return ( return (
<Card className='gap-0 overflow-hidden py-0'> <Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
<CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'> <CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
<Skeleton className='h-6 w-32' /> <Skeleton className='h-6 w-32' />
<Skeleton className='mt-2 h-4 w-48' /> <Skeleton className='mt-2 h-4 w-48' />
...@@ -67,6 +67,7 @@ export function ProfileSettingsCard({ ...@@ -67,6 +67,7 @@ export function ProfileSettingsCard({
title={t('Settings')} title={t('Settings')}
description={t('Configure your account preferences and integrations')} description={t('Configure your account preferences and integrations')}
icon={<Settings className='h-4 w-4' />} icon={<Settings className='h-4 w-4' />}
disableHoverEffect
> >
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className='grid w-full grid-cols-2 items-stretch gap-1 rounded-xl p-1 group-data-horizontal/tabs:h-10'> <TabsList className='grid w-full grid-cols-2 items-stretch gap-1 rounded-xl p-1 group-data-horizontal/tabs:h-10'>
......
...@@ -200,7 +200,7 @@ export function SidebarModulesCard() { ...@@ -200,7 +200,7 @@ export function SidebarModulesCard() {
} }
return ( return (
<Card className='gap-0 overflow-hidden py-0'> <Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
<CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'> <CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<div className='bg-muted flex h-8 w-8 shrink-0 items-center justify-center rounded-lg sm:h-9 sm:w-9'> <div className='bg-muted flex h-8 w-8 shrink-0 items-center justify-center rounded-lg sm:h-9 sm:w-9'>
...@@ -240,7 +240,7 @@ export function SidebarModulesCard() { ...@@ -240,7 +240,7 @@ export function SidebarModulesCard() {
{section.modules.map((mod) => ( {section.modules.map((mod) => (
<div <div
key={mod.key} key={mod.key}
className={`flex min-h-16 items-center justify-between rounded-lg border p-3 transition-opacity ${ className={`flex min-h-16 items-center justify-between rounded-lg border p-3 ${
sectionEnabled ? '' : 'opacity-50' sectionEnabled ? '' : 'opacity-50'
}`} }`}
> >
......
...@@ -350,7 +350,7 @@ export function AccountBindingsTab({ ...@@ -350,7 +350,7 @@ export function AccountBindingsTab({
<Button <Button
variant='ghost' variant='ghost'
size='sm' size='sm'
className='text-destructive hover:text-destructive h-7 shrink-0 px-2.5 text-xs' className='text-destructive h-7 shrink-0 px-2.5 text-xs'
onClick={() => setUnbindTarget(binding)} onClick={() => setUnbindTarget(binding)}
> >
<Unlink className='mr-1 h-3 w-3' /> <Unlink className='mr-1 h-3 w-3' />
......
...@@ -24,8 +24,8 @@ import { ROLE } from '@/lib/roles' ...@@ -24,8 +24,8 @@ import { ROLE } from '@/lib/roles'
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'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { PasswordInput } from '@/components/password-input' import { PasswordInput } from '@/components/password-input'
import { updateUserSettings } from '../../api' import { updateUserSettings } from '../../api'
import { import {
...@@ -35,13 +35,24 @@ import { ...@@ -35,13 +35,24 @@ import {
import { parseUserSettings } from '../../lib' import { parseUserSettings } from '../../lib'
import type { UserProfile, UserSettings, NotifyType } from '../../types' import type { UserProfile, UserSettings, NotifyType } from '../../types'
const NOTIFICATION_ICONS: Record<string, typeof Mail> = { const NOTIFICATION_ICONS: Record<NotifyType, typeof Mail> = {
email: Mail, email: Mail,
webhook: Webhook, webhook: Webhook,
bark: Bell, bark: Bell,
gotify: Server, gotify: Server,
} }
const NOTIFICATION_VALUES = new Set<NotifyType>(
NOTIFICATION_METHODS.map((method) => method.value)
)
function normalizeNotifyType(value: unknown): NotifyType {
return typeof value === 'string' &&
NOTIFICATION_VALUES.has(value as NotifyType)
? (value as NotifyType)
: 'email'
}
// ============================================================================ // ============================================================================
// Settings Tab Component // Settings Tab Component
// ============================================================================ // ============================================================================
...@@ -82,7 +93,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) { ...@@ -82,7 +93,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
if (profile?.setting) { if (profile?.setting) {
const parsed = parseUserSettings(profile.setting) const parsed = parseUserSettings(profile.setting)
setSettings({ setSettings({
notify_type: parsed.notify_type || 'email', notify_type: normalizeNotifyType(parsed.notify_type),
quota_warning_threshold: quota_warning_threshold:
parsed.quota_warning_threshold ?? DEFAULT_QUOTA_WARNING_THRESHOLD, parsed.quota_warning_threshold ?? DEFAULT_QUOTA_WARNING_THRESHOLD,
notification_email: parsed.notification_email ?? '', notification_email: parsed.notification_email ?? '',
...@@ -119,44 +130,42 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) { ...@@ -119,44 +130,42 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
} }
} }
const notifyType = normalizeNotifyType(settings.notify_type)
return ( return (
<div className='space-y-4 sm:space-y-6'> <div className='space-y-4 sm:space-y-6'>
{/* Notification Type */} {/* Notification Type */}
<div className='space-y-2.5'> <div className='space-y-2.5'>
<Label>{t('Notification Method')}</Label> <Label>{t('Notification Method')}</Label>
<RadioGroup <ToggleGroup
value={settings.notify_type} value={[notifyType]}
onValueChange={(value) => onValueChange={(value) => {
updateField('notify_type', value as NotifyType) const nextValue = value.find((item) => item !== notifyType)
} if (nextValue)
className='grid grid-cols-4 gap-1.5 sm:gap-3' updateField('notify_type', normalizeNotifyType(nextValue))
}}
aria-label={t('Notification Method')}
variant='outline'
size='lg'
spacing={2}
className='grid w-full grid-cols-2 gap-2 sm:grid-cols-4 sm:gap-3'
> >
{NOTIFICATION_METHODS.map((method) => { {NOTIFICATION_METHODS.map((method) => {
const Icon = NOTIFICATION_ICONS[method.value] const Icon = NOTIFICATION_ICONS[method.value]
const isSelected = settings.notify_type === method.value
return ( return (
<Label <ToggleGroupItem
key={method.value} key={method.value}
htmlFor={method.value} value={method.value}
className={`flex min-h-16 cursor-pointer flex-col items-center justify-center gap-1.5 rounded-lg border p-2 text-center transition-colors sm:min-h-20 sm:gap-2 sm:border-2 sm:p-3 ${ className='h-auto min-h-14 w-full flex-col gap-1.5 px-3 py-3 sm:min-h-16'
isSelected
? 'border-primary bg-primary/5 text-primary'
: 'border-muted hover:border-muted-foreground/25 hover:bg-muted/50'
}`}
> >
<RadioGroupItem
value={method.value}
id={method.value}
className='sr-only'
/>
<Icon className='h-4 w-4 sm:h-5 sm:w-5' /> <Icon className='h-4 w-4 sm:h-5 sm:w-5' />
<span className='max-w-full truncate text-xs font-medium sm:text-sm'> <span className='max-w-full truncate text-xs font-medium sm:text-sm'>
{t(method.label)} {t(method.label)}
</span> </span>
</Label> </ToggleGroupItem>
) )
})} })}
</RadioGroup> </ToggleGroup>
</div> </div>
{/* Warning Threshold */} {/* Warning Threshold */}
...@@ -178,7 +187,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) { ...@@ -178,7 +187,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
</div> </div>
{/* Email Settings */} {/* Email Settings */}
{settings.notify_type === 'email' && ( {notifyType === 'email' && (
<div className='space-y-1.5'> <div className='space-y-1.5'>
<Label htmlFor='notifyEmail'>{t('Notification Email')}</Label> <Label htmlFor='notifyEmail'>{t('Notification Email')}</Label>
<Input <Input
...@@ -193,7 +202,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) { ...@@ -193,7 +202,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
)} )}
{/* Webhook Settings */} {/* Webhook Settings */}
{settings.notify_type === 'webhook' && ( {notifyType === 'webhook' && (
<> <>
<div className='space-y-1.5'> <div className='space-y-1.5'>
<Label htmlFor='webhookUrl'>{t('Webhook URL')}</Label> <Label htmlFor='webhookUrl'>{t('Webhook URL')}</Label>
...@@ -219,7 +228,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) { ...@@ -219,7 +228,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
)} )}
{/* Bark Settings */} {/* Bark Settings */}
{settings.notify_type === 'bark' && ( {notifyType === 'bark' && (
<div className='space-y-1.5'> <div className='space-y-1.5'>
<Label htmlFor='barkUrl'>{t('Bark Push URL')}</Label> <Label htmlFor='barkUrl'>{t('Bark Push URL')}</Label>
<Input <Input
...@@ -237,7 +246,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) { ...@@ -237,7 +246,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
)} )}
{/* Gotify Settings */} {/* Gotify Settings */}
{settings.notify_type === 'gotify' && ( {notifyType === 'gotify' && (
<> <>
<div className='space-y-1.5'> <div className='space-y-1.5'>
<Label htmlFor='gotifyUrl'>{t('Gotify Server URL')}</Label> <Label htmlFor='gotifyUrl'>{t('Gotify Server URL')}</Label>
...@@ -300,7 +309,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) { ...@@ -300,7 +309,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
href='https://gotify.net/' href='https://gotify.net/'
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'
className='text-primary hover:underline' className='text-primary underline underline-offset-4'
> >
{t('Gotify Documentation')} {t('Gotify Documentation')}
</a> </a>
......
...@@ -51,7 +51,7 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) { ...@@ -51,7 +51,7 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
if (pageLoading || loading) { if (pageLoading || loading) {
return ( return (
<Card className='gap-0 overflow-hidden py-0'> <Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
<CardHeader className='p-3 sm:p-5'> <CardHeader className='p-3 sm:p-5'>
<Skeleton className='h-6 w-48' /> <Skeleton className='h-6 w-48' />
<Skeleton className='mt-2 h-4 w-64' /> <Skeleton className='mt-2 h-4 w-64' />
...@@ -65,7 +65,7 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) { ...@@ -65,7 +65,7 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
return ( return (
<> <>
<Card className='gap-0 overflow-hidden py-0'> <Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
<CardHeader className='p-3 sm:p-5'> <CardHeader className='p-3 sm:p-5'>
<CardTitle className='text-lg tracking-tight sm:text-xl'> <CardTitle className='text-lg tracking-tight sm:text-xl'>
{t('Two-Factor Authentication')} {t('Two-Factor Authentication')}
......
...@@ -79,64 +79,64 @@ export function DataTableRowActions<TData>({ ...@@ -79,64 +79,64 @@ export function DataTableRowActions<TData>({
return ( return (
<div className='-ml-2'> <div className='-ml-2'>
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
<DropdownMenuTrigger <DropdownMenuTrigger
render={ render={
<Button <Button
variant='ghost' variant='ghost'
className='data-popup-open:bg-muted flex h-8 w-8 p-0' className='data-popup-open:bg-muted flex h-8 w-8 p-0'
/> />
} }
>
<DotsHorizontalIcon className='h-4 w-4' />
<span className='sr-only'>{t('Open menu')}</span>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-[160px]'>
<DropdownMenuItem
onClick={() => {
setCurrentRow(redemption)
setOpen('update')
}}
disabled={!canEdit}
> >
{t('Edit')} <DotsHorizontalIcon className='h-4 w-4' />
<DropdownMenuShortcut> <span className='sr-only'>{t('Open menu')}</span>
<Edit size={16} /> </DropdownMenuTrigger>
</DropdownMenuShortcut> <DropdownMenuContent align='end' className='w-[160px]'>
</DropdownMenuItem> <DropdownMenuItem
{canToggle && ( onClick={() => {
<DropdownMenuItem onClick={handleToggleStatus}> setCurrentRow(redemption)
{isEnabled ? ( setOpen('update')
<> }}
{t('Disable')} disabled={!canEdit}
<DropdownMenuShortcut> >
<PowerOff size={16} /> {t('Edit')}
</DropdownMenuShortcut> <DropdownMenuShortcut>
</> <Edit size={16} />
) : ( </DropdownMenuShortcut>
<>
{t('Enable')}
<DropdownMenuShortcut>
<Power size={16} />
</DropdownMenuShortcut>
</>
)}
</DropdownMenuItem> </DropdownMenuItem>
)} {canToggle && (
<DropdownMenuSeparator /> <DropdownMenuItem onClick={handleToggleStatus}>
<DropdownMenuItem {isEnabled ? (
onClick={() => { <>
setCurrentRow(redemption) {t('Disable')}
setOpen('delete') <DropdownMenuShortcut>
}} <PowerOff size={16} />
className='text-destructive focus:text-destructive' </DropdownMenuShortcut>
> </>
{t('Delete')} ) : (
<DropdownMenuShortcut> <>
<Trash2 size={16} /> {t('Enable')}
</DropdownMenuShortcut> <DropdownMenuShortcut>
</DropdownMenuItem> <Power size={16} />
</DropdownMenuContent> </DropdownMenuShortcut>
</DropdownMenu> </>
)}
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
setCurrentRow(redemption)
setOpen('delete')
}}
className='text-destructive focus:text-destructive'
>
{t('Delete')}
<DropdownMenuShortcut>
<Trash2 size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
) )
} }
...@@ -40,43 +40,43 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) { ...@@ -40,43 +40,43 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
return ( return (
<div className='-ml-2'> <div className='-ml-2'>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
render={<Button variant='ghost' className='h-8 w-8 p-0' />} render={<Button variant='ghost' className='h-8 w-8 p-0' />}
>
<MoreHorizontal className='h-4 w-4' />
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem
disabled={!complianceConfirmed}
onClick={() => {
setCurrentRow(row.original)
setOpen('update')
}}
> >
<Pencil className='mr-2 h-4 w-4' /> <MoreHorizontal className='h-4 w-4' />
{t('Edit')} </DropdownMenuTrigger>
</DropdownMenuItem> <DropdownMenuContent align='end'>
<DropdownMenuItem <DropdownMenuItem
disabled={!complianceConfirmed} disabled={!complianceConfirmed}
onClick={() => { onClick={() => {
setCurrentRow(row.original) setCurrentRow(row.original)
setOpen('toggle-status') setOpen('update')
}} }}
> >
{row.original.plan.enabled ? ( <Pencil className='mr-2 h-4 w-4' />
<> {t('Edit')}
<PowerOff className='mr-2 h-4 w-4' /> </DropdownMenuItem>
{t('Disable')} <DropdownMenuItem
</> disabled={!complianceConfirmed}
) : ( onClick={() => {
<> setCurrentRow(row.original)
<Power className='mr-2 h-4 w-4' /> setOpen('toggle-status')
{t('Enable')} }}
</> >
)} {row.original.plan.enabled ? (
</DropdownMenuItem> <>
</DropdownMenuContent> <PowerOff className='mr-2 h-4 w-4' />
</DropdownMenu> {t('Disable')}
</>
) : (
<>
<Power className='mr-2 h-4 w-4' />
{t('Enable')}
</>
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
) )
} }
...@@ -307,8 +307,7 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) { ...@@ -307,8 +307,7 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
{ {
id: 'url', id: 'url',
header: t('Uptime Kuma URL'), header: t('Uptime Kuma URL'),
cellClassName: cellClassName: 'text-primary max-w-xs truncate font-mono text-sm',
'text-primary max-w-xs truncate font-mono text-sm',
cell: (group) => group.url, cell: (group) => group.url,
}, },
{ {
......
...@@ -554,7 +554,9 @@ export function ChannelAffinitySection(props: Props) { ...@@ -554,7 +554,9 @@ export function ChannelAffinitySection(props: Props) {
{ {
id: 'model-regex', id: 'model-regex',
header: t('Model Regex'), header: t('Model Regex'),
cell: (rule) => <RuleBadgeList items={rule.model_regex || []} />, cell: (rule) => (
<RuleBadgeList items={rule.model_regex || []} />
),
}, },
{ {
id: 'key-sources', id: 'key-sources',
......
...@@ -149,9 +149,7 @@ export function AmountDiscountVisualEditor({ ...@@ -149,9 +149,7 @@ export function AmountDiscountVisualEditor({
id: 'amount', id: 'amount',
header: t('Recharge Amount'), header: t('Recharge Amount'),
cell: (discount) => ( cell: (discount) => (
<span className='font-mono text-sm'> <span className='font-mono text-sm'>${discount.amount}</span>
${discount.amount}
</span>
), ),
}, },
{ {
......
...@@ -26,9 +26,7 @@ import { ...@@ -26,9 +26,7 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from '@/components/ui/popover' } from '@/components/ui/popover'
import { import { StaticDataTable } from '@/components/data-table'
StaticDataTable,
} from '@/components/data-table'
import { safeJsonParseWithValidation } from '../utils/json-parser' import { safeJsonParseWithValidation } from '../utils/json-parser'
import { isArray } from '../utils/json-validators' import { isArray } from '../utils/json-validators'
import { import {
......
...@@ -27,9 +27,7 @@ import { ...@@ -27,9 +27,7 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from '@/components/ui/alert-dialog' } from '@/components/ui/alert-dialog'
import { import { StaticDataTable } from '@/components/data-table'
StaticDataTable,
} from '@/components/data-table'
export type ConflictItem = { export type ConflictItem = {
channel: string channel: string
...@@ -86,18 +84,18 @@ export function ConflictConfirmDialog({ ...@@ -86,18 +84,18 @@ export function ConflictConfirmDialog({
id: 'current', id: 'current',
header: t('Current Billing'), header: t('Current Billing'),
cell: (conflict) => ( cell: (conflict) => (
<pre className='text-sm whitespace-pre-wrap'> <pre className='text-sm whitespace-pre-wrap'>
{conflict.current} {conflict.current}
</pre> </pre>
), ),
}, },
{ {
id: 'new', id: 'new',
header: t('Change To'), header: t('Change To'),
cell: (conflict) => ( cell: (conflict) => (
<pre className='text-sm whitespace-pre-wrap'> <pre className='text-sm whitespace-pre-wrap'>
{conflict.newVal} {conflict.newVal}
</pre> </pre>
), ),
}, },
]} ]}
......
...@@ -263,11 +263,11 @@ export const ToolPriceSettings = memo(function ToolPriceSettings({ ...@@ -263,11 +263,11 @@ export const ToolPriceSettings = memo(function ToolPriceSettings({
id: 'tool', id: 'tool',
header: t('Tool identifier'), header: t('Tool identifier'),
cell: (row) => ( cell: (row) => (
<Input <Input
value={row.key} value={row.key}
placeholder='web_search_preview:gpt-4o*' placeholder='web_search_preview:gpt-4o*'
onChange={(e) => updateRow(row.id, 'key', e.target.value)} onChange={(e) => updateRow(row.id, 'key', e.target.value)}
/> />
), ),
}, },
{ {
...@@ -275,15 +275,15 @@ export const ToolPriceSettings = memo(function ToolPriceSettings({ ...@@ -275,15 +275,15 @@ export const ToolPriceSettings = memo(function ToolPriceSettings({
header: t('Price ($/1K calls)'), header: t('Price ($/1K calls)'),
className: 'w-[200px]', className: 'w-[200px]',
cell: (row) => ( cell: (row) => (
<Input <Input
type='number' type='number'
min={0} min={0}
step={0.5} step={0.5}
value={row.price} value={row.price}
onChange={(e) => onChange={(e) =>
updateRow(row.id, 'price', Number(e.target.value) || 0) updateRow(row.id, 'price', Number(e.target.value) || 0)
} }
/> />
), ),
}, },
{ {
...@@ -292,14 +292,14 @@ export const ToolPriceSettings = memo(function ToolPriceSettings({ ...@@ -292,14 +292,14 @@ export const ToolPriceSettings = memo(function ToolPriceSettings({
className: 'w-[80px] text-right', className: 'w-[80px] text-right',
cellClassName: 'text-right', cellClassName: 'text-right',
cell: (row) => ( cell: (row) => (
<Button <Button
variant='ghost' variant='ghost'
size='icon' size='icon'
onClick={() => removeRow(row.id)} onClick={() => removeRow(row.id)}
aria-label={t('Delete')} aria-label={t('Delete')}
> >
<Trash2 className='text-destructive h-4 w-4' /> <Trash2 className='text-destructive h-4 w-4' />
</Button> </Button>
), ),
}, },
]} ]}
......
...@@ -487,7 +487,9 @@ export function DetailsDialog(props: DetailsDialogProps) { ...@@ -487,7 +487,9 @@ export function DetailsDialog(props: DetailsDialogProps) {
// Channel update records which fields changed (stable field tokens); render // Channel update records which fields changed (stable field tokens); render
// them with their localized labels for admins. // them with their localized labels for admins.
const changedFieldTokens = const changedFieldTokens =
isManage && props.isAdmin && Array.isArray(other?.op?.params?.changed_fields) isManage &&
props.isAdmin &&
Array.isArray(other?.op?.params?.changed_fields)
? (other.op.params.changed_fields as string[]) ? (other.op.params.changed_fields as string[])
: [] : []
const changedFieldsText = changedFieldTokens const changedFieldsText = changedFieldTokens
......
...@@ -44,7 +44,7 @@ export function AffiliateRewardsCard({ ...@@ -44,7 +44,7 @@ export function AffiliateRewardsCard({
const { t } = useTranslation() const { t } = useTranslation()
if (loading) { if (loading) {
return ( return (
<Card className='bg-muted/20 py-0'> <Card data-card-hover='false' className='bg-muted/20 py-0'>
<CardContent className='grid gap-4 p-3 sm:p-4 lg:grid-cols-[minmax(220px,1fr)_minmax(220px,0.72fr)_minmax(320px,1.15fr)] lg:items-center'> <CardContent className='grid gap-4 p-3 sm:p-4 lg:grid-cols-[minmax(220px,1fr)_minmax(220px,0.72fr)_minmax(320px,1.15fr)] lg:items-center'>
<div> <div>
<Skeleton className='h-5 w-32' /> <Skeleton className='h-5 w-32' />
...@@ -60,7 +60,7 @@ export function AffiliateRewardsCard({ ...@@ -60,7 +60,7 @@ export function AffiliateRewardsCard({
const hasRewards = (user?.aff_quota ?? 0) > 0 const hasRewards = (user?.aff_quota ?? 0) > 0
return ( return (
<Card className='bg-muted/20 py-0'> <Card data-card-hover='false' className='bg-muted/20 py-0'>
<CardContent className='grid gap-3 p-3 sm:gap-4 sm:p-4 lg:grid-cols-[minmax(200px,1fr)_minmax(180px,0.65fr)_minmax(280px,1fr)] lg:items-center'> <CardContent className='grid gap-3 p-3 sm:gap-4 sm:p-4 lg:grid-cols-[minmax(200px,1fr)_minmax(180px,0.65fr)_minmax(280px,1fr)] lg:items-center'>
<div className='flex min-w-0 items-center gap-2.5'> <div className='flex min-w-0 items-center gap-2.5'>
<div className='bg-background flex size-8 shrink-0 items-center justify-center rounded-lg border'> <div className='bg-background flex size-8 shrink-0 items-center justify-center rounded-lg border'>
......
...@@ -55,7 +55,8 @@ export function CreemProductsSection({ ...@@ -55,7 +55,8 @@ export function CreemProductsSection({
{products.map((product) => ( {products.map((product) => (
<Card <Card
key={product.productId} key={product.productId}
className='hover:border-foreground/50 cursor-pointer transition-all hover:shadow-md' data-card-hover='false'
className='cursor-pointer'
onClick={() => onProductSelect(product)} onClick={() => onProductSelect(product)}
> >
<CardContent className='p-3 text-center sm:p-4'> <CardContent className='p-3 text-center sm:p-4'>
...@@ -63,7 +64,7 @@ export function CreemProductsSection({ ...@@ -63,7 +64,7 @@ export function CreemProductsSection({
<div className='text-muted-foreground mb-2 text-sm'> <div className='text-muted-foreground mb-2 text-sm'>
{t('Quota')}: {formatNumber(product.quota)} {t('Quota')}: {formatNumber(product.quota)}
</div> </div>
<div className='text-lg font-semibold text-indigo-600'> <div className='text-primary text-lg font-semibold'>
{formatCreemPrice(product.price, product.currency)} {formatCreemPrice(product.price, product.currency)}
</div> </div>
</CardContent> </CardContent>
......
...@@ -183,7 +183,7 @@ export function BillingHistoryDialog({ ...@@ -183,7 +183,7 @@ export function BillingHistoryDialog({
return ( return (
<div <div
key={record.id} key={record.id}
className='hover:bg-muted/50 rounded-lg border p-3 transition-colors sm:p-4' className='rounded-lg border p-3 sm:p-4'
> >
{/* Header Row */} {/* Header Row */}
<div className='flex items-start justify-between gap-2'> <div className='flex items-start justify-between gap-2'>
......
...@@ -76,7 +76,7 @@ export function CreemConfirmDialog({ ...@@ -76,7 +76,7 @@ export function CreemConfirmDialog({
</div> </div>
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<span className='text-muted-foreground'>{t('Price')}</span> <span className='text-muted-foreground'>{t('Price')}</span>
<span className='font-medium text-indigo-600'> <span className='text-primary font-medium'>
{formatCreemPrice(product.price, product.currency)} {formatCreemPrice(product.price, product.currency)}
</span> </span>
</div> </div>
......
...@@ -139,7 +139,7 @@ export function RechargeFormCard({ ...@@ -139,7 +139,7 @@ export function RechargeFormCard({
if (loading) { if (loading) {
return ( return (
<Card className='gap-0 overflow-hidden py-0'> <Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
<CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'> <CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
<Skeleton className='h-6 w-32' /> <Skeleton className='h-6 w-32' />
<Skeleton className='mt-2 h-4 w-48' /> <Skeleton className='mt-2 h-4 w-48' />
...@@ -191,6 +191,7 @@ export function RechargeFormCard({ ...@@ -191,6 +191,7 @@ export function RechargeFormCard({
title={t('Add Funds')} title={t('Add Funds')}
description={t('Choose an amount and payment method')} description={t('Choose an amount and payment method')}
icon={<WalletCards className='h-4 w-4' />} icon={<WalletCards className='h-4 w-4' />}
disableHoverEffect
action={ action={
onOpenBilling ? ( onOpenBilling ? (
<Button <Button
...@@ -238,7 +239,7 @@ export function RechargeFormCard({ ...@@ -238,7 +239,7 @@ export function RechargeFormCard({
key={index} key={index}
variant='outline' variant='outline'
className={cn( className={cn(
'hover:border-foreground flex min-h-16 flex-col items-start rounded-lg px-3 py-2.5 text-left whitespace-normal sm:min-h-[72px] sm:p-4', 'flex min-h-16 flex-col items-start rounded-lg px-3 py-2.5 text-left whitespace-normal sm:min-h-[72px] sm:p-4',
selectedPreset === preset.value selectedPreset === preset.value
? 'border-foreground bg-foreground/5 dark:border-foreground dark:bg-foreground/10' ? 'border-foreground bg-foreground/5 dark:border-foreground dark:bg-foreground/10'
: 'border-muted' : 'border-muted'
......
...@@ -235,7 +235,7 @@ export function SubscriptionPlansCard({ ...@@ -235,7 +235,7 @@ export function SubscriptionPlansCard({
if (loading) { if (loading) {
return ( return (
<Card className='gap-0 overflow-hidden py-0'> <Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
<CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'> <CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
<Skeleton className='h-6 w-32' /> <Skeleton className='h-6 w-32' />
</CardHeader> </CardHeader>
...@@ -261,6 +261,7 @@ export function SubscriptionPlansCard({ ...@@ -261,6 +261,7 @@ export function SubscriptionPlansCard({
title={t('Subscription Plans')} title={t('Subscription Plans')}
description={t('Subscribe to a plan for model access')} description={t('Subscribe to a plan for model access')}
icon={<Crown className='h-4 w-4' />} icon={<Crown className='h-4 w-4' />}
disableHoverEffect
contentClassName='space-y-4 sm:space-y-5' contentClassName='space-y-4 sm:space-y-5'
> >
{/* My subscriptions & billing preference */} {/* My subscriptions & billing preference */}
...@@ -539,10 +540,8 @@ export function SubscriptionPlansCard({ ...@@ -539,10 +540,8 @@ export function SubscriptionPlansCard({
return ( return (
<Card <Card
key={plan.id} key={plan.id}
className={cn( data-card-hover='false'
'transition-shadow hover:shadow-md', className={cn(isPopular && 'border-primary/70 shadow-sm')}
isPopular && 'border-primary/70 shadow-sm'
)}
> >
<CardContent className='flex h-full flex-col p-3.5 sm:p-4'> <CardContent className='flex h-full flex-col p-3.5 sm:p-4'>
<div className='mb-2 flex items-start justify-between gap-3'> <div className='mb-2 flex items-start justify-between gap-3'>
......
...@@ -477,19 +477,19 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -477,19 +477,19 @@ For commercial licensing, please contact support@quantumnous.com
/* Micro-interactions — Vercel-style subtle hover/active feedback */ /* Micro-interactions — Vercel-style subtle hover/active feedback */
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
[data-slot='card'] { [data-slot='card']:not([data-card-hover='false']) {
transition: transition:
transform 150ms ease, transform 150ms ease,
box-shadow 150ms ease; box-shadow 150ms ease;
} }
@media (min-width: 641px) { @media (min-width: 641px) {
[data-slot='card']:hover { [data-slot='card']:not([data-card-hover='false']):hover {
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 4px 12px rgb(0 0 0 / 0.06); box-shadow: 0 4px 12px rgb(0 0 0 / 0.06);
} }
.dark [data-slot='card']:hover { .dark [data-slot='card']:not([data-card-hover='false']):hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 0.3); box-shadow: 0 4px 12px rgb(0 0 0 / 0.3);
} }
} }
......
...@@ -25,8 +25,8 @@ declare module '@tanstack/react-table' { ...@@ -25,8 +25,8 @@ declare module '@tanstack/react-table' {
className?: string className?: string
pinned?: 'left' | 'right' pinned?: 'left' | 'right'
// Mobile card list layout hints (used by MobileCardList) // Mobile card list layout hints (used by MobileCardList)
mobileTitle?: boolean // card title area (left, larger text) mobileTitle?: boolean // card title area (left, larger text)
mobileBadge?: boolean // status badge alongside title (right) mobileBadge?: boolean // status badge alongside title (right)
mobileHidden?: boolean // hide this column on mobile entirely mobileHidden?: boolean // hide this column on mobile entirely
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment