@visitwonders/assembly 0.10.0 → 0.11.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.
Files changed (46) hide show
  1. package/README.md +83 -0
  2. package/declarations/form/combobox-field.d.ts +71 -0
  3. package/declarations/form/combobox-field.d.ts.map +1 -0
  4. package/declarations/form/combobox-shared.d.ts +36 -0
  5. package/declarations/form/combobox-shared.d.ts.map +1 -0
  6. package/declarations/form/combobox.d.ts +239 -0
  7. package/declarations/form/combobox.d.ts.map +1 -0
  8. package/declarations/form/index.d.ts +4 -0
  9. package/declarations/form/index.d.ts.map +1 -1
  10. package/declarations/form/multi-combobox-field.d.ts +72 -0
  11. package/declarations/form/multi-combobox-field.d.ts.map +1 -0
  12. package/declarations/form/multi-combobox.d.ts +202 -0
  13. package/declarations/form/multi-combobox.d.ts.map +1 -0
  14. package/declarations/layout/h-stack.d.ts.map +1 -1
  15. package/declarations/layout/stack.d.ts.map +1 -1
  16. package/declarations/layout/v-stack.d.ts.map +1 -1
  17. package/declarations/overlay/popover.d.ts +20 -1
  18. package/declarations/overlay/popover.d.ts.map +1 -1
  19. package/dist/_app_/form/combobox-field.js +1 -0
  20. package/dist/_app_/form/combobox-shared.js +1 -0
  21. package/dist/_app_/form/combobox.js +1 -0
  22. package/dist/_app_/form/multi-combobox-field.js +1 -0
  23. package/dist/_app_/form/multi-combobox.js +1 -0
  24. package/dist/data/{sortable-list-css-211fcfeedc08052ccbac7f51549ce0b1.css → sortable-list-css-03e5d237ea377f7d6056e76cc85b2aaa.css} +8 -4
  25. package/dist/data/sortable-list.js +1 -1
  26. package/dist/form/combobox-field.js +37 -0
  27. package/dist/form/combobox-field.js.map +1 -0
  28. package/dist/form/combobox-shared.js +76 -0
  29. package/dist/form/combobox-shared.js.map +1 -0
  30. package/dist/form/combobox.css +345 -0
  31. package/dist/form/combobox.js +612 -0
  32. package/dist/form/combobox.js.map +1 -0
  33. package/dist/form/{display-field-css-890d9be4b5da61613fd017071f330735.css → display-field-css-502236a2343d47e31e52bdb93a769ca1.css} +2 -2
  34. package/dist/form/display-field.js +1 -1
  35. package/dist/form/index.js +4 -0
  36. package/dist/form/index.js.map +1 -1
  37. package/dist/form/multi-combobox-field.js +36 -0
  38. package/dist/form/multi-combobox-field.js.map +1 -0
  39. package/dist/form/multi-combobox.css +422 -0
  40. package/dist/form/multi-combobox.js +626 -0
  41. package/dist/form/multi-combobox.js.map +1 -0
  42. package/dist/layout/h-stack.js.map +1 -1
  43. package/dist/layout/panel.css +10 -0
  44. package/dist/layout/v-stack.js.map +1 -1
  45. package/dist/overlay/popover.js +19 -1
  46. package/package.json +6 -1
