@styloviz/search-bar 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 StyloViz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @styloviz/search-bar
2
+
3
+ > Search input with live suggestions, keyboard navigation and a clearable field.
4
+
5
+ Part of the **StyloViz UI Kit** — a premium Angular 21 + Tailwind CSS 4 dashboard component library. Standalone, `OnPush`, strongly typed, dark-mode ready.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @styloviz/core @styloviz/search-bar
11
+ ```
12
+
13
+ Requires `@angular/core` and `@angular/common` >= 21. `@styloviz/core` is a peer dependency shared by every component. Prefer everything at once? `@styloviz/all` installs the whole free tier in one command.
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { SvSearchBarComponent } from '@styloviz/search-bar';
19
+
20
+ @Component({
21
+ standalone: true,
22
+ imports: [SvSearchBarComponent],
23
+ template: `
24
+ <sv-search-bar />
25
+ `,
26
+ })
27
+ export class DemoComponent {}
28
+ ```
29
+
30
+ ## Inputs
31
+
32
+ | Input | Type | Default | Description |
33
+ | --- | --- | --- | --- |
34
+ | `value` | `string (two-way)` | `''` | Current input text. Use `[(value)]` for two-way binding. |
35
+ | `label` | `string` | `''` | Floating label text — only shown when `variant="floating"`. Acts as a Material Design-style animated label. |
36
+ | `placeholder` | `string` | `'Search…'` | Placeholder text. |
37
+ | `ariaLabel` | `string` | `''` | Explicit accessible name for the input. Falls back to `label` then `placeholder`. |
38
+ | `shortcutHint` | `string` | `''` | Keyboard shortcut hint shown on the right side of the input when empty. E.g. `'⌘K'`, `'Ctrl+K'`, `'/''`. Empty string hides the badge. |
39
+ | `suggestions` | `SearchSuggestion[]` | `[]` | Suggestion items to display in the dropdown. Pass a filtered/sliced array; the component renders all provided items. Empty array closes the dropdown. |
40
+ | `suggestionsLabel` | `string` | `''` | Label shown above the suggestions list (e.g. 'Recent searches', 'Results'). Empty string hides the header. |
41
+ | `footerLabel` | `string` | `''` | Text displayed at the bottom of the dropdown — useful for "View all results". Empty string hides the footer. |
42
+ | `filters` | `SearchFilter[]` | `[]` | Active filter chips rendered inside the input track. Each chip has an × button that emits `filterRemoved`. |
43
+ | `size` | `SearchBarSize` | `'md'` | Field size: sm, md or lg. |
44
+ | `variant` | `SearchBarVariant` | `'default'` | Visual variant: default, floating, minimal… |
45
+ | `showSearchIcon` | `boolean` | `true` | Show the magnifier icon on the left. |
46
+ | `loading` | `boolean` | `false` | Show the spinning loader inside the search icon area. |
47
+ | `disabled` | `boolean` | `false` | Whether the input is disabled. |
48
+ | `debounceMs` | `number` | `300` | Debounce delay in milliseconds before `searched` emits. Set to 0 to emit on every keystroke. |
49
+ | `customClass` | `string` | `''` | Additional classes on the root wrapper. |
50
+
51
+ ## Outputs
52
+
53
+ | Output | Type | Description |
54
+ | --- | --- | --- |
55
+ | `searched` | `string` | Emitted (debounced) on every value change. |
56
+ | `submitted` | `string` | Emitted when the user presses Enter or clicks a suggestion. |
57
+ | `suggestionSelected` | `SearchSuggestion` | Emitted when a suggestion row is clicked, carrying the suggestion object. |
58
+ | `filterRemoved` | `string` | Emitted when the × on a filter chip is clicked. |
59
+ | `footerClick` | `void` | Emitted when the footer link is clicked. |
60
+ | `cleared` | `void` | Emitted when the input is cleared via the × button. |
61
+
62
+ ## Documentation
63
+
64
+ Full API reference and live demos: https://styloviz.dev/docs
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,470 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, PLATFORM_ID, ApplicationRef, model, input, output, signal, computed, viewChild, effect, TemplateRef, HostListener, ViewChild, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { isPlatformBrowser, NgClass } from '@angular/common';
4
+ import * as i1 from '@angular/forms';
5
+ import { FormsModule } from '@angular/forms';
6
+
7
+ /** Process-wide counter for generating unique ids per instance. */
8
+ let _svSearchUid = 0;
9
+ // ─── Icon paths keyed to suggestion icon type ─────────────────────────────────
10
+ const SUGGESTION_ICONS = {
11
+ search: 'M21 21l-6-6m2-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0z',
12
+ page: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM14 2v6h6',
13
+ user: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
14
+ tag: 'M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82zM7 7h.01',
15
+ };
16
+ // ─── Constants ───────────────────────────────────────────────────────────────
17
+ /** Delay in ms before closing the dropdown on blur — lets suggestion clicks register first. */
18
+ const BLUR_CLOSE_DELAY_MS = 150;
19
+ /** Gap between the input track and the portaled suggestions panel (px). */
20
+ const PANEL_GAP = 4;
21
+ /** Minimum breathing room from the viewport edge before the vertical flip triggers (px). */
22
+ const PANEL_MARGIN = 8;
23
+ /** Preferred panel height used only for the open-above vs open-below decision (px) — ~6 rows. */
24
+ const PANEL_PREFERRED_HEIGHT = 288;
25
+ // ─── Component ───────────────────────────────────────────────────────────────
26
+ /**
27
+ * SearchBar — A premium, fully-featured search input for dashboards.
28
+ *
29
+ * Features:
30
+ * - Signal-based two-way binding via `[(value)]`
31
+ * - Configurable debounce (default 300 ms)
32
+ * - Typeahead dropdown with keyboard navigation (↑ ↓ Enter Escape)
33
+ * - 4 icon types per suggestion row + custom SVG path support
34
+ * - Inline filter chip tokens (removable)
35
+ * - Loading / searching spinner state
36
+ * - Clear button
37
+ * - Keyboard shortcut hint badge (e.g. ⌘K)
38
+ * - 3 sizes × 3 visual variants
39
+ * - Full dark-mode support
40
+ * - Accessible: `role="combobox"`, `aria-expanded`, `aria-activedescendant`
41
+ */
42
+ class SvSearchBarComponent {
43
+ platformId = inject(PLATFORM_ID);
44
+ appRef = inject(ApplicationRef);
45
+ // ── Value ─────────────────────────────────────────────────────────────────
46
+ /** Current input text. Use `[(value)]` for two-way binding. */
47
+ value = model('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
48
+ // ── Content ───────────────────────────────────────────────────────────────
49
+ /**
50
+ * Floating label text — only shown when `variant="floating"`.
51
+ * Acts as a Material Design-style animated label. @default ''
52
+ */
53
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
54
+ /** Placeholder text. @default 'Search…' */
55
+ placeholder = input('Search…', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
56
+ /** Explicit accessible name for the input. Falls back to `label` then `placeholder`. */
57
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
58
+ /**
59
+ * Keyboard shortcut hint shown on the right side of the input when empty.
60
+ * E.g. `'⌘K'`, `'Ctrl+K'`, `'/''`. Empty string hides the badge.
61
+ * @default ''
62
+ */
63
+ shortcutHint = input('', ...(ngDevMode ? [{ debugName: "shortcutHint" }] : /* istanbul ignore next */ []));
64
+ // ── Suggestions ───────────────────────────────────────────────────────────
65
+ /**
66
+ * Suggestion items to display in the dropdown.
67
+ * Pass a filtered/sliced array; the component renders all provided items.
68
+ * Empty array closes the dropdown.
69
+ */
70
+ suggestions = input([], ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
71
+ /**
72
+ * Label shown above the suggestions list (e.g. 'Recent searches', 'Results').
73
+ * Empty string hides the header. @default ''
74
+ */
75
+ suggestionsLabel = input('', ...(ngDevMode ? [{ debugName: "suggestionsLabel" }] : /* istanbul ignore next */ []));
76
+ /**
77
+ * Text displayed at the bottom of the dropdown — useful for "View all results".
78
+ * Empty string hides the footer. @default ''
79
+ */
80
+ footerLabel = input('', ...(ngDevMode ? [{ debugName: "footerLabel" }] : /* istanbul ignore next */ []));
81
+ // ── Filters ───────────────────────────────────────────────────────────────
82
+ /**
83
+ * Active filter chips rendered inside the input track.
84
+ * Each chip has an × button that emits `filterRemoved`.
85
+ */
86
+ filters = input([], ...(ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []));
87
+ // ── Appearance ────────────────────────────────────────────────────────────
88
+ /** Field size: sm, md or lg. @default 'md' */
89
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
90
+ /** Visual variant: default, floating, minimal… @default 'default' */
91
+ variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
92
+ /** Show the magnifier icon on the left. @default true */
93
+ showSearchIcon = input(true, ...(ngDevMode ? [{ debugName: "showSearchIcon" }] : /* istanbul ignore next */ []));
94
+ // ── State ─────────────────────────────────────────────────────────────────
95
+ /** Show the spinning loader inside the search icon area. @default false */
96
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
97
+ /** Whether the input is disabled. @default false */
98
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
99
+ /**
100
+ * Debounce delay in milliseconds before `searched` emits.
101
+ * Set to 0 to emit on every keystroke. @default 300
102
+ */
103
+ debounceMs = input(300, ...(ngDevMode ? [{ debugName: "debounceMs" }] : /* istanbul ignore next */ []));
104
+ /** Additional classes on the root wrapper. */
105
+ customClass = input('', ...(ngDevMode ? [{ debugName: "customClass" }] : /* istanbul ignore next */ []));
106
+ // ── Outputs ───────────────────────────────────────────────────────────────
107
+ /** Emitted (debounced) on every value change. */
108
+ searched = output();
109
+ /** Emitted when the user presses Enter or clicks a suggestion. */
110
+ submitted = output();
111
+ /** Emitted when a suggestion row is clicked, carrying the suggestion object. */
112
+ suggestionSelected = output();
113
+ /** Emitted when the × on a filter chip is clicked. */
114
+ filterRemoved = output();
115
+ /** Emitted when the footer link is clicked. */
116
+ footerClick = output();
117
+ /** Emitted when the input is cleared via the × button. */
118
+ cleared = output();
119
+ // ── Internal state ────────────────────────────────────────────────────────
120
+ isFocused = signal(false, ...(ngDevMode ? [{ debugName: "isFocused" }] : /* istanbul ignore next */ []));
121
+ activeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
122
+ /** Per-instance id base so multiple search bars don't collide. */
123
+ _baseId = `sv-search-${++_svSearchUid}`;
124
+ /** id of the suggestions listbox (referenced by aria-controls). */
125
+ listboxId = `${this._baseId}-listbox`;
126
+ /** DOM id for an option row at index `i` (used by aria-activedescendant). */
127
+ optionId(i) { return `${this._baseId}-opt-${i}`; }
128
+ /** id of the active option, or null when nothing is highlighted. */
129
+ activeDescendantId = computed(() => this.activeIndex() >= 0 ? this.optionId(this.activeIndex()) : null, ...(ngDevMode ? [{ debugName: "activeDescendantId" }] : /* istanbul ignore next */ []));
130
+ /** Accessible name: explicit ariaLabel, then floating label, then placeholder. */
131
+ accessibleName = computed(() => this.ariaLabel() || this.label() || this.placeholder(), ...(ngDevMode ? [{ debugName: "accessibleName" }] : /* istanbul ignore next */ []));
132
+ debounceTimer = null;
133
+ /** Template reference to the native <input> element. */
134
+ inputRef = viewChild('inputEl', ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
135
+ /** Template reference to the track container — used for dropdown positioning. */
136
+ trackRef = viewChild('trackEl', ...(ngDevMode ? [{ debugName: "trackRef" }] : /* istanbul ignore next */ []));
137
+ // ── Body-portal state ────────────────────────────────────────────────────
138
+ // Portals the suggestions panel into document.body so ancestor overflow/
139
+ // transform containers can never clip it (see dropdown-instruction.md).
140
+ /** True once the panel has flipped to open above the track (space below is tight). */
141
+ openAbove = signal(false, ...(ngDevMode ? [{ debugName: "openAbove" }] : /* istanbul ignore next */ []));
142
+ /** Suppresses the dropdown after a viewport resize until focus/typing resumes. */
143
+ resizeSuppressed = signal(false, ...(ngDevMode ? [{ debugName: "resizeSuppressed" }] : /* istanbul ignore next */ []));
144
+ /** Inline style object applied to the portaled panel (position:fixed + viewport coords). */
145
+ panelStyle = signal({}, ...(ngDevMode ? [{ debugName: "panelStyle" }] : /* istanbul ignore next */ []));
146
+ portalEl = null;
147
+ portalView = null;
148
+ /** The ng-template holding the suggestions panel — never rendered inline; instantiated into <body> on open. */
149
+ panelTpl;
150
+ /** Capture-phase scroll listener — fires for ANY scrollable ancestor, not just the window. */
151
+ _rafId = null;
152
+ _scrollHandler = () => {
153
+ if (!this.showDropdown())
154
+ return;
155
+ if (this._rafId !== null)
156
+ return;
157
+ this._rafId = requestAnimationFrame(() => {
158
+ this._rafId = null;
159
+ if (this.showDropdown()) {
160
+ this._calcPos();
161
+ this.portalView?.detectChanges();
162
+ }
163
+ });
164
+ };
165
+ // ── Computed ──────────────────────────────────────────────────────────────
166
+ /**
167
+ * Suggestions pre-processed once per change-detection cycle.
168
+ * Resolves each item's icon path and highlight split so the template
169
+ * can bind to plain properties instead of calling methods inside `@for`.
170
+ */
171
+ computedSuggestions = computed(() => this.suggestions().map(s => ({
172
+ ...s,
173
+ resolvedIconPath: this.getSuggestionIconPath(s),
174
+ highlighted: this.highlightMatch(s.label),
175
+ })), ...(ngDevMode ? [{ debugName: "computedSuggestions" }] : /* istanbul ignore next */ []));
176
+ showDropdown = computed(() => this.isFocused() && !this.resizeSuppressed() && this.computedSuggestions().length > 0, ...(ngDevMode ? [{ debugName: "showDropdown" }] : /* istanbul ignore next */ []));
177
+ showClear = computed(() => this.value().length > 0 && !this.disabled(), ...(ngDevMode ? [{ debugName: "showClear" }] : /* istanbul ignore next */ []));
178
+ isFloating = computed(() => false, ...(ngDevMode ? [{ debugName: "isFloating" }] : /* istanbul ignore next */ []));
179
+ labelIsFloated = computed(() => this.isFocused() || !!this.value(), ...(ngDevMode ? [{ debugName: "labelIsFloated" }] : /* istanbul ignore next */ []));
180
+ floatingLabelClasses = computed(() => {
181
+ const floated = this.labelIsFloated();
182
+ const focused = this.isFocused();
183
+ const hasIcon = this.showSearchIcon();
184
+ // Not-floated position must align with the input text cursor.
185
+ // inputNativePadClass zeroes the left pad when icon is present, so cursor
186
+ // lands at: iconPaddingClass + iconWidth + gap-1.5
187
+ // sm: pl-2.5(10) + 14px + 6px = 30px
188
+ // md: pl-3(12) + 16px + 6px = 34px
189
+ // lg: pl-4(16) + 20px + 6px = 42px
190
+ const notFloatedLeftMap = hasIcon
191
+ ? { sm: 'left-[30px]', md: 'left-[34px]', lg: 'left-[42px]' }
192
+ : { sm: 'left-2', md: 'left-3', lg: 'left-4' };
193
+ const notFloatedLeft = notFloatedLeftMap[this.size()];
194
+ const floatedLeft = 'left-3';
195
+ const textSizeMap = {
196
+ sm: 'text-xs',
197
+ md: 'text-sm',
198
+ lg: 'text-base',
199
+ };
200
+ const textSize = floated ? 'text-[0.7rem] leading-none' : textSizeMap[this.size()];
201
+ const bg = floated ? 'px-1 bg-white dark:bg-gray-900' : '';
202
+ const color = focused && floated
203
+ ? 'text-primary-600 dark:text-primary-400'
204
+ : floated
205
+ ? 'text-gray-500 dark:text-gray-400'
206
+ : 'text-gray-500 dark:text-gray-400';
207
+ return [
208
+ 'pointer-events-none absolute z-10 transition-all duration-200 select-none whitespace-nowrap',
209
+ floated
210
+ ? `top-0 -translate-y-1/2 ${textSize} ${bg} ${floatedLeft}`
211
+ : `top-1/2 -translate-y-1/2 ${textSize} ${notFloatedLeft}`,
212
+ color,
213
+ ].filter(Boolean).join(' ');
214
+ }, ...(ngDevMode ? [{ debugName: "floatingLabelClasses" }] : /* istanbul ignore next */ []));
215
+ showShortcut = computed(() => !!this.shortcutHint() && !this.value() && !this.isFocused() && !this.loading(), ...(ngDevMode ? [{ debugName: "showShortcut" }] : /* istanbul ignore next */ []));
216
+ // ── Class maps ────────────────────────────────────────────────────────────
217
+ wrapperClasses = computed(() => ['relative w-full', this.customClass()].join(' '), ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : /* istanbul ignore next */ []));
218
+ trackClasses = computed(() => {
219
+ const sizeMap = {
220
+ sm: 'h-8 text-xs',
221
+ md: 'h-10 text-sm',
222
+ lg: 'h-12 text-base',
223
+ };
224
+ const variantMap = {
225
+ default: 'bg-white border border-gray-300 dark:bg-gray-900 dark:border-gray-700 shadow-sm',
226
+ filled: 'bg-gray-100 border border-transparent dark:bg-gray-800 dark:border-transparent',
227
+ borderless: 'bg-transparent border border-transparent',
228
+ };
229
+ const focusRing = this.isFocused()
230
+ ? 'ring-2 ring-primary-500/30 border-primary-500 dark:border-primary-400'
231
+ : 'hover:border-gray-400 dark:hover:border-gray-500';
232
+ return [
233
+ 'relative flex w-full items-center gap-1.5 rounded-md transition-all duration-150',
234
+ sizeMap[this.size()],
235
+ variantMap[this.variant()],
236
+ this.disabled() ? 'cursor-not-allowed opacity-60' : focusRing,
237
+ this.filters().length > 0 ? 'flex-wrap py-1' : '',
238
+ ].filter(Boolean).join(' ');
239
+ }, ...(ngDevMode ? [{ debugName: "trackClasses" }] : /* istanbul ignore next */ []));
240
+ inputPaddingClass = computed(() => {
241
+ switch (this.size()) {
242
+ case 'sm': return 'px-2';
243
+ case 'lg': return 'px-4';
244
+ default: return 'px-3';
245
+ }
246
+ }, ...(ngDevMode ? [{ debugName: "inputPaddingClass" }] : /* istanbul ignore next */ []));
247
+ /**
248
+ * When a search icon occupies the left side the input needs no left padding —
249
+ * the icon span + gap-1.5 already create visual spacing.
250
+ * Without an icon, use symmetric px-X. Applies to all variants.
251
+ */
252
+ inputNativePadClass = computed(() => {
253
+ if (this.showSearchIcon()) {
254
+ const rp = { sm: 'pr-2', md: 'pr-3', lg: 'pr-4' };
255
+ return `pl-0 ${rp[this.size()]}`;
256
+ }
257
+ return this.inputPaddingClass();
258
+ }, ...(ngDevMode ? [{ debugName: "inputNativePadClass" }] : /* istanbul ignore next */ []));
259
+ iconSizeClass = computed(() => {
260
+ switch (this.size()) {
261
+ case 'sm': return 'h-3.5 w-3.5';
262
+ case 'lg': return 'h-5 w-5';
263
+ default: return 'h-4 w-4';
264
+ }
265
+ }, ...(ngDevMode ? [{ debugName: "iconSizeClass" }] : /* istanbul ignore next */ []));
266
+ iconPaddingClass = computed(() => {
267
+ switch (this.size()) {
268
+ case 'sm': return 'pl-2.5';
269
+ case 'lg': return 'pl-4';
270
+ default: return 'pl-3';
271
+ }
272
+ }, ...(ngDevMode ? [{ debugName: "iconPaddingClass" }] : /* istanbul ignore next */ []));
273
+ // ── Suggestion helpers ────────────────────────────────────────────────────
274
+ getSuggestionIconPath(s) {
275
+ if (s.iconType === 'custom' && s.iconPath)
276
+ return s.iconPath;
277
+ if (s.iconType && s.iconType !== 'custom')
278
+ return SUGGESTION_ICONS[s.iconType];
279
+ return SUGGESTION_ICONS.search;
280
+ }
281
+ /** Highlight matched portion of a suggestion label. */
282
+ highlightMatch(text) {
283
+ const q = this.value().trim().toLowerCase();
284
+ if (!q)
285
+ return { pre: text, match: '', post: '' };
286
+ const idx = text.toLowerCase().indexOf(q);
287
+ if (idx === -1)
288
+ return { pre: text, match: '', post: '' };
289
+ return {
290
+ pre: text.slice(0, idx),
291
+ match: text.slice(idx, idx + q.length),
292
+ post: text.slice(idx + q.length),
293
+ };
294
+ }
295
+ // ── Event handlers ────────────────────────────────────────────────────────
296
+ onInput(raw) {
297
+ this.resizeSuppressed.set(false);
298
+ this.value.set(raw);
299
+ this.activeIndex.set(-1);
300
+ if (this.debounceTimer)
301
+ clearTimeout(this.debounceTimer);
302
+ const ms = this.debounceMs();
303
+ if (ms > 0) {
304
+ this.debounceTimer = setTimeout(() => this.searched.emit(raw), ms);
305
+ }
306
+ else {
307
+ this.searched.emit(raw);
308
+ }
309
+ }
310
+ onFocus() {
311
+ this.resizeSuppressed.set(false);
312
+ this.isFocused.set(true);
313
+ }
314
+ onBlur() {
315
+ // Delay so click on a suggestion registers before closing dropdown.
316
+ setTimeout(() => this.isFocused.set(false), BLUR_CLOSE_DELAY_MS);
317
+ }
318
+ onKeydown(event) {
319
+ const computed = this.computedSuggestions();
320
+ const count = computed.length;
321
+ if (!this.showDropdown() && event.key !== 'Enter')
322
+ return;
323
+ switch (event.key) {
324
+ case 'ArrowDown':
325
+ event.preventDefault();
326
+ this.activeIndex.update(i => Math.min(i + 1, count - 1));
327
+ break;
328
+ case 'ArrowUp':
329
+ event.preventDefault();
330
+ this.activeIndex.update(i => Math.max(i - 1, -1));
331
+ break;
332
+ case 'Enter': {
333
+ event.preventDefault();
334
+ const idx = this.activeIndex();
335
+ if (idx >= 0 && idx < count) {
336
+ this.selectSuggestion(computed[idx]);
337
+ }
338
+ else {
339
+ this.submitted.emit(this.value());
340
+ this.isFocused.set(false);
341
+ }
342
+ break;
343
+ }
344
+ case 'Escape':
345
+ this.isFocused.set(false);
346
+ this.activeIndex.set(-1);
347
+ this.inputRef()?.nativeElement.blur();
348
+ break;
349
+ }
350
+ }
351
+ selectSuggestion(s) {
352
+ this.value.set(s.label);
353
+ this.isFocused.set(false);
354
+ this.activeIndex.set(-1);
355
+ this.submitted.emit(s.label);
356
+ this.suggestionSelected.emit(s);
357
+ }
358
+ clearValue() {
359
+ this.value.set('');
360
+ this.activeIndex.set(-1);
361
+ this.cleared.emit();
362
+ this.searched.emit('');
363
+ if (isPlatformBrowser(this.platformId)) {
364
+ setTimeout(() => this.inputRef()?.nativeElement.focus());
365
+ }
366
+ }
367
+ onFooterClick() {
368
+ this.isFocused.set(false);
369
+ this.footerClick.emit();
370
+ }
371
+ focus() {
372
+ this.inputRef()?.nativeElement.focus();
373
+ }
374
+ onWindowResize() {
375
+ // Viewport resize invalidates the layout — close rather than reposition
376
+ // with now-stale coordinates (dropdown-instruction.md §8). Reopens
377
+ // naturally on the next focus/input.
378
+ if (this.showDropdown())
379
+ this.resizeSuppressed.set(true);
380
+ }
381
+ _calcPos() {
382
+ const track = this.trackRef()?.nativeElement;
383
+ if (!track)
384
+ return;
385
+ const rect = track.getBoundingClientRect();
386
+ const vh = window.innerHeight;
387
+ const spaceBelow = vh - rect.bottom - PANEL_MARGIN;
388
+ const spaceAbove = rect.top - PANEL_MARGIN;
389
+ const above = spaceBelow < PANEL_PREFERRED_HEIGHT && spaceAbove > spaceBelow;
390
+ this.openAbove.set(above);
391
+ const style = {
392
+ position: 'fixed',
393
+ zIndex: '9999',
394
+ left: `${rect.left}px`,
395
+ width: `${rect.width}px`,
396
+ maxHeight: `${Math.max(120, above ? spaceAbove : spaceBelow)}px`,
397
+ };
398
+ style[above ? 'bottom' : 'top'] = above
399
+ ? `${vh - rect.top + PANEL_GAP}px`
400
+ : `${rect.bottom + PANEL_GAP}px`;
401
+ this.panelStyle.set(style);
402
+ }
403
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
404
+ constructor() {
405
+ // Mount/unmount the body-portaled panel whenever the dropdown's open
406
+ // state changes — the reactive equivalent of Color Picker's toggle().
407
+ effect(() => {
408
+ if (this.showDropdown()) {
409
+ if (!this.portalView)
410
+ this._openPanel();
411
+ }
412
+ else if (this.portalView) {
413
+ this._closePanel();
414
+ }
415
+ });
416
+ }
417
+ ngOnInit() {
418
+ // Capture phase catches scroll from any scrollable ancestor, not just window.
419
+ window.addEventListener('scroll', this._scrollHandler, { passive: true, capture: true });
420
+ }
421
+ ngOnDestroy() {
422
+ window.removeEventListener('scroll', this._scrollHandler, true);
423
+ if (this._rafId !== null)
424
+ cancelAnimationFrame(this._rafId);
425
+ if (this.debounceTimer)
426
+ clearTimeout(this.debounceTimer);
427
+ this._closePanel();
428
+ }
429
+ // ── Body portal ──────────────────────────────────────────────────────────
430
+ _openPanel() {
431
+ if (!this.panelTpl)
432
+ return;
433
+ this._calcPos();
434
+ this.portalEl = document.createElement('div');
435
+ this.portalEl.className = 'sv-search-portal';
436
+ document.body.appendChild(this.portalEl);
437
+ this.portalView = this.panelTpl.createEmbeddedView({});
438
+ this.appRef.attachView(this.portalView);
439
+ this.portalView.rootNodes.forEach((n) => this.portalEl.appendChild(n));
440
+ this.portalView.detectChanges();
441
+ }
442
+ _closePanel() {
443
+ if (this.portalView) {
444
+ this.appRef.detachView(this.portalView);
445
+ this.portalView.destroy();
446
+ this.portalView = null;
447
+ }
448
+ this.portalEl?.remove();
449
+ this.portalEl = null;
450
+ }
451
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvSearchBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
452
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: SvSearchBarComponent, isStandalone: true, selector: "sv-search-bar", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, shortcutHint: { classPropertyName: "shortcutHint", publicName: "shortcutHint", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, suggestionsLabel: { classPropertyName: "suggestionsLabel", publicName: "suggestionsLabel", isSignal: true, isRequired: false, transformFunction: null }, footerLabel: { classPropertyName: "footerLabel", publicName: "footerLabel", isSignal: true, isRequired: false, transformFunction: null }, filters: { classPropertyName: "filters", publicName: "filters", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, showSearchIcon: { classPropertyName: "showSearchIcon", publicName: "showSearchIcon", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, debounceMs: { classPropertyName: "debounceMs", publicName: "debounceMs", isSignal: true, isRequired: false, transformFunction: null }, customClass: { classPropertyName: "customClass", publicName: "customClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", searched: "searched", submitted: "submitted", suggestionSelected: "suggestionSelected", filterRemoved: "filterRemoved", footerClick: "footerClick", cleared: "cleared" }, host: { listeners: { "window:resize": "onWindowResize()" } }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "trackRef", first: true, predicate: ["trackEl"], descendants: true, isSignal: true }, { propertyName: "panelTpl", first: true, predicate: ["panelTpl"], descendants: true, read: TemplateRef }], ngImport: i0, template: "<!-- Root wrapper \u2014 positions the dropdown relative to this element -->\n<div [ngClass]=\"wrapperClasses()\">\n\n <!-- \u2500\u2500 Input track \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div #trackEl [ngClass]=\"trackClasses()\">\n\n <!-- Floating label (animates to border on focus or value) -->\n @if (isFloating() && label()) {\n <label [ngClass]=\"floatingLabelClasses()\" aria-hidden=\"true\">{{ label() }}</label>\n }\n @if (showSearchIcon()) {\n <span\n class=\"flex shrink-0 items-center text-gray-500 dark:text-gray-400\"\n [ngClass]=\"iconPaddingClass()\"\n aria-hidden=\"true\"\n >\n @if (loading()) {\n <!-- Spinner -->\n <svg\n [ngClass]=\"iconSizeClass()\"\n class=\"animate-spin text-primary-500\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\"\n stroke=\"currentColor\" stroke-width=\"4\"/>\n <path class=\"opacity-75\" fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"/>\n </svg>\n } @else {\n <!-- Magnifier -->\n <svg [ngClass]=\"iconSizeClass()\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/>\n <path d=\"m21 21-4.35-4.35\"/>\n </svg>\n }\n </span>\n }\n\n <!-- Active filter chips -->\n @for (filter of filters(); track filter.id) {\n <span\n class=\"inline-flex shrink-0 items-center gap-1 rounded-md\n bg-primary-50 px-2 py-0.5 text-xs font-medium text-primary-700\n dark:bg-primary-900/40 dark:text-primary-300\"\n >\n {{ filter.label }}\n <button\n type=\"button\"\n class=\"ml-0.5 rounded text-primary-400 hover:text-primary-600\n dark:text-primary-400 dark:hover:text-primary-200\n focus:outline-none focus-visible:ring-1 focus-visible:ring-primary-400\"\n (click)=\"filterRemoved.emit(filter.id)\"\n [attr.aria-label]=\"'Remove filter ' + filter.label\"\n >\n <svg class=\"h-3 w-3\" viewBox=\"0 0 14 14\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <path d=\"M3 3l8 8M11 3l-8 8\"/>\n </svg>\n </button>\n </span>\n }\n\n <!-- Text input -->\n <input\n #inputEl\n type=\"search\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n spellcheck=\"false\"\n class=\"min-w-0 flex-1 bg-transparent text-gray-900 placeholder-gray-400\n outline-none dark:text-white dark:placeholder-gray-500\n [&::-webkit-search-cancel-button]:hidden\"\n [ngClass]=\"[\n inputNativePadClass(),\n filters().length > 0 ? 'py-0.5' : 'h-full'\n ]\"\n [placeholder]=\"isFloating() && label() && !labelIsFloated() ? ' ' : placeholder()\"\n [disabled]=\"disabled()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"showDropdown()\"\n [attr.aria-label]=\"accessibleName()\"\n [attr.aria-busy]=\"loading() || null\"\n [attr.aria-autocomplete]=\"suggestions().length ? 'list' : 'none'\"\n [attr.aria-controls]=\"showDropdown() ? listboxId : null\"\n [attr.aria-activedescendant]=\"activeDescendantId()\"\n />\n\n <!-- Keyboard shortcut hint -->\n @if (showShortcut()) {\n <kbd\n class=\"mr-2 hidden shrink-0 items-center rounded border border-gray-200 bg-gray-100\n px-1.5 py-0.5 font-mono text-[10px] text-gray-500\n dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 sm:inline-flex\"\n aria-hidden=\"true\"\n >\n {{ shortcutHint() }}\n </kbd>\n }\n\n <!-- Clear button -->\n @if (showClear()) {\n <button\n type=\"button\"\n class=\"mr-2 shrink-0 rounded-full p-0.5 text-gray-400\n transition-colors hover:bg-gray-100 hover:text-gray-600\n dark:hover:bg-gray-700 dark:hover:text-gray-300\n focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400\"\n (click)=\"clearValue()\"\n aria-label=\"Clear search\"\n >\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 14 14\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <path d=\"M3 3l8 8M11 3l-8 8\"/>\n </svg>\n </button>\n }\n\n </div>\n\n <!-- \u2500\u2500 Suggestions dropdown \u2014 portaled into document.body on open so ancestor\n overflow/transform containers can never clip it (see dropdown-instruction.md). \u2500\u2500 -->\n <ng-template #panelTpl>\n <div\n [id]=\"listboxId + '-panel'\"\n class=\"flex flex-col overflow-hidden rounded-xl\n border border-gray-200 bg-white\n shadow-lg shadow-gray-200/60\n dark:border-gray-700 dark:bg-gray-900 dark:shadow-gray-950/60\"\n [style.position]=\"panelStyle()['position']\"\n [style.zIndex]=\"panelStyle()['zIndex']\"\n [style.top]=\"panelStyle()['top']\"\n [style.bottom]=\"panelStyle()['bottom']\"\n [style.left]=\"panelStyle()['left']\"\n [style.width]=\"panelStyle()['width']\"\n [style.maxHeight]=\"panelStyle()['maxHeight']\"\n >\n\n <!-- Dropdown header label -->\n @if (suggestionsLabel()) {\n <div class=\"border-b border-gray-100 px-3 py-2 dark:border-gray-800\">\n <p class=\"text-[11px] font-semibold uppercase tracking-wider\n text-gray-500 dark:text-gray-400\">\n {{ suggestionsLabel() }}\n </p>\n </div>\n }\n\n <!-- Suggestion rows -->\n <ul\n [attr.id]=\"listboxId\"\n role=\"listbox\"\n [attr.aria-label]=\"suggestionsLabel() || accessibleName() || 'Search suggestions'\"\n class=\"min-h-0 flex-1 overflow-y-auto overscroll-contain py-1\"\n >\n @for (s of computedSuggestions(); track s.id; let i = $index) {\n <li\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"activeIndex() === i\"\n class=\"flex cursor-pointer items-center gap-3 px-3 py-2.5\n transition-colors duration-75 select-none\"\n [ngClass]=\"activeIndex() === i\n ? 'bg-primary-50 dark:bg-primary-900/30'\n : 'hover:bg-gray-50 dark:hover:bg-gray-800/60'\"\n (mousedown)=\"selectSuggestion(s)\"\n (mouseenter)=\"activeIndex.set(i)\"\n >\n <!-- Row icon -->\n <span\n class=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-lg\n text-gray-500 dark:text-gray-400\"\n [ngClass]=\"activeIndex() === i\n ? 'bg-primary-100 text-primary-600 dark:bg-primary-900/50 dark:text-primary-400'\n : 'bg-gray-100 dark:bg-gray-800'\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"s.resolvedIconPath\" />\n </svg>\n </span>\n\n <!-- Text -->\n <span class=\"min-w-0 flex-1\">\n <!-- Highlighted label (pre-computed, no method call in template) -->\n <span class=\"block truncate text-sm font-medium text-gray-900 dark:text-white\">\n {{ s.highlighted.pre }}<mark class=\"bg-transparent font-bold text-primary-600 dark:text-primary-400\">{{ s.highlighted.match }}</mark>{{ s.highlighted.post }}\n </span>\n @if (s.sublabel) {\n <span class=\"block truncate text-xs text-gray-500 dark:text-gray-400\">\n {{ s.sublabel }}\n </span>\n }\n </span>\n\n <!-- Badge -->\n @if (s.badge) {\n <span\n class=\"shrink-0 rounded-full bg-primary-50 px-2 py-0.5\n text-[10px] font-semibold text-primary-600\n dark:bg-primary-900/40 dark:text-primary-400\"\n >\n {{ s.badge }}\n </span>\n }\n\n <!-- Keyboard hint on active row -->\n @if (activeIndex() === i) {\n <kbd class=\"shrink-0 rounded border border-gray-200 bg-gray-100 px-1\n py-0.5 font-mono text-[9px] text-gray-500\n dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400\"\n aria-hidden=\"true\">\n \u21B5\n </kbd>\n }\n\n </li>\n }\n </ul>\n\n <!-- Dropdown footer -->\n @if (footerLabel()) {\n <div class=\"border-t border-gray-100 dark:border-gray-800\">\n <button\n type=\"button\"\n class=\"flex w-full items-center justify-between px-3 py-2.5\n text-xs font-medium text-primary-600 transition-colors\n hover:bg-gray-50 dark:text-primary-400 dark:hover:bg-gray-800/60\n focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-400\"\n (mousedown)=\"onFooterClick()\"\n >\n <span>{{ footerLabel() }}</span>\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M5 12h14M12 5l7 7-7 7\"/>\n </svg>\n </button>\n </div>\n }\n\n </div>\n </ng-template>\n\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
453
+ }
454
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvSearchBarComponent, decorators: [{
455
+ type: Component,
456
+ args: [{ selector: 'sv-search-bar', standalone: true, imports: [NgClass, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Root wrapper \u2014 positions the dropdown relative to this element -->\n<div [ngClass]=\"wrapperClasses()\">\n\n <!-- \u2500\u2500 Input track \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div #trackEl [ngClass]=\"trackClasses()\">\n\n <!-- Floating label (animates to border on focus or value) -->\n @if (isFloating() && label()) {\n <label [ngClass]=\"floatingLabelClasses()\" aria-hidden=\"true\">{{ label() }}</label>\n }\n @if (showSearchIcon()) {\n <span\n class=\"flex shrink-0 items-center text-gray-500 dark:text-gray-400\"\n [ngClass]=\"iconPaddingClass()\"\n aria-hidden=\"true\"\n >\n @if (loading()) {\n <!-- Spinner -->\n <svg\n [ngClass]=\"iconSizeClass()\"\n class=\"animate-spin text-primary-500\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\"\n stroke=\"currentColor\" stroke-width=\"4\"/>\n <path class=\"opacity-75\" fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"/>\n </svg>\n } @else {\n <!-- Magnifier -->\n <svg [ngClass]=\"iconSizeClass()\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/>\n <path d=\"m21 21-4.35-4.35\"/>\n </svg>\n }\n </span>\n }\n\n <!-- Active filter chips -->\n @for (filter of filters(); track filter.id) {\n <span\n class=\"inline-flex shrink-0 items-center gap-1 rounded-md\n bg-primary-50 px-2 py-0.5 text-xs font-medium text-primary-700\n dark:bg-primary-900/40 dark:text-primary-300\"\n >\n {{ filter.label }}\n <button\n type=\"button\"\n class=\"ml-0.5 rounded text-primary-400 hover:text-primary-600\n dark:text-primary-400 dark:hover:text-primary-200\n focus:outline-none focus-visible:ring-1 focus-visible:ring-primary-400\"\n (click)=\"filterRemoved.emit(filter.id)\"\n [attr.aria-label]=\"'Remove filter ' + filter.label\"\n >\n <svg class=\"h-3 w-3\" viewBox=\"0 0 14 14\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <path d=\"M3 3l8 8M11 3l-8 8\"/>\n </svg>\n </button>\n </span>\n }\n\n <!-- Text input -->\n <input\n #inputEl\n type=\"search\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n spellcheck=\"false\"\n class=\"min-w-0 flex-1 bg-transparent text-gray-900 placeholder-gray-400\n outline-none dark:text-white dark:placeholder-gray-500\n [&::-webkit-search-cancel-button]:hidden\"\n [ngClass]=\"[\n inputNativePadClass(),\n filters().length > 0 ? 'py-0.5' : 'h-full'\n ]\"\n [placeholder]=\"isFloating() && label() && !labelIsFloated() ? ' ' : placeholder()\"\n [disabled]=\"disabled()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"showDropdown()\"\n [attr.aria-label]=\"accessibleName()\"\n [attr.aria-busy]=\"loading() || null\"\n [attr.aria-autocomplete]=\"suggestions().length ? 'list' : 'none'\"\n [attr.aria-controls]=\"showDropdown() ? listboxId : null\"\n [attr.aria-activedescendant]=\"activeDescendantId()\"\n />\n\n <!-- Keyboard shortcut hint -->\n @if (showShortcut()) {\n <kbd\n class=\"mr-2 hidden shrink-0 items-center rounded border border-gray-200 bg-gray-100\n px-1.5 py-0.5 font-mono text-[10px] text-gray-500\n dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 sm:inline-flex\"\n aria-hidden=\"true\"\n >\n {{ shortcutHint() }}\n </kbd>\n }\n\n <!-- Clear button -->\n @if (showClear()) {\n <button\n type=\"button\"\n class=\"mr-2 shrink-0 rounded-full p-0.5 text-gray-400\n transition-colors hover:bg-gray-100 hover:text-gray-600\n dark:hover:bg-gray-700 dark:hover:text-gray-300\n focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400\"\n (click)=\"clearValue()\"\n aria-label=\"Clear search\"\n >\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 14 14\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <path d=\"M3 3l8 8M11 3l-8 8\"/>\n </svg>\n </button>\n }\n\n </div>\n\n <!-- \u2500\u2500 Suggestions dropdown \u2014 portaled into document.body on open so ancestor\n overflow/transform containers can never clip it (see dropdown-instruction.md). \u2500\u2500 -->\n <ng-template #panelTpl>\n <div\n [id]=\"listboxId + '-panel'\"\n class=\"flex flex-col overflow-hidden rounded-xl\n border border-gray-200 bg-white\n shadow-lg shadow-gray-200/60\n dark:border-gray-700 dark:bg-gray-900 dark:shadow-gray-950/60\"\n [style.position]=\"panelStyle()['position']\"\n [style.zIndex]=\"panelStyle()['zIndex']\"\n [style.top]=\"panelStyle()['top']\"\n [style.bottom]=\"panelStyle()['bottom']\"\n [style.left]=\"panelStyle()['left']\"\n [style.width]=\"panelStyle()['width']\"\n [style.maxHeight]=\"panelStyle()['maxHeight']\"\n >\n\n <!-- Dropdown header label -->\n @if (suggestionsLabel()) {\n <div class=\"border-b border-gray-100 px-3 py-2 dark:border-gray-800\">\n <p class=\"text-[11px] font-semibold uppercase tracking-wider\n text-gray-500 dark:text-gray-400\">\n {{ suggestionsLabel() }}\n </p>\n </div>\n }\n\n <!-- Suggestion rows -->\n <ul\n [attr.id]=\"listboxId\"\n role=\"listbox\"\n [attr.aria-label]=\"suggestionsLabel() || accessibleName() || 'Search suggestions'\"\n class=\"min-h-0 flex-1 overflow-y-auto overscroll-contain py-1\"\n >\n @for (s of computedSuggestions(); track s.id; let i = $index) {\n <li\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"activeIndex() === i\"\n class=\"flex cursor-pointer items-center gap-3 px-3 py-2.5\n transition-colors duration-75 select-none\"\n [ngClass]=\"activeIndex() === i\n ? 'bg-primary-50 dark:bg-primary-900/30'\n : 'hover:bg-gray-50 dark:hover:bg-gray-800/60'\"\n (mousedown)=\"selectSuggestion(s)\"\n (mouseenter)=\"activeIndex.set(i)\"\n >\n <!-- Row icon -->\n <span\n class=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-lg\n text-gray-500 dark:text-gray-400\"\n [ngClass]=\"activeIndex() === i\n ? 'bg-primary-100 text-primary-600 dark:bg-primary-900/50 dark:text-primary-400'\n : 'bg-gray-100 dark:bg-gray-800'\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"s.resolvedIconPath\" />\n </svg>\n </span>\n\n <!-- Text -->\n <span class=\"min-w-0 flex-1\">\n <!-- Highlighted label (pre-computed, no method call in template) -->\n <span class=\"block truncate text-sm font-medium text-gray-900 dark:text-white\">\n {{ s.highlighted.pre }}<mark class=\"bg-transparent font-bold text-primary-600 dark:text-primary-400\">{{ s.highlighted.match }}</mark>{{ s.highlighted.post }}\n </span>\n @if (s.sublabel) {\n <span class=\"block truncate text-xs text-gray-500 dark:text-gray-400\">\n {{ s.sublabel }}\n </span>\n }\n </span>\n\n <!-- Badge -->\n @if (s.badge) {\n <span\n class=\"shrink-0 rounded-full bg-primary-50 px-2 py-0.5\n text-[10px] font-semibold text-primary-600\n dark:bg-primary-900/40 dark:text-primary-400\"\n >\n {{ s.badge }}\n </span>\n }\n\n <!-- Keyboard hint on active row -->\n @if (activeIndex() === i) {\n <kbd class=\"shrink-0 rounded border border-gray-200 bg-gray-100 px-1\n py-0.5 font-mono text-[9px] text-gray-500\n dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400\"\n aria-hidden=\"true\">\n \u21B5\n </kbd>\n }\n\n </li>\n }\n </ul>\n\n <!-- Dropdown footer -->\n @if (footerLabel()) {\n <div class=\"border-t border-gray-100 dark:border-gray-800\">\n <button\n type=\"button\"\n class=\"flex w-full items-center justify-between px-3 py-2.5\n text-xs font-medium text-primary-600 transition-colors\n hover:bg-gray-50 dark:text-primary-400 dark:hover:bg-gray-800/60\n focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-400\"\n (mousedown)=\"onFooterClick()\"\n >\n <span>{{ footerLabel() }}</span>\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M5 12h14M12 5l7 7-7 7\"/>\n </svg>\n </button>\n </div>\n }\n\n </div>\n </ng-template>\n\n</div>\n" }]
457
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], shortcutHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "shortcutHint", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], suggestionsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestionsLabel", required: false }] }], footerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerLabel", required: false }] }], filters: [{ type: i0.Input, args: [{ isSignal: true, alias: "filters", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], showSearchIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSearchIcon", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], customClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "customClass", required: false }] }], searched: [{ type: i0.Output, args: ["searched"] }], submitted: [{ type: i0.Output, args: ["submitted"] }], suggestionSelected: [{ type: i0.Output, args: ["suggestionSelected"] }], filterRemoved: [{ type: i0.Output, args: ["filterRemoved"] }], footerClick: [{ type: i0.Output, args: ["footerClick"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], inputRef: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], trackRef: [{ type: i0.ViewChild, args: ['trackEl', { isSignal: true }] }], panelTpl: [{
458
+ type: ViewChild,
459
+ args: ['panelTpl', { read: TemplateRef }]
460
+ }], onWindowResize: [{
461
+ type: HostListener,
462
+ args: ['window:resize']
463
+ }] } });
464
+
465
+ /**
466
+ * Generated bundle index. Do not edit.
467
+ */
468
+
469
+ export { SvSearchBarComponent };
470
+ //# sourceMappingURL=styloviz-search-bar.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styloviz-search-bar.mjs","sources":["../../../../projects/search-bar-free/src/lib/search-bar.component.ts","../../../../projects/search-bar-free/src/lib/search-bar.component.html","../../../../projects/search-bar-free/src/styloviz-search-bar.ts"],"sourcesContent":["import {\n ApplicationRef,\n Component,\n HostListener,\n input,\n output,\n model,\n computed,\n effect,\n signal,\n viewChild,\n ElementRef,\n EmbeddedViewRef,\n ChangeDetectionStrategy,\n OnDestroy,\n OnInit,\n TemplateRef,\n ViewChild,\n inject,\n PLATFORM_ID,\n} from '@angular/core';\nimport { isPlatformBrowser, NgClass } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type SearchBarSize = 'sm' | 'md' | 'lg';\nexport type SearchBarVariant = 'default' | 'filled' | 'borderless';\n\n/** Process-wide counter for generating unique ids per instance. */\nlet _svSearchUid = 0;\n\n/**\n * A single suggestion item displayed in the dropdown.\n */\nexport interface SearchSuggestion {\n /** Unique identifier — emitted on selection. */\n id: string;\n /** Primary display text. */\n label: string;\n /** Optional secondary line (e.g. category, path, role). */\n sublabel?: string;\n /**\n * Optional icon type for the suggestion row.\n * - `'search'` — magnifier (recent / query suggestion)\n * - `'page'` — document icon\n * - `'user'` — person icon\n * - `'tag'` — tag icon\n * - `'custom'` — uses `iconPath`\n */\n iconType?: 'search' | 'page' | 'user' | 'tag' | 'custom';\n /** SVG path `d` attribute — used when `iconType === 'custom'`. */\n iconPath?: string;\n /** Highlight / badge label (e.g. \"New\", \"Popular\"). */\n badge?: string;\n}\n\n/**\n * A filter chip rendered inside the search input.\n */\nexport interface SearchFilter {\n id: string;\n label: string;\n}\n\n/**\n * A suggestion item pre-processed for efficient template rendering.\n * Extends `SearchSuggestion` with a resolved icon path and pre-split\n * highlight parts so the template never calls methods inside `@for` loops.\n */\nexport interface ComputedSuggestion extends SearchSuggestion {\n /** Resolved SVG `d` path for the row icon. */\n resolvedIconPath: string;\n /** Pre-split highlight regions for the suggestion label. */\n highlighted: { pre: string; match: string; post: string };\n}\n\n// ─── Icon paths keyed to suggestion icon type ─────────────────────────────────\n\nconst SUGGESTION_ICONS: Record<Exclude<SearchSuggestion['iconType'], 'custom' | undefined>, string> = {\n search: 'M21 21l-6-6m2-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0z',\n page: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM14 2v6h6',\n user: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',\n tag: 'M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82zM7 7h.01',\n};\n\n// ─── Constants ───────────────────────────────────────────────────────────────\n\n/** Delay in ms before closing the dropdown on blur — lets suggestion clicks register first. */\nconst BLUR_CLOSE_DELAY_MS = 150;\n/** Gap between the input track and the portaled suggestions panel (px). */\nconst PANEL_GAP = 4;\n/** Minimum breathing room from the viewport edge before the vertical flip triggers (px). */\nconst PANEL_MARGIN = 8;\n/** Preferred panel height used only for the open-above vs open-below decision (px) — ~6 rows. */\nconst PANEL_PREFERRED_HEIGHT = 288;\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\n/**\n * SearchBar — A premium, fully-featured search input for dashboards.\n *\n * Features:\n * - Signal-based two-way binding via `[(value)]`\n * - Configurable debounce (default 300 ms)\n * - Typeahead dropdown with keyboard navigation (↑ ↓ Enter Escape)\n * - 4 icon types per suggestion row + custom SVG path support\n * - Inline filter chip tokens (removable)\n * - Loading / searching spinner state\n * - Clear button\n * - Keyboard shortcut hint badge (e.g. ⌘K)\n * - 3 sizes × 3 visual variants\n * - Full dark-mode support\n * - Accessible: `role=\"combobox\"`, `aria-expanded`, `aria-activedescendant`\n */\n@Component({\n selector: 'sv-search-bar',\n standalone: true,\n imports: [NgClass, FormsModule],\n templateUrl: './search-bar.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class SvSearchBarComponent implements OnInit, OnDestroy {\n private readonly platformId = inject(PLATFORM_ID);\n private readonly appRef = inject(ApplicationRef);\n\n // ── Value ─────────────────────────────────────────────────────────────────\n\n /** Current input text. Use `[(value)]` for two-way binding. */\n value = model<string>('');\n\n // ── Content ───────────────────────────────────────────────────────────────\n\n /**\n * Floating label text — only shown when `variant=\"floating\"`.\n * Acts as a Material Design-style animated label. @default ''\n */\n label = input<string>('');\n\n /** Placeholder text. @default 'Search…' */\n placeholder = input<string>('Search…');\n\n /** Explicit accessible name for the input. Falls back to `label` then `placeholder`. */\n ariaLabel = input<string>('');\n\n /**\n * Keyboard shortcut hint shown on the right side of the input when empty.\n * E.g. `'⌘K'`, `'Ctrl+K'`, `'/''`. Empty string hides the badge.\n * @default ''\n */\n shortcutHint = input<string>('');\n\n // ── Suggestions ───────────────────────────────────────────────────────────\n\n /**\n * Suggestion items to display in the dropdown.\n * Pass a filtered/sliced array; the component renders all provided items.\n * Empty array closes the dropdown.\n */\n suggestions = input<SearchSuggestion[]>([]);\n\n /**\n * Label shown above the suggestions list (e.g. 'Recent searches', 'Results').\n * Empty string hides the header. @default ''\n */\n suggestionsLabel = input<string>('');\n\n /**\n * Text displayed at the bottom of the dropdown — useful for \"View all results\".\n * Empty string hides the footer. @default ''\n */\n footerLabel = input<string>('');\n\n // ── Filters ───────────────────────────────────────────────────────────────\n\n /**\n * Active filter chips rendered inside the input track.\n * Each chip has an × button that emits `filterRemoved`.\n */\n filters = input<SearchFilter[]>([]);\n\n // ── Appearance ────────────────────────────────────────────────────────────\n\n /** Field size: sm, md or lg. @default 'md' */\n size = input<SearchBarSize>('md');\n\n /** Visual variant: default, floating, minimal… @default 'default' */\n variant = input<SearchBarVariant>('default');\n\n /** Show the magnifier icon on the left. @default true */\n showSearchIcon = input<boolean>(true);\n\n // ── State ─────────────────────────────────────────────────────────────────\n\n /** Show the spinning loader inside the search icon area. @default false */\n loading = input<boolean>(false);\n\n /** Whether the input is disabled. @default false */\n disabled = input<boolean>(false);\n\n /**\n * Debounce delay in milliseconds before `searched` emits.\n * Set to 0 to emit on every keystroke. @default 300\n */\n debounceMs = input<number>(300);\n\n /** Additional classes on the root wrapper. */\n customClass = input<string>('');\n\n // ── Outputs ───────────────────────────────────────────────────────────────\n\n /** Emitted (debounced) on every value change. */\n searched = output<string>();\n\n /** Emitted when the user presses Enter or clicks a suggestion. */\n submitted = output<string>();\n\n /** Emitted when a suggestion row is clicked, carrying the suggestion object. */\n suggestionSelected = output<SearchSuggestion>();\n\n /** Emitted when the × on a filter chip is clicked. */\n filterRemoved = output<string>();\n\n /** Emitted when the footer link is clicked. */\n footerClick = output<void>();\n\n /** Emitted when the input is cleared via the × button. */\n cleared = output<void>();\n\n // ── Internal state ────────────────────────────────────────────────────────\n\n readonly isFocused = signal(false);\n readonly activeIndex = signal(-1);\n\n /** Per-instance id base so multiple search bars don't collide. */\n private readonly _baseId = `sv-search-${++_svSearchUid}`;\n /** id of the suggestions listbox (referenced by aria-controls). */\n readonly listboxId = `${this._baseId}-listbox`;\n /** DOM id for an option row at index `i` (used by aria-activedescendant). */\n optionId(i: number): string { return `${this._baseId}-opt-${i}`; }\n /** id of the active option, or null when nothing is highlighted. */\n readonly activeDescendantId = computed<string | null>(() =>\n this.activeIndex() >= 0 ? this.optionId(this.activeIndex()) : null\n );\n /** Accessible name: explicit ariaLabel, then floating label, then placeholder. */\n readonly accessibleName = computed<string>(() =>\n this.ariaLabel() || this.label() || this.placeholder()\n );\n\n private debounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n /** Template reference to the native <input> element. */\n readonly inputRef = viewChild<ElementRef<HTMLInputElement>>('inputEl');\n /** Template reference to the track container — used for dropdown positioning. */\n readonly trackRef = viewChild<ElementRef<HTMLElement>>('trackEl');\n\n // ── Body-portal state ────────────────────────────────────────────────────\n // Portals the suggestions panel into document.body so ancestor overflow/\n // transform containers can never clip it (see dropdown-instruction.md).\n\n /** True once the panel has flipped to open above the track (space below is tight). */\n readonly openAbove = signal(false);\n /** Suppresses the dropdown after a viewport resize until focus/typing resumes. */\n readonly resizeSuppressed = signal(false);\n /** Inline style object applied to the portaled panel (position:fixed + viewport coords). */\n readonly panelStyle = signal<Record<string, string>>({});\n\n private portalEl: HTMLElement | null = null;\n private portalView: EmbeddedViewRef<object> | null = null;\n\n /** The ng-template holding the suggestions panel — never rendered inline; instantiated into <body> on open. */\n @ViewChild('panelTpl', { read: TemplateRef })\n private readonly panelTpl!: TemplateRef<object>;\n\n /** Capture-phase scroll listener — fires for ANY scrollable ancestor, not just the window. */\n private _rafId: number | null = null;\n private readonly _scrollHandler = (): void => {\n if (!this.showDropdown()) return;\n if (this._rafId !== null) return;\n this._rafId = requestAnimationFrame(() => {\n this._rafId = null;\n if (this.showDropdown()) {\n this._calcPos();\n this.portalView?.detectChanges();\n }\n });\n };\n\n // ── Computed ──────────────────────────────────────────────────────────────\n\n /**\n * Suggestions pre-processed once per change-detection cycle.\n * Resolves each item's icon path and highlight split so the template\n * can bind to plain properties instead of calling methods inside `@for`.\n */\n computedSuggestions = computed<ComputedSuggestion[]>(() =>\n this.suggestions().map(s => ({\n ...s,\n resolvedIconPath: this.getSuggestionIconPath(s),\n highlighted: this.highlightMatch(s.label),\n }))\n );\n\n showDropdown = computed(() =>\n this.isFocused() && !this.resizeSuppressed() && this.computedSuggestions().length > 0\n );\n\n showClear = computed(() => this.value().length > 0 && !this.disabled());\n\n isFloating = computed(() => false);\n labelIsFloated = computed(() => this.isFocused() || !!this.value());\n\n floatingLabelClasses = computed<string>(() => {\n const floated = this.labelIsFloated();\n const focused = this.isFocused();\n const hasIcon = this.showSearchIcon();\n\n // Not-floated position must align with the input text cursor.\n // inputNativePadClass zeroes the left pad when icon is present, so cursor\n // lands at: iconPaddingClass + iconWidth + gap-1.5\n // sm: pl-2.5(10) + 14px + 6px = 30px\n // md: pl-3(12) + 16px + 6px = 34px\n // lg: pl-4(16) + 20px + 6px = 42px\n const notFloatedLeftMap: Record<SearchBarSize, string> = hasIcon\n ? { sm: 'left-[30px]', md: 'left-[34px]', lg: 'left-[42px]' }\n : { sm: 'left-2', md: 'left-3', lg: 'left-4' };\n const notFloatedLeft = notFloatedLeftMap[this.size()];\n const floatedLeft = 'left-3';\n\n const textSizeMap: Record<SearchBarSize, string> = {\n sm: 'text-xs',\n md: 'text-sm',\n lg: 'text-base',\n };\n const textSize = floated ? 'text-[0.7rem] leading-none' : textSizeMap[this.size()];\n const bg = floated ? 'px-1 bg-white dark:bg-gray-900' : '';\n\n const color = focused && floated\n ? 'text-primary-600 dark:text-primary-400'\n : floated\n ? 'text-gray-500 dark:text-gray-400'\n : 'text-gray-500 dark:text-gray-400';\n\n return [\n 'pointer-events-none absolute z-10 transition-all duration-200 select-none whitespace-nowrap',\n floated\n ? `top-0 -translate-y-1/2 ${textSize} ${bg} ${floatedLeft}`\n : `top-1/2 -translate-y-1/2 ${textSize} ${notFloatedLeft}`,\n color,\n ].filter(Boolean).join(' ');\n });\n\n showShortcut = computed(() =>\n !!this.shortcutHint() && !this.value() && !this.isFocused() && !this.loading()\n );\n\n // ── Class maps ────────────────────────────────────────────────────────────\n\n wrapperClasses = computed<string>(() => ['relative w-full', this.customClass()].join(' '));\n\n trackClasses = computed<string>(() => {\n const sizeMap: Record<SearchBarSize, string> = {\n sm: 'h-8 text-xs',\n md: 'h-10 text-sm',\n lg: 'h-12 text-base',\n };\n\n const variantMap: Record<SearchBarVariant, string> = {\n default: 'bg-white border border-gray-300 dark:bg-gray-900 dark:border-gray-700 shadow-sm',\n filled: 'bg-gray-100 border border-transparent dark:bg-gray-800 dark:border-transparent',\n borderless: 'bg-transparent border border-transparent',\n };\n\n const focusRing = this.isFocused()\n ? 'ring-2 ring-primary-500/30 border-primary-500 dark:border-primary-400'\n : 'hover:border-gray-400 dark:hover:border-gray-500';\n\n return [\n 'relative flex w-full items-center gap-1.5 rounded-md transition-all duration-150',\n sizeMap[this.size()],\n variantMap[this.variant()],\n this.disabled() ? 'cursor-not-allowed opacity-60' : focusRing,\n this.filters().length > 0 ? 'flex-wrap py-1' : '',\n ].filter(Boolean).join(' ');\n });\n\n inputPaddingClass = computed<string>(() => {\n switch (this.size()) {\n case 'sm': return 'px-2';\n case 'lg': return 'px-4';\n default: return 'px-3';\n }\n });\n\n /**\n * When a search icon occupies the left side the input needs no left padding —\n * the icon span + gap-1.5 already create visual spacing.\n * Without an icon, use symmetric px-X. Applies to all variants.\n */\n inputNativePadClass = computed<string>(() => {\n if (this.showSearchIcon()) {\n const rp: Record<SearchBarSize, string> = { sm: 'pr-2', md: 'pr-3', lg: 'pr-4' };\n return `pl-0 ${rp[this.size()]}`;\n }\n return this.inputPaddingClass();\n });\n\n iconSizeClass = computed<string>(() => {\n switch (this.size()) {\n case 'sm': return 'h-3.5 w-3.5';\n case 'lg': return 'h-5 w-5';\n default: return 'h-4 w-4';\n }\n });\n\n iconPaddingClass = computed<string>(() => {\n switch (this.size()) {\n case 'sm': return 'pl-2.5';\n case 'lg': return 'pl-4';\n default: return 'pl-3';\n }\n });\n\n // ── Suggestion helpers ────────────────────────────────────────────────────\n\n getSuggestionIconPath(s: SearchSuggestion): string {\n if (s.iconType === 'custom' && s.iconPath) return s.iconPath;\n if (s.iconType && s.iconType !== 'custom') return SUGGESTION_ICONS[s.iconType];\n return SUGGESTION_ICONS.search;\n }\n\n /** Highlight matched portion of a suggestion label. */\n highlightMatch(text: string): { pre: string; match: string; post: string } {\n const q = this.value().trim().toLowerCase();\n if (!q) return { pre: text, match: '', post: '' };\n const idx = text.toLowerCase().indexOf(q);\n if (idx === -1) return { pre: text, match: '', post: '' };\n return {\n pre: text.slice(0, idx),\n match: text.slice(idx, idx + q.length),\n post: text.slice(idx + q.length),\n };\n }\n\n // ── Event handlers ────────────────────────────────────────────────────────\n\n onInput(raw: string): void {\n this.resizeSuppressed.set(false);\n this.value.set(raw);\n this.activeIndex.set(-1);\n\n if (this.debounceTimer) clearTimeout(this.debounceTimer);\n\n const ms = this.debounceMs();\n if (ms > 0) {\n this.debounceTimer = setTimeout(() => this.searched.emit(raw), ms);\n } else {\n this.searched.emit(raw);\n }\n }\n\n onFocus(): void {\n this.resizeSuppressed.set(false);\n this.isFocused.set(true);\n }\n onBlur(): void {\n // Delay so click on a suggestion registers before closing dropdown.\n setTimeout(() => this.isFocused.set(false), BLUR_CLOSE_DELAY_MS);\n }\n\n onKeydown(event: KeyboardEvent): void {\n const computed = this.computedSuggestions();\n const count = computed.length;\n if (!this.showDropdown() && event.key !== 'Enter') return;\n\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault();\n this.activeIndex.update(i => Math.min(i + 1, count - 1));\n break;\n case 'ArrowUp':\n event.preventDefault();\n this.activeIndex.update(i => Math.max(i - 1, -1));\n break;\n case 'Enter': {\n event.preventDefault();\n const idx = this.activeIndex();\n if (idx >= 0 && idx < count) {\n this.selectSuggestion(computed[idx]);\n } else {\n this.submitted.emit(this.value());\n this.isFocused.set(false);\n }\n break;\n }\n case 'Escape':\n this.isFocused.set(false);\n this.activeIndex.set(-1);\n this.inputRef()?.nativeElement.blur();\n break;\n }\n }\n\n selectSuggestion(s: SearchSuggestion): void {\n this.value.set(s.label);\n this.isFocused.set(false);\n this.activeIndex.set(-1);\n this.submitted.emit(s.label);\n this.suggestionSelected.emit(s);\n }\n\n clearValue(): void {\n this.value.set('');\n this.activeIndex.set(-1);\n this.cleared.emit();\n this.searched.emit('');\n if (isPlatformBrowser(this.platformId)) {\n setTimeout(() => this.inputRef()?.nativeElement.focus());\n }\n }\n\n onFooterClick(): void {\n this.isFocused.set(false);\n this.footerClick.emit();\n }\n\n focus(): void {\n this.inputRef()?.nativeElement.focus();\n }\n\n @HostListener('window:resize')\n onWindowResize(): void {\n // Viewport resize invalidates the layout — close rather than reposition\n // with now-stale coordinates (dropdown-instruction.md §8). Reopens\n // naturally on the next focus/input.\n if (this.showDropdown()) this.resizeSuppressed.set(true);\n }\n\n private _calcPos(): void {\n const track = this.trackRef()?.nativeElement;\n if (!track) return;\n\n const rect = track.getBoundingClientRect();\n const vh = window.innerHeight;\n\n const spaceBelow = vh - rect.bottom - PANEL_MARGIN;\n const spaceAbove = rect.top - PANEL_MARGIN;\n const above = spaceBelow < PANEL_PREFERRED_HEIGHT && spaceAbove > spaceBelow;\n\n this.openAbove.set(above);\n\n const style: Record<string, string> = {\n position: 'fixed',\n zIndex: '9999',\n left: `${rect.left}px`,\n width: `${rect.width}px`,\n maxHeight: `${Math.max(120, above ? spaceAbove : spaceBelow)}px`,\n };\n style[above ? 'bottom' : 'top'] = above\n ? `${vh - rect.top + PANEL_GAP}px`\n : `${rect.bottom + PANEL_GAP}px`;\n\n this.panelStyle.set(style);\n }\n\n // ── Lifecycle ─────────────────────────────────────────────────────────────\n\n constructor() {\n // Mount/unmount the body-portaled panel whenever the dropdown's open\n // state changes — the reactive equivalent of Color Picker's toggle().\n effect(() => {\n if (this.showDropdown()) {\n if (!this.portalView) this._openPanel();\n } else if (this.portalView) {\n this._closePanel();\n }\n });\n }\n\n ngOnInit(): void {\n // Capture phase catches scroll from any scrollable ancestor, not just window.\n window.addEventListener('scroll', this._scrollHandler, { passive: true, capture: true });\n }\n\n ngOnDestroy(): void {\n window.removeEventListener('scroll', this._scrollHandler, true);\n if (this._rafId !== null) cancelAnimationFrame(this._rafId);\n if (this.debounceTimer) clearTimeout(this.debounceTimer);\n this._closePanel();\n }\n\n // ── Body portal ──────────────────────────────────────────────────────────\n\n private _openPanel(): void {\n if (!this.panelTpl) return;\n this._calcPos();\n\n this.portalEl = document.createElement('div');\n this.portalEl.className = 'sv-search-portal';\n document.body.appendChild(this.portalEl);\n\n this.portalView = this.panelTpl.createEmbeddedView({});\n this.appRef.attachView(this.portalView);\n this.portalView.rootNodes.forEach((n: Node) => this.portalEl!.appendChild(n));\n this.portalView.detectChanges();\n }\n\n private _closePanel(): void {\n if (this.portalView) {\n this.appRef.detachView(this.portalView);\n this.portalView.destroy();\n this.portalView = null;\n }\n this.portalEl?.remove();\n this.portalEl = null;\n }\n}\n","<!-- Root wrapper — positions the dropdown relative to this element -->\n<div [ngClass]=\"wrapperClasses()\">\n\n <!-- ── Input track ────────────────────────────────────────────────────── -->\n <div #trackEl [ngClass]=\"trackClasses()\">\n\n <!-- Floating label (animates to border on focus or value) -->\n @if (isFloating() && label()) {\n <label [ngClass]=\"floatingLabelClasses()\" aria-hidden=\"true\">{{ label() }}</label>\n }\n @if (showSearchIcon()) {\n <span\n class=\"flex shrink-0 items-center text-gray-500 dark:text-gray-400\"\n [ngClass]=\"iconPaddingClass()\"\n aria-hidden=\"true\"\n >\n @if (loading()) {\n <!-- Spinner -->\n <svg\n [ngClass]=\"iconSizeClass()\"\n class=\"animate-spin text-primary-500\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\"\n stroke=\"currentColor\" stroke-width=\"4\"/>\n <path class=\"opacity-75\" fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"/>\n </svg>\n } @else {\n <!-- Magnifier -->\n <svg [ngClass]=\"iconSizeClass()\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"/>\n <path d=\"m21 21-4.35-4.35\"/>\n </svg>\n }\n </span>\n }\n\n <!-- Active filter chips -->\n @for (filter of filters(); track filter.id) {\n <span\n class=\"inline-flex shrink-0 items-center gap-1 rounded-md\n bg-primary-50 px-2 py-0.5 text-xs font-medium text-primary-700\n dark:bg-primary-900/40 dark:text-primary-300\"\n >\n {{ filter.label }}\n <button\n type=\"button\"\n class=\"ml-0.5 rounded text-primary-400 hover:text-primary-600\n dark:text-primary-400 dark:hover:text-primary-200\n focus:outline-none focus-visible:ring-1 focus-visible:ring-primary-400\"\n (click)=\"filterRemoved.emit(filter.id)\"\n [attr.aria-label]=\"'Remove filter ' + filter.label\"\n >\n <svg class=\"h-3 w-3\" viewBox=\"0 0 14 14\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <path d=\"M3 3l8 8M11 3l-8 8\"/>\n </svg>\n </button>\n </span>\n }\n\n <!-- Text input -->\n <input\n #inputEl\n type=\"search\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n spellcheck=\"false\"\n class=\"min-w-0 flex-1 bg-transparent text-gray-900 placeholder-gray-400\n outline-none dark:text-white dark:placeholder-gray-500\n [&::-webkit-search-cancel-button]:hidden\"\n [ngClass]=\"[\n inputNativePadClass(),\n filters().length > 0 ? 'py-0.5' : 'h-full'\n ]\"\n [placeholder]=\"isFloating() && label() && !labelIsFloated() ? ' ' : placeholder()\"\n [disabled]=\"disabled()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n [attr.aria-expanded]=\"showDropdown()\"\n [attr.aria-label]=\"accessibleName()\"\n [attr.aria-busy]=\"loading() || null\"\n [attr.aria-autocomplete]=\"suggestions().length ? 'list' : 'none'\"\n [attr.aria-controls]=\"showDropdown() ? listboxId : null\"\n [attr.aria-activedescendant]=\"activeDescendantId()\"\n />\n\n <!-- Keyboard shortcut hint -->\n @if (showShortcut()) {\n <kbd\n class=\"mr-2 hidden shrink-0 items-center rounded border border-gray-200 bg-gray-100\n px-1.5 py-0.5 font-mono text-[10px] text-gray-500\n dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 sm:inline-flex\"\n aria-hidden=\"true\"\n >\n {{ shortcutHint() }}\n </kbd>\n }\n\n <!-- Clear button -->\n @if (showClear()) {\n <button\n type=\"button\"\n class=\"mr-2 shrink-0 rounded-full p-0.5 text-gray-400\n transition-colors hover:bg-gray-100 hover:text-gray-600\n dark:hover:bg-gray-700 dark:hover:text-gray-300\n focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400\"\n (click)=\"clearValue()\"\n aria-label=\"Clear search\"\n >\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 14 14\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\">\n <path d=\"M3 3l8 8M11 3l-8 8\"/>\n </svg>\n </button>\n }\n\n </div>\n\n <!-- ── Suggestions dropdown — portaled into document.body on open so ancestor\n overflow/transform containers can never clip it (see dropdown-instruction.md). ── -->\n <ng-template #panelTpl>\n <div\n [id]=\"listboxId + '-panel'\"\n class=\"flex flex-col overflow-hidden rounded-xl\n border border-gray-200 bg-white\n shadow-lg shadow-gray-200/60\n dark:border-gray-700 dark:bg-gray-900 dark:shadow-gray-950/60\"\n [style.position]=\"panelStyle()['position']\"\n [style.zIndex]=\"panelStyle()['zIndex']\"\n [style.top]=\"panelStyle()['top']\"\n [style.bottom]=\"panelStyle()['bottom']\"\n [style.left]=\"panelStyle()['left']\"\n [style.width]=\"panelStyle()['width']\"\n [style.maxHeight]=\"panelStyle()['maxHeight']\"\n >\n\n <!-- Dropdown header label -->\n @if (suggestionsLabel()) {\n <div class=\"border-b border-gray-100 px-3 py-2 dark:border-gray-800\">\n <p class=\"text-[11px] font-semibold uppercase tracking-wider\n text-gray-500 dark:text-gray-400\">\n {{ suggestionsLabel() }}\n </p>\n </div>\n }\n\n <!-- Suggestion rows -->\n <ul\n [attr.id]=\"listboxId\"\n role=\"listbox\"\n [attr.aria-label]=\"suggestionsLabel() || accessibleName() || 'Search suggestions'\"\n class=\"min-h-0 flex-1 overflow-y-auto overscroll-contain py-1\"\n >\n @for (s of computedSuggestions(); track s.id; let i = $index) {\n <li\n [id]=\"optionId(i)\"\n role=\"option\"\n [attr.aria-selected]=\"activeIndex() === i\"\n class=\"flex cursor-pointer items-center gap-3 px-3 py-2.5\n transition-colors duration-75 select-none\"\n [ngClass]=\"activeIndex() === i\n ? 'bg-primary-50 dark:bg-primary-900/30'\n : 'hover:bg-gray-50 dark:hover:bg-gray-800/60'\"\n (mousedown)=\"selectSuggestion(s)\"\n (mouseenter)=\"activeIndex.set(i)\"\n >\n <!-- Row icon -->\n <span\n class=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-lg\n text-gray-500 dark:text-gray-400\"\n [ngClass]=\"activeIndex() === i\n ? 'bg-primary-100 text-primary-600 dark:bg-primary-900/50 dark:text-primary-400'\n : 'bg-gray-100 dark:bg-gray-800'\"\n aria-hidden=\"true\"\n >\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"s.resolvedIconPath\" />\n </svg>\n </span>\n\n <!-- Text -->\n <span class=\"min-w-0 flex-1\">\n <!-- Highlighted label (pre-computed, no method call in template) -->\n <span class=\"block truncate text-sm font-medium text-gray-900 dark:text-white\">\n {{ s.highlighted.pre }}<mark class=\"bg-transparent font-bold text-primary-600 dark:text-primary-400\">{{ s.highlighted.match }}</mark>{{ s.highlighted.post }}\n </span>\n @if (s.sublabel) {\n <span class=\"block truncate text-xs text-gray-500 dark:text-gray-400\">\n {{ s.sublabel }}\n </span>\n }\n </span>\n\n <!-- Badge -->\n @if (s.badge) {\n <span\n class=\"shrink-0 rounded-full bg-primary-50 px-2 py-0.5\n text-[10px] font-semibold text-primary-600\n dark:bg-primary-900/40 dark:text-primary-400\"\n >\n {{ s.badge }}\n </span>\n }\n\n <!-- Keyboard hint on active row -->\n @if (activeIndex() === i) {\n <kbd class=\"shrink-0 rounded border border-gray-200 bg-gray-100 px-1\n py-0.5 font-mono text-[9px] text-gray-500\n dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400\"\n aria-hidden=\"true\">\n ↵\n </kbd>\n }\n\n </li>\n }\n </ul>\n\n <!-- Dropdown footer -->\n @if (footerLabel()) {\n <div class=\"border-t border-gray-100 dark:border-gray-800\">\n <button\n type=\"button\"\n class=\"flex w-full items-center justify-between px-3 py-2.5\n text-xs font-medium text-primary-600 transition-colors\n hover:bg-gray-50 dark:text-primary-400 dark:hover:bg-gray-800/60\n focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-400\"\n (mousedown)=\"onFooterClick()\"\n >\n <span>{{ footerLabel() }}</span>\n <svg class=\"h-3.5 w-3.5\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M5 12h14M12 5l7 7-7 7\"/>\n </svg>\n </button>\n </div>\n }\n\n </div>\n </ng-template>\n\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;;;AA6BA;AACA,IAAI,YAAY,GAAG,CAAC;AA+CpB;AAEA,MAAM,gBAAgB,GAAgF;AACpG,IAAA,MAAM,EAAE,gDAAgD;AACxD,IAAA,IAAI,EAAI,0EAA0E;AAClF,IAAA,IAAI,EAAI,8EAA8E;AACtF,IAAA,GAAG,EAAK,wFAAwF;CACjG;AAED;AAEA;AACA,MAAM,mBAAmB,GAAG,GAAG;AAC/B;AACA,MAAM,SAAS,GAAG,CAAC;AACnB;AACA,MAAM,YAAY,GAAG,CAAC;AACtB;AACA,MAAM,sBAAsB,GAAG,GAAG;AAElC;AAEA;;;;;;;;;;;;;;;AAeG;MAQU,oBAAoB,CAAA;AACd,IAAA,UAAU,GAAG,MAAM,CAAC,WAAW,CAAC;AAChC,IAAA,MAAM,GAAO,MAAM,CAAC,cAAc,CAAC;;;AAKpD,IAAA,KAAK,GAAG,KAAK,CAAS,EAAE,4EAAC;;AAIzB;;;AAGG;AACH,IAAA,KAAK,GAAG,KAAK,CAAS,EAAE,4EAAC;;AAGzB,IAAA,WAAW,GAAG,KAAK,CAAS,SAAS,kFAAC;;AAGtC,IAAA,SAAS,GAAG,KAAK,CAAS,EAAE,gFAAC;AAE7B;;;;AAIG;AACH,IAAA,YAAY,GAAG,KAAK,CAAS,EAAE,mFAAC;;AAIhC;;;;AAIG;AACH,IAAA,WAAW,GAAG,KAAK,CAAqB,EAAE,kFAAC;AAE3C;;;AAGG;AACH,IAAA,gBAAgB,GAAG,KAAK,CAAS,EAAE,uFAAC;AAEpC;;;AAGG;AACH,IAAA,WAAW,GAAG,KAAK,CAAS,EAAE,kFAAC;;AAI/B;;;AAGG;AACH,IAAA,OAAO,GAAG,KAAK,CAAiB,EAAE,8EAAC;;;AAKnC,IAAA,IAAI,GAAM,KAAK,CAAgB,IAAI,2EAAC;;AAGpC,IAAA,OAAO,GAAG,KAAK,CAAmB,SAAS,8EAAC;;AAG5C,IAAA,cAAc,GAAG,KAAK,CAAU,IAAI,qFAAC;;;AAKrC,IAAA,OAAO,GAAG,KAAK,CAAU,KAAK,8EAAC;;AAG/B,IAAA,QAAQ,GAAG,KAAK,CAAU,KAAK,+EAAC;AAEhC;;;AAGG;AACH,IAAA,UAAU,GAAG,KAAK,CAAS,GAAG,iFAAC;;AAG/B,IAAA,WAAW,GAAG,KAAK,CAAS,EAAE,kFAAC;;;IAK/B,QAAQ,GAAQ,MAAM,EAAU;;IAGhC,SAAS,GAAO,MAAM,EAAU;;IAGhC,kBAAkB,GAAG,MAAM,EAAoB;;IAG/C,aAAa,GAAG,MAAM,EAAU;;IAGhC,WAAW,GAAK,MAAM,EAAQ;;IAG9B,OAAO,GAAS,MAAM,EAAQ;;AAIrB,IAAA,SAAS,GAAU,MAAM,CAAC,KAAK,gFAAC;AAChC,IAAA,WAAW,GAAQ,MAAM,CAAC,CAAC,CAAC,kFAAC;;AAGrB,IAAA,OAAO,GAAG,CAAA,UAAA,EAAa,EAAE,YAAY,EAAE;;AAE/C,IAAA,SAAS,GAAG,CAAA,EAAG,IAAI,CAAC,OAAO,UAAU;;AAE9C,IAAA,QAAQ,CAAC,CAAS,EAAA,EAAY,OAAO,CAAA,EAAG,IAAI,CAAC,OAAO,CAAA,KAAA,EAAQ,CAAC,CAAA,CAAE,CAAC,CAAC;;AAExD,IAAA,kBAAkB,GAAG,QAAQ,CAAgB,MACpD,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,GAAG,IAAI,yFACnE;;IAEQ,cAAc,GAAG,QAAQ,CAAS,MACzC,IAAI,CAAC,SAAS,EAAE,IAAI,IAAI,CAAC,KAAK,EAAE,IAAI,IAAI,CAAC,WAAW,EAAE,EAAA,IAAA,SAAA,GAAA,CAAA,EAAA,SAAA,EAAA,gBAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CACvD;IAEO,aAAa,GAAyC,IAAI;;AAGzD,IAAA,QAAQ,GAAI,SAAS,CAA+B,SAAS,+EAAC;;AAE9D,IAAA,QAAQ,GAAI,SAAS,CAA0B,SAAS,+EAAC;;;;;AAOzD,IAAA,SAAS,GAAG,MAAM,CAAC,KAAK,gFAAC;;AAEzB,IAAA,gBAAgB,GAAG,MAAM,CAAC,KAAK,uFAAC;;AAEhC,IAAA,UAAU,GAAG,MAAM,CAAyB,EAAE,iFAAC;IAEhD,QAAQ,GAAyB,IAAI;IACrC,UAAU,GAAmC,IAAI;;AAIxC,IAAA,QAAQ;;IAGjB,MAAM,GAAkB,IAAI;IACnB,cAAc,GAAG,MAAW;AAC3C,QAAA,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE;YAAE;AAC1B,QAAA,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI;YAAE;AAC1B,QAAA,IAAI,CAAC,MAAM,GAAG,qBAAqB,CAAC,MAAK;AACvC,YAAA,IAAI,CAAC,MAAM,GAAG,IAAI;AAClB,YAAA,IAAI,IAAI,CAAC,YAAY,EAAE,EAAE;gBACvB,IAAI,CAAC,QAAQ,EAAE;AACf,gBAAA,IAAI,CAAC,UAAU,EAAE,aAAa,EAAE;YAClC;AACF,QAAA,CAAC,CAAC;AACJ,IAAA,CAAC;;AAID;;;;AAIG;AACH,IAAA,mBAAmB,GAAG,QAAQ,CAAuB,MACnD,IAAI,CAAC,WAAW,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK;AAC3B,QAAA,GAAG,CAAC;AACJ,QAAA,gBAAgB,EAAE,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC;QAC/C,WAAW,EAAO,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC;KAC/C,CAAC,CAAC,0FACJ;IAED,YAAY,GAAG,QAAQ,CAAC,MACtB,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC,MAAM,GAAG,CAAC,EAAA,IAAA,SAAA,GAAA,CAAA,EAAA,SAAA,EAAA,cAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CACtF;IAED,SAAS,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAA,IAAA,SAAA,GAAA,CAAA,EAAA,SAAA,EAAA,WAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CAAC;IAEvE,UAAU,GAAO,QAAQ,CAAC,MAAM,KAAK,iFAAC;AACtC,IAAA,cAAc,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,qFAAC;AAEnE,IAAA,oBAAoB,GAAG,QAAQ,CAAS,MAAK;AAC3C,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,EAAE;AACrC,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,EAAE;AAChC,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,EAAE;;;;;;;QAQrC,MAAM,iBAAiB,GAAkC;AACvD,cAAE,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,aAAa;AAC3D,cAAE,EAAE,EAAE,EAAE,QAAQ,EAAO,EAAE,EAAE,QAAQ,EAAQ,EAAE,EAAE,QAAQ,EAAE;QAC3D,MAAM,cAAc,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACrD,MAAM,WAAW,GAAM,QAAQ;AAE/B,QAAA,MAAM,WAAW,GAAkC;AACjD,YAAA,EAAE,EAAE,SAAS;AACb,YAAA,EAAE,EAAE,SAAS;AACb,YAAA,EAAE,EAAE,WAAW;SAChB;AACD,QAAA,MAAM,QAAQ,GAAG,OAAO,GAAG,4BAA4B,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAClF,MAAM,EAAE,GAAS,OAAO,GAAG,gCAAgC,GAAG,EAAE;AAEhE,QAAA,MAAM,KAAK,GAAG,OAAO,IAAI;AACvB,cAAE;AACF,cAAE;AACF,kBAAE;kBACA,kCAAkC;QAEtC,OAAO;YACL,6FAA6F;YAC7F;AACE,kBAAE,CAAA,uBAAA,EAA0B,QAAQ,IAAI,EAAE,CAAA,CAAA,EAAI,WAAW,CAAA;AACzD,kBAAE,CAAA,yBAAA,EAA4B,QAAQ,CAAA,CAAA,EAAI,cAAc,CAAA,CAAE;YAC5D,KAAK;SACN,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;AAC7B,IAAA,CAAC,2FAAC;AAEF,IAAA,YAAY,GAAG,QAAQ,CAAC,MACtB,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,mFAC/E;;IAID,cAAc,GAAG,QAAQ,CAAS,MAAM,CAAC,iBAAiB,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,qFAAC;AAE1F,IAAA,YAAY,GAAG,QAAQ,CAAS,MAAK;AACnC,QAAA,MAAM,OAAO,GAAkC;AAC7C,YAAA,EAAE,EAAE,cAAc;AAClB,YAAA,EAAE,EAAE,cAAc;AAClB,YAAA,EAAE,EAAE,gBAAgB;SACrB;AAED,QAAA,MAAM,UAAU,GAAqC;AACnD,YAAA,OAAO,EAAK,iFAAiF;AAC7F,YAAA,MAAM,EAAM,gFAAgF;AAC5F,YAAA,UAAU,EAAE,0CAA0C;SACvD;AAED,QAAA,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS;AAC9B,cAAE;cACA,kDAAkD;QAEtD,OAAO;YACL,kFAAkF;AAClF,YAAA,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;AACpB,YAAA,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAC1B,IAAI,CAAC,QAAQ,EAAE,GAAG,+BAA+B,GAAG,SAAS;AAC7D,YAAA,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,GAAG,CAAC,GAAG,gBAAgB,GAAG,EAAE;SAClD,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;AAC7B,IAAA,CAAC,mFAAC;AAEF,IAAA,iBAAiB,GAAG,QAAQ,CAAS,MAAK;AACxC,QAAA,QAAQ,IAAI,CAAC,IAAI,EAAE;AACjB,YAAA,KAAK,IAAI,EAAE,OAAO,MAAM;AACxB,YAAA,KAAK,IAAI,EAAE,OAAO,MAAM;AACxB,YAAA,SAAW,OAAO,MAAM;;AAE5B,IAAA,CAAC,wFAAC;AAEF;;;;AAIG;AACH,IAAA,mBAAmB,GAAG,QAAQ,CAAS,MAAK;AAC1C,QAAA,IAAI,IAAI,CAAC,cAAc,EAAE,EAAE;AACzB,YAAA,MAAM,EAAE,GAAkC,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE;YAChF,OAAO,CAAA,KAAA,EAAQ,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA,CAAE;QAClC;AACA,QAAA,OAAO,IAAI,CAAC,iBAAiB,EAAE;AACjC,IAAA,CAAC,0FAAC;AAEF,IAAA,aAAa,GAAG,QAAQ,CAAS,MAAK;AACpC,QAAA,QAAQ,IAAI,CAAC,IAAI,EAAE;AACjB,YAAA,KAAK,IAAI,EAAE,OAAO,aAAa;AAC/B,YAAA,KAAK,IAAI,EAAE,OAAO,SAAS;AAC3B,YAAA,SAAW,OAAO,SAAS;;AAE/B,IAAA,CAAC,oFAAC;AAEF,IAAA,gBAAgB,GAAG,QAAQ,CAAS,MAAK;AACvC,QAAA,QAAQ,IAAI,CAAC,IAAI,EAAE;AACjB,YAAA,KAAK,IAAI,EAAE,OAAO,QAAQ;AAC1B,YAAA,KAAK,IAAI,EAAE,OAAO,MAAM;AACxB,YAAA,SAAW,OAAO,MAAM;;AAE5B,IAAA,CAAC,uFAAC;;AAIF,IAAA,qBAAqB,CAAC,CAAmB,EAAA;QACvC,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ;YAAE,OAAO,CAAC,CAAC,QAAQ;QAC5D,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ;AAAE,YAAA,OAAO,gBAAgB,CAAC,CAAC,CAAC,QAAQ,CAAC;QAC9E,OAAO,gBAAgB,CAAC,MAAM;IAChC;;AAGA,IAAA,cAAc,CAAC,IAAY,EAAA;AACzB,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE;AAC3C,QAAA,IAAI,CAAC,CAAC;AAAE,YAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;QACjD,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;QACzC,IAAI,GAAG,KAAK,CAAC,CAAC;AAAE,YAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;QACzD,OAAO;YACL,GAAG,EAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;AACzB,YAAA,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC;YACtC,IAAI,EAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC;SAClC;IACH;;AAIA,IAAA,OAAO,CAAC,GAAW,EAAA;AACjB,QAAA,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC;AAChC,QAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;QACnB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAExB,IAAI,IAAI,CAAC,aAAa;AAAE,YAAA,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC;AAExD,QAAA,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE;AAC5B,QAAA,IAAI,EAAE,GAAG,CAAC,EAAE;AACV,YAAA,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACpE;aAAO;AACL,YAAA,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC;QACzB;IACF;IAEA,OAAO,GAAA;AACL,QAAA,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC;AAChC,QAAA,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;IAC1B;IACA,MAAM,GAAA;;AAEJ,QAAA,UAAU,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAClE;AAEA,IAAA,SAAS,CAAC,KAAoB,EAAA;AAC5B,QAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,mBAAmB,EAAE;AAC3C,QAAA,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM;QAC7B,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,KAAK,CAAC,GAAG,KAAK,OAAO;YAAE;AAEnD,QAAA,QAAQ,KAAK,CAAC,GAAG;AACf,YAAA,KAAK,WAAW;gBACd,KAAK,CAAC,cAAc,EAAE;gBACtB,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;gBACxD;AACF,YAAA,KAAK,SAAS;gBACZ,KAAK,CAAC,cAAc,EAAE;gBACtB,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBACjD;YACF,KAAK,OAAO,EAAE;gBACZ,KAAK,CAAC,cAAc,EAAE;AACtB,gBAAA,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,EAAE;gBAC9B,IAAI,GAAG,IAAI,CAAC,IAAI,GAAG,GAAG,KAAK,EAAE;oBAC3B,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;gBACtC;qBAAO;oBACL,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;AACjC,oBAAA,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;gBAC3B;gBACA;YACF;AACA,YAAA,KAAK,QAAQ;AACX,gBAAA,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;gBACzB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBACxB,IAAI,CAAC,QAAQ,EAAE,EAAE,aAAa,CAAC,IAAI,EAAE;gBACrC;;IAEN;AAEA,IAAA,gBAAgB,CAAC,CAAmB,EAAA;QAClC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;AACvB,QAAA,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;QACzB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACxB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;AAC5B,QAAA,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;IACjC;IAEA,UAAU,GAAA;AACR,QAAA,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAClB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACxB,QAAA,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE;AACnB,QAAA,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;AACtB,QAAA,IAAI,iBAAiB,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE;AACtC,YAAA,UAAU,CAAC,MAAM,IAAI,CAAC,QAAQ,EAAE,EAAE,aAAa,CAAC,KAAK,EAAE,CAAC;QAC1D;IACF;IAEA,aAAa,GAAA;AACX,QAAA,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;AACzB,QAAA,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE;IACzB;IAEA,KAAK,GAAA;QACH,IAAI,CAAC,QAAQ,EAAE,EAAE,aAAa,CAAC,KAAK,EAAE;IACxC;IAGA,cAAc,GAAA;;;;QAIZ,IAAI,IAAI,CAAC,YAAY,EAAE;AAAE,YAAA,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC;IAC1D;IAEQ,QAAQ,GAAA;QACd,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,aAAa;AAC5C,QAAA,IAAI,CAAC,KAAK;YAAE;AAEZ,QAAA,MAAM,IAAI,GAAG,KAAK,CAAC,qBAAqB,EAAE;AAC1C,QAAA,MAAM,EAAE,GAAK,MAAM,CAAC,WAAW;QAE/B,MAAM,UAAU,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,GAAG,YAAY;AAClD,QAAA,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,GAAG,YAAY;QAC1C,MAAM,KAAK,GAAG,UAAU,GAAG,sBAAsB,IAAI,UAAU,GAAG,UAAU;AAE5E,QAAA,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;AAEzB,QAAA,MAAM,KAAK,GAA2B;AACpC,YAAA,QAAQ,EAAG,OAAO;AAClB,YAAA,MAAM,EAAK,MAAM;AACjB,YAAA,IAAI,EAAO,CAAA,EAAG,IAAI,CAAC,IAAI,CAAA,EAAA,CAAI;AAC3B,YAAA,KAAK,EAAM,CAAA,EAAG,IAAI,CAAC,KAAK,CAAA,EAAA,CAAI;AAC5B,YAAA,SAAS,EAAE,CAAA,EAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,GAAG,UAAU,GAAG,UAAU,CAAC,CAAA,EAAA,CAAI;SACjE;AACD,QAAA,KAAK,CAAC,KAAK,GAAG,QAAQ,GAAG,KAAK,CAAC,GAAG;cAC9B,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,GAAG,SAAS,CAAA,EAAA;cAC5B,GAAG,IAAI,CAAC,MAAM,GAAG,SAAS,IAAI;AAElC,QAAA,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC;IAC5B;;AAIA,IAAA,WAAA,GAAA;;;QAGE,MAAM,CAAC,MAAK;AACV,YAAA,IAAI,IAAI,CAAC,YAAY,EAAE,EAAE;gBACvB,IAAI,CAAC,IAAI,CAAC,UAAU;oBAAE,IAAI,CAAC,UAAU,EAAE;YACzC;AAAO,iBAAA,IAAI,IAAI,CAAC,UAAU,EAAE;gBAC1B,IAAI,CAAC,WAAW,EAAE;YACpB;AACF,QAAA,CAAC,CAAC;IACJ;IAEA,QAAQ,GAAA;;AAEN,QAAA,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC1F;IAEA,WAAW,GAAA;QACT,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC;AAC/D,QAAA,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI;AAAE,YAAA,oBAAoB,CAAC,IAAI,CAAC,MAAM,CAAC;QAC3D,IAAI,IAAI,CAAC,aAAa;AAAE,YAAA,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC;QACxD,IAAI,CAAC,WAAW,EAAE;IACpB;;IAIQ,UAAU,GAAA;QAChB,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE;QACpB,IAAI,CAAC,QAAQ,EAAE;QAEf,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC;AAC7C,QAAA,IAAI,CAAC,QAAQ,CAAC,SAAS,GAAG,kBAAkB;QAC5C,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC;QAExC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;QACtD,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC;QACvC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAO,KAAK,IAAI,CAAC,QAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;AAC7E,QAAA,IAAI,CAAC,UAAU,CAAC,aAAa,EAAE;IACjC;IAEQ,WAAW,GAAA;AACjB,QAAA,IAAI,IAAI,CAAC,UAAU,EAAE;YACnB,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC;AACvC,YAAA,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE;AACzB,YAAA,IAAI,CAAC,UAAU,GAAG,IAAI;QACxB;AACA,QAAA,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE;AACvB,QAAA,IAAI,CAAC,QAAQ,GAAG,IAAI;IACtB;wGA7eW,oBAAoB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAApB,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,IAAA,EAAA,oBAAoB,usFAqJA,WAAW,EAAA,CAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EC/Q5C,g3UA+PA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EDzIY,OAAO,mFAAE,WAAW,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,oBAAA,EAAA,QAAA,EAAA,8MAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,eAAA,EAAA,QAAA,EAAA,2CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,OAAA,EAAA,QAAA,EAAA,qDAAA,EAAA,MAAA,EAAA,CAAA,MAAA,EAAA,UAAA,EAAA,SAAA,EAAA,gBAAA,CAAA,EAAA,OAAA,EAAA,CAAA,eAAA,CAAA,EAAA,QAAA,EAAA,CAAA,SAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;4FAInB,oBAAoB,EAAA,UAAA,EAAA,CAAA;kBAPhC,SAAS;+BACE,eAAe,EAAA,UAAA,EACb,IAAI,EAAA,OAAA,EACP,CAAC,OAAO,EAAE,WAAW,CAAC,EAAA,eAAA,EAEd,uBAAuB,CAAC,MAAM,EAAA,QAAA,EAAA,g3UAAA,EAAA;AAoIc,SAAA,CAAA,EAAA,cAAA,EAAA,MAAA,EAAA,EAAA,cAAA,EAAA,EAAA,KAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,OAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,EAAA,CAAA,MAAA,EAAA,IAAA,EAAA,CAAA,aAAA,CAAA,EAAA,CAAA,EAAA,KAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,OAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,WAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,aAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,SAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,WAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,cAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,WAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,aAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,gBAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,kBAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,WAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,aAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,SAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,IAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,MAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,SAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,cAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,gBAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,SAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,QAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,UAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,UAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,YAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,WAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,KAAA,EAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,KAAA,EAAA,aAAA,EAAA,QAAA,EAAA,KAAA,EAAA,CAAA,EAAA,CAAA,EAAA,QAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,MAAA,EAAA,IAAA,EAAA,CAAA,UAAA,CAAA,EAAA,CAAA,EAAA,SAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,MAAA,EAAA,IAAA,EAAA,CAAA,WAAA,CAAA,EAAA,CAAA,EAAA,kBAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,MAAA,EAAA,IAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,CAAA,EAAA,aAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,MAAA,EAAA,IAAA,EAAA,CAAA,eAAA,CAAA,EAAA,CAAA,EAAA,WAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,MAAA,EAAA,IAAA,EAAA,CAAA,aAAA,CAAA,EAAA,CAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,MAAA,EAAA,IAAA,EAAA,CAAA,SAAA,CAAA,EAAA,CAAA,EAAA,QAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,CAAA,SAAA,EAAA,IAAA,EAAA,CAAA,SAAS,kEAEd,SAAS,EAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA,EAAA,CAAA,EAAA,QAAA,EAAA,CAAA;sBAiBhE,SAAS;AAAC,gBAAA,IAAA,EAAA,CAAA,UAAU,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE;;sBAmQ3C,YAAY;uBAAC,eAAe;;;AElhB/B;;AAEG;;;;"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@styloviz/search-bar",
3
+ "version": "0.1.1",
4
+ "description": "Search input with live suggestions, keyboard navigation and a clearable field.",
5
+ "license": "MIT",
6
+ "author": "sazzad-bs23",
7
+ "homepage": "https://styloviz.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/rhossain/angular-tailwind-dashboard-components.git",
11
+ "directory": "projects/search-bar-free"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/rhossain/angular-tailwind-dashboard-components/issues"
15
+ },
16
+ "peerDependencies": {
17
+ "@angular/common": ">=21.0.0",
18
+ "@angular/core": ">=21.0.0",
19
+ "@angular/forms": ">=21.0.0"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "engines": {
25
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
26
+ },
27
+ "sideEffects": false,
28
+ "module": "fesm2022/styloviz-search-bar.mjs",
29
+ "typings": "types/styloviz-search-bar.d.ts",
30
+ "exports": {
31
+ "./package.json": {
32
+ "default": "./package.json"
33
+ },
34
+ ".": {
35
+ "types": "./types/styloviz-search-bar.d.ts",
36
+ "default": "./fesm2022/styloviz-search-bar.mjs"
37
+ }
38
+ },
39
+ "type": "module",
40
+ "dependencies": {
41
+ "tslib": "^2.3.0"
42
+ }
43
+ }
@@ -0,0 +1,218 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { OnInit, OnDestroy, ElementRef } from '@angular/core';
3
+
4
+ type SearchBarSize = 'sm' | 'md' | 'lg';
5
+ type SearchBarVariant = 'default' | 'filled' | 'borderless';
6
+ /**
7
+ * A single suggestion item displayed in the dropdown.
8
+ */
9
+ interface SearchSuggestion {
10
+ /** Unique identifier — emitted on selection. */
11
+ id: string;
12
+ /** Primary display text. */
13
+ label: string;
14
+ /** Optional secondary line (e.g. category, path, role). */
15
+ sublabel?: string;
16
+ /**
17
+ * Optional icon type for the suggestion row.
18
+ * - `'search'` — magnifier (recent / query suggestion)
19
+ * - `'page'` — document icon
20
+ * - `'user'` — person icon
21
+ * - `'tag'` — tag icon
22
+ * - `'custom'` — uses `iconPath`
23
+ */
24
+ iconType?: 'search' | 'page' | 'user' | 'tag' | 'custom';
25
+ /** SVG path `d` attribute — used when `iconType === 'custom'`. */
26
+ iconPath?: string;
27
+ /** Highlight / badge label (e.g. "New", "Popular"). */
28
+ badge?: string;
29
+ }
30
+ /**
31
+ * A filter chip rendered inside the search input.
32
+ */
33
+ interface SearchFilter {
34
+ id: string;
35
+ label: string;
36
+ }
37
+ /**
38
+ * A suggestion item pre-processed for efficient template rendering.
39
+ * Extends `SearchSuggestion` with a resolved icon path and pre-split
40
+ * highlight parts so the template never calls methods inside `@for` loops.
41
+ */
42
+ interface ComputedSuggestion extends SearchSuggestion {
43
+ /** Resolved SVG `d` path for the row icon. */
44
+ resolvedIconPath: string;
45
+ /** Pre-split highlight regions for the suggestion label. */
46
+ highlighted: {
47
+ pre: string;
48
+ match: string;
49
+ post: string;
50
+ };
51
+ }
52
+ /**
53
+ * SearchBar — A premium, fully-featured search input for dashboards.
54
+ *
55
+ * Features:
56
+ * - Signal-based two-way binding via `[(value)]`
57
+ * - Configurable debounce (default 300 ms)
58
+ * - Typeahead dropdown with keyboard navigation (↑ ↓ Enter Escape)
59
+ * - 4 icon types per suggestion row + custom SVG path support
60
+ * - Inline filter chip tokens (removable)
61
+ * - Loading / searching spinner state
62
+ * - Clear button
63
+ * - Keyboard shortcut hint badge (e.g. ⌘K)
64
+ * - 3 sizes × 3 visual variants
65
+ * - Full dark-mode support
66
+ * - Accessible: `role="combobox"`, `aria-expanded`, `aria-activedescendant`
67
+ */
68
+ declare class SvSearchBarComponent implements OnInit, OnDestroy {
69
+ private readonly platformId;
70
+ private readonly appRef;
71
+ /** Current input text. Use `[(value)]` for two-way binding. */
72
+ value: _angular_core.ModelSignal<string>;
73
+ /**
74
+ * Floating label text — only shown when `variant="floating"`.
75
+ * Acts as a Material Design-style animated label. @default ''
76
+ */
77
+ label: _angular_core.InputSignal<string>;
78
+ /** Placeholder text. @default 'Search…' */
79
+ placeholder: _angular_core.InputSignal<string>;
80
+ /** Explicit accessible name for the input. Falls back to `label` then `placeholder`. */
81
+ ariaLabel: _angular_core.InputSignal<string>;
82
+ /**
83
+ * Keyboard shortcut hint shown on the right side of the input when empty.
84
+ * E.g. `'⌘K'`, `'Ctrl+K'`, `'/''`. Empty string hides the badge.
85
+ * @default ''
86
+ */
87
+ shortcutHint: _angular_core.InputSignal<string>;
88
+ /**
89
+ * Suggestion items to display in the dropdown.
90
+ * Pass a filtered/sliced array; the component renders all provided items.
91
+ * Empty array closes the dropdown.
92
+ */
93
+ suggestions: _angular_core.InputSignal<SearchSuggestion[]>;
94
+ /**
95
+ * Label shown above the suggestions list (e.g. 'Recent searches', 'Results').
96
+ * Empty string hides the header. @default ''
97
+ */
98
+ suggestionsLabel: _angular_core.InputSignal<string>;
99
+ /**
100
+ * Text displayed at the bottom of the dropdown — useful for "View all results".
101
+ * Empty string hides the footer. @default ''
102
+ */
103
+ footerLabel: _angular_core.InputSignal<string>;
104
+ /**
105
+ * Active filter chips rendered inside the input track.
106
+ * Each chip has an × button that emits `filterRemoved`.
107
+ */
108
+ filters: _angular_core.InputSignal<SearchFilter[]>;
109
+ /** Field size: sm, md or lg. @default 'md' */
110
+ size: _angular_core.InputSignal<SearchBarSize>;
111
+ /** Visual variant: default, floating, minimal… @default 'default' */
112
+ variant: _angular_core.InputSignal<SearchBarVariant>;
113
+ /** Show the magnifier icon on the left. @default true */
114
+ showSearchIcon: _angular_core.InputSignal<boolean>;
115
+ /** Show the spinning loader inside the search icon area. @default false */
116
+ loading: _angular_core.InputSignal<boolean>;
117
+ /** Whether the input is disabled. @default false */
118
+ disabled: _angular_core.InputSignal<boolean>;
119
+ /**
120
+ * Debounce delay in milliseconds before `searched` emits.
121
+ * Set to 0 to emit on every keystroke. @default 300
122
+ */
123
+ debounceMs: _angular_core.InputSignal<number>;
124
+ /** Additional classes on the root wrapper. */
125
+ customClass: _angular_core.InputSignal<string>;
126
+ /** Emitted (debounced) on every value change. */
127
+ searched: _angular_core.OutputEmitterRef<string>;
128
+ /** Emitted when the user presses Enter or clicks a suggestion. */
129
+ submitted: _angular_core.OutputEmitterRef<string>;
130
+ /** Emitted when a suggestion row is clicked, carrying the suggestion object. */
131
+ suggestionSelected: _angular_core.OutputEmitterRef<SearchSuggestion>;
132
+ /** Emitted when the × on a filter chip is clicked. */
133
+ filterRemoved: _angular_core.OutputEmitterRef<string>;
134
+ /** Emitted when the footer link is clicked. */
135
+ footerClick: _angular_core.OutputEmitterRef<void>;
136
+ /** Emitted when the input is cleared via the × button. */
137
+ cleared: _angular_core.OutputEmitterRef<void>;
138
+ readonly isFocused: _angular_core.WritableSignal<boolean>;
139
+ readonly activeIndex: _angular_core.WritableSignal<number>;
140
+ /** Per-instance id base so multiple search bars don't collide. */
141
+ private readonly _baseId;
142
+ /** id of the suggestions listbox (referenced by aria-controls). */
143
+ readonly listboxId: string;
144
+ /** DOM id for an option row at index `i` (used by aria-activedescendant). */
145
+ optionId(i: number): string;
146
+ /** id of the active option, or null when nothing is highlighted. */
147
+ readonly activeDescendantId: _angular_core.Signal<string | null>;
148
+ /** Accessible name: explicit ariaLabel, then floating label, then placeholder. */
149
+ readonly accessibleName: _angular_core.Signal<string>;
150
+ private debounceTimer;
151
+ /** Template reference to the native <input> element. */
152
+ readonly inputRef: _angular_core.Signal<ElementRef<HTMLInputElement> | undefined>;
153
+ /** Template reference to the track container — used for dropdown positioning. */
154
+ readonly trackRef: _angular_core.Signal<ElementRef<HTMLElement> | undefined>;
155
+ /** True once the panel has flipped to open above the track (space below is tight). */
156
+ readonly openAbove: _angular_core.WritableSignal<boolean>;
157
+ /** Suppresses the dropdown after a viewport resize until focus/typing resumes. */
158
+ readonly resizeSuppressed: _angular_core.WritableSignal<boolean>;
159
+ /** Inline style object applied to the portaled panel (position:fixed + viewport coords). */
160
+ readonly panelStyle: _angular_core.WritableSignal<Record<string, string>>;
161
+ private portalEl;
162
+ private portalView;
163
+ /** The ng-template holding the suggestions panel — never rendered inline; instantiated into <body> on open. */
164
+ private readonly panelTpl;
165
+ /** Capture-phase scroll listener — fires for ANY scrollable ancestor, not just the window. */
166
+ private _rafId;
167
+ private readonly _scrollHandler;
168
+ /**
169
+ * Suggestions pre-processed once per change-detection cycle.
170
+ * Resolves each item's icon path and highlight split so the template
171
+ * can bind to plain properties instead of calling methods inside `@for`.
172
+ */
173
+ computedSuggestions: _angular_core.Signal<ComputedSuggestion[]>;
174
+ showDropdown: _angular_core.Signal<boolean>;
175
+ showClear: _angular_core.Signal<boolean>;
176
+ isFloating: _angular_core.Signal<boolean>;
177
+ labelIsFloated: _angular_core.Signal<boolean>;
178
+ floatingLabelClasses: _angular_core.Signal<string>;
179
+ showShortcut: _angular_core.Signal<boolean>;
180
+ wrapperClasses: _angular_core.Signal<string>;
181
+ trackClasses: _angular_core.Signal<string>;
182
+ inputPaddingClass: _angular_core.Signal<string>;
183
+ /**
184
+ * When a search icon occupies the left side the input needs no left padding —
185
+ * the icon span + gap-1.5 already create visual spacing.
186
+ * Without an icon, use symmetric px-X. Applies to all variants.
187
+ */
188
+ inputNativePadClass: _angular_core.Signal<string>;
189
+ iconSizeClass: _angular_core.Signal<string>;
190
+ iconPaddingClass: _angular_core.Signal<string>;
191
+ getSuggestionIconPath(s: SearchSuggestion): string;
192
+ /** Highlight matched portion of a suggestion label. */
193
+ highlightMatch(text: string): {
194
+ pre: string;
195
+ match: string;
196
+ post: string;
197
+ };
198
+ onInput(raw: string): void;
199
+ onFocus(): void;
200
+ onBlur(): void;
201
+ onKeydown(event: KeyboardEvent): void;
202
+ selectSuggestion(s: SearchSuggestion): void;
203
+ clearValue(): void;
204
+ onFooterClick(): void;
205
+ focus(): void;
206
+ onWindowResize(): void;
207
+ private _calcPos;
208
+ constructor();
209
+ ngOnInit(): void;
210
+ ngOnDestroy(): void;
211
+ private _openPanel;
212
+ private _closePanel;
213
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvSearchBarComponent, never>;
214
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SvSearchBarComponent, "sv-search-bar", never, { "value": { "alias": "value"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "shortcutHint": { "alias": "shortcutHint"; "required": false; "isSignal": true; }; "suggestions": { "alias": "suggestions"; "required": false; "isSignal": true; }; "suggestionsLabel": { "alias": "suggestionsLabel"; "required": false; "isSignal": true; }; "footerLabel": { "alias": "footerLabel"; "required": false; "isSignal": true; }; "filters": { "alias": "filters"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "showSearchIcon": { "alias": "showSearchIcon"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "debounceMs": { "alias": "debounceMs"; "required": false; "isSignal": true; }; "customClass": { "alias": "customClass"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "searched": "searched"; "submitted": "submitted"; "suggestionSelected": "suggestionSelected"; "filterRemoved": "filterRemoved"; "footerClick": "footerClick"; "cleared": "cleared"; }, never, never, true, never>;
215
+ }
216
+
217
+ export { SvSearchBarComponent };
218
+ export type { SearchBarSize, SearchBarVariant, SearchFilter, SearchSuggestion };