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