Files
yellowjacket/frontend/src/components/search-bar/search-bar.ts
T
yonlu aed90d7b17 feat(08-04): convert sidebar em-based spacing to px and apply icon/type tokens
- Sidebar: convert all em-based padding/gap to px values, icon sizes to --yj-icon-md
- Now-playing: cover placeholder icon to --yj-icon-lg, text sizes to type tokens
- Search-bar: search icon to --yj-icon-sm, input font to --yj-text-md
- Audio-player: convert em-based gap/padding/margin to px values
- Seek-bar: tooltip font to --yj-text-lg, margin from em to px
- Volume-control: padding/margin from em to px, thumb sizes from em to px
- All components import designTokens and include in static styles
2026-03-04 23:23:25 -05:00

180 lines
4.9 KiB
TypeScript

import { LitElement, html, css, nothing } from 'lit';
import { customElement, query } from 'lit/decorators.js';
import { SearchController } from '@store/controllers/search-controller';
import '@awesome.me/webawesome/dist/components/icon/icon.js';
import { designTokens } from '../../styles/tokens.css';
/**
* Global search bar displayed in the top bar.
* Hides itself when the active view is not searchable.
*/
@customElement('search-bar')
export class SearchBar extends LitElement {
private searchCtrl = new SearchController(this);
private searchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
@query('input')
private inputEl!: HTMLInputElement;
static override styles = [designTokens, css`
:host {
display: flex;
align-items: center;
}
:host([hidden]) {
display: none;
}
.search-container {
display: flex;
align-items: center;
background: var(--yj-bg-surface, #212529);
border: 1px solid var(--yj-border-subtle, #555);
border-radius: 6px;
padding: 0 10px;
gap: 8px;
height: 32px;
min-width: 200px;
max-width: 360px;
width: 100%;
transition: border-color 0.15s ease;
}
.search-container:focus-within {
border-color: var(--yj-accent, #ffd43b);
}
.search-icon {
color: var(--yj-text-tertiary, #888);
font-size: var(--yj-icon-sm);
flex-shrink: 0;
}
input {
flex: 1;
background: none;
border: none;
outline: none;
color: var(--yj-text-primary, #fff);
font-size: var(--yj-text-md);
font-family: inherit;
min-width: 0;
}
input::placeholder {
color: var(--yj-text-tertiary, #888);
}
.clear-button {
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--yj-text-tertiary, #888);
cursor: pointer;
padding: 0;
font-size: var(--yj-text-sm);
flex-shrink: 0;
}
.clear-button:hover {
color: var(--yj-text-primary, #fff);
}
`];
override updated() {
// Toggle the hidden attribute based on whether the
// current view supports searching.
if (this.searchCtrl.isSearchableView) {
this.removeAttribute('hidden');
} else {
this.setAttribute('hidden', '');
}
}
/**
* Focus the search input and select all text.
* Called by the global Ctrl+F handler.
*/
focusInput(): void {
if (!this.inputEl) return;
this.inputEl.focus();
this.inputEl.select();
}
private handleInput = (e: Event) => {
const input = e.target as HTMLInputElement;
const value = input.value;
if (this.searchDebounceTimer !== null) {
clearTimeout(this.searchDebounceTimer);
this.searchDebounceTimer = null;
}
if (value === '') {
// Instant clear for responsive feedback.
this.searchCtrl.term = '';
} else {
this.searchDebounceTimer = setTimeout(() => {
this.searchDebounceTimer = null;
this.searchCtrl.term = value;
}, 150);
}
};
private handleClear = () => {
this.searchCtrl.term = '';
if (this.inputEl) {
this.inputEl.value = '';
this.inputEl.focus();
}
};
private handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
this.searchCtrl.term = '';
if (this.inputEl) {
this.inputEl.value = '';
this.inputEl.blur();
}
}
};
override render() {
const term = this.searchCtrl.term;
return html`
<div class="search-container">
<wa-icon
class="search-icon"
name="magnifying-glass"
></wa-icon>
<input
type="text"
placeholder="Search..."
.value=${term}
@input=${this.handleInput}
@keydown=${this.handleKeydown}
/>
${term
? html`
<button
class="clear-button"
@click=${this.handleClear}
>
<wa-icon
name="xmark"
></wa-icon>
</button>
`
: nothing}
</div>
`;
}
}