Files
kashilo/js/components/search-box.js

952 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { t, i18n } from '../i18n.js'
const CATEGORIES = {
electronics: ['phones', 'computers', 'tv_audio', 'gaming', 'appliances'],
vehicles: ['cars', 'motorcycles', 'bikes', 'parts'],
furniture: ['living', 'bedroom', 'office', 'outdoor_furniture'],
clothing: ['women', 'men', 'kids', 'shoes', 'accessories'],
sports: ['fitness', 'outdoor', 'winter', 'water', 'team_sports'],
books: ['fiction', 'nonfiction', 'textbooks', 'music_movies'],
garden: ['plants', 'tools', 'outdoor_living', 'decoration'],
other: ['collectibles', 'art', 'handmade', 'services']
}
const COUNTRIES = ['ch', 'de', 'at', 'fr', 'it', 'li']
const RADIUS_OPTIONS = [5, 10, 20, 50, 100, 200]
class SearchBox extends HTMLElement {
static get observedAttributes() {
return ['category', 'subcategory', 'country', 'query']
}
constructor() {
super()
this.loadFiltersFromStorage()
}
loadFiltersFromStorage() {
const saved = localStorage.getItem('searchFilters')
if (saved) {
try {
const filters = JSON.parse(saved)
this.selectedCategory = filters.category || ''
this.selectedSubcategory = filters.subcategory || ''
this.selectedCountry = filters.country || 'ch'
this.selectedRadius = filters.radius || 50
this.useCurrentLocation = filters.useCurrentLocation || false
this.searchQuery = filters.query || ''
} catch (e) {
this.resetFilters()
}
} else {
this.resetFilters()
}
}
resetFilters() {
this.selectedCategory = ''
this.selectedSubcategory = ''
this.selectedCountry = 'ch'
this.selectedRadius = 50
this.useCurrentLocation = false
this.searchQuery = ''
this.geoLoading = false
this.currentLat = null
this.currentLng = null
}
saveFiltersToStorage() {
const filters = {
category: this.selectedCategory,
subcategory: this.selectedSubcategory,
country: this.selectedCountry,
radius: this.selectedRadius,
useCurrentLocation: this.useCurrentLocation,
query: this.searchQuery
}
localStorage.setItem('searchFilters', JSON.stringify(filters))
}
connectedCallback() {
// Override from attributes if provided
if (this.hasAttribute('category')) {
this.selectedCategory = this.getAttribute('category')
}
if (this.hasAttribute('subcategory')) {
this.selectedSubcategory = this.getAttribute('subcategory')
}
if (this.hasAttribute('country')) {
this.selectedCountry = this.getAttribute('country')
}
if (this.hasAttribute('query')) {
this.searchQuery = this.getAttribute('query')
}
this.render()
this.setupEventListeners()
this.unsubscribe = i18n.subscribe(() => {
this.render()
this.setupEventListeners()
})
}
disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe()
if (this._closeDropdown) {
document.removeEventListener('click', this._closeDropdown)
}
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return
switch (name) {
case 'category':
this.selectedCategory = newValue || ''
break
case 'subcategory':
this.selectedSubcategory = newValue || ''
break
case 'country':
this.selectedCountry = newValue || 'ch'
break
case 'query':
this.searchQuery = newValue || ''
break
}
if (this.isConnected) {
this.render()
this.setupEventListeners()
}
}
render() {
this.innerHTML = /* html */`
<form class="search-box" id="search-form">
<div class="search-row search-row-query">
<div class="search-field search-field-query">
<svg class="field-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text" id="search-query" placeholder="${t('header.searchPlaceholder')}" value="${this.searchQuery}" />
</div>
</div>
${this.renderFilters()}
<div class="search-row search-row-submit">
<button type="submit" class="btn btn-primary btn-search">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<span class="btn-search-text">${t('search.searchButton')}</span>
</button>
</div>
</form>
`
}
renderFilters() {
// Track which category accordion is expanded
this._expandedCategory = this._expandedCategory || ''
return /* html */`
<!-- Accordion Category Dropdown -->
<div class="search-row search-row-filters">
<div class="category-dropdown">
<button type="button" class="category-dropdown-trigger" id="category-trigger">
<span class="category-dropdown-label">
${this.selectedCategory
? (this.selectedSubcategory
? `${t(`categories.${this.selectedCategory}`)} ${t(`subcategories.${this.selectedSubcategory}`)}`
: t(`categories.${this.selectedCategory}`))
: t('search.allCategories')}
</span>
<svg class="category-dropdown-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div class="category-menu" id="category-menu">
<div class="category-menu-inner">
<button type="button" class="category-item category-item--all ${!this.selectedCategory ? 'active' : ''}" data-category="" data-subcategory="">
${t('search.allCategories')}
</button>
${Object.keys(CATEGORIES).map(cat => `
<div class="category-accordion ${this._expandedCategory === cat ? 'expanded' : ''}">
<button type="button" class="category-item ${this.selectedCategory === cat ? 'active' : ''}" data-category="${cat}">
<span>${t(`categories.${cat}`)}</span>
<svg class="category-item-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div class="subcategory-list">
<button type="button" class="subcategory-item ${this.selectedCategory === cat && !this.selectedSubcategory ? 'active' : ''}" data-category="${cat}" data-subcategory="">
${t('search.allIn')} ${t(`categories.${cat}`)}
</button>
${CATEGORIES[cat].map(sub => `
<button type="button" class="subcategory-item ${this.selectedCategory === cat && this.selectedSubcategory === sub ? 'active' : ''}" data-category="${cat}" data-subcategory="${sub}">
${t(`subcategories.${sub}`)}
</button>
`).join('')}
</div>
</div>
`).join('')}
</div>
</div>
</div>
<!-- Location (inline on desktop) -->
<div class="filter-location">
<div class="search-field search-field-country">
<svg class="field-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<select id="country-select">
<option value="current" ${this.useCurrentLocation ? 'selected' : ''}>
📍 ${t('search.currentLocation')}
</option>
${COUNTRIES.map(c => `
<option value="${c}" ${!this.useCurrentLocation && this.selectedCountry === c ? 'selected' : ''}>
${t(`countries.${c}`)}
</option>
`).join('')}
</select>
</div>
</div>
<!-- Radius (inline on desktop) -->
<div class="filter-radius ${!this.useCurrentLocation ? 'hidden' : ''}">
<div class="search-field search-field-radius">
<select id="radius-select">
${RADIUS_OPTIONS.map(r => `
<option value="${r}" ${this.selectedRadius === r ? 'selected' : ''}>
${r} km
</option>
`).join('')}
</select>
</div>
</div>
</div>
<!-- Mobile only: Location row -->
<div class="search-row search-row-location mobile-only">
<div class="search-field search-field-country">
<svg class="field-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<select id="country-select-mobile">
<option value="current" ${this.useCurrentLocation ? 'selected' : ''}>
📍 ${t('search.currentLocation')}
</option>
${COUNTRIES.map(c => `
<option value="${c}" ${!this.useCurrentLocation && this.selectedCountry === c ? 'selected' : ''}>
${t(`countries.${c}`)}
</option>
`).join('')}
</select>
</div>
</div>
<!-- Mobile only: Radius row -->
<div class="search-row search-row-radius mobile-only ${!this.useCurrentLocation ? 'hidden' : ''}">
<div class="search-field search-field-radius">
<select id="radius-select-mobile">
${RADIUS_OPTIONS.map(r => `
<option value="${r}" ${this.selectedRadius === r ? 'selected' : ''}>
${r} km
</option>
`).join('')}
</select>
</div>
</div>
`
}
setupEventListeners() {
const form = this.querySelector('#search-form')
const queryInput = this.querySelector('#search-query')
// Desktop selects
const countrySelect = this.querySelector('#country-select')
const radiusSelect = this.querySelector('#radius-select')
// Mobile selects
const countrySelectMobile = this.querySelector('#country-select-mobile')
const radiusSelectMobile = this.querySelector('#radius-select-mobile')
// Accordion dropdown
const categoryTrigger = this.querySelector('#category-trigger')
const categoryMenu = this.querySelector('#category-menu')
form?.addEventListener('submit', (e) => {
e.preventDefault()
this.handleSearch()
})
queryInput?.addEventListener('input', (e) => {
this.searchQuery = e.target.value
})
// Toggle dropdown
if (categoryTrigger && categoryMenu) {
categoryTrigger.addEventListener('click', (e) => {
e.preventDefault()
e.stopPropagation()
categoryMenu.classList.toggle('open')
})
}
// Close dropdown on outside click
this._closeDropdown = (e) => {
if (!this.contains(e.target)) {
categoryMenu?.classList.remove('open')
}
}
document.addEventListener('click', this._closeDropdown)
// Category accordion headers - toggle expand
this.querySelectorAll('.category-accordion > .category-item').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault()
e.stopPropagation()
const cat = item.dataset.category
const accordion = item.closest('.category-accordion')
// Toggle this accordion
if (this._expandedCategory === cat) {
this._expandedCategory = ''
accordion?.classList.remove('expanded')
} else {
// Close other accordions
this.querySelectorAll('.category-accordion.expanded').forEach(el => {
el.classList.remove('expanded')
})
this._expandedCategory = cat
accordion?.classList.add('expanded')
}
})
})
// "All categories" button
this.querySelector('.category-item--all')?.addEventListener('click', (e) => {
e.stopPropagation()
this.selectedCategory = ''
this.selectedSubcategory = ''
this._expandedCategory = ''
this.saveFiltersToStorage()
categoryMenu?.classList.remove('open')
this.render()
this.setupEventListeners()
})
// Subcategory items - select category + subcategory
this.querySelectorAll('.subcategory-item').forEach(item => {
item.addEventListener('click', (e) => {
e.stopPropagation()
this.selectedCategory = item.dataset.category
this.selectedSubcategory = item.dataset.subcategory
this._expandedCategory = ''
this.saveFiltersToStorage()
categoryMenu?.classList.remove('open')
this.render()
this.setupEventListeners()
})
})
// Country select handler (both desktop and mobile)
const handleCountryChange = (e) => {
if (e.target.value === 'current') {
this.useCurrentLocation = true
this.requestGeolocation()
} else {
this.useCurrentLocation = false
this.selectedCountry = e.target.value
}
this.saveFiltersToStorage()
this.render()
this.setupEventListeners()
}
countrySelect?.addEventListener('change', handleCountryChange)
countrySelectMobile?.addEventListener('change', handleCountryChange)
// Radius select handler (both desktop and mobile)
const handleRadiusChange = (e) => {
this.selectedRadius = parseInt(e.target.value)
this.saveFiltersToStorage()
}
radiusSelect?.addEventListener('change', handleRadiusChange)
radiusSelectMobile?.addEventListener('change', handleRadiusChange)
// Adjust select width to selected option (desktop only)
this.adjustSelectWidth(countrySelect)
this.adjustSelectWidth(radiusSelect)
}
adjustSelectWidth(select) {
if (!select) return
// Only apply fixed width on desktop (768px+)
if (window.innerWidth < 768) {
select.style.width = ''
return
}
// Create hidden span to measure text width
const measurer = document.createElement('span')
measurer.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap;font:inherit;'
select.parentElement.appendChild(measurer)
const selectedOption = select.options[select.selectedIndex]
measurer.textContent = selectedOption ? selectedOption.textContent : ''
// Add padding for arrow, icon and buffer
select.style.width = (measurer.offsetWidth + 90) + 'px'
measurer.remove()
}
handleSearch() {
const params = new URLSearchParams()
if (this.searchQuery) params.set('q', this.searchQuery)
if (this.selectedCategory) params.set('category', this.selectedCategory)
if (this.selectedSubcategory) params.set('sub', this.selectedSubcategory)
if (this.useCurrentLocation && this.currentLat && this.currentLng) {
params.set('lat', this.currentLat)
params.set('lng', this.currentLng)
params.set('radius', this.selectedRadius)
} else if (!this.useCurrentLocation) {
params.set('country', this.selectedCountry)
}
this.saveFiltersToStorage()
// Emit custom event
const event = new CustomEvent('search', {
bubbles: true,
cancelable: true,
detail: {
query: this.searchQuery,
category: this.selectedCategory,
subcategory: this.selectedSubcategory,
country: this.selectedCountry,
useCurrentLocation: this.useCurrentLocation,
lat: this.currentLat,
lng: this.currentLng,
radius: this.selectedRadius,
params: params.toString()
}
})
const cancelled = !this.dispatchEvent(event)
// Navigate to search page unless event was cancelled
if (!cancelled && !this.hasAttribute('no-navigate')) {
const url = '#/search' + (params.toString() ? '?' + params.toString() : '')
window.location.hash = url
}
}
requestGeolocation() {
if (!('geolocation' in navigator)) {
this.handleGeoError()
return
}
this.geoLoading = true
this.updateGeoButton()
navigator.geolocation.getCurrentPosition(
(position) => {
this.currentLat = position.coords.latitude
this.currentLng = position.coords.longitude
this.geoLoading = false
this.updateGeoButton()
},
(error) => {
console.warn('Geolocation error:', error)
this.handleGeoError()
},
{ timeout: 10000, enableHighAccuracy: false }
)
}
handleGeoError() {
// Keep useCurrentLocation = true, just stop loading indicator
// User can still search by current location (backend will handle it)
this.geoLoading = false
this.updateGeoButton()
}
updateGeoButton() {
const countrySelect = this.querySelector('#country-select')
if (!countrySelect) return
if (this.geoLoading) {
countrySelect.disabled = true
const currentOption = countrySelect.querySelector('option[value="current"]')
if (currentOption) {
currentOption.textContent = `${t('search.locating')}`
}
} else {
countrySelect.disabled = false
const currentOption = countrySelect.querySelector('option[value="current"]')
if (currentOption) {
currentOption.textContent = `📍 ${t('search.currentLocation')}`
}
}
}
// Public API
getFilters() {
return {
query: this.searchQuery,
category: this.selectedCategory,
subcategory: this.selectedSubcategory,
country: this.selectedCountry,
useCurrentLocation: this.useCurrentLocation,
lat: this.currentLat,
lng: this.currentLng,
radius: this.selectedRadius
}
}
setFilters(filters) {
if (filters.query !== undefined) this.searchQuery = filters.query
if (filters.category !== undefined) this.selectedCategory = filters.category
if (filters.subcategory !== undefined) this.selectedSubcategory = filters.subcategory
if (filters.country !== undefined) this.selectedCountry = filters.country
if (filters.radius !== undefined) this.selectedRadius = filters.radius
if (filters.useCurrentLocation !== undefined) this.useCurrentLocation = filters.useCurrentLocation
this.saveFiltersToStorage()
this.render()
this.setupEventListeners()
}
clearFilters() {
this.resetFilters()
localStorage.removeItem('searchFilters')
this.render()
this.setupEventListeners()
}
}
customElements.define('search-box', SearchBox)
const style = document.createElement('style')
style.textContent = /* css */`
search-box {
display: block;
}
search-box .search-box {
max-width: 800px;
width: 100%;
margin: 0 auto;
background: var(--color-bg);
border: 2px solid var(--color-border);
border-radius: var(--radius-lg);
box-sizing: border-box;
}
search-box .search-row {
display: flex;
border-bottom: 1px solid var(--color-border);
}
search-box .search-row:last-child {
border-bottom: none;
}
search-box .search-field {
flex: 1;
display: flex;
align-items: center;
position: relative;
}
search-box .search-field + .search-field {
border-left: 1px solid var(--color-border);
}
search-box .search-field.hidden,
search-box .search-row.hidden {
display: none;
}
search-box .field-icon {
position: absolute;
left: var(--space-md);
color: var(--color-text-muted);
pointer-events: none;
}
search-box .search-field input,
search-box .search-field select {
width: 100%;
padding: var(--space-md);
padding-left: 2.75rem;
border: none;
background: transparent;
font-size: var(--font-size-base);
color: var(--color-text);
}
search-box .search-field select {
padding-left: var(--space-md);
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236B5B95' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right var(--space-md) center;
padding-right: 2.5rem;
}
search-box .search-field-country select {
padding-left: 2.75rem;
}
search-box .search-field input:focus,
search-box .search-field select:focus {
background: var(--color-bg-secondary);
}
search-box .search-field input:focus-visible,
search-box .search-field select:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: -2px;
}
/* Mobile: Search query field top corners */
search-box .search-field-query input {
border-radius: calc(var(--radius-lg) - 2px) calc(var(--radius-lg) - 2px) 0 0;
}
search-box .search-field-query input:focus {
border-radius: calc(var(--radius-lg) - 2px) calc(var(--radius-lg) - 2px) 0 0;
}
search-box .search-field input::placeholder {
color: var(--color-text-muted);
}
search-box .search-row-submit {
padding: var(--space-sm);
background: var(--color-bg-secondary);
border-radius: 0 0 calc(var(--radius-lg) - 2px) calc(var(--radius-lg) - 2px);
}
search-box .btn-search {
width: 100%;
padding: var(--space-md);
gap: var(--space-sm);
}
/* Mobile: full width for location/radius */
search-box .search-row-location .search-field,
search-box .search-row-radius .search-field {
flex: 1;
}
search-box .search-row-location .search-field select,
search-box .search-row-radius .search-field select {
width: 100%;
}
/* Mobile: hide desktop-only filter elements */
search-box .filter-location,
search-box .filter-radius {
display: none;
}
/* Desktop: two-row layout */
@media (min-width: 768px) {
/* Hide mobile-only rows on desktop */
search-box .mobile-only {
display: none !important;
}
/* Show inline filters on desktop */
search-box .filter-location,
search-box .filter-radius {
display: flex;
border-left: 1px solid var(--color-border);
}
search-box .filter-radius.hidden {
display: none;
}
search-box .filter-location .search-field,
search-box .filter-radius .search-field {
flex: 0 0 auto;
}
search-box .filter-location select,
search-box .filter-radius select {
width: auto;
}
search-box .search-box {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
border-radius: var(--radius-xl);
}
search-box .search-box .search-row {
border-bottom: none;
}
/* Row 1: Search field + Button */
search-box .search-box .search-row-query {
grid-column: 1;
grid-row: 1;
border-right: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
}
search-box .search-box .search-row-submit {
grid-column: 2;
grid-row: 1;
padding: var(--space-xs);
background: transparent;
border-bottom: 1px solid var(--color-border);
}
/* Row 2: Filters */
search-box .search-box .search-row-filters {
grid-column: 1 / -1;
grid-row: 2;
display: flex;
border-right: none;
}
search-box .search-box .btn-search {
width: auto;
padding: var(--space-md);
border-radius: var(--radius-md);
}
/* Desktop: Submit row - top-right corner inherits from parent */
search-box .search-box .search-row-submit {
border-radius: 0 calc(var(--radius-xl) - 2px) 0 0;
}
search-box .search-box .btn-search-text {
display: none;
}
/* Category truncation */
search-box .search-box .category-dropdown-label {
max-width: 250px;
}
/* Desktop: Search query field - only top-left rounded */
search-box .search-field-query input {
border-radius: calc(var(--radius-xl) - 2px) 0 0 0;
}
/* Desktop: Category dropdown trigger - bottom-left rounded on hover/focus */
search-box .category-dropdown-trigger:hover,
search-box .category-dropdown-trigger:focus {
border-radius: 0 0 0 calc(var(--radius-xl) - 2px);
}
/* Desktop: Last filter (location/radius) - bottom-right rounded */
search-box .filter-location:last-child select,
search-box .filter-radius:not(.hidden):last-child select {
border-radius: 0 0 calc(var(--radius-xl) - 2px) 0;
}
search-box .filter-location:last-child select:hover,
search-box .filter-location:last-child select:focus,
search-box .filter-radius:not(.hidden):last-child select:hover,
search-box .filter-radius:not(.hidden):last-child select:focus {
background: var(--color-bg-secondary);
border-radius: 0 0 calc(var(--radius-xl) - 2px) 0;
}
}
/* Category Dropdown (Desktop) */
search-box .category-dropdown {
position: relative;
flex: 1;
min-width: 0;
}
search-box .category-dropdown-trigger {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-md);
background: transparent;
border: none;
font-size: var(--font-size-base);
color: var(--color-text);
cursor: pointer;
text-align: left;
transition: background var(--transition-fast);
}
search-box .category-dropdown-trigger:hover {
background: var(--color-bg-secondary);
}
search-box .category-dropdown-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
search-box .category-dropdown-arrow {
flex-shrink: 0;
margin-left: var(--space-sm);
color: var(--color-text-muted);
transition: transform var(--transition-fast);
}
search-box .category-menu.open + .category-dropdown-arrow,
search-box .category-dropdown-trigger:focus .category-dropdown-arrow {
transform: rotate(180deg);
}
search-box .category-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all var(--transition-fast);
z-index: var(--z-dropdown);
max-width: calc(100vw - 2 * var(--space-md));
}
search-box .category-menu.open {
opacity: 1;
visibility: visible;
transform: translateY(4px);
}
search-box .category-menu-inner {
padding: var(--space-xs);
max-height: 400px;
overflow-y: auto;
}
search-box .category-item,
search-box .category-item--all {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
background: transparent;
border: none;
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
color: var(--color-text);
cursor: pointer;
text-align: left;
transition: background var(--transition-fast);
}
search-box .category-item span {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
search-box .category-item:hover,
search-box .category-item--all:hover {
background: var(--color-bg-secondary);
}
search-box .category-item.active,
search-box .category-item--all.active {
background: var(--color-primary-light);
color: var(--color-primary);
}
search-box .category-item-arrow {
flex-shrink: 0;
color: var(--color-text-muted);
transition: transform var(--transition-fast);
}
/* Accordion */
search-box .category-accordion {
border-top: 1px solid var(--color-border);
}
search-box .category-accordion:first-of-type {
border-top: none;
}
search-box .category-accordion .category-item-arrow {
transform: rotate(0deg);
}
search-box .category-accordion.expanded .category-item-arrow {
transform: rotate(180deg);
}
search-box .subcategory-list {
display: none;
padding: 0 var(--space-xs) var(--space-xs);
}
search-box .category-accordion.expanded .subcategory-list {
display: block;
}
search-box .subcategory-item {
width: 100%;
display: block;
padding: var(--space-sm) var(--space-md);
padding-left: var(--space-xl);
background: transparent;
border: none;
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
cursor: pointer;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: background var(--transition-fast), color var(--transition-fast);
}
search-box .subcategory-item:hover {
background: var(--color-bg-secondary);
color: var(--color-text);
}
search-box .subcategory-item.active {
background: var(--color-primary-light);
color: var(--color-primary);
}
`
document.head.appendChild(style)
export { SearchBox, CATEGORIES, COUNTRIES, RADIUS_OPTIONS }