@@ -0,0 +1,626 @@
1
+ import "./multi-combobox.css"
2
+ import Component from '@glimmer/component';
3
+ import { tracked, cached } from '@glimmer/tracking';
4
+ import { on } from '@ember/modifier';
5
+ import { fn } from '@ember/helper';
6
+ import { modifier } from 'ember-modifier';
7
+ import { uniqueId } from 'ember-primitives/utils';
8
+ import Popover from '../overlay/popover.js';
9
+ import Icon from '../media/icon.js';
10
+ import { ChevronDown, X } from 'lucide';
11
+ import { isOptionGroup, flattenOptions, splitOnMatch } from './combobox-shared.js';
12
+ import { precompileTemplate } from '@ember/template-compilation';
13
+ import { setComponentTemplate } from '@ember/component';
14
+ import { g, i, n } from 'decorator-transforms/runtime';
15
+
16
+ ;
17
+
18
+ /** Rows skipped per PageUp / PageDown keypress. */const PAGE_STEP = 10;
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ // ============================================================================
24
+ // Derived types
25
+ // ============================================================================
26
+
27
+ // ============================================================================
28
+ // Main Component
29
+ // ============================================================================
30
+ class MultiComboBox extends Component {
31
+ static {
32
+ g(this.prototype, "isOpen", [tracked], function () {
33
+ return false;
34
+ });
35
+ }
36
+ #isOpen = (i(this, "isOpen"), void 0);
37
+ static {
38
+ g(this.prototype, "query", [tracked], function () {
39
+ return '';
40
+ });
41
+ }
42
+ #query = (i(this, "query"), void 0);
43
+ static {
44
+ g(this.prototype, "activeId", [tracked], function () {
45
+ return null;
46
+ });
47
+ }
48
+ #activeId = (i(this, "activeId"), void 0);
49
+ static {
50
+ g(this.prototype, "remoteItems", [tracked], function () {
51
+ return null;
52
+ });
53
+ }
54
+ #remoteItems = (i(this, "remoteItems"), void 0);
55
+ static {
56
+ g(this.prototype, "isLoadingInternal", [tracked], function () {
57
+ return false;
58
+ });
59
+ }
60
+ #isLoadingInternal = (i(this, "isLoadingInternal"), void 0);
61
+ searchSeq = 0;
62
+ searchTimer = null;
63
+ inputElement = null;
64
+ listboxElement = null;
65
+ componentId = `multi-combobox-${uniqueId()}`;
66
+ listboxId = `${this.componentId}-listbox`;
67
+ createOptionId = `${this.componentId}-create`;
68
+ willDestroy() {
69
+ super.willDestroy();
70
+ if (this.searchTimer) clearTimeout(this.searchTimer);
71
+ }
72
+ // --------------------------------------------------------------------------
73
+ // Items + filtering
74
+ // --------------------------------------------------------------------------
75
+ get isAsync() {
76
+ return this.args.onSearch !== undefined;
77
+ }
78
+ get effectiveItems() {
79
+ if (this.isAsync && this.remoteItems !== null) {
80
+ return this.remoteItems;
81
+ }
82
+ return this.args.items ?? [];
83
+ }
84
+ get values() {
85
+ return this.args.values ?? [];
86
+ }
87
+ get valueSet() {
88
+ return new Set(this.values);
89
+ }
90
+ static {
91
+ n(this.prototype, "valueSet", [cached]);
92
+ }
93
+ get normalizedGroups() {
94
+ const items = this.effectiveItems;
95
+ if (items.length === 0) return [];
96
+ if (isOptionGroup(items[0])) {
97
+ return items.map((g, i) => ({
98
+ label: g.label,
99
+ headingId: `${this.componentId}-group-${i}`,
100
+ options: g.options
101
+ }));
102
+ }
103
+ return [{
104
+ label: null,
105
+ headingId: null,
106
+ options: items
107
+ }];
108
+ }
109
+ get renderedGroups() {
110
+ const trimmed = this.query.trim().toLowerCase();
111
+ const skipLocalFilter = this.isAsync || !trimmed;
112
+ const selected = this.valueSet;
113
+ const result = [];
114
+ for (const group of this.normalizedGroups) {
115
+ const matched = skipLocalFilter ? group.options : group.options.filter(option => {
116
+ const inLabel = option.label.toLowerCase().includes(trimmed);
117
+ const inDescription = option.description ? option.description.toLowerCase().includes(trimmed) : false;
118
+ return inLabel || inDescription;
119
+ });
120
+ if (matched.length === 0) continue;
121
+ const rows = matched.map(option => {
122
+ const optionId = `${this.componentId}-option-${option.value}`;
123
+ return {
124
+ kind: 'option',
125
+ option,
126
+ optionId,
127
+ isActive: this.activeId === optionId,
128
+ isSelected: selected.has(option.value)
129
+ };
130
+ });
131
+ result.push({
132
+ key: group.headingId ?? 'ungrouped',
133
+ label: group.label,
134
+ headingId: group.headingId,
135
+ rows
136
+ });
137
+ }
138
+ return result;
139
+ }
140
+ static {
141
+ n(this.prototype, "renderedGroups", [cached]);
142
+ }
143
+ get navigableRows() {
144
+ const rows = [];
145
+ for (const group of this.renderedGroups) {
146
+ for (const row of group.rows) {
147
+ if (!row.option.isDisabled) rows.push(row);
148
+ }
149
+ }
150
+ const create = this.createRow;
151
+ if (create) rows.push(create);
152
+ return rows;
153
+ }
154
+ static {
155
+ n(this.prototype, "navigableRows", [cached]);
156
+ }
157
+ get hasOptionResults() {
158
+ return this.renderedGroups.some(g => g.rows.length > 0);
159
+ }
160
+ // --------------------------------------------------------------------------
161
+ // Chips (from the selected values)
162
+ // --------------------------------------------------------------------------
163
+ get maxVisibleChips() {
164
+ return this.args.maxVisibleChips ?? 3;
165
+ }
166
+ get selectedOptions() {
167
+ // Resolve `values` to their option objects by looking across all
168
+ // items (incl. async payload) AND the current render. If an option
169
+ // isn't found in items, fabricate a minimal one so the chip still
170
+ // shows — common with async-backed data where items lag.
171
+ const flat = flattenOptions(this.effectiveItems);
172
+ const byValue = new Map(flat.map(o => [o.value, o]));
173
+ return this.values.map(v => byValue.get(v) ?? {
174
+ value: v,
175
+ label: v
176
+ });
177
+ }
178
+ get chips() {
179
+ return this.selectedOptions.map(option => ({
180
+ value: option.value,
181
+ label: option.label,
182
+ option
183
+ }));
184
+ }
185
+ get visibleChips() {
186
+ return this.chips.slice(0, this.maxVisibleChips);
187
+ }
188
+ get overflowCount() {
189
+ return Math.max(0, this.chips.length - this.maxVisibleChips);
190
+ }
191
+ get hasOverflow() {
192
+ return this.overflowCount > 0;
193
+ }
194
+ // --------------------------------------------------------------------------
195
+ // Create row
196
+ // --------------------------------------------------------------------------
197
+ get isCreateVisible() {
198
+ if (!this.args.isCreatable) return false;
199
+ const trimmed = this.query.trim();
200
+ if (!trimmed) return false;
201
+ const flat = flattenOptions(this.effectiveItems);
202
+ const lower = trimmed.toLowerCase();
203
+ return !flat.some(opt => opt.label.toLowerCase() === lower);
204
+ }
205
+ get createRow() {
206
+ if (!this.isCreateVisible) return null;
207
+ const query = this.query.trim();
208
+ const label = this.args.createLabel ? this.args.createLabel(query) : `Create "${query}"`;
209
+ return {
210
+ kind: 'create',
211
+ optionId: this.createOptionId,
212
+ isActive: this.activeId === this.createOptionId,
213
+ query,
214
+ label
215
+ };
216
+ }
217
+ // --------------------------------------------------------------------------
218
+ // Async search
219
+ // --------------------------------------------------------------------------
220
+ get isLoading() {
221
+ if (this.args.isLoading !== undefined) return this.args.isLoading;
222
+ return this.isLoadingInternal;
223
+ }
224
+ get loadingText() {
225
+ return this.args.loadingText ?? 'Loading…';
226
+ }
227
+ get searchDebounceMs() {
228
+ return this.args.searchDebounceMs ?? 200;
229
+ }
230
+ scheduleAsyncSearch(query) {
231
+ if (!this.args.onSearch) return;
232
+ if (this.searchTimer) clearTimeout(this.searchTimer);
233
+ this.isLoadingInternal = true;
234
+ this.searchTimer = setTimeout(() => {
235
+ this.searchTimer = null;
236
+ void this.runAsyncSearch(query);
237
+ }, this.searchDebounceMs);
238
+ }
239
+ async runAsyncSearch(query) {
240
+ if (!this.args.onSearch) return;
241
+ const seq = ++this.searchSeq;
242
+ try {
243
+ const result = await this.args.onSearch(query);
244
+ if (seq !== this.searchSeq) return;
245
+ this.remoteItems = result;
246
+ } catch (error) {
247
+ // Only surface the error for the latest request; stale errors
248
+ // are as uninteresting as stale results.
249
+ if (seq === this.searchSeq) {
250
+ this.args.onSearchError?.(error);
251
+ }
252
+ } finally {
253
+ if (seq === this.searchSeq) {
254
+ this.isLoadingInternal = false;
255
+ this.resetActiveToFirstNavigable();
256
+ }
257
+ }
258
+ }
259
+ // --------------------------------------------------------------------------
260
+ // Derived UI state
261
+ // --------------------------------------------------------------------------
262
+ get hasAnyValue() {
263
+ return this.values.length > 0;
264
+ }
265
+ get isClearable() {
266
+ return this.args.isClearable ?? true;
267
+ }
268
+ get showClearButton() {
269
+ return this.isClearable && this.hasAnyValue && !this.args.isDisabled;
270
+ }
271
+ get noResultsText() {
272
+ return this.args.noResultsText ?? 'No results';
273
+ }
274
+ /** Text for the aria-live region — see combobox.gts for rationale. */
275
+ get announcement() {
276
+ if (!this.isOpen) return '';
277
+ if (this.isLoading) return this.loadingText;
278
+ const optionCount = this.navigableRows.filter(r => r.kind === 'option').length;
279
+ if (optionCount === 0) {
280
+ if (this.isCreateVisible) return 'Create new option available';
281
+ return this.noResultsText;
282
+ }
283
+ return `${optionCount} ${optionCount === 1 ? 'option' : 'options'} available`;
284
+ }
285
+ get showEmptyState() {
286
+ if (this.isLoading) return false;
287
+ if (this.hasOptionResults) return false;
288
+ if (this.isCreateVisible) return false;
289
+ return true;
290
+ }
291
+ get ariaExpanded() {
292
+ return this.isOpen ? 'true' : 'false';
293
+ }
294
+ get ariaRequired() {
295
+ return this.args.isRequired ? 'true' : undefined;
296
+ }
297
+ get ariaInvalid() {
298
+ return this.args.isInvalid ? 'true' : undefined;
299
+ }
300
+ // --------------------------------------------------------------------------
301
+ // Element registration
302
+ // --------------------------------------------------------------------------
303
+ setupInput = modifier(element => {
304
+ this.inputElement = element;
305
+ return () => {
306
+ this.inputElement = null;
307
+ };
308
+ });
309
+ setupListbox = modifier(element => {
310
+ this.listboxElement = element;
311
+ return () => {
312
+ this.listboxElement = null;
313
+ };
314
+ });
315
+ // --------------------------------------------------------------------------
316
+ // Event handlers — input (the trigger)
317
+ // --------------------------------------------------------------------------
318
+ handleInputFocus = event => {
319
+ this.args.onFocus?.(event);
320
+ };
321
+ handleInputBlur = event => {
322
+ this.args.onBlur?.(event);
323
+ };
324
+ handleInputClick = () => {
325
+ if (this.args.isDisabled) return;
326
+ if (!this.isOpen) this.openDropdown();
327
+ };
328
+ handleInput = event => {
329
+ const target = event.target;
330
+ this.query = target.value;
331
+ if (!this.isOpen) this.openDropdown({
332
+ resetActive: false
333
+ });
334
+ if (this.isAsync) {
335
+ this.scheduleAsyncSearch(this.query);
336
+ } else {
337
+ this.resetActiveToFirstNavigable();
338
+ }
339
+ };
340
+ handleInputKeyDown = event => {
341
+ // Backspace on an empty input removes the last chip — available
342
+ // whether open or closed.
343
+ if (event.key === 'Backspace' && this.query === '' && this.values.length > 0) {
344
+ event.preventDefault();
345
+ this.removeLastChip();
346
+ return;
347
+ }
348
+ if (!this.isOpen) {
349
+ switch (event.key) {
350
+ case 'ArrowDown':
351
+ case 'ArrowUp':
352
+ event.preventDefault();
353
+ this.openDropdown();
354
+ return;
355
+ case 'Escape':
356
+ if (this.showClearButton) {
357
+ event.preventDefault();
358
+ this.clearAll();
359
+ }
360
+ return;
361
+ default:
362
+ return;
363
+ }
364
+ }
365
+ switch (event.key) {
366
+ case 'ArrowDown':
367
+ event.preventDefault();
368
+ this.moveActive(1);
369
+ break;
370
+ case 'ArrowUp':
371
+ event.preventDefault();
372
+ this.moveActive(-1);
373
+ break;
374
+ case 'PageDown':
375
+ event.preventDefault();
376
+ this.movePage(1);
377
+ break;
378
+ case 'PageUp':
379
+ event.preventDefault();
380
+ this.movePage(-1);
381
+ break;
382
+ case 'Home':
383
+ event.preventDefault();
384
+ this.setActiveToFirst();
385
+ break;
386
+ case 'End':
387
+ event.preventDefault();
388
+ this.setActiveToLast();
389
+ break;
390
+ case 'Enter':
391
+ event.preventDefault();
392
+ this.activateActive();
393
+ break;
394
+ case 'Escape':
395
+ event.preventDefault();
396
+ this.closeDropdown();
397
+ break;
398
+ case 'Tab':
399
+ this.closeDropdown();
400
+ break;
401
+ }
402
+ };
403
+ handleChevronMouseDown = event => {
404
+ event.preventDefault();
405
+ };
406
+ handleChevronClick = event => {
407
+ event.preventDefault();
408
+ if (this.args.isDisabled) return;
409
+ if (this.isOpen) {
410
+ this.closeDropdown();
411
+ } else {
412
+ this.openDropdown();
413
+ }
414
+ this.inputElement?.focus({
415
+ preventScroll: true
416
+ });
417
+ };
418
+ // --------------------------------------------------------------------------
419
+ // Open / close
420
+ // --------------------------------------------------------------------------
421
+ openDropdown = (opts = {}) => {
422
+ if (this.args.isDisabled || this.isOpen) return;
423
+ this.isOpen = true;
424
+ if (opts.resetActive !== false) {
425
+ this.query = '';
426
+ this.resetActiveToInitial();
427
+ } else {
428
+ this.resetActiveToFirstNavigable();
429
+ }
430
+ this.args.onOpen?.();
431
+ };
432
+ closeDropdown = () => {
433
+ // Guard so onClose doesn't double-fire; several sources can call
434
+ // this when the dropdown is already closed.
435
+ const wasOpen = this.isOpen;
436
+ this.isOpen = false;
437
+ this.query = '';
438
+ this.activeId = null;
439
+ if (this.isAsync) {
440
+ this.remoteItems = null;
441
+ this.isLoadingInternal = false;
442
+ if (this.searchTimer) {
443
+ clearTimeout(this.searchTimer);
444
+ this.searchTimer = null;
445
+ }
446
+ }
447
+ if (wasOpen) this.args.onClose?.();
448
+ };
449
+ handleOpenChange = open => {
450
+ if (open) {
451
+ this.openDropdown();
452
+ } else {
453
+ this.closeDropdown();
454
+ }
455
+ };
456
+ // --------------------------------------------------------------------------
457
+ // Active-row management
458
+ // --------------------------------------------------------------------------
459
+ resetActiveToInitial() {
460
+ const rows = this.navigableRows;
461
+ if (rows.length === 0) {
462
+ this.activeId = null;
463
+ return;
464
+ }
465
+ this.activeId = rows[0].optionId;
466
+ }
467
+ resetActiveToFirstNavigable() {
468
+ const rows = this.navigableRows;
469
+ this.activeId = rows.length > 0 ? rows[0].optionId : null;
470
+ }
471
+ setActiveToFirst() {
472
+ const rows = this.navigableRows;
473
+ if (rows.length === 0) return;
474
+ this.activeId = rows[0].optionId;
475
+ this.scrollActiveIntoView();
476
+ }
477
+ setActiveToLast() {
478
+ const rows = this.navigableRows;
479
+ if (rows.length === 0) return;
480
+ this.activeId = rows[rows.length - 1].optionId;
481
+ this.scrollActiveIntoView();
482
+ }
483
+ moveActive(direction) {
484
+ const rows = this.navigableRows;
485
+ if (rows.length === 0) {
486
+ this.activeId = null;
487
+ return;
488
+ }
489
+ const currentIndex = this.activeId ? rows.findIndex(r => r.optionId === this.activeId) : -1;
490
+ let nextIndex;
491
+ if (currentIndex === -1) {
492
+ nextIndex = direction === 1 ? 0 : rows.length - 1;
493
+ } else {
494
+ nextIndex = currentIndex + direction;
495
+ if (nextIndex < 0) nextIndex = rows.length - 1;
496
+ if (nextIndex >= rows.length) nextIndex = 0;
497
+ }
498
+ this.activeId = rows[nextIndex].optionId;
499
+ this.scrollActiveIntoView();
500
+ }
501
+ /** Move by `PAGE_STEP` rows; clamped (no wrap) at the list ends. */
502
+ movePage(direction) {
503
+ const rows = this.navigableRows;
504
+ if (rows.length === 0) {
505
+ this.activeId = null;
506
+ return;
507
+ }
508
+ const currentIndex = this.activeId ? rows.findIndex(r => r.optionId === this.activeId) : direction === 1 ? -1 : rows.length;
509
+ const targetIndex = currentIndex + direction * PAGE_STEP;
510
+ const clamped = Math.max(0, Math.min(rows.length - 1, targetIndex));
511
+ this.activeId = rows[clamped].optionId;
512
+ this.scrollActiveIntoView();
513
+ }
514
+ scrollActiveIntoView() {
515
+ if (!this.activeId) return;
516
+ const node = document.getElementById(this.activeId);
517
+ node?.scrollIntoView({
518
+ block: 'nearest'
519
+ });
520
+ }
521
+ // --------------------------------------------------------------------------
522
+ // Selection / activation
523
+ // --------------------------------------------------------------------------
524
+ activateActive() {
525
+ if (!this.activeId) return;
526
+ const row = this.navigableRows.find(r => r.optionId === this.activeId);
527
+ if (!row) return;
528
+ if (row.kind === 'create') {
529
+ this.fireCreate();
530
+ return;
531
+ }
532
+ if (row.option.isDisabled) return;
533
+ this.toggleOption(row.option);
534
+ }
535
+ toggleOption = option => {
536
+ if (option.isDisabled) return;
537
+ const selected = this.valueSet;
538
+ const nextValues = selected.has(option.value) ? this.values.filter(v => v !== option.value) : [...this.values, option.value];
539
+ this.emitChange(nextValues);
540
+ // Clear the query so the filter resets but keep the dropdown open
541
+ // — multi-select is explicit about staying open until the user
542
+ // dismisses.
543
+ this.query = '';
544
+ this.resetActiveToFirstNavigable();
545
+ this.inputElement?.focus({
546
+ preventScroll: true
547
+ });
548
+ };
549
+ fireCreate = () => {
550
+ if (!this.args.onCreate) return;
551
+ const query = this.query.trim();
552
+ if (!query) return;
553
+ this.args.onCreate(query);
554
+ this.query = '';
555
+ this.resetActiveToFirstNavigable();
556
+ this.inputElement?.focus({
557
+ preventScroll: true
558
+ });
559
+ };
560
+ removeLastChip() {
561
+ if (this.values.length === 0) return;
562
+ const nextValues = this.values.slice(0, -1);
563
+ this.emitChange(nextValues);
564
+ }
565
+ removeChip = value => {
566
+ const nextValues = this.values.filter(v => v !== value);
567
+ this.emitChange(nextValues);
568
+ this.inputElement?.focus({
569
+ preventScroll: true
570
+ });
571
+ };
572
+ handleChipRemoveMouseDown = event => {
573
+ event.preventDefault();
574
+ };
575
+ handleChipRemove = (value, event) => {
576
+ event.preventDefault();
577
+ event.stopPropagation();
578
+ this.removeChip(value);
579
+ };
580
+ clearAll() {
581
+ this.emitChange([]);
582
+ this.inputElement?.focus({
583
+ preventScroll: true
584
+ });
585
+ }
586
+ handleClearMouseDown = event => {
587
+ event.preventDefault();
588
+ };
589
+ handleClearClick = event => {
590
+ event.preventDefault();
591
+ event.stopPropagation();
592
+ this.clearAll();
593
+ };
594
+ handleOptionMouseDown = event => {
595
+ event.preventDefault();
596
+ };
597
+ handleOptionMouseEnter = optionId => {
598
+ this.activeId = optionId;
599
+ };
600
+ emitChange(nextValues) {
601
+ const flat = flattenOptions(this.effectiveItems);
602
+ const byValue = new Map(flat.map(o => [o.value, o]));
603
+ const nextOptions = nextValues.map(v => byValue.get(v) ?? {
604
+ value: v,
605
+ label: v
606
+ });
607
+ this.args.onChange?.(nextValues, nextOptions);
608
+ }
609
+ static {
610
+ setComponentTemplate(precompileTemplate("{{!-- Mousedown handlers preventDefault to keep focus on the input when\n clicking chips / chevron / options \u2014 without this, clicks move\n focus off the input and fire spurious blur events. --}}\n{{!-- template-lint-disable no-pointer-down-event-binding --}}\n<Popover @trigger=\"manual\" @open={{this.isOpen}} @onOpenChange={{this.handleOpenChange}} @dismissOnClickOutside={{true}} @dismissOnEscape={{false}} class=\"multi-combobox_e1a041f66\" data-invalid={{if @isInvalid \"true\"}} data-disabled={{if @isDisabled \"true\"}} data-open={{if this.isOpen \"true\"}} data-test-multi-combobox ...attributes as |popover|>\n {{#if @name}}\n {{#each this.values as |v|}}\n <input type=\"hidden\" name={{@name}} value={{v}} data-test-multi-combobox-input-hidden />\n {{/each}}\n {{/if}}\n\n {{!-- Visually-hidden aria-live region for result-count\n announcements. See combobox.gts for rationale. --}}\n <div class=\"multi-combobox-sr-only_e1a041f66\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" data-test-multi-combobox-announcement>{{this.announcement}}</div>\n\n <div {{popover.registerTrigger}} class=\"multi-combobox-trigger_e1a041f66\" data-test-multi-combobox-trigger-wrapper>\n <div class=\"multi-combobox-chips_e1a041f66\" data-test-multi-combobox-chips>\n {{#each this.visibleChips key=\"value\" as |chip|}}\n <span class=\"multi-combobox-chip_e1a041f66\" data-test-multi-combobox-chip data-value={{chip.value}}>\n <span class=\"multi-combobox-chip-label_e1a041f66\">{{chip.label}}</span>\n <button type=\"button\" class=\"multi-combobox-chip-remove_e1a041f66\" aria-label=\"Remove {{chip.label}}\" tabindex=\"-1\" {{on \"mousedown\" this.handleChipRemoveMouseDown}} {{on \"click\" (fn this.handleChipRemove chip.value)}} data-test-multi-combobox-chip-remove>\n <Icon @icon={{X}} @size=\"sm\" />\n </button>\n </span>\n {{/each}}\n {{#if this.hasOverflow}}\n <span class=\"multi-combobox-more_e1a041f66\" data-test-multi-combobox-more>+{{this.overflowCount}}\n more</span>\n {{/if}}\n\n {{!-- template-lint-disable no-redundant-role --}}\n <input {{this.setupInput}} {{on \"click\" this.handleInputClick}} {{on \"focus\" this.handleInputFocus}} {{on \"blur\" this.handleInputBlur}} {{on \"input\" this.handleInput}} {{on \"keydown\" this.handleInputKeyDown}} id={{@id}} type=\"text\" class=\"multi-combobox-input_e1a041f66\" role=\"combobox\" aria-autocomplete=\"list\" aria-haspopup=\"listbox\" aria-expanded={{this.ariaExpanded}} aria-controls={{this.listboxId}} aria-activedescendant={{this.activeId}} aria-required={{this.ariaRequired}} aria-invalid={{this.ariaInvalid}} aria-describedby={{@aria-describedby}} autocomplete=\"off\" spellcheck=\"false\" placeholder={{unless this.hasAnyValue @placeholder}} value={{this.query}} disabled={{@isDisabled}} data-test-multi-combobox-trigger data-test-multi-combobox-input />\n </div>\n\n {{#if this.showClearButton}}\n <button type=\"button\" class=\"multi-combobox-clear_e1a041f66\" aria-label=\"Clear all\" tabindex=\"-1\" {{on \"mousedown\" this.handleClearMouseDown}} {{on \"click\" this.handleClearClick}} data-test-multi-combobox-clear>\n <Icon @icon={{X}} @size=\"sm\" />\n </button>\n {{/if}}\n\n <button type=\"button\" class=\"multi-combobox-chevron_e1a041f66\" aria-label={{if this.isOpen \"Close\" \"Open\"}} tabindex=\"-1\" disabled={{@isDisabled}} {{on \"mousedown\" this.handleChevronMouseDown}} {{on \"click\" this.handleChevronClick}} data-test-multi-combobox-chevron>\n <Icon @icon={{ChevronDown}} @size=\"sm\" />\n </button>\n </div>\n\n <popover.Content @side=\"bottom\" @align=\"start\" @sideOffset={{4}} @animation=\"scale\" class=\"multi-combobox-popover-content_e1a041f66\">\n <div id={{this.listboxId}} role=\"listbox\" aria-multiselectable=\"true\" class=\"multi-combobox-listbox_e1a041f66\" tabindex=\"-1\" {{this.setupListbox}} data-test-multi-combobox-listbox>\n {{#each this.renderedGroups key=\"key\" as |group|}}\n {{#if group.label}}\n <div role=\"group\" aria-labelledby={{group.headingId}} class=\"multi-combobox-group_e1a041f66\" data-test-multi-combobox-group>\n <div id={{group.headingId}} class=\"multi-combobox-group-heading_e1a041f66\" data-test-multi-combobox-group-heading>{{group.label}}</div>\n {{!-- template-lint-disable require-context-role --}}\n {{#each group.rows key=\"optionId\" as |row|}}\n <div id={{row.optionId}} role=\"option\" class=\"multi-combobox-option_e1a041f66\" aria-selected={{if row.isSelected \"true\" \"false\"}} aria-disabled={{if row.option.isDisabled \"true\"}} data-active={{if row.isActive \"true\"}} data-selected={{if row.isSelected \"true\"}} data-disabled={{if row.option.isDisabled \"true\"}} data-option-id={{row.optionId}} data-value={{row.option.value}} {{on \"mousedown\" this.handleOptionMouseDown}} {{on \"click\" (fn this.toggleOption row.option)}} {{on \"mouseenter\" (fn this.handleOptionMouseEnter row.optionId)}} data-test-multi-combobox-option>\n <div class=\"multi-combobox-option-content_e1a041f66\">\n <span class=\"multi-combobox-option-label_e1a041f66\" data-test-multi-combobox-option-label>\n {{~#each (splitOnMatch row.option.label this.query) as |segment|~}}\n {{~#if segment.isMatch~}}\n <span class=\"multi-combobox-mark_e1a041f66\" data-test-multi-combobox-mark>{{segment.text}}</span>\n {{~else~}}\n {{segment.text}}\n {{~/if~}}\n {{~/each~}}\n </span>\n {{#if row.option.description}}\n <span class=\"multi-combobox-option-description_e1a041f66\" data-test-multi-combobox-option-description>\n {{~#each (splitOnMatch row.option.description this.query) as |segment|~}}\n {{~#if segment.isMatch~}}\n <span class=\"multi-combobox-mark_e1a041f66\" data-test-multi-combobox-mark>{{segment.text}}</span>\n {{~else~}}\n {{segment.text}}\n {{~/if~}}\n {{~/each~}}\n </span>\n {{/if}}\n </div>\n <span class=\"multi-combobox-checkbox_e1a041f66\" data-checked={{if row.isSelected \"true\"}} aria-hidden=\"true\"></span>\n </div>\n {{/each}}\n </div>\n {{else}}\n {{#each group.rows key=\"optionId\" as |row|}}\n <div id={{row.optionId}} role=\"option\" class=\"multi-combobox-option_e1a041f66\" aria-selected={{if row.isSelected \"true\" \"false\"}} aria-disabled={{if row.option.isDisabled \"true\"}} data-active={{if row.isActive \"true\"}} data-selected={{if row.isSelected \"true\"}} data-disabled={{if row.option.isDisabled \"true\"}} data-option-id={{row.optionId}} data-value={{row.option.value}} {{on \"mousedown\" this.handleOptionMouseDown}} {{on \"click\" (fn this.toggleOption row.option)}} {{on \"mouseenter\" (fn this.handleOptionMouseEnter row.optionId)}} data-test-multi-combobox-option>\n <div class=\"multi-combobox-option-content_e1a041f66\">\n <span class=\"multi-combobox-option-label_e1a041f66\" data-test-multi-combobox-option-label>\n {{~#each (splitOnMatch row.option.label this.query) as |segment|~}}\n {{~#if segment.isMatch~}}\n <span class=\"multi-combobox-mark_e1a041f66\" data-test-multi-combobox-mark>{{segment.text}}</span>\n {{~else~}}\n {{segment.text}}\n {{~/if~}}\n {{~/each~}}\n </span>\n {{#if row.option.description}}\n <span class=\"multi-combobox-option-description_e1a041f66\" data-test-multi-combobox-option-description>\n {{~#each (splitOnMatch row.option.description this.query) as |segment|~}}\n {{~#if segment.isMatch~}}\n <span class=\"multi-combobox-mark_e1a041f66\" data-test-multi-combobox-mark>{{segment.text}}</span>\n {{~else~}}\n {{segment.text}}\n {{~/if~}}\n {{~/each~}}\n </span>\n {{/if}}\n </div>\n <span class=\"multi-combobox-checkbox_e1a041f66\" data-checked={{if row.isSelected \"true\"}} aria-hidden=\"true\"></span>\n </div>\n {{/each}}\n {{/if}}\n {{/each}}\n\n {{#if this.createRow}}\n <div id={{this.createRow.optionId}} role=\"option\" class=\"multi-combobox-option_e1a041f66 multi-combobox-create_e1a041f66\" aria-selected=\"false\" data-active={{if this.createRow.isActive \"true\"}} data-option-id={{this.createRow.optionId}} {{on \"mousedown\" this.handleOptionMouseDown}} {{on \"click\" this.fireCreate}} {{on \"mouseenter\" (fn this.handleOptionMouseEnter this.createRow.optionId)}} data-test-multi-combobox-create>\n <span class=\"multi-combobox-create-icon_e1a041f66\" aria-hidden=\"true\">+</span>\n <span class=\"multi-combobox-option-label_e1a041f66\">{{this.createRow.label}}</span>\n </div>\n {{/if}}\n\n {{#if this.isLoading}}\n <div class=\"multi-combobox-loading_e1a041f66\" data-test-multi-combobox-loading>{{this.loadingText}}</div>\n {{else if this.showEmptyState}}\n <div class=\"multi-combobox-empty_e1a041f66\" data-test-multi-combobox-empty>{{this.noResultsText}}</div>\n {{/if}}\n </div>\n </popover.Content>\n</Popover>", {
611
+ strictMode: true,
612
+ scope: () => ({
613
+ Popover,
614
+ on,
615
+ fn,
616
+ Icon,
617
+ X,
618
+ ChevronDown,
619
+ splitOnMatch
620
+ })
621
+ }), this);
622
+ }
623
+ }
624
+
625
+ export { MultiComboBox as default };
626
+ //# sourceMappingURL=multi-combobox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multi-combobox.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"h-stack.js","sources":["../../src/layout/h-stack.gts"],"sourcesContent":["import type { TOC } from '@ember/component/template-only';\nimport Stack from './stack.gts';\n\nexport interface HStackSignature {\n Element: HTMLDivElement;\n Args: {\n gap?: 'none' | '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';\n align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';\n justify?: 'start' | 'center' | 'end' | 'between';\n wrap?: boolean;\n inline?: boolean;\n };\n Blocks: {\n default: [];\n };\n}\n\nconst HStack: TOC<HStackSignature> = <template>\n <Stack\n @direction=\"row\"\n @gap={{@gap}}\n @align={{@align}}\n @justify={{@justify}}\n @wrap={{@wrap}}\n @inline={{@inline}}\n ...attributes\n >\n {{yield}}\n </Stack>\n</template>;\n\nexport default HStack;\n"],"names":["HStack","setComponentTemplate","precompileTemplate","strictMode","scope","Stack","templateOnly"],"mappings":";;;;;AAiBA,MAAMA,MAAY,GAAAC,oBAAA,CAAmBC,kBAAA,CAAA,2JAAA,EAYrC;EAAAC,UAAA,EAAA,IAAA;AAAAC,EAAAA,KAAA,EAAAA,OAAA;AAAAC,IAAAA;AAAA,GAAA;AAAU,CAAA,CAAA,EAAAC,YAAA,EAAA;;;;"}
1
+ {"version":3,"file":"h-stack.js","sources":["../../src/layout/h-stack.gts"],"sourcesContent":["import type { TOC } from '@ember/component/template-only';\nimport Stack from './stack.gts';\n\nexport interface HStackSignature {\n Element: HTMLDivElement;\n Args: {\n gap?:\n | 'none'\n | '2xs'\n | 'xs'\n | 'sm'\n | 'md'\n | 'lg'\n | 'xl'\n | '2xl'\n | '3xl'\n | '4xl'\n | '5xl';\n align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';\n justify?: 'start' | 'center' | 'end' | 'between';\n wrap?: boolean;\n inline?: boolean;\n };\n Blocks: {\n default: [];\n };\n}\n\nconst HStack: TOC<HStackSignature> = <template>\n <Stack\n @direction=\"row\"\n @gap={{@gap}}\n @align={{@align}}\n @justify={{@justify}}\n @wrap={{@wrap}}\n @inline={{@inline}}\n ...attributes\n >\n {{yield}}\n </Stack>\n</template>;\n\nexport default HStack;\n"],"names":["HStack","setComponentTemplate","precompileTemplate","strictMode","scope","Stack","templateOnly"],"mappings":";;;;;AA4BA,MAAMA,MAAY,GAAAC,oBAAA,CAAmBC,kBAAA,CAAA,2JAAA,EAYrC;EAAAC,UAAA,EAAA,IAAA;AAAAC,EAAAA,KAAA,EAAAA,OAAA;AAAAC,IAAAA;AAAA,GAAA;AAAU,CAAA,CAAA,EAAAC,YAAA,EAAA;;;;"}
@@ -227,3 +227,13 @@
227
227
  .panel_efcbf2ade[data-variant="filled"][data-tone="neutral"] > .panel-footer_efcbf2ade {
228
228
  border-top-color: rgba(0, 0, 0, 0.1);
229
229
  }
