@akcelik/strct 1.19.0 → 1.22.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.
@@ -7224,21 +7224,57 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
7224
7224
 
7225
7225
  let comboboxCounter = 0;
7226
7226
  /**
7227
- * Filterable single-select (autocomplete). CVA-compatible, fully keyboard
7228
- * driven (↑/↓ to move, Enter to pick, Esc to close).
7227
+ * Filterable select (autocomplete). CVA-compatible, fully keyboard driven
7228
+ * (↑/↓ move, Home/End jump, Enter picks, Esc closes) with the shared select
7229
+ * ergonomics: an aligned ✓ lead slot marks the current choice, the typed
7230
+ * match is emphasised in each label, and disabled options gray out and are
7231
+ * skipped.
7232
+ *
7229
7233
  * <strct-combobox [options]="opts" [(ngModel)]="selected" placeholder="Pick…" />
7234
+ *
7235
+ * Variants: `clearable` adds an × that resets the selection; `multiple`
7236
+ * switches the value to an array and renders the picks as removable chips —
7237
+ * the list stays open while picking and Backspace on an empty query removes
7238
+ * the last chip. Options may carry `group` labels (rendered as headers),
7239
+ * `disabled`, a leading `icon` and a secondary `description` line.
7240
+ * `allowCustomValue` appends a "Use \"…\"" row while typing so the typed
7241
+ * text itself can be committed as a free-form value.
7230
7242
  */
