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
+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