feat: Add reusable filter/search components for consistency
NEW COMPONENTS in PremiumTable.tsx: 1. SearchInput - Consistent search box styling across all pages - Left-aligned search icon - Clear button (X) when text entered - Props: value, onChange, placeholder, onClear 2. TabBar - Consistent tab styling with counts - Support for accent/warning/default colors - Optional icons - Wraps on mobile - Props: tabs[], activeTab, onChange 3. FilterBar - Simple flex container for filter rows - Responsive: stacks on mobile, row on desktop - Props: children 4. SelectDropdown - Consistent select styling - Custom chevron icon - Props: value, onChange, options[] 5. ActionButton - Consistent button styling - Variants: primary (accent), secondary (outlined), ghost - Sizes: default, small - Optional icon - Props: children, onClick, disabled, variant, size, icon These components ensure visual consistency across all Command Center pages for search, filtering, and actions.
This commit is contained in:
@ -432,3 +432,201 @@ export function SectionHeader({
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SEARCH INPUT (consistent search styling)
|
||||
// ============================================================================
|
||||
|
||||
import { Search, X } from 'lucide-react'
|
||||
|
||||
export function SearchInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Search...',
|
||||
onClear,
|
||||
className,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
placeholder?: string
|
||||
onClear?: () => void
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={clsx("relative", className)}>
|
||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-muted" />
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full h-10 pl-10 pr-9 bg-background-secondary/50 border border-border/40 rounded-xl
|
||||
text-sm text-foreground placeholder:text-foreground-subtle
|
||||
focus:outline-none focus:border-accent/50 focus:bg-background-secondary/80 transition-all"
|
||||
/>
|
||||
{value && (onClear || onChange) && (
|
||||
<button
|
||||
onClick={() => onClear ? onClear() : onChange('')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground-subtle hover:text-foreground transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TAB BAR (consistent tab styling)
|
||||
// ============================================================================
|
||||
|
||||
interface TabItem {
|
||||
id: string
|
||||
label: string
|
||||
icon?: React.ComponentType<{ className?: string }>
|
||||
count?: number
|
||||
color?: 'default' | 'accent' | 'warning'
|
||||
}
|
||||
|
||||
export function TabBar({
|
||||
tabs,
|
||||
activeTab,
|
||||
onChange,
|
||||
className,
|
||||
}: {
|
||||
tabs: TabItem[]
|
||||
activeTab: string
|
||||
onChange: (id: string) => void
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={clsx("flex flex-wrap items-center gap-1.5 p-1.5 bg-background-secondary/30 border border-border/30 rounded-xl w-fit", className)}>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeTab === tab.id
|
||||
const Icon = tab.icon
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => onChange(tab.id)}
|
||||
className={clsx(
|
||||
"flex items-center gap-2 px-3.5 py-2 text-sm font-medium rounded-lg transition-all",
|
||||
isActive
|
||||
? tab.color === 'warning'
|
||||
? "bg-amber-500 text-background shadow-md"
|
||||
: tab.color === 'accent'
|
||||
? "bg-accent text-background shadow-md shadow-accent/20"
|
||||
: "bg-foreground/10 text-foreground"
|
||||
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
|
||||
)}
|
||||
>
|
||||
{Icon && <Icon className="w-4 h-4" />}
|
||||
<span className="hidden sm:inline">{tab.label}</span>
|
||||
{tab.count !== undefined && (
|
||||
<span className={clsx(
|
||||
"text-xs px-1.5 py-0.5 rounded-md tabular-nums",
|
||||
isActive ? "bg-background/20" : "bg-foreground/10"
|
||||
)}>
|
||||
{tab.count}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FILTER BAR (row of filters: search + select + buttons)
|
||||
// ============================================================================
|
||||
|
||||
export function FilterBar({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={clsx("flex flex-col sm:flex-row gap-3 sm:items-center", className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SELECT DROPDOWN (consistent select styling)
|
||||
// ============================================================================
|
||||
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
|
||||
export function SelectDropdown({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
className,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
options: { value: string; label: string }[]
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={clsx("relative", className)}>
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="h-10 pl-3.5 pr-9 bg-background-secondary/50 border border-border/40 rounded-xl
|
||||
text-sm text-foreground appearance-none cursor-pointer
|
||||
focus:outline-none focus:border-accent/50 focus:bg-background-secondary/80 transition-all"
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-muted pointer-events-none" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ACTION BUTTON (consistent button styling)
|
||||
// ============================================================================
|
||||
|
||||
export function ActionButton({
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
variant = 'primary',
|
||||
size = 'default',
|
||||
icon: Icon,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode
|
||||
onClick?: () => void
|
||||
disabled?: boolean
|
||||
variant?: 'primary' | 'secondary' | 'ghost'
|
||||
size?: 'small' | 'default'
|
||||
icon?: React.ComponentType<{ className?: string }>
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={clsx(
|
||||
"flex items-center justify-center gap-2 font-medium rounded-xl transition-all",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
size === 'small' ? "h-8 px-3 text-xs" : "h-10 px-4 text-sm",
|
||||
variant === 'primary' && "bg-accent text-background hover:bg-accent-hover shadow-lg shadow-accent/20",
|
||||
variant === 'secondary' && "bg-foreground/10 text-foreground hover:bg-foreground/15 border border-border/40",
|
||||
variant === 'ghost' && "text-foreground-muted hover:text-foreground hover:bg-foreground/5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{Icon && <Icon className={size === 'small' ? "w-3.5 h-3.5" : "w-4 h-4"} />}
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user