7231
7243
  class StrctCombobox {
7232
- host = inject((ElementRef));
7244
+ host = inject(ElementRef);
7245
+ inputEl = viewChild.required('input');
7233
7246
  listId = `strct-cbx-${++comboboxCounter}`;
7234
- /** Available options. */
7247
+ /** Available options (`disabled` grays out; `group` renders headers). */
7235
7248
  options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
7236
7249
  /** Placeholder text when empty. */
7237
7250
  placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
7238
7251
  /** Show a skeleton placeholder while options are loading. */
7239
7252
  loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
7253
+ /** Show an × that resets the selection. */
7254
+ clearable = input(false, { ...(ngDevMode ? { debugName: "clearable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
7255
+ /**
7256
+ * Multi-select: the value becomes an array and the picks render as
7257
+ * removable chips; the list stays open while picking.
7258
+ */
7259
+ multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
7260
+ /** Text shown when the filter matches nothing (localizable). */
7261
+ emptyText = input('No matches', ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
7262
+ /** Accessible label of the × clear button (localizable). */
7263
+ clearLabel = input('Clear selection', ...(ngDevMode ? [{ debugName: "clearLabel" }] : /* istanbul ignore next */ []));
7264
+ /** Accessible label prefix of a chip's × button (localizable). */
7265
+ removeLabel = input('Remove', ...(ngDevMode ? [{ debugName: "removeLabel" }] : /* istanbul ignore next */ []));
7266
+ /**
7267
+ * Allow committing the typed text itself: while the query has no exact
7268
+ * label match, a "Use \"…\"" row pinned to the list end (Enter or click)
7269
+ * commits the raw text as the value — appended to the array in `multiple`.
7270
+ */
7271
+ allowCustomValue = input(false, { ...(ngDevMode ? { debugName: "allowCustomValue" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
7272
+ /** Verb prefix of the free-form row (localizable) — renders `Use "query"`. */
7273
+ customText = input('Use', ...(ngDevMode ? [{ debugName: "customText" }] : /* istanbul ignore next */ []));
7240
7274
  query = signal('', ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
7241
7275
  value = signal(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
7276
+ /** Selection in `multiple` mode. */
7277
+ values = signal([], ...(ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []));
7242
7278
  open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
7243
7279
  activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
7244
7280
  isDisabled = signal(false, ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
@@ -7250,19 +7286,79 @@ class StrctCombobox {
7250
7286
  return this.options();
7251
7287
  return this.options().filter((o) => o.label.toLowerCase().includes(q));
7252
7288
  }, ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
7289
+ /** Flat option list (no headers) — keyboard navigation and ids live here. */
7290
+ flat = computed(() => this.filtered(), ...(ngDevMode ? [{ debugName: "flat" }] : /* istanbul ignore next */ []));
7291
+ /**
7292
+ * The free-form query committable right now (allowCustomValue): a trimmed
7293
+ * dirty query with no exact label match — and, in multiple mode, not
7294
+ * already picked. Rendered as an extra row at index `flat().length`.
7295
+ */
7296
+ customRow = computed(() => {
7297
+ if (!this.allowCustomValue() || !this.dirty())
7298
+ return null;
7299
+ const q = this.query().trim();
7300
+ if (!q)
7301
+ return null;
7302
+ const ql = q.toLowerCase();
7303
+ if (this.options().some((o) => o.label.toLowerCase() === ql))
7304
+ return null;
7305
+ if (this.multiple() && this.values().includes(q))
7306
+ return null;
7307
+ return q;
7308
+ }, ...(ngDevMode ? [{ debugName: "customRow" }] : /* istanbul ignore next */ []));
7309
+ /** Navigable row count: options plus the custom row when shown. */
7310
+ navCount = computed(() => this.flat().length + (this.customRow() ? 1 : 0), ...(ngDevMode ? [{ debugName: "navCount" }] : /* istanbul ignore next */ []));
7311
+ /** Render rows: group headers interleaved with their options. */
7312
+ rows = computed(() => {
7313
+ const out = [];
7314
+ let lastGroup;
7315
+ this.filtered().forEach((opt, index) => {
7316
+ if (opt.group !== undefined && opt.group !== lastGroup) {
7317
+ out.push({ key: `h:${opt.group}`, header: opt.group });
7318
+ }
7319
+ lastGroup = opt.group;
7320
+ out.push({ key: `o:${index}`, opt, index });
7321
+ });
7322
+ return out;
7323
+ }, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
7324
+ hasSelection = computed(() => this.multiple() ? this.values().length > 0 : this.value() !== null, ...(ngDevMode ? [{ debugName: "hasSelection" }] : /* istanbul ignore next */ []));
7253
7325
  onChange = () => { };
7254
7326
  onTouched = () => { };
7327
+ isSelected(v) {
7328
+ return this.multiple() ? this.values().includes(v) : this.value() === v;
7329
+ }
7330
+ optionOf(v) {
7331
+ return this.options().find((o) => o.value === v);
7332
+ }
7333
+ labelOf(v) {
7334
+ return this.optionOf(v)?.label ?? String(v);
7335
+ }
7336
+ /** The typed match emphasised inside a label (first occurrence). */
7337
+ segments(label) {
7338
+ const q = this.dirty() ? this.query().toLowerCase().trim() : '';
7339
+ const at = q ? label.toLowerCase().indexOf(q) : -1;
7340
+ if (at < 0)
7341
+ return { pre: label, match: '', post: '' };
7342
+ return {
7343
+ pre: label.slice(0, at),
7344
+ match: label.slice(at, at + q.length),
7345
+ post: label.slice(at + q.length),
7346
+ };
7347
+ }
7348
+ focusInput() {
7349
+ this.inputEl().nativeElement.focus();
7350
+ }
7255
7351
  openList() {
7256
7352
  if (this.isDisabled())
7257
7353
  return;
7258
7354
  this.open.set(true);
7259
- this.activeIndex.set(this.indexOfValue());
7355
+ this.activeIndex.set(this.initialIndex());
7260
7356
  }
7261
7357
  onType(event) {
7262
7358
  this.dirty.set(true);
7263
7359
  this.query.set(event.target.value);
7264
7360
  this.open.set(true);
7265
- this.activeIndex.set(0);
7361
+ this.activeIndex.set(this.firstEnabled(0));
7266
7362
  }
7267
7363
  onKeydown(event) {
7268
7364
  switch (event.key) {
@@ -7276,15 +7372,36 @@ class StrctCombobox {
7276
7372
  event.preventDefault();
7277
7373
  this.move(-1);
7278
7374
  break;
7375
+ case 'Home':
7376
+ case 'End':
7377
+ if (!this.open())
7378
+ return;
7379
+ event.preventDefault();
7380
+ this.activeIndex.set(event.key === 'Home' ? this.firstEnabled(0) : this.lastEnabled());
7381
+ this.scrollActiveIntoView();
7382
+ break;
7279
7383
  case 'Enter': {
7280
7384
  if (!this.open())
7281
7385
  return;
7282
7386
  event.preventDefault();
7283
- const opt = this.filtered()[this.activeIndex()];
7284
- if (opt)
7387
+ const i = this.activeIndex();
7388
+ if (i >= this.flat().length) {
7389
+ this.commitCustom();
7390
+ return;
7391
+ }
7392
+ const opt = this.flat()[i];
7393
+ if (opt && !opt.disabled)
7285
7394
  this.commit(opt);
7286
7395
  break;
7287
7396
  }
7397
+ case 'Backspace':
7398
+ if (this.multiple() && !this.query() && this.values().length) {
7399
+ this.removeValue(this.values()[this.values().length - 1]);
7400
+ }
7401
+ break;
7402
+ case 'Tab':
7403
+ this.close();
7404
+ break;
7288
7405
  case 'Escape':
7289
7406
  if (this.open()) {
7290
7407
  event.preventDefault();
@@ -7294,16 +7411,61 @@ class StrctCombobox {
7294
7411
  }
7295
7412
  }
7296
7413
  move(delta) {
7297
- const len = this.filtered().length;
7298
- if (!len)
7414
+ const opts = this.flat();
7415
+ const n = this.navCount();
7416
+ if (!n)
7299
7417
  return;
7300
- this.activeIndex.set((this.activeIndex() + delta + len) % len);
7418
+ let i = this.activeIndex();
7419
+ let guard = n;
7420
+ do {
7421
+ i = (i + delta + n) % n;
7422
+ // The custom row (index ≥ opts.length) is always enabled.
7423
+ } while (i < opts.length && opts[i].disabled && --guard > 0);
7424
+ this.activeIndex.set(i);
7425
+ this.scrollActiveIntoView();
7426
+ }
7427
+ firstEnabled(from) {
7428
+ const opts = this.flat();
7429
+ for (let i = from; i < opts.length; i++)
7430
+ if (!opts[i].disabled)
7431
+ return i;
7432
+ return this.customRow() ? opts.length : 0;
7433
+ }
7434
+ lastEnabled() {
7435
+ const opts = this.flat();
7436
+ if (this.customRow())
7437
+ return opts.length; // custom row is always last & enabled
7438
+ for (let i = opts.length - 1; i >= 0; i--)
7439
+ if (!opts[i].disabled)
7440
+ return i;
7441
+ return 0;
7442
+ }
7443
+ scrollActiveIntoView() {
7444
+ setTimeout(() => {
7445
+ document
7446
+ .getElementById(`${this.listId}-${this.activeIndex()}`)
7447
+ ?.scrollIntoView({ block: 'nearest' });
7448
+ });
7301
7449
  }
7302
7450
  select(opt, event) {
7303
7451
  event.preventDefault(); // keep focus, avoid blur reordering
7452
+ if (opt.disabled)
7453
+ return;
7304
7454
  this.commit(opt);
7305
7455
  }
7306
7456
  commit(opt) {
7457
+ if (this.multiple()) {
7458
+ const next = this.values().includes(opt.value)
7459
+ ? this.values().filter((v) => v !== opt.value)
7460
+ : [...this.values(), opt.value];
7461
+ this.values.set(next);
7462
+ this.query.set('');
7463
+ this.dirty.set(false);
7464
+ this.activeIndex.set(this.options().indexOf(opt));
7465
+ this.onChange([...next]);
7466
+ this.onTouched();
7467
+ return; // the list stays open while picking
7468
+ }
7307
7469
  this.value.set(opt.value);
7308
7470
  this.query.set(opt.label);
7309
7471
  this.dirty.set(false);
@@ -7311,6 +7473,57 @@ class StrctCombobox {
7311
7473
  this.onChange(opt.value);
7312
7474
  this.onTouched();
7313
7475
  }
7476
+ /**
7477
+ * Commit the typed text itself as a free-form value (allowCustomValue).
7478
+ * Multiple mode appends it and keeps picking; single mode takes it and
7479
+ * closes.
7480
+ */
7481
+ commitCustom(event) {
7482
+ event?.preventDefault();
7483
+ const q = this.customRow();
7484
+ if (!q)
7485
+ return;
7486
+ if (this.multiple()) {
7487
+ const next = [...this.values(), q];
7488
+ this.values.set(next);
7489
+ this.query.set('');
7490
+ this.dirty.set(false);
7491
+ this.onChange([...next]);
7492
+ this.onTouched();
7493
+ return; // the list stays open while picking
7494
+ }
7495
+ this.value.set(q);
7496
+ this.query.set(q);
7497
+ this.dirty.set(false);
7498
+ this.open.set(false);
7499
+ this.onChange(q);
7500
+ this.onTouched();
7501
+ }
7502
+ removeValue(v, event) {
7503
+ event?.stopPropagation();
7504
+ const next = this.values().filter((x) => x !== v);
7505
+ this.values.set(next);
7506
+ this.onChange([...next]);
7507
+ this.onTouched();
7508
+ // A clicked chip × vanishes with its chip — keep focus in the control.
7509
+ if (event)
7510
+ this.focusInput();
7511
+ }
7512
+ clear(event) {
7513
+ event.stopPropagation();
7514
+ if (this.multiple()) {
7515
+ this.values.set([]);
7516
+ this.onChange([]);
7517
+ }
7518
+ else {
7519
+ this.value.set(null);
7520
+ this.onChange(null);
7521
+ }
7522
+ this.query.set('');
7523
+ this.dirty.set(false);
7524
+ this.onTouched();
7525
+ this.focusInput();
7526
+ }
7314
7527
  onDocClick(event) {
7315
7528
  if (this.open() && !this.host.nativeElement.contains(event.target)) {
7316
7529
  this.close();
@@ -7321,15 +7534,27 @@ class StrctCombobox {
7321
7534
  this.dirty.set(false);
7322
7535
  this.syncQueryToValue();
7323
7536
  }
7324
- indexOfValue() {
7325
- const idx = this.filtered().findIndex((o) => o.value === this.value());
7326
- return idx < 0 ? 0 : idx;
7537
+ /** Highlight starts on the selected option — or the first enabled one. */
7538
+ initialIndex() {
7539
+ const opts = this.flat();
7540
+ const sel = opts.findIndex((o) => this.isSelected(o.value) && !o.disabled);
7541
+ return sel < 0 ? this.firstEnabled(0) : sel;
7327
7542
  }
7328
7543
  syncQueryToValue() {
7329
- const match = this.options().find((o) => o.value === this.value());
7330
- this.query.set(match?.label ?? '');
7544
+ if (this.multiple()) {
7545
+ this.query.set('');
7546
+ return;
7547
+ }
7548
+ const v = this.value();
7549
+ const match = this.options().find((o) => o.value === v);
7550
+ // A custom value matches no option — echo the raw text instead of blanking.
7551
+ this.query.set(match?.label ?? (v == null ? '' : String(v)));
7331
7552
  }
7332
7553
  writeValue(value) {
7554
+ if (this.multiple()) {
7555
+ this.values.set(Array.isArray(value) ? value : []);
7556
+ return;
7557
+ }
7333
7558
  this.value.set(value);
7334
7559
  this.syncQueryToValue();
7335
7560
  }
@@ -7343,128 +7568,607 @@ class StrctCombobox {
7343
7568
  this.isDisabled.set(isDisabled);
7344
7569
  }
7345
7570
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctCombobox, deps: [], target: i0.ɵɵFactoryTarget.Component });
7346
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctCombobox, isStandalone: true, selector: "strct-combobox", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocClick($event)" }, classAttribute: "strct-cbx" }, providers: [
7571
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctCombobox, isStandalone: true, selector: "strct-combobox", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, clearLabel: { classPropertyName: "clearLabel", publicName: "clearLabel", isSignal: true, isRequired: false, transformFunction: null }, removeLabel: { classPropertyName: "removeLabel", publicName: "removeLabel", isSignal: true, isRequired: false, transformFunction: null }, allowCustomValue: { classPropertyName: "allowCustomValue", publicName: "allowCustomValue", isSignal: true, isRequired: false, transformFunction: null }, customText: { classPropertyName: "customText", publicName: "customText", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocClick($event)" }, classAttribute: "strct-cbx" }, providers: [
7347
7572
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StrctCombobox), multi: true },
7348
- ], ngImport: i0, template: `
7349
- <div #field class="strct-cbx__field">
7573
+ ], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["input"], descendants: true, isSignal: true }], ngImport: i0, template: `
7574
+ <div
7575
+ #field
7576
+ class="strct-cbx__field"
7577
+ [class.strct-control]="multiple()"
7578
+ [class.strct-cbx__field--multi]="multiple()"
7579
+ (click)="focusInput()"
7580
+ >
7581
+ @if (multiple()) {
7582
+ @for (v of values(); track $index) {
7583
+ <span class="strct-cbx__chip">
7584
+ @if (optionOf(v)?.icon; as chipIcon) {
7585
+ <strct-icon class="strct-cbx__chip-icon" [name]="chipIcon" [size]="11" />
7586
+ }
7587
+ {{ labelOf(v) }}
7588
+ <button
7589
+ type="button"
7590
+ class="strct-cbx__chip-x"
7591
+ [attr.aria-label]="removeLabel() + ' ' + labelOf(v)"
7592
+ [disabled]="isDisabled()"
7593
+ (click)="removeValue(v, $event)"
7594
+ >
7595
+ <strct-icon strictName="close" [size]="10" [strokeWidth]="1.8" />
7596
+ </button>
7597
+ </span>
7598
+ }
7599
+ }
7350
7600
  <input
7351
7601
  #input
7352
7602
  type="text"
7353
- class="strct-control strct-cbx__input"
7603
+ class="strct-cbx__input"
7604
+ [class.strct-control]="!multiple()"
7605
+ [class.strct-cbx__input--bare]="multiple()"
7354
7606
  role="combobox"
7355
7607
  autocomplete="off"
7356
7608
  [attr.aria-expanded]="open()"
7357
7609
  [attr.aria-controls]="listId"
7358
- [attr.aria-activedescendant]="
7359
- open() && filtered().length ? listId + '-' + activeIndex() : null
7360
- "
7361
- [placeholder]="placeholder()"
7610
+ [attr.aria-activedescendant]="open() && navCount() ? listId + '-' + activeIndex() : null"
7611
+ [placeholder]="multiple() && values().length ? '' : placeholder()"
7362
7612
  [value]="query()"
7363
7613
  [disabled]="isDisabled()"
7364
7614
  (focus)="openList()"
7615
+ (click)="openList()"
7365
7616
  (input)="onType($event)"
7366
7617
  (keydown)="onKeydown($event)"
7367
7618
  (blur)="onTouched()"
7368
7619
  />
7369
- <strct-icon class="strct-cbx__caret" name="chevronDown" [size]="14" />
7620
+ @if (clearable() && hasSelection() && !isDisabled()) {
7621
+ <button
7622
+ type="button"
7623
+ class="strct-cbx__clear"
7624
+ [attr.aria-label]="clearLabel()"
7625
+ (click)="clear($event)"
7626
+ >
7627
+ <strct-icon strictName="close" [size]="12" [strokeWidth]="1.6" />
7628
+ </button>
7629
+ }
7630
+ <strct-icon class="strct-cbx__caret" strictName="chevronDown" [size]="14" />
7370
7631
  </div>
7371
7632
  @if (open()) {
7372
7633
  <div
7373
7634
  class="strct-cbx__menu"
7374
7635
  role="listbox"
7375
7636
  [id]="listId"
7637
+ [attr.aria-multiselectable]="multiple() || null"
7376
7638
  [strctOverlay]="field"
7377
7639
  strctOverlayPlacement="bottom-start"
7378
7640
  [strctOverlayMatchWidth]="true"
7641
+ (mousedown)="$event.preventDefault()"
7379
7642
  >
7380
7643
  @if (loading()) {
7381
7644
  <div class="strct-cbx__skeleton">
7382
7645
  <div class="strct-cbx__skeleton-block"></div>
7383
7646
  </div>
7384
7647
  } @else {
7385
- @for (opt of filtered(); track opt.value; let i = $index) {
7648
+ @for (row of rows(); track row.key) {
7649
+ @if (row.header !== undefined) {
7650
+ <div class="strct-cbx__group" role="presentation">{{ row.header }}</div>
7651
+ } @else {
7652
+ <div
7653
+ class="strct-cbx__opt"
7654
+ [id]="listId + '-' + row.index"
7655
+ [class.strct-cbx__opt--selected]="isSelected(row.opt!.value)"
7656
+ [class.strct-cbx__opt--highlight]="row.index === activeIndex()"
7657
+ role="option"
7658
+ [attr.aria-selected]="isSelected(row.opt!.value)"
7659
+ [attr.aria-disabled]="row.opt!.disabled || null"
7660
+ (mousedown)="select(row.opt!, $event)"
7661
+ (mousemove)="!row.opt!.disabled && activeIndex.set(row.index!)"
7662
+ >
7663
+ <span class="strct-cbx__check" aria-hidden="true">
7664
+ @if (isSelected(row.opt!.value)) {
7665
+ <strct-icon strictName="check" [size]="12" [strokeWidth]="1.8" />
7666
+ }
7667
+ </span>
7668
+ @if (row.opt!.icon) {
7669
+ <strct-icon class="strct-cbx__opt-icon" [name]="row.opt!.icon" [size]="14" />
7670
+ }
7671
+ @let seg = segments(row.opt!.label);
7672
+ <span class="strct-cbx__opt-text">
7673
+ <span class="strct-cbx__label"
7674
+ >{{ seg.pre }}<span class="strct-cbx__match">{{ seg.match }}</span
7675
+ >{{ seg.post }}</span
7676
+ >
7677
+ @if (row.opt!.description) {
7678
+ <span class="strct-cbx__opt-desc">{{ row.opt!.description }}</span>
7679
+ }
7680
+ </span>
7681
+ </div>
7682
+ }
7683
+ } @empty {
7684
+ @if (!customRow()) {
7685
+ <div class="strct-cbx__empty">{{ emptyText() }}</div>
7686
+ }
7687
+ }
7688
+ @if (customRow(); as customQuery) {
7386
7689
  <div
7387
- class="strct-cbx__opt"
7388
- [id]="listId + '-' + i"
7389
- [class.strct-cbx__opt--active]="opt.value === value()"
7390
- [class.strct-cbx__opt--highlight]="i === activeIndex()"
7690
+ class="strct-cbx__opt strct-cbx__opt--custom"
7691
+ [id]="listId + '-' + flat().length"
7692
+ [class.strct-cbx__opt--highlight]="activeIndex() === flat().length"
7391
7693
  role="option"
7392
- [attr.aria-selected]="opt.value === value()"
7393
- (mousedown)="select(opt, $event)"
7394
- (mousemove)="activeIndex.set(i)"
7694
+ aria-selected="false"
7695
+ (mousedown)="commitCustom($event)"
7696
+ (mousemove)="activeIndex.set(flat().length)"
7395
7697
  >
7396
- {{ opt.label }}
7698
+ <span class="strct-cbx__check" aria-hidden="true"></span>
7699
+ <strct-icon class="strct-cbx__opt-icon" strictName="plus" [size]="13" />
7700
+ <span class="strct-cbx__label">{{ customText() }} "{{ customQuery }}"</span>
7397
7701
  </div>
7398
- } @empty {
7399
- <div class="strct-cbx__empty">No matches</div>
7400
7702
  }
7401
7703
  }
7402
7704
  </div>
7403
7705
  }
7404
- `, isInline: true, styles: [".strct-cbx{position:relative;display:block;width:100%}.strct-cbx__field{position:relative}.strct-cbx__input{padding-inline-end:30px}.strct-cbx__caret{position:absolute;right:9px;top:50%;transform:translateY(-50%);color:var(--t3);pointer-events:none}.strct-cbx__menu{z-index:200;max-height:220px;overflow-y:auto;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh)}.strct-cbx__opt{padding:7px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-cbx__opt--highlight{background:var(--bg-3)}.strct-cbx__opt--active{color:var(--acc)}.strct-cbx__opt--active.strct-cbx__opt--highlight{background:var(--acc-m)}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-cbx__skeleton{padding:9px 10px}.strct-cbx__skeleton-block{height:12px;background:var(--bg-3);border-radius:var(--radius-sm);animation:strct-skeleton-pulse 1.4s ease infinite}.strct-cbx__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
7706
+ `, isInline: true, styles: [".strct-cbx{position:relative;display:block;width:100%}.strct-cbx__field{position:relative}.strct-cbx__input{padding-inline-end:30px}.strct-cbx__field--multi{display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding-block:4px;cursor:text}.strct-cbx__field--multi:focus-within{outline:none;border-color:var(--acc50);box-shadow:0 0 0 3px var(--acc18);background:var(--bg-1)}.strct-cbx__input--bare{flex:1;min-width:60px;padding:2px 0;font:inherit;font-size:13px;color:var(--t1);background:none;border:none;outline:none}.strct-cbx__chip{display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 8px;font-size:12px;color:var(--t1);background:var(--bg-3);border:1px solid var(--b2);border-radius:var(--radius-sm)}.strct-cbx__chip-x{display:inline-flex;align-items:center;padding:2px;color:var(--t3);background:none;border:none;border-radius:3px;cursor:pointer}.strct-cbx__chip-x:hover{color:var(--t1);background:var(--bg-4)}.strct-cbx__clear{position:absolute;right:26px;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;padding:2px;color:var(--t3);background:none;border:none;border-radius:3px;cursor:pointer}.strct-cbx__clear:hover{color:var(--t1)}.strct-cbx__caret{position:absolute;right:9px;top:50%;transform:translateY(-50%);color:var(--t3);pointer-events:none}.strct-cbx__menu{z-index:200;max-height:240px;overflow-y:auto;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh)}.strct-cbx__group{padding:7px 10px 3px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--t3)}.strct-cbx__opt{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-cbx__opt--highlight{background:var(--bg-3)}.strct-cbx__opt--selected{font-weight:600;background:var(--acc-s)}.strct-cbx__opt--selected.strct-cbx__opt--highlight{background:var(--acc-m)}.strct-cbx__opt[aria-disabled=true]{color:var(--t4);cursor:default}.strct-cbx__check{display:inline-flex;width:14px;flex:none;color:var(--acc)}.strct-cbx__opt-icon{display:inline-flex;flex:none;color:var(--t3)}.strct-cbx__opt--selected .strct-cbx__opt-icon{color:var(--t1)}.strct-cbx__opt-text{display:flex;flex-direction:column;min-width:0}.strct-cbx__opt-desc{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;font-weight:400;color:var(--t3)}.strct-cbx__opt--custom{border-top:1px solid var(--b2);border-radius:0 0 5px 5px;color:var(--t2)}.strct-cbx__chip-icon{display:inline-flex;color:var(--t3)}.strct-cbx__label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-cbx__match{font-weight:700;color:var(--acc)}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-cbx__skeleton{padding:9px 10px}.strct-cbx__skeleton-block{height:12px;background:var(--bg-3);border-radius:var(--radius-sm);animation:strct-skeleton-pulse 1.4s ease infinite}.strct-cbx__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
7405
7707
  }
7406
7708
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctCombobox, decorators: [{
7407
7709
  type: Component,
7408
7710
  args: [{ selector: 'strct-combobox', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon, StrctOverlay], providers: [
7409
7711
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StrctCombobox), multi: true },
7410
7712
  ], template: `
7411
- <div #field class="strct-cbx__field">
7713
+ <div
7714
+ #field
7715
+ class="strct-cbx__field"
7716
+ [class.strct-control]="multiple()"
7717
+ [class.strct-cbx__field--multi]="multiple()"
7718
+ (click)="focusInput()"
7719
+ >
7720
+ @if (multiple()) {
7721
+ @for (v of values(); track $index) {
7722
+ <span class="strct-cbx__chip">
7723
+ @if (optionOf(v)?.icon; as chipIcon) {
7724
+ <strct-icon class="strct-cbx__chip-icon" [name]="chipIcon" [size]="11" />
7725
+ }
7726
+ {{ labelOf(v) }}
7727
+ <button
7728
+ type="button"
7729
+ class="strct-cbx__chip-x"
7730
+ [attr.aria-label]="removeLabel() + ' ' + labelOf(v)"
7731
+ [disabled]="isDisabled()"
7732
+ (click)="removeValue(v, $event)"
7733
+ >
7734
+ <strct-icon strictName="close" [size]="10" [strokeWidth]="1.8" />
7735
+ </button>
7736
+ </span>
7737
+ }
7738
+ }
7412
7739
  <input
7413
7740
  #input
7414
7741
  type="text"
7415
- class="strct-control strct-cbx__input"
7742
+ class="strct-cbx__input"
7743
+ [class.strct-control]="!multiple()"
7744
+ [class.strct-cbx__input--bare]="multiple()"
7416
7745
  role="combobox"
7417
7746
  autocomplete="off"
7418
7747
  [attr.aria-expanded]="open()"
7419
7748
  [attr.aria-controls]="listId"
7420
- [attr.aria-activedescendant]="
7421
- open() && filtered().length ? listId + '-' + activeIndex() : null
7422
- "
7423
- [placeholder]="placeholder()"
7749
+ [attr.aria-activedescendant]="open() && navCount() ? listId + '-' + activeIndex() : null"
7750
+ [placeholder]="multiple() && values().length ? '' : placeholder()"
7424
7751
  [value]="query()"
7425
7752
  [disabled]="isDisabled()"
7426
7753
  (focus)="openList()"
7754
+ (click)="openList()"
7427
7755
  (input)="onType($event)"
7428
7756
  (keydown)="onKeydown($event)"
7429
7757
  (blur)="onTouched()"
7430
7758
  />
7431
- <strct-icon class="strct-cbx__caret" name="chevronDown" [size]="14" />
7759
+ @if (clearable() && hasSelection() && !isDisabled()) {
7760
+ <button
7761
+ type="button"
7762
+ class="strct-cbx__clear"
7763
+ [attr.aria-label]="clearLabel()"
7764
+ (click)="clear($event)"
7765
+ >
7766
+ <strct-icon strictName="close" [size]="12" [strokeWidth]="1.6" />
7767
+ </button>
7768
+ }
7769
+ <strct-icon class="strct-cbx__caret" strictName="chevronDown" [size]="14" />
7432
7770
  </div>
7433
7771
  @if (open()) {
7434
7772
  <div
7435
7773
  class="strct-cbx__menu"
7436
7774
  role="listbox"
7437
7775
  [id]="listId"
7776
+ [attr.aria-multiselectable]="multiple() || null"
7438
7777
  [strctOverlay]="field"
7439
7778
  strctOverlayPlacement="bottom-start"
7440
7779
  [strctOverlayMatchWidth]="true"
7780
+ (mousedown)="$event.preventDefault()"
7441
7781
  >
7442
7782
  @if (loading()) {
7443
7783
  <div class="strct-cbx__skeleton">
7444
7784
  <div class="strct-cbx__skeleton-block"></div>
7445
7785
  </div>
7446
7786
  } @else {
7447
- @for (opt of filtered(); track opt.value; let i = $index) {
7787
+ @for (row of rows(); track row.key) {
7788
+ @if (row.header !== undefined) {
7789
+ <div class="strct-cbx__group" role="presentation">{{ row.header }}</div>
7790
+ } @else {
7791
+ <div
7792
+ class="strct-cbx__opt"
7793
+ [id]="listId + '-' + row.index"
7794
+ [class.strct-cbx__opt--selected]="isSelected(row.opt!.value)"
7795
+ [class.strct-cbx__opt--highlight]="row.index === activeIndex()"
7796
+ role="option"
7797
+ [attr.aria-selected]="isSelected(row.opt!.value)"
7798
+ [attr.aria-disabled]="row.opt!.disabled || null"
7799
+ (mousedown)="select(row.opt!, $event)"
7800
+ (mousemove)="!row.opt!.disabled && activeIndex.set(row.index!)"
7801
+ >
7802
+ <span class="strct-cbx__check" aria-hidden="true">
7803
+ @if (isSelected(row.opt!.value)) {
7804
+ <strct-icon strictName="check" [size]="12" [strokeWidth]="1.8" />
7805
+ }
7806
+ </span>
7807
+ @if (row.opt!.icon) {
7808
+ <strct-icon class="strct-cbx__opt-icon" [name]="row.opt!.icon" [size]="14" />
7809
+ }
7810
+ @let seg = segments(row.opt!.label);
7811
+ <span class="strct-cbx__opt-text">
7812
+ <span class="strct-cbx__label"
7813
+ >{{ seg.pre }}<span class="strct-cbx__match">{{ seg.match }}</span
7814
+ >{{ seg.post }}</span
7815
+ >
7816
+ @if (row.opt!.description) {
7817
+ <span class="strct-cbx__opt-desc">{{ row.opt!.description }}</span>
7818
+ }
7819
+ </span>
7820
+ </div>
7821
+ }
7822
+ } @empty {
7823
+ @if (!customRow()) {
7824
+ <div class="strct-cbx__empty">{{ emptyText() }}</div>
7825
+ }
7826
+ }
7827
+ @if (customRow(); as customQuery) {
7448
7828
  <div
7449
- class="strct-cbx__opt"
7450
- [id]="listId + '-' + i"
7451
- [class.strct-cbx__opt--active]="opt.value === value()"
7452
- [class.strct-cbx__opt--highlight]="i === activeIndex()"
7829
+ class="strct-cbx__opt strct-cbx__opt--custom"
7830
+ [id]="listId + '-' + flat().length"
7831
+ [class.strct-cbx__opt--highlight]="activeIndex() === flat().length"
7453
7832
  role="option"
7454
- [attr.aria-selected]="opt.value === value()"
7455
- (mousedown)="select(opt, $event)"
7456
- (mousemove)="activeIndex.set(i)"
7833
+ aria-selected="false"
7834
+ (mousedown)="commitCustom($event)"
7835
+ (mousemove)="activeIndex.set(flat().length)"
7457
7836
  >
7458
- {{ opt.label }}
7837
+ <span class="strct-cbx__check" aria-hidden="true"></span>
7838
+ <strct-icon class="strct-cbx__opt-icon" strictName="plus" [size]="13" />
7839
+ <span class="strct-cbx__label">{{ customText() }} "{{ customQuery }}"</span>
7459
7840
  </div>
7460
- } @empty {
7461
- <div class="strct-cbx__empty">No matches</div>
7462
7841
  }
7463
7842
  }
7464
7843
  </div>
7465
7844
  }
7466
- `, host: { class: 'strct-cbx' }, styles: [".strct-cbx{position:relative;display:block;width:100%}.strct-cbx__field{position:relative}.strct-cbx__input{padding-inline-end:30px}.strct-cbx__caret{position:absolute;right:9px;top:50%;transform:translateY(-50%);color:var(--t3);pointer-events:none}.strct-cbx__menu{z-index:200;max-height:220px;overflow-y:auto;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh)}.strct-cbx__opt{padding:7px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-cbx__opt--highlight{background:var(--bg-3)}.strct-cbx__opt--active{color:var(--acc)}.strct-cbx__opt--active.strct-cbx__opt--highlight{background:var(--acc-m)}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-cbx__skeleton{padding:9px 10px}.strct-cbx__skeleton-block{height:12px;background:var(--bg-3);border-radius:var(--radius-sm);animation:strct-skeleton-pulse 1.4s ease infinite}.strct-cbx__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"] }]
7467
- }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], onDocClick: [{
7845
+ `, host: { class: 'strct-cbx' }, styles: [".strct-cbx{position:relative;display:block;width:100%}.strct-cbx__field{position:relative}.strct-cbx__input{padding-inline-end:30px}.strct-cbx__field--multi{display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding-block:4px;cursor:text}.strct-cbx__field--multi:focus-within{outline:none;border-color:var(--acc50);box-shadow:0 0 0 3px var(--acc18);background:var(--bg-1)}.strct-cbx__input--bare{flex:1;min-width:60px;padding:2px 0;font:inherit;font-size:13px;color:var(--t1);background:none;border:none;outline:none}.strct-cbx__chip{display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 8px;font-size:12px;color:var(--t1);background:var(--bg-3);border:1px solid var(--b2);border-radius:var(--radius-sm)}.strct-cbx__chip-x{display:inline-flex;align-items:center;padding:2px;color:var(--t3);background:none;border:none;border-radius:3px;cursor:pointer}.strct-cbx__chip-x:hover{color:var(--t1);background:var(--bg-4)}.strct-cbx__clear{position:absolute;right:26px;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;padding:2px;color:var(--t3);background:none;border:none;border-radius:3px;cursor:pointer}.strct-cbx__clear:hover{color:var(--t1)}.strct-cbx__caret{position:absolute;right:9px;top:50%;transform:translateY(-50%);color:var(--t3);pointer-events:none}.strct-cbx__menu{z-index:200;max-height:240px;overflow-y:auto;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh)}.strct-cbx__group{padding:7px 10px 3px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--t3)}.strct-cbx__opt{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-cbx__opt--highlight{background:var(--bg-3)}.strct-cbx__opt--selected{font-weight:600;background:var(--acc-s)}.strct-cbx__opt--selected.strct-cbx__opt--highlight{background:var(--acc-m)}.strct-cbx__opt[aria-disabled=true]{color:var(--t4);cursor:default}.strct-cbx__check{display:inline-flex;width:14px;flex:none;color:var(--acc)}.strct-cbx__opt-icon{display:inline-flex;flex:none;color:var(--t3)}.strct-cbx__opt--selected .strct-cbx__opt-icon{color:var(--t1)}.strct-cbx__opt-text{display:flex;flex-direction:column;min-width:0}.strct-cbx__opt-desc{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;font-weight:400;color:var(--t3)}.strct-cbx__opt--custom{border-top:1px solid var(--b2);border-radius:0 0 5px 5px;color:var(--t2)}.strct-cbx__chip-icon{display:inline-flex;color:var(--t3)}.strct-cbx__label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-cbx__match{font-weight:700;color:var(--acc)}@keyframes strct-skeleton-pulse{0%,to{opacity:.4}50%{opacity:.7}}.strct-cbx__skeleton{padding:9px 10px}.strct-cbx__skeleton-block{height:12px;background:var(--bg-3);border-radius:var(--radius-sm);animation:strct-skeleton-pulse 1.4s ease infinite}.strct-cbx__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"] }]
7846
+ }], propDecorators: { inputEl: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], removeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "removeLabel", required: false }] }], allowCustomValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomValue", required: false }] }], customText: [{ type: i0.Input, args: [{ isSignal: true, alias: "customText", required: false }] }], onDocClick: [{
7847
+ type: HostListener,
7848
+ args: ['document:click', ['$event']]
7849
+ }] } });
7850
+
7851
+ let selectCounter = 0;
7852
+ /**
7853
+ * Select-only combobox (APG pattern): a real button trigger wearing the shared
7854
+ * `.strct-control` look, opening a token-styled listbox — so the option list
7855
+ * matches the theme instead of the OS popup a native `<select>` shows.
7856
+ * CVA-compatible; options are non-filterable (reach for `strct-combobox` when
7857
+ * the list needs typing to narrow).
7858
+ *
7859
+ * <strct-field label="Region">
7860
+ * <strct-select [options]="regions" [(ngModel)]="region" placeholder="Pick a region" />
7861
+ * </strct-field>
7862
+ *
7863
+ * Keyboard follows the native select: ArrowDown/Up and Enter/Space open,
7864
+ * arrows move (skipping disabled options), Home/End jump, typing jumps to the
7865
+ * matching label (typeahead), Enter/Space commit, Escape/Tab close without
7866
+ * committing. The selected option carries a leading ✓ and gets the highlight
7867
+ * when reopening — the same select ergonomics as `strct-dropdown-item
7868
+ * [selected]`.
7869
+ */
7870
+ class StrctSelect {
7871
+ host = inject(ElementRef);
7872
+ listId = `strct-sel-${++selectCounter}`;
7873
+ /** Available options (set `disabled: true` on an option to gray it out). */
7874
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
7875
+ /** Muted text shown while no value is selected (localizable). */
7876
+ placeholder = input('Select…', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
7877
+ /** Accessible name of the listbox (localizable). */
7878
+ listLabel = input('', ...(ngDevMode ? [{ debugName: "listLabel" }] : /* istanbul ignore next */ []));
7879
+ /** Text shown when `options` is empty (localizable). */
7880
+ emptyText = input('No options', ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
7881
+ /** Static disable flag (forms also drive it via setDisabledState). */
7882
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
7883
+ value = signal(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
7884
+ open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
7885
+ activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
7886
+ isDisabled = signal(false, ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
7887
+ selectedOption = computed(() => this.options().find((o) => o.value === this.value()), ...(ngDevMode ? [{ debugName: "selectedOption" }] : /* istanbul ignore next */ []));
7888
+ /** Typeahead buffer — clears half a second after the last keystroke. */
7889
+ typed = '';
7890
+ typedTimer = null;
7891
+ onChange = () => { };
7892
+ onTouched = () => { };
7893
+ toggle() {
7894
+ if (this.open())
7895
+ this.close();
7896
+ else
7897
+ this.openList();
7898
+ }
7899
+ openList() {
7900
+ if (this.isDisabled() || this.disabled())
7901
+ return;
7902
+ this.open.set(true);
7903
+ this.activeIndex.set(this.initialIndex());
7904
+ this.scrollActiveIntoView();
7905
+ }
7906
+ close() {
7907
+ this.open.set(false);
7908
+ }
7909
+ onKeydown(event) {
7910
+ const key = event.key;
7911
+ const opts = this.options();
7912
+ if (key === 'ArrowDown' || key === 'ArrowUp') {
7913
+ event.preventDefault();
7914
+ if (!this.open())
7915
+ return this.openList();
7916
+ this.move(key === 'ArrowDown' ? 1 : -1);
7917
+ }
7918
+ else if (key === 'Home' || key === 'End') {
7919
+ if (!this.open())
7920
+ return;
7921
+ event.preventDefault();
7922
+ this.moveTo(key === 'Home' ? 0 : opts.length - 1, key === 'Home' ? 1 : -1);
7923
+ }
7924
+ else if (key === 'Enter' || key === ' ') {
7925
+ event.preventDefault();
7926
+ if (!this.open())
7927
+ return this.openList();
7928
+ const opt = opts[this.activeIndex()];
7929
+ if (opt && !opt.disabled)
7930
+ this.commit(opt);
7931
+ }
7932
+ else if (key === 'Escape') {
7933
+ if (this.open()) {
7934
+ event.preventDefault();
7935
+ this.close();
7936
+ }
7937
+ }
7938
+ else if (key === 'Tab') {
7939
+ this.close();
7940
+ }
7941
+ else if (key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
7942
+ this.typeahead(key);
7943
+ }
7944
+ }
7945
+ /**
7946
+ * Native-select typeahead: letters accumulate into a prefix that jumps to
7947
+ * the matching label; repeating one letter cycles its matches instead.
7948
+ */
7949
+ typeahead(char) {
7950
+ const lower = char.toLowerCase();
7951
+ if (this.typedTimer)
7952
+ clearTimeout(this.typedTimer);
7953
+ this.typedTimer = setTimeout(() => (this.typed = ''), 500);
7954
+ const repeatCycle = this.typed.length > 0 && [...this.typed].every((c) => c === lower);
7955
+ this.typed += lower;
7956
+ const prefix = repeatCycle ? lower : this.typed;
7957
+ const opts = this.options();
7958
+ if (!opts.length)
7959
+ return;
7960
+ const wasOpen = this.open();
7961
+ if (!wasOpen)
7962
+ this.openList();
7963
+ const from = this.activeIndex();
7964
+ for (let step = repeatCycle || !wasOpen ? 1 : 0; step <= opts.length; step++) {
7965
+ const i = (from + step) % opts.length;
7966
+ const opt = opts[i];
7967
+ if (!opt.disabled && opt.label.toLowerCase().startsWith(prefix)) {
7968
+ this.activeIndex.set(i);
7969
+ this.scrollActiveIntoView();
7970
+ return;
7971
+ }
7972
+ }
7973
+ }
7974
+ move(delta) {
7975
+ const opts = this.options();
7976
+ if (!opts.length)
7977
+ return;
7978
+ let i = this.activeIndex();
7979
+ let guard = opts.length;
7980
+ do {
7981
+ i = (i + delta + opts.length) % opts.length;
7982
+ } while (opts[i].disabled && --guard > 0);
7983
+ this.activeIndex.set(i);
7984
+ this.scrollActiveIntoView();
7985
+ }
7986
+ /** Jump to `index`, walking `dir` past disabled options. */
7987
+ moveTo(index, dir) {
7988
+ const opts = this.options();
7989
+ let i = index;
7990
+ for (let n = 0; n < opts.length && opts[i]?.disabled; n++) {
7991
+ i = (i + dir + opts.length) % opts.length;
7992
+ }
7993
+ if (opts[i] && !opts[i].disabled) {
7994
+ this.activeIndex.set(i);
7995
+ this.scrollActiveIntoView();
7996
+ }
7997
+ }
7998
+ pick(opt, event) {
7999
+ event.preventDefault(); // keep focus on the trigger button
8000
+ if (opt.disabled)
8001
+ return;
8002
+ this.commit(opt);
8003
+ }
8004
+ commit(opt) {
8005
+ this.value.set(opt.value);
8006
+ this.close();
8007
+ this.onChange(opt.value);
8008
+ this.onTouched();
8009
+ }
8010
+ onBlur() {
8011
+ // Any mousedown inside the list preventDefaults, so focus never leaves
8012
+ // the trigger mid-interaction; outside clicks close via onDocClick and
8013
+ // Tab closes in onKeydown — blur only marks the control touched.
8014
+ this.onTouched();
8015
+ }
8016
+ /** Highlight starts on the selected option — or the first enabled one. */
8017
+ initialIndex() {
8018
+ const opts = this.options();
8019
+ const sel = opts.findIndex((o) => o.value === this.value() && !o.disabled);
8020
+ if (sel >= 0)
8021
+ return sel;
8022
+ const first = opts.findIndex((o) => !o.disabled);
8023
+ return first < 0 ? 0 : first;
8024
+ }
8025
+ scrollActiveIntoView() {
8026
+ setTimeout(() => {
8027
+ document
8028
+ .getElementById(`${this.listId}-${this.activeIndex()}`)
8029
+ ?.scrollIntoView({ block: 'nearest' });
8030
+ });
8031
+ }
8032
+ onDocClick(event) {
8033
+ if (this.open() && !this.host.nativeElement.contains(event.target)) {
8034
+ this.close();
8035
+ }
8036
+ }
8037
+ writeValue(value) {
8038
+ this.value.set(value);
8039
+ }
8040
+ registerOnChange(fn) {
8041
+ this.onChange = fn;
8042
+ }
8043
+ registerOnTouched(fn) {
8044
+ this.onTouched = fn;
8045
+ }
8046
+ setDisabledState(isDisabled) {
8047
+ this.isDisabled.set(isDisabled);
8048
+ }
8049
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
8050
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.16", type: StrctSelect, isStandalone: true, selector: "strct-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, listLabel: { classPropertyName: "listLabel", publicName: "listLabel", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocClick($event)" }, classAttribute: "strct-sel" }, providers: [
8051
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StrctSelect), multi: true },
8052
+ ], ngImport: i0, template: `
8053
+ <button
8054
+ #btn
8055
+ type="button"
8056
+ strctField
8057
+ class="strct-control strct-sel__btn"
8058
+ role="combobox"
8059
+ aria-haspopup="listbox"
8060
+ [attr.aria-expanded]="open()"
8061
+ [attr.aria-controls]="open() ? listId : null"
8062
+ [attr.aria-activedescendant]="open() ? listId + '-' + activeIndex() : null"
8063
+ [disabled]="isDisabled() || disabled()"
8064
+ (click)="toggle()"
8065
+ (keydown)="onKeydown($event)"
8066
+ (blur)="onBlur()"
8067
+ >
8068
+ <span class="strct-sel__value" [class.strct-sel__value--placeholder]="!selectedOption()">
8069
+ {{ selectedOption()?.label ?? placeholder() }}
8070
+ </span>
8071
+ <strct-icon class="strct-sel__caret" strictName="chevronDown" [size]="14" />
8072
+ </button>
8073
+ @if (open()) {
8074
+ <div
8075
+ class="strct-sel__list"
8076
+ role="listbox"
8077
+ [id]="listId"
8078
+ [attr.aria-label]="listLabel() || null"
8079
+ [strctOverlay]="btn"
8080
+ strctOverlayPlacement="bottom-start"
8081
+ [strctOverlayMatchWidth]="true"
8082
+ (mousedown)="$event.preventDefault()"
8083
+ >
8084
+ @for (opt of options(); track opt.value; let i = $index) {
8085
+ <div
8086
+ class="strct-sel__opt"
8087
+ [id]="listId + '-' + i"
8088
+ [class.strct-sel__opt--highlight]="i === activeIndex()"
8089
+ [class.strct-sel__opt--selected]="opt.value === value()"
8090
+ role="option"
8091
+ [attr.aria-selected]="opt.value === value()"
8092
+ [attr.aria-disabled]="opt.disabled || null"
8093
+ (mousedown)="pick(opt, $event)"
8094
+ (mousemove)="!opt.disabled && activeIndex.set(i)"
8095
+ >
8096
+ <span class="strct-sel__check" aria-hidden="true">
8097
+ @if (opt.value === value()) {
8098
+ <strct-icon strictName="check" [size]="12" [strokeWidth]="1.8" />
8099
+ }
8100
+ </span>
8101
+ {{ opt.label }}
8102
+ </div>
8103
+ } @empty {
8104
+ <div class="strct-sel__empty">{{ emptyText() }}</div>
8105
+ }
8106
+ </div>
8107
+ }
8108
+ `, isInline: true, styles: [".strct-sel{position:relative;display:block;width:100%}.strct-sel__btn{display:flex;align-items:center;gap:8px;text-align:start;cursor:pointer}.strct-sel__btn:disabled{cursor:not-allowed}.strct-sel__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-sel__value--placeholder{color:var(--t3)}.strct-sel__caret{flex:none;color:var(--t3)}.strct-sel__list{z-index:200;max-height:240px;overflow-y:auto;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh)}.strct-sel__opt{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-sel__opt--highlight{background:var(--bg-3)}.strct-sel__opt--selected{font-weight:600;background:var(--acc-s)}.strct-sel__opt--selected.strct-sel__opt--highlight{background:var(--acc-m)}.strct-sel__opt[aria-disabled=true]{color:var(--t4);cursor:default}.strct-sel__check{display:inline-flex;width:14px;flex:none;color:var(--acc)}.strct-sel__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
8109
+ }
8110
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: StrctSelect, decorators: [{
8111
+ type: Component,
8112
+ args: [{ selector: 'strct-select', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [StrctIcon, StrctOverlay], providers: [
8113
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StrctSelect), multi: true },
8114
+ ], template: `
8115
+ <button
8116
+ #btn
8117
+ type="button"
8118
+ strctField
8119
+ class="strct-control strct-sel__btn"
8120
+ role="combobox"
8121
+ aria-haspopup="listbox"
8122
+ [attr.aria-expanded]="open()"
8123
+ [attr.aria-controls]="open() ? listId : null"
8124
+ [attr.aria-activedescendant]="open() ? listId + '-' + activeIndex() : null"
8125
+ [disabled]="isDisabled() || disabled()"
8126
+ (click)="toggle()"
8127
+ (keydown)="onKeydown($event)"
8128
+ (blur)="onBlur()"
8129
+ >
8130
+ <span class="strct-sel__value" [class.strct-sel__value--placeholder]="!selectedOption()">
8131
+ {{ selectedOption()?.label ?? placeholder() }}
8132
+ </span>
8133
+ <strct-icon class="strct-sel__caret" strictName="chevronDown" [size]="14" />
8134
+ </button>
8135
+ @if (open()) {
8136
+ <div
8137
+ class="strct-sel__list"
8138
+ role="listbox"
8139
+ [id]="listId"
8140
+ [attr.aria-label]="listLabel() || null"
8141
+ [strctOverlay]="btn"
8142
+ strctOverlayPlacement="bottom-start"
8143
+ [strctOverlayMatchWidth]="true"
8144
+ (mousedown)="$event.preventDefault()"
8145
+ >
8146
+ @for (opt of options(); track opt.value; let i = $index) {
8147
+ <div
8148
+ class="strct-sel__opt"
8149
+ [id]="listId + '-' + i"
8150
+ [class.strct-sel__opt--highlight]="i === activeIndex()"
8151
+ [class.strct-sel__opt--selected]="opt.value === value()"
8152
+ role="option"
8153
+ [attr.aria-selected]="opt.value === value()"
8154
+ [attr.aria-disabled]="opt.disabled || null"
8155
+ (mousedown)="pick(opt, $event)"
8156
+ (mousemove)="!opt.disabled && activeIndex.set(i)"
8157
+ >
8158
+ <span class="strct-sel__check" aria-hidden="true">
8159
+ @if (opt.value === value()) {
8160
+ <strct-icon strictName="check" [size]="12" [strokeWidth]="1.8" />
8161
+ }
8162
+ </span>
8163
+ {{ opt.label }}
8164
+ </div>
8165
+ } @empty {
8166
+ <div class="strct-sel__empty">{{ emptyText() }}</div>
8167
+ }
8168
+ </div>
8169
+ }
8170
+ `, host: { class: 'strct-sel' }, styles: [".strct-sel{position:relative;display:block;width:100%}.strct-sel__btn{display:flex;align-items:center;gap:8px;text-align:start;cursor:pointer}.strct-sel__btn:disabled{cursor:not-allowed}.strct-sel__value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.strct-sel__value--placeholder{color:var(--t3)}.strct-sel__caret{flex:none;color:var(--t3)}.strct-sel__list{z-index:200;max-height:240px;overflow-y:auto;padding:4px;background:var(--bg-1);border:1px solid var(--b2);border-radius:7px;box-shadow:var(--shh)}.strct-sel__opt{display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1)}.strct-sel__opt--highlight{background:var(--bg-3)}.strct-sel__opt--selected{font-weight:600;background:var(--acc-s)}.strct-sel__opt--selected.strct-sel__opt--highlight{background:var(--acc-m)}.strct-sel__opt[aria-disabled=true]{color:var(--t4);cursor:default}.strct-sel__check{display:inline-flex;width:14px;flex:none;color:var(--acc)}.strct-sel__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"] }]
8171
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], listLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "listLabel", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], onDocClick: [{
7468
8172
  type: HostListener,
7469
8173
  args: ['document:click', ['$event']]
7470
8174
  }] } });
@@ -15992,5 +16696,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImpo
15992
16696
  * Generated bundle index. Do not edit.
15993
16697
  */
15994
16698
 
15995
- export { STRCT_ICONS, STRCT_ICON_GROUPS, STRCT_ICON_NAMES, STRCT_MASKS, STRCT_PALETTES, STRCT_RAW_ICONS, STRCT_TIME_RANGE_PRESETS, STRCT_WIZARD_DEFAULTS, StrctAccordion, StrctAccordionPanel, StrctAlert, StrctAnnouncer, StrctAvatar, StrctBadge, StrctBreadcrumb, StrctBreadcrumbItem, StrctButton, StrctButtonGroup, StrctBytesPipe, StrctCard, StrctCardBlock, StrctCardFooter, StrctCardHeader, StrctCascadeHost, StrctCascadeNode, StrctCascadeSelect, StrctCellDef, StrctCellStatus, StrctChart, StrctCheckbox, StrctChips, StrctCode, StrctColorPicker, StrctCombobox, StrctCommandPalette, StrctContextMenu, StrctContextMenuTrigger, StrctCopy, StrctDatagrid, StrctDatagridActionBar, StrctDatepicker, StrctDesc, StrctDescriptionList, StrctDiff, StrctDivider, StrctDonut, StrctDrawer, StrctDrawerFooter, StrctDropdown, StrctDropdownDivider, StrctDropdownItem, StrctDropdownTrigger, StrctDurationPipe, StrctEmptyState, StrctField, StrctFile, StrctFilterBar, StrctFlow, StrctFooter, StrctGauge, StrctHeader, StrctHero, StrctHotkeysHelp, StrctHotkeysService, StrctIcon, StrctInput, StrctInputMask, StrctInputOtp, StrctKbd, StrctKnob, StrctLogViewer, StrctLogin, StrctMenuPanel, StrctMenuService, StrctMenubar, StrctMetricTile, StrctModal, StrctNav, StrctNavItem, StrctOverlay, StrctPageHeader, StrctPageHeaderActions, StrctPageHeaderCrumbs, StrctPagination, StrctPassword, StrctProgress, StrctRadio, StrctRadioGroup, StrctRail, StrctRange, StrctRatePipe, StrctRating, StrctReorder, StrctReorderItem, StrctRowDetailDef, StrctSearchbox, StrctSectionMenu, StrctSegmented, StrctShell, StrctShellService, StrctSiPipe, StrctSignpost, StrctSkeleton, StrctSparkline, StrctSpeedDial, StrctSpinner, StrctSplitButton, StrctSplitter, StrctStack, StrctStackItem, StrctStep, StrctSubmenu, StrctTab, StrctTable, StrctTabs, StrctTag, StrctThemeService, StrctThemeSwitcher, StrctTimeRangePicker, StrctTimeline, StrctTimelineItem, StrctToastOutlet, StrctToastService, StrctToggle, StrctTooltip, StrctTour, StrctTransfer, StrctTree, StrctTreeNode, StrctVerticalNav, StrctWatermark, StrctWizard, StrctWizardAside, parseAnsi, provideStrctWizardDefaults, registerStrctIcon, strctComputeDiff, strctFormatBytes, strctFormatDuration, strctFormatRate, strctFormatSi, strctValidationIcon, strctValidationTone };
16699
+ export { STRCT_ICONS, STRCT_ICON_GROUPS, STRCT_ICON_NAMES, STRCT_MASKS, STRCT_PALETTES, STRCT_RAW_ICONS, STRCT_TIME_RANGE_PRESETS, STRCT_WIZARD_DEFAULTS, StrctAccordion, StrctAccordionPanel, StrctAlert, StrctAnnouncer, StrctAvatar, StrctBadge, StrctBreadcrumb, StrctBreadcrumbItem, StrctButton, StrctButtonGroup, StrctBytesPipe, StrctCard, StrctCardBlock, StrctCardFooter, StrctCardHeader, StrctCascadeHost, StrctCascadeNode, StrctCascadeSelect, StrctCellDef, StrctCellStatus, StrctChart, StrctCheckbox, StrctChips, StrctCode, StrctColorPicker, StrctCombobox, StrctCommandPalette, StrctContextMenu, StrctContextMenuTrigger, StrctCopy, StrctDatagrid, StrctDatagridActionBar, StrctDatepicker, StrctDesc, StrctDescriptionList, StrctDiff, StrctDivider, StrctDonut, StrctDrawer, StrctDrawerFooter, StrctDropdown, StrctDropdownDivider, StrctDropdownItem, StrctDropdownTrigger, StrctDurationPipe, StrctEmptyState, StrctField, StrctFile, StrctFilterBar, StrctFlow, StrctFooter, StrctGauge, StrctHeader, StrctHero, StrctHotkeysHelp, StrctHotkeysService, StrctIcon, StrctInput, StrctInputMask, StrctInputOtp, StrctKbd, StrctKnob, StrctLogViewer, StrctLogin, StrctMenuPanel, StrctMenuService, StrctMenubar, StrctMetricTile, StrctModal, StrctNav, StrctNavItem, StrctOverlay, StrctPageHeader, StrctPageHeaderActions, StrctPageHeaderCrumbs, StrctPagination, StrctPassword, StrctProgress, StrctRadio, StrctRadioGroup, StrctRail, StrctRange, StrctRatePipe, StrctRating, StrctReorder, StrctReorderItem, StrctRowDetailDef, StrctSearchbox, StrctSectionMenu, StrctSegmented, StrctSelect, StrctShell, StrctShellService, StrctSiPipe, StrctSignpost, StrctSkeleton, StrctSparkline, StrctSpeedDial, StrctSpinner, StrctSplitButton, StrctSplitter, StrctStack, StrctStackItem, StrctStep, StrctSubmenu, StrctTab, StrctTable, StrctTabs, StrctTag, StrctThemeService, StrctThemeSwitcher, StrctTimeRangePicker, StrctTimeline, StrctTimelineItem, StrctToastOutlet, StrctToastService, StrctToggle, StrctTooltip, StrctTour, StrctTransfer, StrctTree, StrctTreeNode, StrctVerticalNav, StrctWatermark, StrctWizard, StrctWizardAside, parseAnsi, provideStrctWizardDefaults, registerStrctIcon, strctComputeDiff, strctFormatBytes, strctFormatDuration, strctFormatRate, strctFormatSi, strctValidationIcon, strctValidationTone };
15996
16700
  //# sourceMappingURL=akcelik-strct.mjs.map