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:
yves.gugger
2025-12-10 16:30:38 +01:00
parent a73d8d3897
commit 249c82976d

View File

@ -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>
)
}