✨ 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:
+91
-23
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user