diff --git a/src/lib/components/collection/CollectionFilters.svelte b/src/lib/components/collection/CollectionFilters.svelte index 0294bc89..a863e9a1 100644 --- a/src/lib/components/collection/CollectionFilters.svelte +++ b/src/lib/components/collection/CollectionFilters.svelte @@ -9,6 +9,8 @@ import ViewModeToggle from '$lib/components/ui/ViewModeToggle.svelte' import { createQuery, queryOptions } from '@tanstack/svelte-query' import { entityAdapter } from '$lib/api/adapters/entity.adapter' + import { DropdownMenu } from 'bits-ui' + import Icon from '$lib/components/Icon.svelte' type EntityType = 'character' | 'weapon' | 'summon' @@ -288,78 +290,158 @@ proficiencyFilters.length > 0 || genderFilters.length > 0 ) + + // Overflow detection state + type FilterKey = 'element' | 'rarity' | 'season' | 'series' | 'race' | 'proficiency' | 'gender' + + interface FilterConfig { + key: FilterKey + options: { value: number | string; label: string; color?: string }[] + value: (number | string)[] + onChange: (value: (number | string)[]) => void + placeholder: string + } + + // Filters that always go in the "More" dropdown + const moreFilterKeys: FilterKey[] = ['race', 'gender'] + + // Unified filter configuration + const filterConfigs = $derived([ + { + key: 'element', + options: elements, + value: elementFilters, + onChange: (v) => handleElementChange(v as number[]), + placeholder: 'Element' + }, + { + key: 'rarity', + options: rarities, + value: rarityFilters, + onChange: (v) => handleRarityChange(v as number[]), + placeholder: 'Rarity' + }, + { + key: 'season', + options: seasons, + value: seasonFilters, + onChange: (v) => handleSeasonChange(v as number[]), + placeholder: 'Season' + }, + { + key: 'series', + options: seriesOptions, + value: seriesFilters, + onChange: (v) => handleSeriesChange(v), + placeholder: entityType === 'weapon' ? 'Weapon Series' : 'Series' + }, + { + key: 'race', + options: races, + value: raceFilters, + onChange: (v) => handleRaceChange(v as number[]), + placeholder: 'Race' + }, + { + key: 'proficiency', + options: proficiencies, + value: proficiencyFilters, + onChange: (v) => handleProficiencyChange(v as number[]), + placeholder: entityType === 'weapon' ? 'Weapon Type' : 'Proficiency' + }, + { + key: 'gender', + options: genders, + value: genderFilters, + onChange: (v) => handleGenderChange(v as number[]), + placeholder: 'Gender' + } + ]) + + // Active filters based on visibility settings + const activeFilters = $derived( + filterConfigs.filter((f) => effectiveShowFilters[f.key]) + ) + + // Filters visible in the main row (not in moreFilterKeys) + const visibleFilters = $derived( + activeFilters.filter((f) => !moreFilterKeys.includes(f.key)) + ) + + // Filters in the "More" dropdown + const moreFilters = $derived( + activeFilters.filter((f) => moreFilterKeys.includes(f.key)) + ) + + const showMoreButton = $derived(moreFilters.length > 0) + + // Toggle function for submenu multi-select behavior + function toggleFilterValue(filter: FilterConfig, value: number | string) { + const currentValues = filter.value + const newValues = currentValues.includes(value) + ? currentValues.filter((v) => v !== value) + : [...currentValues, value] + + filter.onChange(newValues) + }
- {#if effectiveShowFilters.element} + {#each visibleFilters as filter (filter.key)} - {/if} + {/each} - {#if effectiveShowFilters.rarity} - - {/if} + {#if showMoreButton} + + + More + + - {#if effectiveShowFilters.season} - - {/if} + + + {#each moreFilters as filter (filter.key)} + + + {filter.placeholder} + {#if filter.value.length > 0} + {filter.value.length} + {/if} + + - {#if effectiveShowFilters.series} - - {/if} - - {#if effectiveShowFilters.race} - - {/if} - - {#if effectiveShowFilters.proficiency} - - {/if} - - {#if effectiveShowFilters.gender} - + + {#each filter.options as option (option.value)} + {@const isSelected = filter.value.includes(option.value)} + { + e.preventDefault() + toggleFilterValue(filter, option.value) + }} + > + {#if option.color} + + {/if} + {option.label} + {#if isSelected} + + {/if} + + {/each} + + + {/each} + + + {/if} {#if hasActiveFilters} @@ -389,6 +471,8 @@ @use '$src/themes/spacing' as *; @use '$src/themes/layout' as *; @use '$src/themes/typography' as *; + @use '$src/themes/colors' as *; + @use '$src/themes/effects' as *; .filters-container { display: flex; @@ -440,4 +524,152 @@ text-decoration: underline; } } + + // More button trigger - matches MultiSelect trigger styling + :global(.more-trigger) { + all: unset; + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + display: inline-flex; + align-items: center; + gap: $unit-half; + padding: $unit-half $unit; + font-size: $font-small; + font-family: var(--font-family); + min-height: $unit-3x; + color: var(--text-tertiary); + background-color: var(--input-bg); + border-radius: $input-corner; + border: 1px solid var(--border-color, transparent); + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease; + + &:hover { + background-color: var(--input-bg-hover); + } + + &:focus-visible { + outline: 2px solid $blue; + outline-offset: 2px; + } + + :global(svg) { + flex-shrink: 0; + color: var(--text-tertiary); + } + } + + // More dropdown content + :global(.more-menu-content) { + background: var(--dialog-bg); + border-radius: $card-corner; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + padding: $unit-half; + min-width: calc($unit * 18); + z-index: 50; + animation: fadeIn 0.15s ease-out; + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + } + + // Submenu trigger + :global(.more-menu-subtrigger) { + display: flex; + align-items: center; + gap: $unit; + padding: $unit calc($unit * 1.5); + border-radius: $item-corner-small; + cursor: pointer; + font-size: $font-small; + color: var(--text-primary); + transition: background-color 0.1s ease; + + &:hover, + &[data-highlighted] { + background: var(--option-bg-hover); + } + + .submenu-label { + flex: 1; + } + + .selection-badge { + background: var(--accent-color, #{$blue}); + color: white; + font-size: 11px; + font-weight: $medium; + padding: 2px 6px; + border-radius: $full-corner; + min-width: 18px; + text-align: center; + } + + :global(.submenu-chevron) { + color: var(--text-tertiary); + margin-left: auto; + } + } + + // Submenu content + :global(.submenu-content) { + background: var(--dialog-bg); + border-radius: $card-corner; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + padding: $unit-half; + min-width: calc($unit * 16); + max-height: 280px; + overflow: auto; + z-index: 51; + } + + // Submenu items + :global(.submenu-item) { + display: flex; + align-items: center; + gap: $unit; + padding: $unit calc($unit * 1.5); + border-radius: $item-corner-small; + cursor: pointer; + font-size: $font-small; + color: var(--text-primary); + transition: background-color 0.1s ease; + + &:hover, + &[data-highlighted] { + background: var(--option-bg-hover); + } + + &.selected { + .item-label { + font-weight: $medium; + } + } + + .color-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + + .item-label { + flex: 1; + } + + :global(.check-icon) { + color: var(--accent-color); + margin-left: auto; + } + }