@melodicdev/components 1.1.0 → 1.2.0

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.
@@ -977,7 +977,7 @@ var V = class {
977
977
  };
978
978
  this._resolversExecutedForPath = m;
979
979
  }
980
- if (a ? history.replaceState(l, "", m) : history.pushState(l, "", m), this._currentPath = m, c) {
980
+ if (this.setCurrentMatches(u), a ? history.replaceState(l, "", m) : history.pushState(l, "", m), this._currentPath = m, c) {
981
981
  const h = m.includes("#") ? m.split("#")[1] : null;
982
982
  if (h) {
983
983
  const p = document.getElementById(h);
@@ -1083,8 +1083,10 @@ var V = class {
1083
1083
  return;
1084
1084
  }
1085
1085
  this._currentPath = r;
1086
- const a = new CustomEvent("NavigationEvent", { detail: De("push", t.state, "", window.location.pathname) });
1087
- window.dispatchEvent(a);
1086
+ const a = this.matchPath(window.location.pathname);
1087
+ this.setCurrentMatches(a);
1088
+ const o = new CustomEvent("NavigationEvent", { detail: De("push", t.state, "", window.location.pathname) });
1089
+ window.dispatchEvent(o);
1088
1090
  }
1089
1091
  async executeResolver(t, r) {
1090
1092
  const l = t.resolve(r);
@@ -0,0 +1,127 @@
1
+ import type { IElementRef, OnCreate, OnDestroy } from '@melodicdev/core';
2
+ import type { Size } from '../../../types/index.js';
3
+ import type { AutocompleteOption, AutocompleteSearchFn } from './autocomplete.types.js';
4
+ /**
5
+ * ml-autocomplete - Typeahead/autocomplete form component
6
+ *
7
+ * Provides a text input with dropdown suggestions. Supports static option lists
8
+ * or async search via a promise-returning function.
9
+ *
10
+ * @example
11
+ * ```html
12
+ * <ml-autocomplete
13
+ * label="Search users"
14
+ * placeholder="Type to search..."
15
+ * .options=${userOptions}
16
+ * ></ml-autocomplete>
17
+ *
18
+ * <ml-autocomplete
19
+ * label="Search"
20
+ * .searchFn=${async (query) => fetch(`/api/search?q=${query}`).then(r => r.json())}
21
+ * .debounce=${300}
22
+ * ></ml-autocomplete>
23
+ * ```
24
+ *
25
+ * @fires ml:change - Emitted when selection changes
26
+ * @fires ml:search - Emitted when search query changes
27
+ * @fires ml:open - Emitted when dropdown opens
28
+ * @fires ml:close - Emitted when dropdown closes
29
+ */
30
+ export declare class AutocompleteComponent implements IElementRef, OnCreate, OnDestroy {
31
+ elementRef: HTMLElement;
32
+ /** Label text */
33
+ label: string;
34
+ /** Placeholder text */
35
+ placeholder: string;
36
+ /** Hint text shown below input */
37
+ hint: string;
38
+ /** Error message (shows error state when set) */
39
+ error: string;
40
+ /** Component size */
41
+ size: Size;
42
+ /** Disable the component */
43
+ disabled: boolean;
44
+ /** Mark as required */
45
+ required: boolean;
46
+ /** Enable multi-select mode */
47
+ multiple: boolean;
48
+ /** Currently selected value (single mode) */
49
+ value: string;
50
+ /** Currently selected values (multi mode) */
51
+ values: string[];
52
+ /** Static options list */
53
+ options: AutocompleteOption[];
54
+ /** Async search function */
55
+ searchFn: AutocompleteSearchFn | null;
56
+ /** Debounce ms for async search */
57
+ debounce: number;
58
+ /** Min chars before searching (0 = show on focus) */
59
+ minChars: number;
60
+ /** Show search icon */
61
+ showIcon: boolean;
62
+ /** Current search query */
63
+ search: string;
64
+ /** Whether dropdown is open */
65
+ isOpen: boolean;
66
+ /** Currently focused option index */
67
+ focusedIndex: number;
68
+ /** Loading state for async search */
69
+ _loading: boolean;
70
+ /** Resolved async results */
71
+ _asyncOptions: AutocompleteOption[];
72
+ private readonly _handleKeyDown;
73
+ private readonly _handleDocumentClick;
74
+ private _handleScroll;
75
+ private _debounceTimer;
76
+ private _syncingValues;
77
+ onCreate(): void;
78
+ onDestroy(): void;
79
+ onPropertyChange(name: string): void;
80
+ /** Get the selected option (single mode) */
81
+ get selectedOption(): AutocompleteOption | undefined;
82
+ /** Get selected options (multi mode) */
83
+ get selectedOptions(): AutocompleteOption[];
84
+ /** All available options (static or async) */
85
+ get allOptions(): AutocompleteOption[];
86
+ /** Filtered options for display */
87
+ get filteredOptions(): AutocompleteOption[];
88
+ get hasValue(): boolean;
89
+ /** Display text for the input in single mode */
90
+ get displayText(): string;
91
+ /** Open the dropdown */
92
+ open: () => void;
93
+ /** Close the dropdown */
94
+ close: () => void;
95
+ /** Select an option */
96
+ selectOption: (option: AutocompleteOption) => void;
97
+ /** Handle click on option */
98
+ handleOptionClick: (event: Event, option: AutocompleteOption) => void;
99
+ /** Handle input changes */
100
+ handleInput: (event: Event) => void;
101
+ /** Handle input focus */
102
+ handleFocus: () => void;
103
+ /** Handle input click */
104
+ handleInputClick: (event: Event) => void;
105
+ /** Handle tag remove in multi mode */
106
+ handleTagRemove: (event: Event, value: string) => void;
107
+ /** Clear the current value (single mode) */
108
+ handleClear: (event: Event) => void;
109
+ private toggleOption;
110
+ private debouncedSearch;
111
+ private executeSearch;
112
+ /** Close dropdown on clicks outside the component */
113
+ private onDocumentClick;
114
+ private addDocumentClickListener;
115
+ private removeDocumentClickListener;
116
+ private positionDropdown;
117
+ private addScrollListener;
118
+ private removeScrollListener;
119
+ private getDropdownEl;
120
+ private onKeyDown;
121
+ private focusNextOption;
122
+ private focusPreviousOption;
123
+ private findFirstEnabledIndex;
124
+ private updateValues;
125
+ private areValuesEqual;
126
+ }
127
+ //# sourceMappingURL=autocomplete.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autocomplete.component.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/autocomplete/autocomplete.component.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,491 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { MelodicComponent } from '@melodicdev/core';
8
+ import { computePosition, offset, flip, shift } from '../../../utils/positioning/index.js';
9
+ import { autocompleteTemplate } from './autocomplete.template.js';
10
+ import { autocompleteStyles } from './autocomplete.styles.js';
11
+ /**
12
+ * ml-autocomplete - Typeahead/autocomplete form component
13
+ *
14
+ * Provides a text input with dropdown suggestions. Supports static option lists
15
+ * or async search via a promise-returning function.
16
+ *
17
+ * @example
18
+ * ```html
19
+ * <ml-autocomplete
20
+ * label="Search users"
21
+ * placeholder="Type to search..."
22
+ * .options=${userOptions}
23
+ * ></ml-autocomplete>
24
+ *
25
+ * <ml-autocomplete
26
+ * label="Search"
27
+ * .searchFn=${async (query) => fetch(`/api/search?q=${query}`).then(r => r.json())}
28
+ * .debounce=${300}
29
+ * ></ml-autocomplete>
30
+ * ```
31
+ *
32
+ * @fires ml:change - Emitted when selection changes
33
+ * @fires ml:search - Emitted when search query changes
34
+ * @fires ml:open - Emitted when dropdown opens
35
+ * @fires ml:close - Emitted when dropdown closes
36
+ */
37
+ let AutocompleteComponent = class AutocompleteComponent {
38
+ constructor() {
39
+ /** Label text */
40
+ this.label = '';
41
+ /** Placeholder text */
42
+ this.placeholder = 'Search';
43
+ /** Hint text shown below input */
44
+ this.hint = '';
45
+ /** Error message (shows error state when set) */
46
+ this.error = '';
47
+ /** Component size */
48
+ this.size = 'md';
49
+ /** Disable the component */
50
+ this.disabled = false;
51
+ /** Mark as required */
52
+ this.required = false;
53
+ /** Enable multi-select mode */
54
+ this.multiple = false;
55
+ /** Currently selected value (single mode) */
56
+ this.value = '';
57
+ /** Currently selected values (multi mode) */
58
+ this.values = [];
59
+ /** Static options list */
60
+ this.options = [];
61
+ /** Async search function */
62
+ this.searchFn = null;
63
+ /** Debounce ms for async search */
64
+ this.debounce = 300;
65
+ /** Min chars before searching (0 = show on focus) */
66
+ this.minChars = 0;
67
+ /** Show search icon */
68
+ this.showIcon = true;
69
+ /** Current search query */
70
+ this.search = '';
71
+ /** Whether dropdown is open */
72
+ this.isOpen = false;
73
+ /** Currently focused option index */
74
+ this.focusedIndex = -1;
75
+ /** Loading state for async search */
76
+ this._loading = false;
77
+ /** Resolved async results */
78
+ this._asyncOptions = [];
79
+ this._handleKeyDown = this.onKeyDown.bind(this);
80
+ this._handleDocumentClick = this.onDocumentClick.bind(this);
81
+ this._handleScroll = null;
82
+ this._debounceTimer = null;
83
+ this._syncingValues = false;
84
+ /** Open the dropdown */
85
+ this.open = () => {
86
+ if (this.disabled || this.isOpen)
87
+ return;
88
+ if (this.minChars > 0 && this.search.length < this.minChars)
89
+ return;
90
+ const dropdownEl = this.getDropdownEl();
91
+ if (!dropdownEl)
92
+ return;
93
+ dropdownEl.showPopover();
94
+ this.isOpen = true;
95
+ this.focusedIndex = this.findFirstEnabledIndex();
96
+ this.positionDropdown();
97
+ this.addScrollListener();
98
+ this.addDocumentClickListener();
99
+ this.elementRef.dispatchEvent(new CustomEvent('ml:open', { bubbles: true, composed: true }));
100
+ };
101
+ /** Close the dropdown */
102
+ this.close = () => {
103
+ if (!this.isOpen)
104
+ return;
105
+ this.getDropdownEl()?.hidePopover();
106
+ this.isOpen = false;
107
+ this.focusedIndex = -1;
108
+ this.removeScrollListener();
109
+ this.removeDocumentClickListener();
110
+ this.elementRef.dispatchEvent(new CustomEvent('ml:close', { bubbles: true, composed: true }));
111
+ };
112
+ /** Select an option */
113
+ this.selectOption = (option) => {
114
+ if (option.disabled)
115
+ return;
116
+ if (this.multiple) {
117
+ this.toggleOption(option);
118
+ return;
119
+ }
120
+ this.value = option.value;
121
+ this.search = '';
122
+ this.close();
123
+ this.elementRef.dispatchEvent(new CustomEvent('ml:change', {
124
+ bubbles: true,
125
+ composed: true,
126
+ detail: { value: this.value, option }
127
+ }));
128
+ };
129
+ /** Handle click on option */
130
+ this.handleOptionClick = (event, option) => {
131
+ event.stopPropagation();
132
+ this.selectOption(option);
133
+ };
134
+ /** Handle input changes */
135
+ this.handleInput = (event) => {
136
+ if (this.disabled)
137
+ return;
138
+ const target = event.target;
139
+ this.search = target.value;
140
+ // In single mode, clear the current value when the user types
141
+ if (!this.multiple && this.value) {
142
+ this.value = '';
143
+ this.elementRef.dispatchEvent(new CustomEvent('ml:change', {
144
+ bubbles: true,
145
+ composed: true,
146
+ detail: { value: '', option: null }
147
+ }));
148
+ }
149
+ this.elementRef.dispatchEvent(new CustomEvent('ml:search', {
150
+ bubbles: true,
151
+ composed: true,
152
+ detail: { query: this.search }
153
+ }));
154
+ if (this.minChars > 0 && this.search.length < this.minChars) {
155
+ this.close();
156
+ return;
157
+ }
158
+ this.focusedIndex = this.findFirstEnabledIndex();
159
+ if (!this.isOpen) {
160
+ this.open();
161
+ }
162
+ if (this.searchFn) {
163
+ this.debouncedSearch(this.search);
164
+ }
165
+ };
166
+ /** Handle input focus */
167
+ this.handleFocus = () => {
168
+ if (this.disabled)
169
+ return;
170
+ if (this.minChars === 0) {
171
+ if (this.searchFn && this._asyncOptions.length === 0) {
172
+ this.debouncedSearch('');
173
+ }
174
+ if (!this.isOpen) {
175
+ this.open();
176
+ }
177
+ }
178
+ };
179
+ /** Handle input click */
180
+ this.handleInputClick = (event) => {
181
+ event.stopPropagation();
182
+ if (!this.isOpen && this.minChars === 0) {
183
+ if (this.searchFn && this._asyncOptions.length === 0) {
184
+ this.debouncedSearch('');
185
+ }
186
+ this.open();
187
+ }
188
+ };
189
+ /** Handle tag remove in multi mode */
190
+ this.handleTagRemove = (event, value) => {
191
+ event.stopPropagation();
192
+ if (this.disabled)
193
+ return;
194
+ this.values = this.values.filter((item) => item !== value);
195
+ this.elementRef.dispatchEvent(new CustomEvent('ml:change', {
196
+ bubbles: true,
197
+ composed: true,
198
+ detail: { values: [...this.values], options: this.selectedOptions }
199
+ }));
200
+ };
201
+ /** Clear the current value (single mode) */
202
+ this.handleClear = (event) => {
203
+ event.stopPropagation();
204
+ if (this.disabled)
205
+ return;
206
+ this.value = '';
207
+ this.search = '';
208
+ this.elementRef.dispatchEvent(new CustomEvent('ml:change', {
209
+ bubbles: true,
210
+ composed: true,
211
+ detail: { value: '', option: null }
212
+ }));
213
+ // Focus the input after clearing
214
+ const input = this.elementRef.shadowRoot?.querySelector('.ml-autocomplete__input');
215
+ input?.focus();
216
+ };
217
+ }
218
+ onCreate() {
219
+ this.elementRef.addEventListener('keydown', this._handleKeyDown);
220
+ }
221
+ onDestroy() {
222
+ this.elementRef.removeEventListener('keydown', this._handleKeyDown);
223
+ this.removeScrollListener();
224
+ this.removeDocumentClickListener();
225
+ if (this._debounceTimer) {
226
+ clearTimeout(this._debounceTimer);
227
+ }
228
+ }
229
+ onPropertyChange(name) {
230
+ if (this._syncingValues)
231
+ return;
232
+ if (name === 'multiple') {
233
+ if (this.multiple) {
234
+ if (!this.values.length && this.value) {
235
+ this.updateValues([this.value]);
236
+ }
237
+ return;
238
+ }
239
+ if (this.values.length) {
240
+ this.value = this.values[0] ?? '';
241
+ }
242
+ this.updateValues([]);
243
+ this.search = '';
244
+ return;
245
+ }
246
+ if (name === 'values') {
247
+ const rawValues = this.values;
248
+ let normalized = [];
249
+ if (typeof rawValues === 'string') {
250
+ normalized = rawValues
251
+ .split(',')
252
+ .map((v) => v.trim())
253
+ .filter((v) => v.length > 0);
254
+ }
255
+ else if (Array.isArray(rawValues)) {
256
+ normalized = rawValues.filter((v) => typeof v === 'string');
257
+ }
258
+ normalized = Array.from(new Set(normalized));
259
+ if (!this.areValuesEqual(this.values, normalized)) {
260
+ this.updateValues(normalized);
261
+ }
262
+ if (!this.multiple) {
263
+ this.value = normalized[0] ?? '';
264
+ this.updateValues([]);
265
+ }
266
+ return;
267
+ }
268
+ if (name === 'value' && this.multiple) {
269
+ if (this.value) {
270
+ const nextValues = Array.from(new Set([...this.values, this.value]));
271
+ if (!this.areValuesEqual(this.values, nextValues)) {
272
+ this.updateValues(nextValues);
273
+ }
274
+ }
275
+ }
276
+ }
277
+ /** Get the selected option (single mode) */
278
+ get selectedOption() {
279
+ return this.allOptions.find((opt) => opt.value === this.value);
280
+ }
281
+ /** Get selected options (multi mode) */
282
+ get selectedOptions() {
283
+ if (!this.multiple) {
284
+ return this.selectedOption ? [this.selectedOption] : [];
285
+ }
286
+ return this.allOptions.filter((opt) => this.values.includes(opt.value));
287
+ }
288
+ /** All available options (static or async) */
289
+ get allOptions() {
290
+ return this.searchFn ? this._asyncOptions : this.options;
291
+ }
292
+ /** Filtered options for display */
293
+ get filteredOptions() {
294
+ if (this.searchFn) {
295
+ return this._asyncOptions;
296
+ }
297
+ const query = this.search.trim().toLowerCase();
298
+ if (!query)
299
+ return this.options;
300
+ return this.options.filter((option) => {
301
+ const labelMatch = option.label.toLowerCase().includes(query);
302
+ const valueMatch = option.value.toLowerCase().includes(query);
303
+ const subtitleMatch = option.subtitle?.toLowerCase().includes(query) ?? false;
304
+ return labelMatch || valueMatch || subtitleMatch;
305
+ });
306
+ }
307
+ get hasValue() {
308
+ return this.multiple ? this.values.length > 0 : !!this.value;
309
+ }
310
+ /** Display text for the input in single mode */
311
+ get displayText() {
312
+ if (this.multiple)
313
+ return '';
314
+ return this.selectedOption?.label || '';
315
+ }
316
+ toggleOption(option) {
317
+ const exists = this.values.includes(option.value);
318
+ this.values = exists ? this.values.filter((v) => v !== option.value) : [...this.values, option.value];
319
+ this.elementRef.dispatchEvent(new CustomEvent('ml:change', {
320
+ bubbles: true,
321
+ composed: true,
322
+ detail: { values: [...this.values], options: this.selectedOptions, option }
323
+ }));
324
+ // Clear search and refocus input
325
+ this.search = '';
326
+ const input = this.elementRef.shadowRoot?.querySelector('.ml-autocomplete__input');
327
+ input?.focus();
328
+ }
329
+ debouncedSearch(query) {
330
+ if (this._debounceTimer) {
331
+ clearTimeout(this._debounceTimer);
332
+ }
333
+ this._debounceTimer = setTimeout(() => {
334
+ this.executeSearch(query);
335
+ }, this.debounce);
336
+ }
337
+ async executeSearch(query) {
338
+ if (!this.searchFn)
339
+ return;
340
+ this._loading = true;
341
+ try {
342
+ this._asyncOptions = await this.searchFn(query);
343
+ this.focusedIndex = this.findFirstEnabledIndex();
344
+ }
345
+ finally {
346
+ this._loading = false;
347
+ }
348
+ }
349
+ /** Close dropdown on clicks outside the component */
350
+ onDocumentClick(event) {
351
+ const path = event.composedPath();
352
+ if (!path.includes(this.elementRef)) {
353
+ this.close();
354
+ }
355
+ }
356
+ addDocumentClickListener() {
357
+ document.addEventListener('click', this._handleDocumentClick, true);
358
+ }
359
+ removeDocumentClickListener() {
360
+ document.removeEventListener('click', this._handleDocumentClick, true);
361
+ }
362
+ positionDropdown() {
363
+ const triggerEl = this.elementRef.shadowRoot?.querySelector('.ml-autocomplete__trigger');
364
+ const dropdownEl = this.getDropdownEl();
365
+ if (!triggerEl || !dropdownEl)
366
+ return;
367
+ dropdownEl.style.width = `${triggerEl.offsetWidth}px`;
368
+ const { x, y } = computePosition(triggerEl, dropdownEl, {
369
+ placement: 'bottom-start',
370
+ middleware: [offset(4), flip(), shift({ padding: 8 })]
371
+ });
372
+ dropdownEl.style.left = `${x}px`;
373
+ dropdownEl.style.top = `${y}px`;
374
+ }
375
+ addScrollListener() {
376
+ this._handleScroll = (event) => {
377
+ const dropdownEl = this.getDropdownEl();
378
+ if (dropdownEl?.contains(event.target))
379
+ return;
380
+ this.close();
381
+ };
382
+ window.addEventListener('scroll', this._handleScroll, true);
383
+ }
384
+ removeScrollListener() {
385
+ if (this._handleScroll) {
386
+ window.removeEventListener('scroll', this._handleScroll, true);
387
+ this._handleScroll = null;
388
+ }
389
+ }
390
+ getDropdownEl() {
391
+ return this.elementRef.shadowRoot?.querySelector('.ml-autocomplete__dropdown');
392
+ }
393
+ onKeyDown(event) {
394
+ if (this.disabled)
395
+ return;
396
+ switch (event.key) {
397
+ case 'Enter':
398
+ if (this.isOpen && this.focusedIndex >= 0) {
399
+ event.preventDefault();
400
+ const option = this.filteredOptions[this.focusedIndex];
401
+ if (option && !option.disabled) {
402
+ this.selectOption(option);
403
+ }
404
+ }
405
+ break;
406
+ case 'Escape':
407
+ event.preventDefault();
408
+ this.close();
409
+ break;
410
+ case 'ArrowDown':
411
+ event.preventDefault();
412
+ if (!this.isOpen) {
413
+ this.open();
414
+ }
415
+ else {
416
+ this.focusNextOption();
417
+ }
418
+ break;
419
+ case 'ArrowUp':
420
+ event.preventDefault();
421
+ if (this.isOpen) {
422
+ this.focusPreviousOption();
423
+ }
424
+ break;
425
+ case 'Backspace':
426
+ if (this.multiple && this.search === '' && this.values.length > 0) {
427
+ const lastValue = this.values[this.values.length - 1];
428
+ this.values = this.values.slice(0, -1);
429
+ this.elementRef.dispatchEvent(new CustomEvent('ml:change', {
430
+ bubbles: true,
431
+ composed: true,
432
+ detail: { values: [...this.values], options: this.selectedOptions, removedValue: lastValue }
433
+ }));
434
+ }
435
+ break;
436
+ case 'Tab':
437
+ this.close();
438
+ break;
439
+ default:
440
+ break;
441
+ }
442
+ }
443
+ focusNextOption() {
444
+ const options = this.filteredOptions;
445
+ let index = this.focusedIndex + 1;
446
+ while (index < options.length) {
447
+ if (!options[index].disabled) {
448
+ this.focusedIndex = index;
449
+ return;
450
+ }
451
+ index++;
452
+ }
453
+ }
454
+ focusPreviousOption() {
455
+ const options = this.filteredOptions;
456
+ let index = this.focusedIndex - 1;
457
+ while (index >= 0) {
458
+ if (!options[index].disabled) {
459
+ this.focusedIndex = index;
460
+ return;
461
+ }
462
+ index--;
463
+ }
464
+ }
465
+ findFirstEnabledIndex() {
466
+ return this.filteredOptions.findIndex((opt) => !opt.disabled);
467
+ }
468
+ updateValues(values) {
469
+ this._syncingValues = true;
470
+ this.values = values;
471
+ this._syncingValues = false;
472
+ }
473
+ areValuesEqual(left, right) {
474
+ if (left.length !== right.length)
475
+ return false;
476
+ for (let i = 0; i < left.length; i++) {
477
+ if (left[i] !== right[i])
478
+ return false;
479
+ }
480
+ return true;
481
+ }
482
+ };
483
+ AutocompleteComponent = __decorate([
484
+ MelodicComponent({
485
+ selector: 'ml-autocomplete',
486
+ template: autocompleteTemplate,
487
+ styles: autocompleteStyles,
488
+ attributes: ['label', 'placeholder', 'hint', 'error', 'size', 'disabled', 'required', 'value', 'multiple']
489
+ })
490
+ ], AutocompleteComponent);
491
+ export { AutocompleteComponent };
@@ -0,0 +1,2 @@
1
+ export declare const autocompleteStyles: () => import("@melodicdev/core").TemplateResult;
2
+ //# sourceMappingURL=autocomplete.styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autocomplete.styles.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/autocomplete/autocomplete.styles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,kBAAkB,iDAgW9B,CAAC"}