diff --git a/src/design-system/composites/FilterBar/FilterBar.module.css b/src/design-system/composites/FilterBar/FilterBar.module.css new file mode 100644 index 0000000..08d23f2 --- /dev/null +++ b/src/design-system/composites/FilterBar/FilterBar.module.css @@ -0,0 +1,58 @@ +.section { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + padding: 12px 16px; + box-shadow: var(--shadow-card); +} + +.row { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; +} + +.searchWrap { + flex: 1; + max-width: 360px; + min-width: 180px; +} + +.sep { + width: 1px; + height: 22px; + background: var(--border); + margin: 0 2px; + flex-shrink: 0; +} + +.filterGroup { + display: flex; + align-items: center; + gap: 5px; + flex-wrap: wrap; +} + +.activeTags { + display: flex; + gap: 6px; + flex-wrap: wrap; + align-items: center; + margin-top: 8px; +} + +.clearAll { + font-size: 11px; + color: var(--text-muted); + cursor: pointer; + padding: 3px 6px; + background: none; + border: none; + font-family: var(--font-body); + transition: color 0.12s; +} + +.clearAll:hover { + color: var(--error); +} diff --git a/src/design-system/composites/FilterBar/FilterBar.tsx b/src/design-system/composites/FilterBar/FilterBar.tsx new file mode 100644 index 0000000..f67c3c2 --- /dev/null +++ b/src/design-system/composites/FilterBar/FilterBar.tsx @@ -0,0 +1,119 @@ +import { useState, type ChangeEvent } from 'react' +import styles from './FilterBar.module.css' +import { Input } from '../../primitives/Input/Input' +import { FilterPill } from '../../primitives/FilterPill/FilterPill' +import { Tag } from '../../primitives/Tag/Tag' + +export interface FilterOption { + label: string + value: string + count?: number + color?: 'success' | 'error' | 'running' +} + +export interface ActiveFilter { + label: string + value: string +} + +interface FilterBarProps { + filters: FilterOption[] + activeFilters: ActiveFilter[] + onFilterChange: (filters: ActiveFilter[]) => void + searchPlaceholder?: string + searchValue?: string + onSearchChange?: (value: string) => void + className?: string +} + +export function FilterBar({ + filters, + activeFilters, + onFilterChange, + searchPlaceholder = 'Search...', + searchValue, + onSearchChange, + className, +}: FilterBarProps) { + const [internalSearch, setInternalSearch] = useState('') + const search = searchValue !== undefined ? searchValue : internalSearch + + function handleSearchChange(e: ChangeEvent) { + if (onSearchChange) { + onSearchChange(e.target.value) + } else { + setInternalSearch(e.target.value) + } + } + + function toggleFilter(filter: FilterOption) { + const isActive = activeFilters.some((f) => f.value === filter.value) + if (isActive) { + onFilterChange(activeFilters.filter((f) => f.value !== filter.value)) + } else { + onFilterChange([...activeFilters, { label: filter.label, value: filter.value }]) + } + } + + function removeFilter(value: string) { + onFilterChange(activeFilters.filter((f) => f.value !== value)) + } + + function clearAll() { + onFilterChange([]) + } + + const hasActiveFilters = activeFilters.length > 0 + + return ( +
+
+
+ + + + + } + /> +
+ + {filters.length > 0 &&
} + + {filters.length > 0 && ( +
+ {filters.map((filter) => ( + f.value === filter.value)} + onClick={() => toggleFilter(filter)} + /> + ))} +
+ )} +
+ + {hasActiveFilters && ( +
+ {activeFilters.map((filter) => ( + removeFilter(filter.value)} + color="auto" + /> + ))} + +
+ )} +
+ ) +} diff --git a/src/design-system/composites/ShortcutsBar/ShortcutsBar.module.css b/src/design-system/composites/ShortcutsBar/ShortcutsBar.module.css new file mode 100644 index 0000000..e27199c --- /dev/null +++ b/src/design-system/composites/ShortcutsBar/ShortcutsBar.module.css @@ -0,0 +1,31 @@ +.bar { + position: fixed; + bottom: 12px; + right: 12px; + display: flex; + gap: 10px; + z-index: 50; + animation: fadeIn 0.5s ease-out 0.5s both; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +.hint { + display: flex; + align-items: center; + gap: 5px; + font-size: 10px; + color: var(--text-muted); + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + padding: 4px 8px; + box-shadow: var(--shadow-sm); +} + +.label { + white-space: nowrap; +} diff --git a/src/design-system/composites/ShortcutsBar/ShortcutsBar.tsx b/src/design-system/composites/ShortcutsBar/ShortcutsBar.tsx new file mode 100644 index 0000000..97122be --- /dev/null +++ b/src/design-system/composites/ShortcutsBar/ShortcutsBar.tsx @@ -0,0 +1,27 @@ +import styles from './ShortcutsBar.module.css' +import { KeyboardHint } from '../../primitives/KeyboardHint/KeyboardHint' + +interface Shortcut { + keys: string + label: string +} + +interface ShortcutsBarProps { + shortcuts: Shortcut[] + className?: string +} + +export function ShortcutsBar({ shortcuts, className }: ShortcutsBarProps) { + if (shortcuts.length === 0) return null + + return ( +
+ {shortcuts.map((shortcut, index) => ( +
+ + {shortcut.label} +
+ ))} +
+ ) +}