230
+
231
+ /* Strip separators at the structural edge — a header-only or
232
+ footer-only Panel has nothing to separate. */
233
+ .panel_efcbf2ade > .panel-header_efcbf2ade:last-child {
234
+ border-bottom: none;
235
+ }
236
+
237
+ .panel_efcbf2ade > .panel-footer_efcbf2ade:first-child {
238
+ border-top: none;
239
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"v-stack.js","sources":["../../src/layout/v-stack.gts"],"sourcesContent":["import type { TOC } from '@ember/component/template-only';\nimport Stack from './stack.gts';\n\nexport interface VStackSignature {\n Element: HTMLDivElement;\n Args: {\n gap?: 'none' | '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';\n align?: 'start' | 'center' | 'end' | 'stretch';\n justify?: 'start' | 'center' | 'end' | 'between';\n inline?: boolean;\n };\n Blocks: {\n default: [];\n };\n}\n\nconst VStack: TOC<VStackSignature> = <template>\n <Stack\n @direction=\"column\"\n @gap={{@gap}}\n @align={{@align}}\n @justify={{@justify}}\n @inline={{@inline}}\n ...attributes\n >\n {{yield}}\n </Stack>\n</template>;\n\nexport default VStack;\n"],"names":["VStack","setComponentTemplate","precompileTemplate","strictMode","scope","Stack","templateOnly"],"mappings":";;;;;AAgBA,MAAMA,MAAY,GAAAC,oBAAA,CAAmBC,kBAAA,CAAA,8IAAA,EAWrC;EAAAC,UAAA,EAAA,IAAA;AAAAC,EAAAA,KAAA,EAAAA,OAAA;AAAAC,IAAAA;AAAA,GAAA;AAAU,CAAA,CAAA,EAAAC,YAAA,EAAA;;;;"}
1
+ {"version":3,"file":"v-stack.js","sources":["../../src/layout/v-stack.gts"],"sourcesContent":["import type { TOC } from '@ember/component/template-only';\nimport Stack from './stack.gts';\n\nexport interface VStackSignature {\n Element: HTMLDivElement;\n Args: {\n gap?:\n | 'none'\n | '2xs'\n | 'xs'\n | 'sm'\n | 'md'\n | 'lg'\n | 'xl'\n | '2xl'\n | '3xl'\n | '4xl'\n | '5xl';\n align?: 'start' | 'center' | 'end' | 'stretch';\n justify?: 'start' | 'center' | 'end' | 'between';\n inline?: boolean;\n };\n Blocks: {\n default: [];\n };\n}\n\nconst VStack: TOC<VStackSignature> = <template>\n <Stack\n @direction=\"column\"\n @gap={{@gap}}\n @align={{@align}}\n @justify={{@justify}}\n @inline={{@inline}}\n ...attributes\n >\n {{yield}}\n </Stack>\n</template>;\n\nexport default VStack;\n"],"names":["VStack","setComponentTemplate","precompileTemplate","strictMode","scope","Stack","templateOnly"],"mappings":";;;;;AA2BA,MAAMA,MAAY,GAAAC,oBAAA,CAAmBC,kBAAA,CAAA,8IAAA,EAWrC;EAAAC,UAAA,EAAA,IAAA;AAAAC,EAAAA,KAAA,EAAAA,OAAA;AAAAC,IAAAA;AAAA,GAAA;AAAU,CAAA,CAAA,EAAAC,YAAA,EAAA;;;;"}