@besaitech/ng-design-system 0.0.58 → 0.0.60

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, input, booleanAttribute, computed, ChangeDetectionStrategy, Component, isDevMode, effect, numberAttribute, output, signal, forwardRef, viewChild, viewChildren, afterRenderEffect, model, ElementRef, Directive, Injector, contentChild, untracked, afterNextRender, TemplateRef, linkedSignal, DestroyRef, Injectable, contentChildren } from '@angular/core';
2
+ import { inject, input, booleanAttribute, computed, ChangeDetectionStrategy, Component, isDevMode, effect, numberAttribute, output, signal, forwardRef, viewChild, viewChildren, afterRenderEffect, model, ElementRef, Directive, Injector, contentChild, untracked, afterNextRender, TemplateRef, linkedSignal, DestroyRef, afterEveryRender, Injectable, contentChildren } from '@angular/core';
3
3
  import { DomSanitizer } from '@angular/platform-browser';
4
4
  import * as i1 from '@angular/forms';
5
5
  import { NG_VALUE_ACCESSOR, NG_VALIDATORS, FormsModule } from '@angular/forms';
@@ -107,6 +107,9 @@ const SDS_ICONS = {
107
107
  'arrow-right': '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
108
108
  'refresh-cw': '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/>',
109
109
  'loader-circle': '<path d="M21 12a9 9 0 1 1-6.219-8.56"/>',
110
+ // Added for sds-address-autocomplete: the "Use my current location" row and its failure notice (Lucide).
111
+ 'locate-fixed': '<line x1="2" x2="5" y1="12" y2="12"/><line x1="19" x2="22" y1="12" y2="12"/><line x1="12" x2="12" y1="2" y2="5"/><line x1="12" x2="12" y1="19" y2="22"/><circle cx="12" cy="12" r="7"/><circle cx="12" cy="12" r="3"/>',
112
+ 'locate-off': '<path d="M12 19v3"/><path d="M12 2v3"/><path d="M18.89 13.24a7 7 0 0 0-8.13-8.13"/><path d="M19 12h3"/><path d="M2 12h3"/><path d="m2 2 20 20"/><path d="M7.05 7.05a7 7 0 0 0 9.9 9.9"/>',
110
113
  bug: '<path d="M12 20v-9"/><path d="M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z"/><path d="M14.12 3.88 16 2"/><path d="M21 21a4 4 0 0 0-3.81-4"/><path d="M21 5a4 4 0 0 1-3.55 3.97"/><path d="M22 13h-4"/><path d="M3 21a4 4 0 0 1 3.81-4"/><path d="M3 5a4 4 0 0 0 3.55 3.97"/><path d="M6 13H2"/><path d="m8 2 1.88 1.88"/><path d="M9 7.13V6a3 3 0 1 1 6 0v1.13"/>',
111
114
  'scroll-text': '<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>',
112
115
  // Added for the dark/light switcher (sds-theme-toggle) and the command palette (sds-search-command).
@@ -291,7 +294,7 @@ const VARIANTS$2 = {
291
294
  'dark:hover:bg-white/10 dark:active:bg-white/15',
292
295
  danger: 'bg-danger text-white shadow-sm hover:brightness-95 active:brightness-90',
293
296
  };
294
- const SIZES$c = {
297
+ const SIZES$d = {
295
298
  sm: 'h-8 px-3 text-sm',
296
299
  md: 'h-10 px-4 text-sm',
297
300
  lg: 'h-12 px-5 text-base',
@@ -351,7 +354,7 @@ class SdsButton {
351
354
  BASE,
352
355
  this.shape() === 'pill' ? 'rounded-full' : 'rounded-control',
353
356
  VARIANTS$2[this.variant()],
354
- SIZES$c[size],
357
+ SIZES$d[size],
355
358
  this.iconOnly() ? ICON_ONLY[size] : '',
356
359
  this.block() ? 'w-full' : '',
357
360
  ]
@@ -403,8 +406,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
403
406
  `, styles: [".sds-spinner{width:1em;height:1em;border:2px solid currentColor;border-right-color:transparent;border-radius:9999px;animation:sds-spin .6s linear infinite}@keyframes sds-spin{to{transform:rotate(360deg)}}\n"] }]
404
407
  }], ctorParameters: () => [], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
405
408
 
406
- let nextId$n = 0;
407
- const SIZES$b = {
409
+ let nextId$o = 0;
410
+ const SIZES$c = {
408
411
  sm: 'h-9 text-sm',
409
412
  md: 'h-10 text-sm',
410
413
  lg: 'h-12 text-base',
@@ -416,7 +419,7 @@ const SIZES$b = {
416
419
  * sidebar search field).
417
420
  */
418
421
  class SdsInput {
419
- autoId = `sds-input-${nextId$n++}`;
422
+ autoId = `sds-input-${nextId$o++}`;
420
423
  /**
421
424
  * `id` of the native `<input>` — for a `<label for>` outside the component, or to focus the field with
422
425
  * `document.getElementById(id).focus()` (after adding a row, say). Empty (default) generates one. Set it
@@ -520,7 +523,7 @@ class SdsInput {
520
523
  }, ...(ngDevMode ? [{ debugName: "fieldPadding" }] : []));
521
524
  wrapperClasses = computed(() => {
522
525
  const danger = !!this.error() || this.invalid();
523
- const parts = [SIZES$b[this.size()], this.pill() ? 'rounded-full' : 'rounded-control'];
526
+ const parts = [SIZES$c[this.size()], this.pill() ? 'rounded-full' : 'rounded-control'];
524
527
  if (this.isDisabled())
525
528
  parts.push('opacity-60 cursor-not-allowed');
526
529
  if (this.filled()) {
@@ -776,7 +779,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
776
779
  `, styles: ["input:-webkit-autofill,input:-webkit-autofill:hover,input:-webkit-autofill:focus,input:-webkit-autofill:active{transition:background-color 100000s ease-in-out 0s;-webkit-text-fill-color:currentColor;caret-color:currentColor}\n"] }]
777
780
  }], propDecorators: { inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], leadingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadingIcon", required: false }] }], trailingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingIcon", required: false }] }], trailingAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingAction", required: false }] }], trailingActionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingActionLabel", required: false }] }], trailingActionPressed: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingActionPressed", required: false }] }], trailingActionClick: [{ type: i0.Output, args: ["trailingActionClick"] }] } });
778
781
 
779
- let nextId$m = 0;
782
+ let nextId$n = 0;
780
783
  const HEIGHTS = {
781
784
  sm: 'h-9',
782
785
  md: 'h-10',
@@ -804,7 +807,7 @@ const TEXT = {
804
807
  * with "." ("1.234" in nl-BE) stays a thousands separator.
805
808
  */
806
809
  class SdsInputNumber {
807
- inputId = `sds-input-number-${nextId$m++}`;
810
+ inputId = `sds-input-number-${nextId$n++}`;
808
811
  fieldRef = viewChild('field', ...(ngDevMode ? [{ debugName: "fieldRef" }] : []));
809
812
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
810
813
  /** Accessible name used when no visible [label] is provided. */
@@ -1588,8 +1591,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
1588
1591
  }]
1589
1592
  }], ctorParameters: () => [], propDecorators: { fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedby", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], currencyDisplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "currencyDisplay", required: false }] }], useGrouping: [{ type: i0.Input, args: [{ isSignal: true, alias: "useGrouping", required: false }] }], minFractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "minFractionDigits", required: false }] }], maxFractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFractionDigits", required: false }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], suffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffix", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], showButtons: [{ type: i0.Input, args: [{ isSignal: true, alias: "showButtons", required: false }] }], buttonLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonLayout", required: false }] }], incrementIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "incrementIcon", required: false }] }], decrementIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "decrementIcon", required: false }] }], allowEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowEmpty", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], incrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "incrementLabel", required: false }] }], decrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "decrementLabel", required: false }] }] } });
1590
1593
 
1591
- let nextId$l = 0;
1592
- const SIZES$a = {
1594
+ let nextId$m = 0;
1595
+ const SIZES$b = {
1593
1596
  sm: 'h-9 text-sm',
1594
1597
  md: 'h-10 text-sm',
1595
1598
  lg: 'h-12 text-base',
@@ -1629,7 +1632,7 @@ function parseTime(value) {
1629
1632
  * <sds-time-input [ariaLabel]="'Closes at'" [ngModel]="end()" (ngModelChange)="end.set($event)" clearable />
1630
1633
  */
1631
1634
  class SdsTimeInput {
1632
- fieldId = `sds-time-input-${nextId$l++}`;
1635
+ fieldId = `sds-time-input-${nextId$m++}`;
1633
1636
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
1634
1637
  /** Accessible name of the field when there is no visible [label]. */
1635
1638
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -1676,7 +1679,7 @@ class SdsTimeInput {
1676
1679
  return null;
1677
1680
  }, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
1678
1681
  wrapperClasses = computed(() => {
1679
- const parts = [SIZES$a[this.size()], 'rounded-control'];
1682
+ const parts = [SIZES$b[this.size()], 'rounded-control'];
1680
1683
  if (this.isDisabled())
1681
1684
  parts.push('opacity-60 cursor-not-allowed');
1682
1685
  if (this.appearance() === 'filled') {
@@ -2158,8 +2161,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
2158
2161
  }]
2159
2162
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], showIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showIcon", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], withSeconds: [{ type: i0.Input, args: [{ isSignal: true, alias: "withSeconds", required: false }] }], placeholderText: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderText", required: false }] }], hoursLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoursLabel", required: false }] }], minutesLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "minutesLabel", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], hoursEl: [{ type: i0.ViewChild, args: ['hoursEl', { isSignal: true }] }], minutesEl: [{ type: i0.ViewChild, args: ['minutesEl', { isSignal: true }] }] } });
2160
2163
 
2161
- let nextId$k = 0;
2162
- const SIZES$9 = {
2164
+ let nextId$l = 0;
2165
+ const SIZES$a = {
2163
2166
  sm: 'min-h-9 text-sm',
2164
2167
  md: 'min-h-10 text-sm',
2165
2168
  lg: 'min-h-12 text-base',
@@ -2178,7 +2181,7 @@ const SIZES$9 = {
2178
2181
  * `<sds-localized-input [languages]="['fr','nl','en']" [(ngModel)]="title" />`.
2179
2182
  */
2180
2183
  class SdsLocalizedInput {
2181
- inputId = `sds-localized-input-${nextId$k++}`;
2184
+ inputId = `sds-localized-input-${nextId$l++}`;
2182
2185
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
2183
2186
  hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
2184
2187
  placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
@@ -2203,7 +2206,7 @@ class SdsLocalizedInput {
2203
2206
  cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
2204
2207
  isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
2205
2208
  wrapperClasses = computed(() => {
2206
- const parts = [SIZES$9[this.size()]];
2209
+ const parts = [SIZES$a[this.size()]];
2207
2210
  parts.push(this.isDisabled()
2208
2211
  ? 'border-border opacity-60 cursor-not-allowed'
2209
2212
  : 'border-border focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25');
@@ -2434,8 +2437,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
2434
2437
  }]
2435
2438
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], languages: [{ type: i0.Input, args: [{ isSignal: true, alias: "languages", required: false }] }], currentLanguage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentLanguage", required: false }] }], multiline: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiline", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], languageSwitcherLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "languageSwitcherLabel", required: false }] }], untranslatedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "untranslatedLabel", required: false }] }] } });
2436
2439
 
2437
- let nextId$j = 0;
2438
- const SIZES$8 = {
2440
+ let nextId$k = 0;
2441
+ const SIZES$9 = {
2439
2442
  md: 'h-12 w-11 text-lg',
2440
2443
  lg: 'h-14 w-12 text-xl',
2441
2444
  };
@@ -2451,7 +2454,7 @@ const SIZES$8 = {
2451
2454
  * first box and have it land correctly.
2452
2455
  */
2453
2456
  class SdsOtpInput {
2454
- groupId = `sds-otp-${nextId$j++}`;
2457
+ groupId = `sds-otp-${nextId$k++}`;
2455
2458
  /** How many boxes / characters the code has. */
2456
2459
  length = input(6, ...(ngDevMode ? [{ debugName: "length", transform: numberAttribute }] : [{ transform: numberAttribute }]));
2457
2460
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
@@ -2487,7 +2490,7 @@ class SdsOtpInput {
2487
2490
  return null;
2488
2491
  }, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
2489
2492
  cellClasses = computed(() => {
2490
- const parts = ['rounded-control border', SIZES$8[this.size()]];
2493
+ const parts = ['rounded-control border', SIZES$9[this.size()]];
2491
2494
  parts.push(this.error()
2492
2495
  ? 'border-danger focus:border-danger focus:ring-2 focus:ring-danger/25'
2493
2496
  : 'border-border text-ink focus:border-brand-500 focus:ring-2 focus:ring-brand-500/25');
@@ -2751,8 +2754,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
2751
2754
  }]
2752
2755
  }], ctorParameters: () => [], propDecorators: { length: [{ type: i0.Input, args: [{ isSignal: true, alias: "length", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], inputMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputMode", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], autoFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocus", required: false }] }], cellLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellLabel", required: false }] }], completed: [{ type: i0.Output, args: ["completed"] }], cells: [{ type: i0.ViewChildren, args: ['cell', { isSignal: true }] }] } });
2753
2756
 
2754
- let nextId$i = 0;
2755
- const SIZES$7 = {
2757
+ let nextId$j = 0;
2758
+ const SIZES$8 = {
2756
2759
  sm: 'text-sm px-3 py-2',
2757
2760
  md: 'text-sm px-3.5 py-2.5',
2758
2761
  lg: 'text-base px-4 py-3',
@@ -2763,7 +2766,7 @@ const SIZES$7 = {
2763
2766
  * [(ngModel)].
2764
2767
  */
2765
2768
  class SdsTextarea {
2766
- fieldId = `sds-textarea-${nextId$i++}`;
2769
+ fieldId = `sds-textarea-${nextId$j++}`;
2767
2770
  field = viewChild('field', ...(ngDevMode ? [{ debugName: "field" }] : []));
2768
2771
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
2769
2772
  /** Accessible name used when no visible [label] is provided. */
@@ -2820,7 +2823,7 @@ class SdsTextarea {
2820
2823
  fieldClasses = computed(() => {
2821
2824
  const parts = [
2822
2825
  'block w-full rounded-control border text-ink outline-none transition-colors placeholder:text-ink-faint',
2823
- SIZES$7[this.size()],
2826
+ SIZES$8[this.size()],
2824
2827
  ];
2825
2828
  if (this.autoResize())
2826
2829
  parts.push('overflow-hidden');
@@ -3030,8 +3033,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
3030
3033
  }]
3031
3034
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], autoResize: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoResize", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], showCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCount", required: false }] }], countLimitLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "countLimitLabel", required: false }] }], countLabelTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "countLabelTemplate", required: false }] }], resize: [{ type: i0.Input, args: [{ isSignal: true, alias: "resize", required: false }] }] } });
3032
3035
 
3033
- let nextId$h = 0;
3034
- const SIZES$6 = {
3036
+ let nextId$i = 0;
3037
+ const SIZES$7 = {
3035
3038
  sm: { track: 'h-5 w-9', knob: 'h-4 w-4', on: 'translate-x-4', icon: 11 },
3036
3039
  md: { track: 'h-6 w-11', knob: 'h-5 w-5', on: 'translate-x-5', icon: 13 },
3037
3040
  lg: { track: 'h-7 w-[3.4rem]', knob: 'h-6 w-6', on: 'translate-x-7', icon: 15 },
@@ -3043,7 +3046,7 @@ const SIZES$6 = {
3043
3046
  * states.
3044
3047
  */
3045
3048
  class SdsToggleSwitch {
3046
- fieldId = `sds-toggle-switch-${nextId$h++}`;
3049
+ fieldId = `sds-toggle-switch-${nextId$i++}`;
3047
3050
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
3048
3051
  /** Accessible name used when no visible [label] is provided. */
3049
3052
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -3071,7 +3074,7 @@ class SdsToggleSwitch {
3071
3074
  cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
3072
3075
  checked = computed(() => this.value() === this.trueValue(), ...(ngDevMode ? [{ debugName: "checked" }] : []));
3073
3076
  isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
3074
- sizing = computed(() => SIZES$6[this.size()], ...(ngDevMode ? [{ debugName: "sizing" }] : []));
3077
+ sizing = computed(() => SIZES$7[this.size()], ...(ngDevMode ? [{ debugName: "sizing" }] : []));
3075
3078
  describedBy = computed(() => {
3076
3079
  if (this.error())
3077
3080
  return `${this.fieldId}-err`;
@@ -3290,7 +3293,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
3290
3293
  }]
3291
3294
  }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], trueValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "trueValue", required: false }] }], falseValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "falseValue", required: false }] }], onIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "onIcon", required: false }] }], offIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "offIcon", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }] } });
3292
3295
 
3293
- let nextId$g = 0;
3296
+ let nextId$h = 0;
3294
3297
  /**
3295
3298
  * Boolean checkbox. Implements ControlValueAccessor — use with reactive forms
3296
3299
  * or `[(ngModel)]`. Renders a styled box (brand-filled with a check when on)
@@ -3305,7 +3308,7 @@ let nextId$g = 0;
3305
3308
  * <sds-checkbox label="Auto-merge" [(ngModel)]="autoMerge" />
3306
3309
  */
3307
3310
  class SdsCheckbox {
3308
- id = `sds-checkbox-${nextId$g++}`;
3311
+ id = `sds-checkbox-${nextId$h++}`;
3309
3312
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
3310
3313
  /** Accessible name used when no visible [label] is provided. */
3311
3314
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -3453,7 +3456,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
3453
3456
  }]
3454
3457
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }] } });
3455
3458
 
3456
- let nextId$f = 0;
3459
+ let nextId$g = 0;
3457
3460
  /**
3458
3461
  * Mounted radios by group name. A pick updates the rest of its group, because a native radio says
3459
3462
  * nothing when the browser unchecks it — without this a reactive form would leave the old choice drawn.
@@ -3479,7 +3482,7 @@ const groups = new Map();
3479
3482
  * <sds-radio name="size" value="large" label="Grand" [(ngModel)]="size" />
3480
3483
  */
3481
3484
  class SdsRadio {
3482
- id = `sds-radio-${nextId$f++}`;
3485
+ id = `sds-radio-${nextId$g++}`;
3483
3486
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
3484
3487
  /** Accessible name used when no visible [label] is provided. */
3485
3488
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -4055,7 +4058,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
4055
4058
 
4056
4059
  /** Activation order: none → contains → may-contain → none. */
4057
4060
  const ORDER = ['none', 'contains', 'may-contain'];
4058
- let nextId$e = 0;
4061
+ let nextId$f = 0;
4059
4062
  /**
4060
4063
  * A chip that cycles through three states — `none`, `contains`, `may-contain` — built for the 14-tile
4061
4064
  * allergen declaration grid, but generic (any "no / yes / maybe" declaration).
@@ -4075,7 +4078,7 @@ let nextId$e = 0;
4075
4078
  * containsLabel="Contains" mayContainLabel="May contain" noneLabel="Not present" />
4076
4079
  */
4077
4080
  class SdsTriStateChip {
4078
- chipId = `sds-tri-state-chip-${nextId$e++}`;
4081
+ chipId = `sds-tri-state-chip-${nextId$f++}`;
4079
4082
  /** Current state. Two-way: `[(value)]`. */
4080
4083
  value = model('none', ...(ngDevMode ? [{ debugName: "value" }] : []));
4081
4084
  /** Visible name (e.g. the allergen). Required — the chip has no other accessible name. */
@@ -4300,7 +4303,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
4300
4303
  }]
4301
4304
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showState: [{ type: i0.Input, args: [{ isSignal: true, alias: "showState", required: false }] }], noneLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "noneLabel", required: false }] }], containsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "containsLabel", required: false }] }], mayContainLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "mayContainLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }] } });
4302
4305
 
4303
- let nextId$d = 0;
4306
+ let nextId$e = 0;
4304
4307
  /**
4305
4308
  * Segmented control: a small set of mutually exclusive options shown side by side (e.g. "Pick exactly /
4306
4309
  * at least / up to"). Radio-group semantics: `role="radiogroup"` with `role="radio"` segments, one tab
@@ -4316,7 +4319,7 @@ let nextId$d = 0;
4316
4319
  */
4317
4320
  class SdsSegmented {
4318
4321
  host = inject(ElementRef);
4319
- baseId = `sds-segmented-${nextId$d++}`;
4322
+ baseId = `sds-segmented-${nextId$e++}`;
4320
4323
  /** Options: `{ value, label, icon?, disabled? }` objects, or bare strings/numbers. */
4321
4324
  options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
4322
4325
  /** Selected value. Two-way: `[(value)]`. */
@@ -4594,7 +4597,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
4594
4597
  `,
4595
4598
  }]
4596
4599
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }, { type: i0.Output, args: ["selectedChange"] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showCheck: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCheck", required: false }] }] } });
4597
- let nextId$c = 0;
4600
+ let nextId$d = 0;
4598
4601
  /**
4599
4602
  * A set of chips: pick ONE (a filter row such as "All · Missing photo · Hidden", VAT presets, a date
4600
4603
  * range) or, with `multiple`, SEVERAL (categories of an item, days of the week, dietary tags). Unlike
@@ -4617,7 +4620,7 @@ let nextId$c = 0;
4617
4620
  */
4618
4621
  class SdsChipGroup {
4619
4622
  host = inject(ElementRef);
4620
- baseId = `sds-chip-group-${nextId$c++}`;
4623
+ baseId = `sds-chip-group-${nextId$d++}`;
4621
4624
  /** Options: `{ value, label, icon?, count?, disabled? }` objects, or bare strings/numbers. */
4622
4625
  options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
4623
4626
  /** Picked value (single) or values (multiple). Two-way: `[(value)]`. */
@@ -5519,7 +5522,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
5519
5522
  }]
5520
5523
  }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], image: [{ type: i0.Input, args: [{ isSignal: true, alias: "image", required: false }] }], imageAlt: [{ type: i0.Input, args: [{ isSignal: true, alias: "imageAlt", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], note: [{ type: i0.Input, args: [{ isSignal: true, alias: "note", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], availableLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableLabel", required: false }] }], soldOutLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "soldOutLabel", required: false }] }], hiddenLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hiddenLabel", required: false }] }], activate: [{ type: i0.Output, args: ["activate"] }], imageError: [{ type: i0.Output, args: ["imageError"] }] } });
5521
5524
 
5522
- let nextId$b = 0;
5525
+ let nextId$c = 0;
5523
5526
  /**
5524
5527
  * Marks the element inside `<sds-menu>` that opens it. It may be a native button or a component that
5525
5528
  * renders one (e.g. `<sds-button sdsMenuTrigger>`): the menu wires `aria-haspopup`, `aria-expanded` and
@@ -5567,7 +5570,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
5567
5570
  class SdsMenu {
5568
5571
  host = inject(ElementRef);
5569
5572
  injector = inject(Injector);
5570
- menuId = `sds-menu-${nextId$b++}`;
5573
+ menuId = `sds-menu-${nextId$c++}`;
5571
5574
  triggerId = `${this.menuId}-trigger`;
5572
5575
  /** Actions and separators, in order. */
5573
5576
  items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
@@ -6163,7 +6166,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
6163
6166
  standalone: true,
6164
6167
  }]
6165
6168
  }] });
6166
- let nextId$a = 0;
6169
+ let nextId$b = 0;
6167
6170
  /** Fastest auto-scroll step, in px per animation frame (reached at the very edge). */
6168
6171
  const AUTO_SCROLL_MAX_STEP = 18;
6169
6172
  /** The nearest ancestor that scrolls vertically (below the document, which is handled as the window). */
@@ -6232,7 +6235,7 @@ function moveItem(list, from, to) {
6232
6235
  class SdsSortableList {
6233
6236
  host = inject(ElementRef);
6234
6237
  injector = inject(Injector);
6235
- instructionsId = `sds-sortable-${nextId$a++}-help`;
6238
+ instructionsId = `sds-sortable-${nextId$b++}-help`;
6236
6239
  /** The items, in order. Two-way: `[(items)]` receives the new order on drop. */
6237
6240
  items = model([], ...(ngDevMode ? [{ debugName: "items" }] : []));
6238
6241
  /** Property used as a stable identity for rows (strongly recommended). */
@@ -6824,7 +6827,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
6824
6827
  }]
6825
6828
  }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }, { type: i0.Output, args: ["itemsChange"] }], dataKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataKey", required: false }] }], labelField: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelField", required: false }] }], itemLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemLabel", required: false }] }], itemDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemDisabled", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], autoScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoScroll", required: false }] }], autoScrollEdge: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoScrollEdge", required: false }] }], handleLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "handleLabel", required: false }] }], instructions: [{ type: i0.Input, args: [{ isSignal: true, alias: "instructions", required: false }] }], pickedUpMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "pickedUpMessage", required: false }] }], movedMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "movedMessage", required: false }] }], droppedMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "droppedMessage", required: false }] }], cancelledMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelledMessage", required: false }] }], reorder: [{ type: i0.Output, args: ["reorder"] }], itemTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSortableItem), { isSignal: true }] }], handleTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSortableHandle), { isSignal: true }] }] } });
6826
6829
 
6827
- let nextId$9 = 0;
6830
+ let nextId$a = 0;
6828
6831
  /** "1.5 MB" style size (decimal units, like file managers show them), for messages. */
6829
6832
  function sdsFormatBytes(bytes) {
6830
6833
  if (!Number.isFinite(bytes) || bytes < 0)
@@ -6881,7 +6884,7 @@ function sdsFileMatchesAccept(file, accept) {
6881
6884
  * (files)="upload($event)" (rejected)="onRejected($event)" />
6882
6885
  */
6883
6886
  class SdsFileDrop {
6884
- zoneId = `sds-file-drop-${nextId$9++}`;
6887
+ zoneId = `sds-file-drop-${nextId$a++}`;
6885
6888
  fileInput = viewChild('fileInput', ...(ngDevMode ? [{ debugName: "fileInput" }] : []));
6886
6889
  /** Accepted types, native `accept` syntax. Empty = anything. */
6887
6890
  accept = input('', ...(ngDevMode ? [{ debugName: "accept" }] : []));
@@ -7263,7 +7266,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
7263
7266
  }]
7264
7267
  }], propDecorators: { fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], dropLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "dropLabel", required: false }] }], browseLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "browseLabel", required: false }] }], loadingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingLabel", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], wrongTypeMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrongTypeMessage", required: false }] }], tooLargeMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooLargeMessage", required: false }] }], tooManyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooManyMessage", required: false }] }], files: [{ type: i0.Output, args: ["files"] }], rejected: [{ type: i0.Output, args: ["rejected"] }] } });
7265
7268
 
7266
- let nextId$8 = 0;
7269
+ let nextId$9 = 0;
7267
7270
  /**
7268
7271
  * Crops an image to a FIXED aspect ratio (e.g. the shop's card ratio): the frame is fixed and the image
7269
7272
  * moves and zooms behind it, always covering the frame, so every crop is valid.
@@ -7281,7 +7284,7 @@ let nextId$8 = 0;
7281
7284
  * <sds-button (click)="upload(await cropper.toBlob({ width: 1600 }))">Save</sds-button>
7282
7285
  */
7283
7286
  class SdsImageCropper {
7284
- helpId = `sds-image-cropper-${nextId$8++}-help`;
7287
+ helpId = `sds-image-cropper-${nextId$9++}-help`;
7285
7288
  viewport = viewChild('viewport', ...(ngDevMode ? [{ debugName: "viewport" }] : []));
7286
7289
  /** Image to crop: a URL, or a Blob/File. */
7287
7290
  src = input(null, ...(ngDevMode ? [{ debugName: "src" }] : []));
@@ -8629,7 +8632,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8629
8632
  }]
8630
8633
  }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], showActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showActions", required: false }] }], showTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTime", required: false }] }], showDone: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDone", required: false }] }], timeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeLabel", required: false }] }], hoursLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoursLabel", required: false }] }], minutesLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "minutesLabel", required: false }] }], doneLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "doneLabel", required: false }] }], calendarLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "calendarLabel", required: false }] }], previousLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], todayActionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "todayActionLabel", required: false }] }], clearActionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearActionLabel", required: false }] }], dateSelect: [{ type: i0.Output, args: ["dateSelect"] }], done: [{ type: i0.Output, args: ["done"] }] } });
8631
8634
 
8632
- const SIZES$5 = {
8635
+ const SIZES$6 = {
8633
8636
  sm: 'h-9 text-sm',
8634
8637
  md: 'h-10 text-sm',
8635
8638
  lg: 'h-12 text-base',
@@ -8645,7 +8648,7 @@ const SIZES$5 = {
8645
8648
  class SdsDatePicker {
8646
8649
  host = inject(ElementRef);
8647
8650
  injector = inject(Injector);
8648
- fieldId = `sds-date-${nextId$7++}`;
8651
+ fieldId = `sds-date-${nextId$8++}`;
8649
8652
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
8650
8653
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
8651
8654
  placeholder = input('dd/mm/yyyy', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
@@ -8721,7 +8724,7 @@ class SdsDatePicker {
8721
8724
  /** Whether the clear button is shown (affects the field's right padding). */
8722
8725
  showClear = computed(() => this.clearable() && !!this.value() && !this.isDisabled(), ...(ngDevMode ? [{ debugName: "showClear" }] : []));
8723
8726
  wrapperClasses = computed(() => {
8724
- const parts = [SIZES$5[this.size()]];
8727
+ const parts = [SIZES$6[this.size()]];
8725
8728
  parts.push(this.error()
8726
8729
  ? 'border-danger focus-within:border-danger focus-within:ring-2 focus-within:ring-danger/25'
8727
8730
  : 'border-border focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25');
@@ -9163,7 +9166,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9163
9166
  `,
9164
9167
  }]
9165
9168
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], inline: [{ type: i0.Input, args: [{ isSignal: true, alias: "inline", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], showActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showActions", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTime", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], timeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeLabel", required: false }] }], hoursLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoursLabel", required: false }] }], minutesLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "minutesLabel", required: false }] }], doneLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "doneLabel", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], openCalendarLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "openCalendarLabel", required: false }] }], closeCalendarLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeCalendarLabel", required: false }] }], panelLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelLabel", required: false }] }], calendarLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "calendarLabel", required: false }] }], previousLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], todayActionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "todayActionLabel", required: false }] }], clearActionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearActionLabel", required: false }] }], calendar: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SdsCalendar), { isSignal: true }] }], trigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }] } });
9166
- let nextId$7 = 0;
9169
+ let nextId$8 = 0;
9167
9170
 
9168
9171
  /**
9169
9172
  * Supplies a custom template for each option row (and the selected value shown
@@ -9242,12 +9245,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9242
9245
  }]
9243
9246
  }] });
9244
9247
 
9245
- const SIZES$4 = {
9248
+ const SIZES$5 = {
9246
9249
  sm: 'h-9 text-sm',
9247
9250
  md: 'h-10 text-sm',
9248
9251
  lg: 'h-12 text-base',
9249
9252
  };
9250
- let nextId$6 = 0;
9253
+ let nextId$7 = 0;
9251
9254
  /**
9252
9255
  * Single-select dropdown. Options may be primitives or objects (configure via
9253
9256
  * [optionLabel]/[optionValue]/[optionDisabled]). The overlay is appended to
@@ -9281,7 +9284,7 @@ let nextId$6 = 0;
9281
9284
  class SdsSelect {
9282
9285
  host = inject(ElementRef);
9283
9286
  injector = inject(Injector);
9284
- selectId = `sds-select-${nextId$6++}`;
9287
+ selectId = `sds-select-${nextId$7++}`;
9285
9288
  listId = `${this.selectId}-list`;
9286
9289
  options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
9287
9290
  optionLabel = input('label', ...(ngDevMode ? [{ debugName: "optionLabel" }] : []));
@@ -9344,7 +9347,7 @@ class SdsSelect {
9344
9347
  showClear = computed(() => this.clearable() && this.value() != null && !this.isDisabled(), ...(ngDevMode ? [{ debugName: "showClear" }] : []));
9345
9348
  activeDescId = computed(() => this.highlightedIndex() >= 0 ? this.optionId(this.highlightedIndex()) : null, ...(ngDevMode ? [{ debugName: "activeDescId" }] : []));
9346
9349
  wrapperClasses = computed(() => {
9347
- const parts = [SIZES$4[this.size()]];
9350
+ const parts = [SIZES$5[this.size()]];
9348
9351
  if (this.filled()) {
9349
9352
  // border-transparent (not border-0) so a filled field is the same height as a bordered one when a
9350
9353
  // form mixes them — mirrors sds-input's filled appearance.
@@ -9986,12 +9989,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9986
9989
  }]
9987
9990
  }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: false }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: false }] }], optionDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDisabled", required: false }] }], dataKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataKey", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], filterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterPlaceholder", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], filterLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterLabel", required: false }] }], optionTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSelectOption), { isSignal: true }] }], emptyTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSelectEmpty), { isSignal: true }] }], footerTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSelectFooter), { isSignal: true }] }], trigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }] } });
9988
9991
 
9989
- const SIZES$3 = {
9992
+ const SIZES$4 = {
9990
9993
  sm: 'min-h-9 text-sm',
9991
9994
  md: 'min-h-10 text-sm',
9992
9995
  lg: 'min-h-12 text-base',
9993
9996
  };
9994
- let nextId$5 = 0;
9997
+ let nextId$6 = 0;
9995
9998
  /**
9996
9999
  * Multi-select dropdown (value is an array). Options may be primitives or
9997
10000
  * objects (configure via [optionLabel]/[optionValue]/[optionDisabled]). The
@@ -10024,7 +10027,7 @@ let nextId$5 = 0;
10024
10027
  class SdsMultiSelect {
10025
10028
  host = inject(ElementRef);
10026
10029
  injector = inject(Injector);
10027
- selectId = `sds-multiselect-${nextId$5++}`;
10030
+ selectId = `sds-multiselect-${nextId$6++}`;
10028
10031
  listId = `${this.selectId}-list`;
10029
10032
  options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
10030
10033
  optionLabel = input('label', ...(ngDevMode ? [{ debugName: "optionLabel" }] : []));
@@ -10120,7 +10123,7 @@ class SdsMultiSelect {
10120
10123
  return i >= 0 ? this.optionId(i) : null;
10121
10124
  }, ...(ngDevMode ? [{ debugName: "activeDescId" }] : []));
10122
10125
  wrapperClasses = computed(() => {
10123
- const parts = [SIZES$3[this.size()]];
10126
+ const parts = [SIZES$4[this.size()]];
10124
10127
  parts.push(this.error()
10125
10128
  ? 'border-danger focus:border-danger focus:ring-2 focus:ring-danger/25'
10126
10129
  : 'border-border focus:border-brand-500 focus:ring-2 focus:ring-brand-500/25');
@@ -10902,12 +10905,12 @@ function sdsMatchDialCode(e164Digits, countries) {
10902
10905
  return best;
10903
10906
  }
10904
10907
 
10905
- const SIZES$2 = {
10908
+ const SIZES$3 = {
10906
10909
  sm: 'h-9 text-sm',
10907
10910
  md: 'h-10 text-sm',
10908
10911
  lg: 'h-12 text-base',
10909
10912
  };
10910
- let nextId$4 = 0;
10913
+ let nextId$5 = 0;
10911
10914
  /**
10912
10915
  * International phone number input: a country selector (ISO chip + dial code)
10913
10916
  * with a searchable, body-appended dropdown, plus a formatted national-number
@@ -10922,7 +10925,7 @@ let nextId$4 = 0;
10922
10925
  class SdsPhoneInput {
10923
10926
  host = inject(ElementRef);
10924
10927
  injector = inject(Injector);
10925
- fieldId = `sds-phone-${nextId$4++}`;
10928
+ fieldId = `sds-phone-${nextId$5++}`;
10926
10929
  listId = `${this.fieldId}-list`;
10927
10930
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
10928
10931
  hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
@@ -11096,7 +11099,7 @@ class SdsPhoneInput {
11096
11099
  return null;
11097
11100
  }, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
11098
11101
  wrapperClasses = computed(() => {
11099
- const parts = [SIZES$2[this.size()]];
11102
+ const parts = [SIZES$3[this.size()]];
11100
11103
  parts.push(this.showError()
11101
11104
  ? 'border-danger focus-within:border-danger focus-within:ring-2 focus-within:ring-danger/25'
11102
11105
  : 'border-border focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25');
@@ -11642,129 +11645,1086 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
11642
11645
  }]
11643
11646
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], defaultCountry: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultCountry", required: false }] }], preferredCountries: [{ type: i0.Input, args: [{ isSignal: true, alias: "preferredCountries", required: false }] }], onlyCountries: [{ type: i0.Input, args: [{ isSignal: true, alias: "onlyCountries", required: false }] }], showExampleHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "showExampleHint", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], requiredMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "requiredMessage", required: false }] }], countryLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "countryLabel", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], searchLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchLabel", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], noResultsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "noResultsLabel", required: false }] }], incompleteMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "incompleteMessage", required: false }] }], invalidMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidMessage", required: false }] }], lengthSeparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "lengthSeparator", required: false }] }], exampleHintTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "exampleHintTemplate", required: false }] }], phoneChange: [{ type: i0.Output, args: ["phoneChange"] }], anchor: [{ type: i0.ViewChild, args: ['anchor', { isSignal: true }] }], field: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }] } });
11644
11647
 
11645
- const SIZES$1 = {
11646
- sm: 'h-8 min-w-8 text-xs',
11647
- md: 'h-9 min-w-9 text-sm',
11648
- lg: 'h-11 min-w-11 text-base',
11648
+ /**
11649
+ * App-owned rows rendered inside an `<sds-address-autocomplete>` panel — saved addresses, a "Enter it
11650
+ * manually" action… They sit outside the suggestion listbox (the arrow keys walk the suggestions only),
11651
+ * below the suggestions by default or between the current-location row and the suggestions with
11652
+ * `extraPosition="above-results"`.
11653
+ *
11654
+ * @example
11655
+ * <sds-address-autocomplete [search]="search" (selected)="pick($event)">
11656
+ * <ng-template sdsAddressExtra let-query let-close="close">
11657
+ * @for (a of savedMatching(query); track a.id) {
11658
+ * <button type="button" (click)="useSaved(a); close()">{{ a.label }}</button>
11659
+ * }
11660
+ * </ng-template>
11661
+ * </sds-address-autocomplete>
11662
+ */
11663
+ class SdsAddressExtra {
11664
+ /** The wrapped template, rendered by the host autocomplete via ngTemplateOutlet. */
11665
+ template = inject(TemplateRef);
11666
+ /** Lets the compiler type-check `let-*` bindings against SdsAddressExtraContext. */
11667
+ static ngTemplateContextGuard(_dir, _ctx) {
11668
+ return true;
11669
+ }
11670
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsAddressExtra, deps: [], target: i0.ɵɵFactoryTarget.Directive });
11671
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.25", type: SdsAddressExtra, isStandalone: true, selector: "ng-template[sdsAddressExtra]", ngImport: i0 });
11672
+ }
11673
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsAddressExtra, decorators: [{
11674
+ type: Directive,
11675
+ args: [{
11676
+ selector: 'ng-template[sdsAddressExtra]',
11677
+ standalone: true,
11678
+ }]
11679
+ }] });
11680
+
11681
+ const SIZES$2 = {
11682
+ sm: 'h-9 text-sm',
11683
+ md: 'h-10 text-sm',
11684
+ lg: 'h-12 text-base',
11649
11685
  };
11650
- const ICON_SIZES = { sm: 15, md: 16, lg: 18 };
11651
- const BTN_BASE = 'inline-flex items-center justify-center rounded-control px-1.5 font-medium ' +
11652
- 'transition-colors select-none focus-visible:outline-none focus-visible:ring-2 ' +
11653
- 'focus-visible:ring-brand-500 focus-visible:ring-offset-1';
11686
+ let nextId$4 = 0;
11654
11687
  /**
11655
- * Pagination control (mirrors PrimeNG's paginator). Drives a sliding window of
11656
- * page links with first/prev/next/last navigation, an optional rows-per-page
11657
- * dropdown (reuses <sds-select>), a current-page report and a jump-to-page input.
11688
+ * Address search field — a combobox over an app-supplied geocoder. Typing (at least `minChars`
11689
+ * characters) runs the `search` provider, debounced by `debounceMs`; only the latest query's response is
11690
+ * applied (a slower, older one is discarded). Results show the street + number on top and "postcode
11691
+ * city" beneath. With a `locate` provider, a "Use my current location" row sits on top until a search
11692
+ * starts; if the provider finds no position, an inline notice says so (no toast).
11658
11693
  *
11659
- * State is exposed as two-way models: [first] (zero-based index of the first
11660
- * record on the page) and [rows] (page size). A combined (pageChange) event
11661
- * carries the full {@link SdsPaginatorState} on every change.
11694
+ * The component does no HTTP and no geolocation itself — both are provider inputs, so it works with any
11695
+ * backend. The value (`[(ngModel)]` / `formControl`) is the display text of the field; a
11696
+ * pick writes the suggestion's `label` into it and emits `(selected)` with the whole suggestion
11697
+ * (coordinates + structured parts). Free text that was never picked emits nothing — treat the field as
11698
+ * "an address was chosen" only after `(selected)`.
11699
+ *
11700
+ * Keyboard: ↓/↑ walk the rows (opening the panel), Enter picks, Esc closes (or, `inline`, clears the
11701
+ * highlight and lets Esc bubble — a dialog around it still closes). ARIA: the field is a `combobox`
11702
+ * (`aria-autocomplete="list"`) driving a `listbox` through `aria-activedescendant`.
11703
+ *
11704
+ * By default the suggestions open in an overlay appended to `<body>` (safe inside scrolling drawers and
11705
+ * dialogs). `inline` renders them in the page flow instead, always visible — for a full-height address
11706
+ * sheet. App rows (saved addresses…) go in a projected `<ng-template sdsAddressExtra>`.
11707
+ *
11708
+ * Every user-facing string has an English default; an app serving other languages passes translations.
11662
11709
  *
11663
11710
  * @example
11664
- * <sds-paginator
11665
- * [totalRecords]="120"
11666
- * [(first)]="first"
11667
- * [(rows)]="rows"
11668
- * [rowsPerPageOptions]="[10, 20, 50]"
11669
- * showCurrentPageReport
11670
- * (pageChange)="load($event)"
11711
+ * <sds-address-autocomplete
11712
+ * label="Address"
11713
+ * [search]="searchAddress"
11714
+ * [locate]="locateMe"
11715
+ * [(ngModel)]="addressText"
11716
+ * (selected)="onAddress($event)"
11671
11717
  * />
11672
11718
  */
11673
- class SdsPaginator {
11674
- /** Total number of records to paginate. */
11675
- totalRecords = input(0, ...(ngDevMode ? [{ debugName: "totalRecords", transform: numberAttribute }] : [{ transform: numberAttribute }]));
11676
- /** Zero-based index of the first record on the current page (two-way). */
11677
- first = model(0, ...(ngDevMode ? [{ debugName: "first" }] : []));
11678
- /** Rows displayed per page (two-way). */
11679
- rows = model(10, ...(ngDevMode ? [{ debugName: "rows" }] : []));
11680
- /** When non-empty, renders a rows-per-page dropdown with these choices. */
11681
- rowsPerPageOptions = input([], ...(ngDevMode ? [{ debugName: "rowsPerPageOptions" }] : []));
11682
- /** Maximum count of numbered page links shown in the sliding window. */
11683
- pageLinkSize = input(5, ...(ngDevMode ? [{ debugName: "pageLinkSize", transform: numberAttribute }] : [{ transform: numberAttribute }]));
11684
- showFirstLastIcon = input(true, ...(ngDevMode ? [{ debugName: "showFirstLastIcon", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11685
- showPageLinks = input(true, ...(ngDevMode ? [{ debugName: "showPageLinks", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11686
- showCurrentPageReport = input(false, ...(ngDevMode ? [{ debugName: "showCurrentPageReport", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11687
- /** Tokens: {first} {last} {totalRecords} {currentPage} {totalPages} {rows}. */
11688
- currentPageReportTemplate = input('{first} – {last} of {totalRecords}', ...(ngDevMode ? [{ debugName: "currentPageReportTemplate" }] : []));
11689
- showJumpToPageInput = input(false, ...(ngDevMode ? [{ debugName: "showJumpToPageInput", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11690
- rowsPerPageLabel = input('Rows per page', ...(ngDevMode ? [{ debugName: "rowsPerPageLabel" }] : []));
11691
- jumpToPageLabel = input('Go to', ...(ngDevMode ? [{ debugName: "jumpToPageLabel" }] : []));
11719
+ class SdsAddressAutocomplete {
11720
+ host = inject(ElementRef);
11721
+ autoId = `sds-address-${nextId$4++}`;
11722
+ locateListId = `${this.autoId}-here`;
11723
+ listId = `${this.autoId}-list`;
11724
+ // --- providers ------------------------------------------------------------
11725
+ /** Required. The address search: the app's geocode call for the typed (trimmed) query. */
11726
+ search = input.required(...(ngDevMode ? [{ debugName: "search" }] : []));
11692
11727
  /**
11693
- * Accessible name for the <nav> wrapping the page controls, and for the four icon-only
11694
- * first/prev/next/last buttons. Those buttons have no text, so these strings are their ONLY
11695
- * accessible names — the English defaults suit an English-first app, but an app serving more than
11696
- * one language MUST pass translated values.
11728
+ * Optional. When set, a "Use my current location" row shows on top until a search starts. The app does
11729
+ * the geolocation (with its own timeout) and the reverse geocode; resolve `null` (or reject) when no
11730
+ * position can be had — the row then shows {@link locateFailedText} inline. A position without a street
11731
+ * is still a result: return `{ label: '', latitude, longitude }`.
11697
11732
  */
11698
- paginationLabel = input('Pagination', ...(ngDevMode ? [{ debugName: "paginationLabel" }] : []));
11699
- firstPageLabel = input('First page', ...(ngDevMode ? [{ debugName: "firstPageLabel" }] : []));
11700
- previousPageLabel = input('Previous page', ...(ngDevMode ? [{ debugName: "previousPageLabel" }] : []));
11701
- nextPageLabel = input('Next page', ...(ngDevMode ? [{ debugName: "nextPageLabel" }] : []));
11702
- lastPageLabel = input('Last page', ...(ngDevMode ? [{ debugName: "lastPageLabel" }] : []));
11733
+ locate = input(null, ...(ngDevMode ? [{ debugName: "locate" }] : []));
11734
+ // --- behaviour ------------------------------------------------------------
11735
+ /** The search runs once the trimmed query has at least this many characters. */
11736
+ minChars = input(3, ...(ngDevMode ? [{ debugName: "minChars", transform: numberAttribute }] : [{ transform: numberAttribute }]));
11737
+ /** Quiet period after the last keystroke before the search runs, in ms. */
11738
+ debounceMs = input(250, ...(ngDevMode ? [{ debugName: "debounceMs", transform: numberAttribute }] : [{ transform: numberAttribute }]));
11739
+ /** Render the rows in the page flow, always visible (an address sheet) instead of an overlay. */
11740
+ inline = input(false, ...(ngDevMode ? [{ debugName: "inline", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11741
+ /** Where the projected `sdsAddressExtra` rows go. */
11742
+ extraPosition = input('below', ...(ngDevMode ? [{ debugName: "extraPosition" }] : []));
11703
11743
  /**
11704
- * Accessible name of a numbered page link. `{page}` is replaced with the 1-based page number —
11705
- * a token rather than a prefix because word order is not the same in every language.
11744
+ * Show {@link noResultsText} when a search finds nothing. Turn it off when the app's own extra rows
11745
+ * already answer the query (e.g. saved addresses that match it).
11706
11746
  */
11707
- pageLinkLabel = input('Page {page}', ...(ngDevMode ? [{ debugName: "pageLinkLabel" }] : []));
11747
+ showNoResults = input(true, ...(ngDevMode ? [{ debugName: "showNoResults", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11748
+ // --- field ----------------------------------------------------------------
11708
11749
  /**
11709
- * Accessible name of the jump-to-page field. Separate from the visible {@link jumpToPageLabel}
11710
- * ("Go to") on purpose: the visible text is terse because it sits next to the field, while the
11711
- * accessible name has to stand alone.
11750
+ * The field text — the picked suggestion's label, or what the user typed. It is the CVA value: bind it
11751
+ * with `[(ngModel)]` / `formControl` (there is deliberately no `[value]` input).
11712
11752
  */
11713
- jumpToPageAriaLabel = input('Go to page', ...(ngDevMode ? [{ debugName: "jumpToPageAriaLabel" }] : []));
11714
- size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
11753
+ value = signal('', ...(ngDevMode ? [{ debugName: "value" }] : []));
11754
+ /** `id` of the native input (for an outside `<label for>`); empty generates one. */
11755
+ inputId = input('', ...(ngDevMode ? [{ debugName: "inputId" }] : []));
11756
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
11757
+ /** Accessible name when there is no visible [label]. */
11758
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
11759
+ placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
11760
+ hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
11761
+ error = input('', ...(ngDevMode ? [{ debugName: "error" }] : []));
11762
+ /** Danger frame + `aria-invalid` without a message. */
11763
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11764
+ /** Asterisk after the label + `aria-required`. It does not validate. */
11765
+ required = input(false, ...(ngDevMode ? [{ debugName: "required", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11715
11766
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11716
- /** Fires after any page or page-size change with the full paginator state. */
11717
- pageChange = output();
11718
- pageCount = computed(() => Math.max(1, Math.ceil(this.totalRecords() / Math.max(1, this.rows()))), ...(ngDevMode ? [{ debugName: "pageCount" }] : []));
11719
- page = computed(() => Math.min(this.pageCount() - 1, Math.floor(this.first() / Math.max(1, this.rows()))), ...(ngDevMode ? [{ debugName: "page" }] : []));
11720
- firstDisabled = computed(() => this.disabled() || this.page() <= 0, ...(ngDevMode ? [{ debugName: "firstDisabled" }] : []));
11721
- lastDisabled = computed(() => this.disabled() || this.page() >= this.pageCount() - 1, ...(ngDevMode ? [{ debugName: "lastDisabled" }] : []));
11722
- iconSize = computed(() => ICON_SIZES[this.size()], ...(ngDevMode ? [{ debugName: "iconSize" }] : []));
11723
- // Sliding window of zero-based page indices, centred on the current page.
11724
- pageLinks = computed(() => {
11725
- const pages = this.pageCount();
11726
- const visible = Math.min(this.pageLinkSize(), pages);
11727
- let start = Math.max(0, Math.ceil(this.page() - visible / 2));
11728
- const end = Math.min(pages - 1, start + visible - 1);
11729
- start = Math.max(0, start - (visible - (end - start + 1)));
11730
- return Array.from({ length: end - start + 1 }, (_, i) => start + i);
11731
- }, ...(ngDevMode ? [{ debugName: "pageLinks" }] : []));
11732
- reportText = computed(() => {
11733
- const total = this.totalRecords();
11734
- const first = total === 0 ? 0 : this.first() + 1;
11735
- const last = Math.min(this.first() + this.rows(), total);
11736
- return this.currentPageReportTemplate()
11737
- .replace('{first}', String(first))
11738
- .replace('{last}', String(last))
11739
- .replace('{totalRecords}', String(total))
11740
- .replace('{currentPage}', String(this.page() + 1))
11741
- .replace('{totalPages}', String(this.pageCount()))
11742
- .replace('{rows}', String(this.rows()));
11743
- }, ...(ngDevMode ? [{ debugName: "reportText" }] : []));
11744
- hostClass = computed(() => {
11745
- const base = 'flex flex-wrap items-center gap-x-4 gap-y-3';
11746
- const justify = this.showCurrentPageReport() ? 'justify-between' : 'justify-center';
11747
- return `${base} ${justify}`;
11748
- }, ...(ngDevMode ? [{ debugName: "hostClass" }] : []));
11749
- pageLinkAriaLabel(page) {
11750
- return this.pageLinkLabel().replace('{page}', String(page + 1));
11751
- }
11752
- navClass(isDisabled) {
11753
- const cls = `${BTN_BASE} ${SIZES$1[this.size()]} px-0`;
11754
- return isDisabled
11755
- ? `${cls} text-ink-faint opacity-50 cursor-not-allowed`
11756
- : `${cls} text-ink-soft hover:bg-muted cursor-pointer`;
11757
- }
11758
- linkClass(active) {
11759
- const cls = `${BTN_BASE} ${SIZES$1[this.size()]}`;
11760
- if (this.disabled())
11761
- return `${cls} text-ink-faint opacity-50 cursor-not-allowed`;
11762
- return active
11763
- ? `${cls} bg-brand-600 text-white hover:bg-brand-700 ${DARK_BRAND_FILL_HOVER} cursor-pointer`
11764
- : `${cls} text-ink-soft hover:bg-muted cursor-pointer`;
11767
+ /** A clear (×) button while the field has text. */
11768
+ clearable = input(false, ...(ngDevMode ? [{ debugName: "clearable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11769
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
11770
+ /** `filled`: borderless on a muted ground (a search pill inside a sheet). */
11771
+ appearance = input('default', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
11772
+ shape = input('control', ...(ngDevMode ? [{ debugName: "shape" }] : []));
11773
+ /** Decorative icon at the start of the field; `null` for none. */
11774
+ leadingIcon = input('search', ...(ngDevMode ? [{ debugName: "leadingIcon" }] : []));
11775
+ // --- strings (English defaults — pass translations) ------------------------
11776
+ useLocationText = input('Use my current location', ...(ngDevMode ? [{ debugName: "useLocationText" }] : []));
11777
+ locatingText = input('Locating…', ...(ngDevMode ? [{ debugName: "locatingText" }] : []));
11778
+ locateFailedText = input("Couldn't find your location. Check location access, or type your address.", ...(ngDevMode ? [{ debugName: "locateFailedText" }] : []));
11779
+ searchingText = input('Searching…', ...(ngDevMode ? [{ debugName: "searchingText" }] : []));
11780
+ noResultsText = input('No matching address', ...(ngDevMode ? [{ debugName: "noResultsText" }] : []));
11781
+ searchErrorText = input("Couldn't search addresses right now. Try again.", ...(ngDevMode ? [{ debugName: "searchErrorText" }] : []));
11782
+ /** Optional heading above the suggestions (e.g. "Results"); empty shows none. */
11783
+ resultsLabel = input('', ...(ngDevMode ? [{ debugName: "resultsLabel" }] : []));
11784
+ /** Accessible name of the icon-only clear button. */
11785
+ clearLabel = input('Clear', ...(ngDevMode ? [{ debugName: "clearLabel" }] : []));
11786
+ /** A suggestion or the current location was picked. The emitted copy carries `source`. */
11787
+ selected = output();
11788
+ extraTpl = contentChild(SdsAddressExtra, ...(ngDevMode ? [{ debugName: "extraTpl" }] : []));
11789
+ fieldRef = viewChild('field', ...(ngDevMode ? [{ debugName: "fieldRef" }] : []));
11790
+ inputRef = viewChild('inputEl', ...(ngDevMode ? [{ debugName: "inputRef" }] : []));
11791
+ // --- state ----------------------------------------------------------------
11792
+ /** The trimmed text the user typed — '' until they type, and again after a pick or a written value. */
11793
+ query = signal('', ...(ngDevMode ? [{ debugName: "query" }] : []));
11794
+ suggestions = signal([], ...(ngDevMode ? [{ debugName: "suggestions" }] : []));
11795
+ loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
11796
+ /** The latest search failed (network / server) — not the same as "no matches". */
11797
+ failed = signal(false, ...(ngDevMode ? [{ debugName: "failed" }] : []));
11798
+ locating = signal(false, ...(ngDevMode ? [{ debugName: "locating" }] : []));
11799
+ locateFailed = signal(false, ...(ngDevMode ? [{ debugName: "locateFailed" }] : []));
11800
+ open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
11801
+ highlighted = signal(-1, ...(ngDevMode ? [{ debugName: "highlighted" }] : []));
11802
+ cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
11803
+ /** Monotonic id: only the latest search's response is applied. */
11804
+ seq = 0;
11805
+ timer;
11806
+ destroyed = false;
11807
+ panelEl = null;
11808
+ fieldId = computed(() => this.inputId() || this.autoId, ...(ngDevMode ? [{ debugName: "fieldId" }] : []));
11809
+ isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
11810
+ pill = computed(() => this.shape() === 'pill', ...(ngDevMode ? [{ debugName: "pill" }] : []));
11811
+ isSearching = computed(() => this.query().length >= this.minChars(), ...(ngDevMode ? [{ debugName: "isSearching" }] : []));
11812
+ /** The current-location row: offered (a `locate` provider) and no search engaged yet. */
11813
+ showLocate = computed(() => !!this.locate() && !this.isSearching(), ...(ngDevMode ? [{ debugName: "showLocate" }] : []));
11814
+ rows = computed(() => {
11815
+ const rows = [];
11816
+ if (this.showLocate())
11817
+ rows.push({ kind: 'locate' });
11818
+ if (this.isSearching()) {
11819
+ this.suggestions().forEach((suggestion, index) => rows.push({ kind: 'suggestion', suggestion, index }));
11820
+ }
11821
+ return rows;
11822
+ }, ...(ngDevMode ? [{ debugName: "rows" }] : []));
11823
+ suggestionRows = computed(() => {
11824
+ const offset = this.showLocate() ? 1 : 0;
11825
+ return this.isSearching()
11826
+ ? this.suggestions().map((suggestion, i) => ({ suggestion, rowIndex: i + offset }))
11827
+ : [];
11828
+ }, ...(ngDevMode ? [{ debugName: "suggestionRows" }] : []));
11829
+ /** Anything to show in the panel at all. */
11830
+ hasContent = computed(() => this.showLocate() || this.isSearching() || !!this.extraTpl(), ...(ngDevMode ? [{ debugName: "hasContent" }] : []));
11831
+ /** The rows are visible: always when `inline`, while open (and non-empty) as an overlay. */
11832
+ panelShown = computed(() => !this.isDisabled() && this.hasContent() && (this.inline() || this.open()), ...(ngDevMode ? [{ debugName: "panelShown" }] : []));
11833
+ controlsIds = computed(() => {
11834
+ const ids = [];
11835
+ if (this.showLocate())
11836
+ ids.push(this.locateListId);
11837
+ if (this.isSearching() && this.suggestions().length)
11838
+ ids.push(this.listId);
11839
+ return ids.length ? ids.join(' ') : null;
11840
+ }, ...(ngDevMode ? [{ debugName: "controlsIds" }] : []));
11841
+ activeDescId = computed(() => this.panelShown() && this.highlighted() >= 0 && this.highlighted() < this.rows().length
11842
+ ? this.optionId(this.highlighted())
11843
+ : null, ...(ngDevMode ? [{ debugName: "activeDescId" }] : []));
11844
+ listLabel = computed(() => this.label() || this.ariaLabel() || this.placeholder() || null, ...(ngDevMode ? [{ debugName: "listLabel" }] : []));
11845
+ describedBy = computed(() => {
11846
+ if (this.error())
11847
+ return `${this.fieldId()}-err`;
11848
+ if (this.hint())
11849
+ return `${this.fieldId()}-hint`;
11850
+ return null;
11851
+ }, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
11852
+ fieldPadding = computed(() => {
11853
+ const left = this.leadingIcon()
11854
+ ? this.pill()
11855
+ ? 'pl-11'
11856
+ : 'pl-10'
11857
+ : this.pill()
11858
+ ? 'pl-5'
11859
+ : 'pl-3.5';
11860
+ return `${left} pr-10`;
11861
+ }, ...(ngDevMode ? [{ debugName: "fieldPadding" }] : []));
11862
+ wrapperClasses = computed(() => {
11863
+ const danger = !!this.error() || this.invalid();
11864
+ const parts = [SIZES$2[this.size()], this.pill() ? 'rounded-full' : 'rounded-control'];
11865
+ if (this.isDisabled())
11866
+ parts.push('opacity-60 cursor-not-allowed');
11867
+ if (this.appearance() === 'filled') {
11868
+ parts.push('border-transparent bg-muted text-ink');
11869
+ parts.push(danger
11870
+ ? 'border-danger focus-within:ring-2 focus-within:ring-danger/30'
11871
+ : 'focus-within:ring-2 focus-within:ring-brand-500/25');
11872
+ }
11873
+ else {
11874
+ parts.push('bg-surface text-ink');
11875
+ parts.push(danger
11876
+ ? 'border-danger focus-within:border-danger focus-within:ring-2 focus-within:ring-danger/25'
11877
+ : 'border-border focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25');
11878
+ }
11879
+ return parts.join(' ');
11880
+ }, ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
11881
+ extraContext = computed(() => ({
11882
+ $implicit: this.query(),
11883
+ query: this.query(),
11884
+ searching: this.isSearching(),
11885
+ close: () => this.close(),
11886
+ }), ...(ngDevMode ? [{ debugName: "extraContext" }] : []));
11887
+ constructor() {
11888
+ // The overlay lives on <body> (so a scrolling drawer can't clip it); keep it under the field.
11889
+ afterEveryRender(() => this.syncPanel());
11765
11890
  }
11766
- firstPage() {
11767
- this.changePage(0);
11891
+ // --- rendering helpers ------------------------------------------------------
11892
+ optionId(rowIndex) {
11893
+ return `${this.autoId}-opt-${rowIndex}`;
11894
+ }
11895
+ rowClass(rowIndex, busy) {
11896
+ if (busy)
11897
+ return 'cursor-wait';
11898
+ return this.highlighted() === rowIndex
11899
+ ? 'cursor-pointer bg-muted'
11900
+ : 'cursor-pointer hover:bg-muted';
11901
+ }
11902
+ /** Street + number when the geocoder split it out, else the full label. */
11903
+ primaryLine(s) {
11904
+ if (s.street)
11905
+ return [s.street, s.houseNumber].filter(Boolean).join(' ');
11906
+ return s.label ?? '';
11907
+ }
11908
+ /** "postcode city" — or, when the primary is already the full label, the country instead. */
11909
+ secondaryLine(s) {
11910
+ const cityLine = [s.postcode, s.city].filter(Boolean).join(' ');
11911
+ return s.street ? cityLine : cityLine || s.countryCode || '';
11912
+ }
11913
+ // --- input ------------------------------------------------------------------
11914
+ onInput(event) {
11915
+ const text = event.target.value;
11916
+ this.value.set(text);
11917
+ this.onChange(text);
11918
+ this.locateFailed.set(false); // typing is the way on after a failed locate
11919
+ this.open.set(true);
11920
+ this.runSearch(text);
11921
+ }
11922
+ /** Focus or a click on the (already focused) field opens the overlay. */
11923
+ onFocus() {
11924
+ if (!this.inline())
11925
+ this.open.set(true);
11926
+ }
11927
+ runSearch(text) {
11928
+ clearTimeout(this.timer);
11929
+ const q = text.trim();
11930
+ const seq = ++this.seq;
11931
+ this.query.set(q);
11932
+ this.failed.set(false);
11933
+ this.highlighted.set(-1);
11934
+ if (q.length < this.minChars()) {
11935
+ this.suggestions.set([]);
11936
+ this.loading.set(false);
11937
+ return;
11938
+ }
11939
+ this.loading.set(true);
11940
+ this.timer = setTimeout(() => {
11941
+ let pending;
11942
+ try {
11943
+ pending = Promise.resolve(this.search()(q));
11944
+ }
11945
+ catch (e) {
11946
+ pending = Promise.reject(e);
11947
+ }
11948
+ pending.then((results) => {
11949
+ if (seq !== this.seq || this.destroyed)
11950
+ return; // stale
11951
+ this.suggestions.set(results ?? []);
11952
+ this.loading.set(false);
11953
+ }, () => {
11954
+ if (seq !== this.seq || this.destroyed)
11955
+ return;
11956
+ this.suggestions.set([]);
11957
+ this.loading.set(false);
11958
+ this.failed.set(true);
11959
+ });
11960
+ }, this.debounceMs());
11961
+ }
11962
+ /** Drop any in-flight search and return to the idle (no query) state. */
11963
+ resetSearch() {
11964
+ clearTimeout(this.timer);
11965
+ this.seq++;
11966
+ this.query.set('');
11967
+ this.suggestions.set([]);
11968
+ this.loading.set(false);
11969
+ this.failed.set(false);
11970
+ this.highlighted.set(-1);
11971
+ }
11972
+ clear() {
11973
+ this.value.set('');
11974
+ this.onChange('');
11975
+ this.onTouched();
11976
+ this.resetSearch();
11977
+ this.inputRef()?.nativeElement.focus();
11978
+ }
11979
+ // --- picking ----------------------------------------------------------------
11980
+ pick(s, source) {
11981
+ const text = s.label ?? '';
11982
+ this.value.set(text);
11983
+ this.onChange(text);
11984
+ this.onTouched();
11985
+ this.resetSearch();
11986
+ this.locateFailed.set(false);
11987
+ this.close();
11988
+ this.selected.emit({ ...s, source });
11989
+ }
11990
+ /** The current-location row: the `locate` provider, then a pick — or the inline failure notice. */
11991
+ async useCurrentLocation() {
11992
+ const locate = this.locate();
11993
+ if (!locate || this.locating())
11994
+ return;
11995
+ this.locateFailed.set(false);
11996
+ this.locating.set(true);
11997
+ let here = null;
11998
+ try {
11999
+ here = await locate();
12000
+ }
12001
+ catch {
12002
+ here = null;
12003
+ }
12004
+ if (this.destroyed)
12005
+ return;
12006
+ this.locating.set(false);
12007
+ if (!here) {
12008
+ this.locateFailed.set(true);
12009
+ return;
12010
+ }
12011
+ this.pick(here, 'location');
12012
+ }
12013
+ activate(rowIndex) {
12014
+ const row = this.rows()[rowIndex];
12015
+ if (!row)
12016
+ return;
12017
+ if (row.kind === 'locate')
12018
+ void this.useCurrentLocation();
12019
+ else
12020
+ this.pick(row.suggestion, 'search');
12021
+ }
12022
+ // --- keyboard ---------------------------------------------------------------
12023
+ onKeydown(event) {
12024
+ if (this.isDisabled())
12025
+ return;
12026
+ const count = this.rows().length;
12027
+ switch (event.key) {
12028
+ case 'ArrowDown':
12029
+ case 'ArrowUp': {
12030
+ event.preventDefault();
12031
+ if (!this.panelShown()) {
12032
+ this.open.set(true);
12033
+ if (!this.panelShown())
12034
+ return;
12035
+ }
12036
+ if (!count)
12037
+ return;
12038
+ const delta = event.key === 'ArrowDown' ? 1 : -1;
12039
+ const from = this.highlighted();
12040
+ const next = from < 0 ? (delta > 0 ? 0 : count - 1) : (from + delta + count) % count;
12041
+ this.highlighted.set(next);
12042
+ this.scrollHighlightedIntoView();
12043
+ return;
12044
+ }
12045
+ case 'Enter':
12046
+ if (!this.panelShown())
12047
+ return;
12048
+ // Keep Enter from submitting a surrounding form while the rows are up.
12049
+ event.preventDefault();
12050
+ if (this.highlighted() >= 0)
12051
+ this.activate(this.highlighted());
12052
+ return;
12053
+ case 'Escape':
12054
+ if (!this.inline() && this.open() && this.panelShown()) {
12055
+ event.preventDefault();
12056
+ event.stopPropagation(); // close the panel, not the drawer / dialog around it
12057
+ this.close();
12058
+ }
12059
+ else if (this.highlighted() >= 0) {
12060
+ this.highlighted.set(-1);
12061
+ }
12062
+ return;
12063
+ case 'Tab':
12064
+ this.close();
12065
+ return;
12066
+ }
12067
+ }
12068
+ scrollHighlightedIntoView() {
12069
+ setTimeout(() => {
12070
+ const root = this.panelEl ?? this.host.nativeElement;
12071
+ root
12072
+ .querySelector(`[data-index="${this.highlighted()}"]`)
12073
+ ?.scrollIntoView?.({ block: 'nearest' });
12074
+ });
12075
+ }
12076
+ // --- overlay ----------------------------------------------------------------
12077
+ /** Close the overlay (a no-op `inline`, where the rows are always shown). */
12078
+ close() {
12079
+ this.open.set(false);
12080
+ this.highlighted.set(-1);
12081
+ this.detachPanel();
12082
+ }
12083
+ reflow = () => this.positionPanel();
12084
+ listening = false;
12085
+ syncPanel() {
12086
+ if (this.inline() || !this.panelShown()) {
12087
+ if (this.panelEl)
12088
+ this.detachPanel();
12089
+ return;
12090
+ }
12091
+ if (!this.panelEl) {
12092
+ this.panelEl = this.host.nativeElement.querySelector('[data-sds-address-panel]');
12093
+ if (!this.panelEl)
12094
+ return;
12095
+ document.body.appendChild(this.panelEl);
12096
+ if (!this.listening) {
12097
+ window.addEventListener('scroll', this.reflow, true);
12098
+ window.addEventListener('resize', this.reflow);
12099
+ this.listening = true;
12100
+ }
12101
+ }
12102
+ this.positionPanel();
12103
+ }
12104
+ detachPanel() {
12105
+ if (this.listening) {
12106
+ window.removeEventListener('scroll', this.reflow, true);
12107
+ window.removeEventListener('resize', this.reflow);
12108
+ this.listening = false;
12109
+ }
12110
+ this.panelEl?.remove();
12111
+ this.panelEl = null;
12112
+ }
12113
+ positionPanel() {
12114
+ const fieldEl = this.fieldRef()?.nativeElement;
12115
+ const panelEl = this.panelEl;
12116
+ if (!fieldEl || !panelEl)
12117
+ return;
12118
+ const r = fieldEl.getBoundingClientRect();
12119
+ const gap = 4;
12120
+ const margin = 8;
12121
+ panelEl.style.width = `${Math.round(r.width)}px`;
12122
+ const ph = panelEl.offsetHeight || 240;
12123
+ let top = r.bottom + gap;
12124
+ if (top + ph > window.innerHeight - margin && r.top - gap - ph > margin) {
12125
+ top = r.top - gap - ph;
12126
+ }
12127
+ panelEl.style.top = `${Math.round(top)}px`;
12128
+ panelEl.style.left = `${Math.round(Math.max(margin, r.left))}px`;
12129
+ }
12130
+ onDocumentClick(event) {
12131
+ if (this.inline() || !this.open())
12132
+ return;
12133
+ const target = event.target;
12134
+ const inField = this.host.nativeElement.contains(target);
12135
+ const inPanel = !!this.panelEl && this.panelEl.contains(target);
12136
+ if (!inField && !inPanel)
12137
+ this.close();
12138
+ }
12139
+ ngOnDestroy() {
12140
+ this.destroyed = true;
12141
+ clearTimeout(this.timer);
12142
+ this.detachPanel();
12143
+ }
12144
+ // --- ControlValueAccessor -----------------------------------------------------
12145
+ onChange = () => { };
12146
+ onTouched = () => { };
12147
+ /** A written value is display text only: it never runs a search. */
12148
+ writeValue(value) {
12149
+ this.value.set(value ?? '');
12150
+ this.resetSearch();
12151
+ }
12152
+ registerOnChange(fn) {
12153
+ this.onChange = fn;
12154
+ }
12155
+ registerOnTouched(fn) {
12156
+ this.onTouched = fn;
12157
+ }
12158
+ setDisabledState(isDisabled) {
12159
+ this.cvaDisabled.set(isDisabled);
12160
+ if (isDisabled)
12161
+ this.close();
12162
+ }
12163
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsAddressAutocomplete, deps: [], target: i0.ɵɵFactoryTarget.Component });
12164
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsAddressAutocomplete, isStandalone: true, selector: "sds-address-autocomplete", inputs: { search: { classPropertyName: "search", publicName: "search", isSignal: true, isRequired: true, transformFunction: null }, locate: { classPropertyName: "locate", publicName: "locate", isSignal: true, isRequired: false, transformFunction: null }, minChars: { classPropertyName: "minChars", publicName: "minChars", isSignal: true, isRequired: false, transformFunction: null }, debounceMs: { classPropertyName: "debounceMs", publicName: "debounceMs", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, extraPosition: { classPropertyName: "extraPosition", publicName: "extraPosition", isSignal: true, isRequired: false, transformFunction: null }, showNoResults: { classPropertyName: "showNoResults", publicName: "showNoResults", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, leadingIcon: { classPropertyName: "leadingIcon", publicName: "leadingIcon", isSignal: true, isRequired: false, transformFunction: null }, useLocationText: { classPropertyName: "useLocationText", publicName: "useLocationText", isSignal: true, isRequired: false, transformFunction: null }, locatingText: { classPropertyName: "locatingText", publicName: "locatingText", isSignal: true, isRequired: false, transformFunction: null }, locateFailedText: { classPropertyName: "locateFailedText", publicName: "locateFailedText", isSignal: true, isRequired: false, transformFunction: null }, searchingText: { classPropertyName: "searchingText", publicName: "searchingText", isSignal: true, isRequired: false, transformFunction: null }, noResultsText: { classPropertyName: "noResultsText", publicName: "noResultsText", isSignal: true, isRequired: false, transformFunction: null }, searchErrorText: { classPropertyName: "searchErrorText", publicName: "searchErrorText", isSignal: true, isRequired: false, transformFunction: null }, resultsLabel: { classPropertyName: "resultsLabel", publicName: "resultsLabel", isSignal: true, isRequired: false, transformFunction: null }, clearLabel: { classPropertyName: "clearLabel", publicName: "clearLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selected" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, classAttribute: "block" }, providers: [
12165
+ {
12166
+ provide: NG_VALUE_ACCESSOR,
12167
+ useExisting: forwardRef(() => SdsAddressAutocomplete),
12168
+ multi: true,
12169
+ },
12170
+ ], queries: [{ propertyName: "extraTpl", first: true, predicate: SdsAddressExtra, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "fieldRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "inputRef", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
12171
+ @if (label()) {
12172
+ <label [for]="fieldId()" class="mb-1.5 block text-sm font-medium text-ink">
12173
+ {{ label() }}
12174
+ @if (required()) {
12175
+ <span class="ml-0.5 text-danger" aria-hidden="true">*</span>
12176
+ }
12177
+ </label>
12178
+ }
12179
+
12180
+ <div
12181
+ #field
12182
+ class="relative flex items-center border transition-colors"
12183
+ [class]="wrapperClasses()"
12184
+ >
12185
+ @if (leadingIcon()) {
12186
+ <sds-icon
12187
+ [name]="leadingIcon()!"
12188
+ [size]="18"
12189
+ class="pointer-events-none absolute opacity-70"
12190
+ [class]="pill() ? 'left-4' : 'left-3'"
12191
+ />
12192
+ }
12193
+ <input
12194
+ #inputEl
12195
+ [id]="fieldId()"
12196
+ type="text"
12197
+ role="combobox"
12198
+ autocomplete="off"
12199
+ spellcheck="false"
12200
+ aria-autocomplete="list"
12201
+ [attr.aria-expanded]="panelShown()"
12202
+ [attr.aria-controls]="controlsIds()"
12203
+ [attr.aria-activedescendant]="activeDescId()"
12204
+ [attr.aria-label]="!label() && ariaLabel() ? ariaLabel() : null"
12205
+ [attr.aria-invalid]="error() || invalid() ? 'true' : null"
12206
+ [attr.aria-required]="required() ? 'true' : null"
12207
+ [attr.aria-describedby]="describedBy()"
12208
+ [placeholder]="placeholder()"
12209
+ [disabled]="isDisabled()"
12210
+ [value]="value()"
12211
+ class="w-full bg-transparent outline-none placeholder:text-current/50 disabled:cursor-not-allowed"
12212
+ [class]="fieldPadding()"
12213
+ (input)="onInput($event)"
12214
+ (focus)="onFocus()"
12215
+ (click)="onFocus()"
12216
+ (blur)="onTouched()"
12217
+ (keydown)="onKeydown($event)"
12218
+ />
12219
+ @if (clearable() && value() && !isDisabled()) {
12220
+ <button
12221
+ type="button"
12222
+ tabindex="-1"
12223
+ [attr.aria-label]="clearLabel()"
12224
+ class="absolute right-2.5 inline-flex h-6 w-6 items-center justify-center rounded-full opacity-60 hover:bg-black/5 hover:opacity-100"
12225
+ (click)="clear()"
12226
+ >
12227
+ <sds-icon name="x" [size]="15" />
12228
+ </button>
12229
+ } @else if (loading()) {
12230
+ <sds-icon
12231
+ name="loader-circle"
12232
+ [size]="16"
12233
+ class="pointer-events-none absolute right-3 animate-spin opacity-60"
12234
+ />
12235
+ }
12236
+ </div>
12237
+
12238
+ @if (error()) {
12239
+ <p [id]="fieldId() + '-err'" class="mt-1.5 text-xs font-medium text-danger">{{ error() }}</p>
12240
+ } @else if (hint()) {
12241
+ <p [id]="fieldId() + '-hint'" class="mt-1.5 text-xs text-ink-faint">{{ hint() }}</p>
12242
+ }
12243
+
12244
+ @if (inline()) {
12245
+ <div class="mt-2" data-sds-address-panel-inline>
12246
+ <ng-container [ngTemplateOutlet]="panelBody" />
12247
+ </div>
12248
+ } @else if (panelShown()) {
12249
+ <!-- Appended to document.body on open (see syncPanel). -->
12250
+ <div
12251
+ data-sds-address-panel
12252
+ class="fixed z-[1000] max-h-80 overflow-auto rounded-card border border-border bg-surface p-1 shadow-lg"
12253
+ >
12254
+ <ng-container [ngTemplateOutlet]="panelBody" />
12255
+ </div>
12256
+ }
12257
+
12258
+ <ng-template #panelBody>
12259
+ @if (showLocate()) {
12260
+ <ul [id]="locateListId" role="listbox" [attr.aria-label]="listLabel()">
12261
+ <li
12262
+ [id]="optionId(0)"
12263
+ role="option"
12264
+ data-index="0"
12265
+ data-sds-address-locate
12266
+ [attr.aria-selected]="highlighted() === 0"
12267
+ [attr.aria-disabled]="locating() ? 'true' : null"
12268
+ [attr.aria-busy]="locating() ? 'true' : null"
12269
+ class="flex items-center gap-3 rounded-control px-3 py-2.5"
12270
+ [class]="rowClass(0, locating())"
12271
+ (mousedown)="$event.preventDefault()"
12272
+ (click)="useCurrentLocation()"
12273
+ (mouseenter)="highlighted.set(0)"
12274
+ >
12275
+ <span
12276
+ class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-ink"
12277
+ >
12278
+ @if (locating()) {
12279
+ <sds-icon name="loader-circle" [size]="18" class="animate-spin" />
12280
+ } @else {
12281
+ <sds-icon name="locate-fixed" [size]="18" />
12282
+ }
12283
+ </span>
12284
+ <span class="min-w-0 flex-1 truncate font-semibold text-ink">
12285
+ {{ locating() ? locatingText() : useLocationText() }}
12286
+ </span>
12287
+ </li>
12288
+ </ul>
12289
+ @if (locateFailed()) {
12290
+ <p
12291
+ data-sds-address-locate-failed
12292
+ role="alert"
12293
+ class="mx-1 mb-1 mt-0.5 flex items-center gap-2 rounded-control bg-accent-red-soft px-3 py-2.5 text-sm text-accent-red"
12294
+ >
12295
+ <sds-icon name="locate-off" [size]="16" class="shrink-0" />{{ locateFailedText() }}
12296
+ </p>
12297
+ }
12298
+ }
12299
+
12300
+ @if (extraTpl() && extraPosition() === 'above-results') {
12301
+ <ng-container
12302
+ [ngTemplateOutlet]="extraTpl()!.template"
12303
+ [ngTemplateOutletContext]="extraContext()"
12304
+ />
12305
+ }
12306
+
12307
+ @if (isSearching()) {
12308
+ @if (suggestions().length) {
12309
+ @if (resultsLabel()) {
12310
+ <div
12311
+ class="px-3 pb-1 pt-1.5 text-xs font-semibold uppercase tracking-wide text-ink-soft"
12312
+ >
12313
+ {{ resultsLabel() }}
12314
+ </div>
12315
+ }
12316
+ <ul [id]="listId" role="listbox" [attr.aria-label]="listLabel()">
12317
+ @for (row of suggestionRows(); track $index) {
12318
+ <li
12319
+ [id]="optionId(row.rowIndex)"
12320
+ role="option"
12321
+ data-sds-address-suggestion
12322
+ [attr.data-index]="row.rowIndex"
12323
+ [attr.aria-selected]="highlighted() === row.rowIndex"
12324
+ class="flex items-center gap-3 rounded-control px-3 py-2.5"
12325
+ [class]="rowClass(row.rowIndex, false)"
12326
+ (mousedown)="$event.preventDefault()"
12327
+ (click)="pick(row.suggestion, 'search')"
12328
+ (mouseenter)="highlighted.set(row.rowIndex)"
12329
+ >
12330
+ <span
12331
+ class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-ink"
12332
+ >
12333
+ <sds-icon name="map-pin" [size]="18" />
12334
+ </span>
12335
+ <span class="min-w-0 flex-1">
12336
+ <span class="block truncate font-semibold text-ink">{{
12337
+ primaryLine(row.suggestion)
12338
+ }}</span>
12339
+ @if (secondaryLine(row.suggestion); as second) {
12340
+ <span class="block truncate text-xs text-ink-soft">{{ second }}</span>
12341
+ }
12342
+ </span>
12343
+ </li>
12344
+ }
12345
+ </ul>
12346
+ } @else if (loading()) {
12347
+ <p
12348
+ role="status"
12349
+ data-sds-address-loading
12350
+ class="flex items-center justify-center gap-2 py-6 text-sm text-ink-soft"
12351
+ >
12352
+ <sds-icon name="loader-circle" [size]="16" class="animate-spin" />{{ searchingText() }}
12353
+ </p>
12354
+ } @else if (failed()) {
12355
+ <p
12356
+ role="alert"
12357
+ data-sds-address-error
12358
+ class="mx-1 my-1 flex items-center gap-2 rounded-control bg-accent-red-soft px-3 py-2.5 text-sm text-accent-red"
12359
+ >
12360
+ <sds-icon name="alert-circle" [size]="16" class="shrink-0" />{{ searchErrorText() }}
12361
+ </p>
12362
+ } @else if (showNoResults()) {
12363
+ <p role="status" data-sds-address-empty class="py-6 text-center text-sm text-ink-soft">
12364
+ {{ noResultsText() }}
12365
+ </p>
12366
+ }
12367
+ }
12368
+
12369
+ @if (extraTpl() && extraPosition() === 'below') {
12370
+ <ng-container
12371
+ [ngTemplateOutlet]="extraTpl()!.template"
12372
+ [ngTemplateOutletContext]="extraContext()"
12373
+ />
12374
+ }
12375
+ </ng-template>
12376
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth", "filled"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
12377
+ }
12378
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsAddressAutocomplete, decorators: [{
12379
+ type: Component,
12380
+ args: [{
12381
+ selector: 'sds-address-autocomplete',
12382
+ changeDetection: ChangeDetectionStrategy.OnPush,
12383
+ imports: [SdsIcon, NgTemplateOutlet],
12384
+ providers: [
12385
+ {
12386
+ provide: NG_VALUE_ACCESSOR,
12387
+ useExisting: forwardRef(() => SdsAddressAutocomplete),
12388
+ multi: true,
12389
+ },
12390
+ ],
12391
+ host: {
12392
+ class: 'block',
12393
+ '(document:click)': 'onDocumentClick($event)',
12394
+ },
12395
+ template: `
12396
+ @if (label()) {
12397
+ <label [for]="fieldId()" class="mb-1.5 block text-sm font-medium text-ink">
12398
+ {{ label() }}
12399
+ @if (required()) {
12400
+ <span class="ml-0.5 text-danger" aria-hidden="true">*</span>
12401
+ }
12402
+ </label>
12403
+ }
12404
+
12405
+ <div
12406
+ #field
12407
+ class="relative flex items-center border transition-colors"
12408
+ [class]="wrapperClasses()"
12409
+ >
12410
+ @if (leadingIcon()) {
12411
+ <sds-icon
12412
+ [name]="leadingIcon()!"
12413
+ [size]="18"
12414
+ class="pointer-events-none absolute opacity-70"
12415
+ [class]="pill() ? 'left-4' : 'left-3'"
12416
+ />
12417
+ }
12418
+ <input
12419
+ #inputEl
12420
+ [id]="fieldId()"
12421
+ type="text"
12422
+ role="combobox"
12423
+ autocomplete="off"
12424
+ spellcheck="false"
12425
+ aria-autocomplete="list"
12426
+ [attr.aria-expanded]="panelShown()"
12427
+ [attr.aria-controls]="controlsIds()"
12428
+ [attr.aria-activedescendant]="activeDescId()"
12429
+ [attr.aria-label]="!label() && ariaLabel() ? ariaLabel() : null"
12430
+ [attr.aria-invalid]="error() || invalid() ? 'true' : null"
12431
+ [attr.aria-required]="required() ? 'true' : null"
12432
+ [attr.aria-describedby]="describedBy()"
12433
+ [placeholder]="placeholder()"
12434
+ [disabled]="isDisabled()"
12435
+ [value]="value()"
12436
+ class="w-full bg-transparent outline-none placeholder:text-current/50 disabled:cursor-not-allowed"
12437
+ [class]="fieldPadding()"
12438
+ (input)="onInput($event)"
12439
+ (focus)="onFocus()"
12440
+ (click)="onFocus()"
12441
+ (blur)="onTouched()"
12442
+ (keydown)="onKeydown($event)"
12443
+ />
12444
+ @if (clearable() && value() && !isDisabled()) {
12445
+ <button
12446
+ type="button"
12447
+ tabindex="-1"
12448
+ [attr.aria-label]="clearLabel()"
12449
+ class="absolute right-2.5 inline-flex h-6 w-6 items-center justify-center rounded-full opacity-60 hover:bg-black/5 hover:opacity-100"
12450
+ (click)="clear()"
12451
+ >
12452
+ <sds-icon name="x" [size]="15" />
12453
+ </button>
12454
+ } @else if (loading()) {
12455
+ <sds-icon
12456
+ name="loader-circle"
12457
+ [size]="16"
12458
+ class="pointer-events-none absolute right-3 animate-spin opacity-60"
12459
+ />
12460
+ }
12461
+ </div>
12462
+
12463
+ @if (error()) {
12464
+ <p [id]="fieldId() + '-err'" class="mt-1.5 text-xs font-medium text-danger">{{ error() }}</p>
12465
+ } @else if (hint()) {
12466
+ <p [id]="fieldId() + '-hint'" class="mt-1.5 text-xs text-ink-faint">{{ hint() }}</p>
12467
+ }
12468
+
12469
+ @if (inline()) {
12470
+ <div class="mt-2" data-sds-address-panel-inline>
12471
+ <ng-container [ngTemplateOutlet]="panelBody" />
12472
+ </div>
12473
+ } @else if (panelShown()) {
12474
+ <!-- Appended to document.body on open (see syncPanel). -->
12475
+ <div
12476
+ data-sds-address-panel
12477
+ class="fixed z-[1000] max-h-80 overflow-auto rounded-card border border-border bg-surface p-1 shadow-lg"
12478
+ >
12479
+ <ng-container [ngTemplateOutlet]="panelBody" />
12480
+ </div>
12481
+ }
12482
+
12483
+ <ng-template #panelBody>
12484
+ @if (showLocate()) {
12485
+ <ul [id]="locateListId" role="listbox" [attr.aria-label]="listLabel()">
12486
+ <li
12487
+ [id]="optionId(0)"
12488
+ role="option"
12489
+ data-index="0"
12490
+ data-sds-address-locate
12491
+ [attr.aria-selected]="highlighted() === 0"
12492
+ [attr.aria-disabled]="locating() ? 'true' : null"
12493
+ [attr.aria-busy]="locating() ? 'true' : null"
12494
+ class="flex items-center gap-3 rounded-control px-3 py-2.5"
12495
+ [class]="rowClass(0, locating())"
12496
+ (mousedown)="$event.preventDefault()"
12497
+ (click)="useCurrentLocation()"
12498
+ (mouseenter)="highlighted.set(0)"
12499
+ >
12500
+ <span
12501
+ class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-ink"
12502
+ >
12503
+ @if (locating()) {
12504
+ <sds-icon name="loader-circle" [size]="18" class="animate-spin" />
12505
+ } @else {
12506
+ <sds-icon name="locate-fixed" [size]="18" />
12507
+ }
12508
+ </span>
12509
+ <span class="min-w-0 flex-1 truncate font-semibold text-ink">
12510
+ {{ locating() ? locatingText() : useLocationText() }}
12511
+ </span>
12512
+ </li>
12513
+ </ul>
12514
+ @if (locateFailed()) {
12515
+ <p
12516
+ data-sds-address-locate-failed
12517
+ role="alert"
12518
+ class="mx-1 mb-1 mt-0.5 flex items-center gap-2 rounded-control bg-accent-red-soft px-3 py-2.5 text-sm text-accent-red"
12519
+ >
12520
+ <sds-icon name="locate-off" [size]="16" class="shrink-0" />{{ locateFailedText() }}
12521
+ </p>
12522
+ }
12523
+ }
12524
+
12525
+ @if (extraTpl() && extraPosition() === 'above-results') {
12526
+ <ng-container
12527
+ [ngTemplateOutlet]="extraTpl()!.template"
12528
+ [ngTemplateOutletContext]="extraContext()"
12529
+ />
12530
+ }
12531
+
12532
+ @if (isSearching()) {
12533
+ @if (suggestions().length) {
12534
+ @if (resultsLabel()) {
12535
+ <div
12536
+ class="px-3 pb-1 pt-1.5 text-xs font-semibold uppercase tracking-wide text-ink-soft"
12537
+ >
12538
+ {{ resultsLabel() }}
12539
+ </div>
12540
+ }
12541
+ <ul [id]="listId" role="listbox" [attr.aria-label]="listLabel()">
12542
+ @for (row of suggestionRows(); track $index) {
12543
+ <li
12544
+ [id]="optionId(row.rowIndex)"
12545
+ role="option"
12546
+ data-sds-address-suggestion
12547
+ [attr.data-index]="row.rowIndex"
12548
+ [attr.aria-selected]="highlighted() === row.rowIndex"
12549
+ class="flex items-center gap-3 rounded-control px-3 py-2.5"
12550
+ [class]="rowClass(row.rowIndex, false)"
12551
+ (mousedown)="$event.preventDefault()"
12552
+ (click)="pick(row.suggestion, 'search')"
12553
+ (mouseenter)="highlighted.set(row.rowIndex)"
12554
+ >
12555
+ <span
12556
+ class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-ink"
12557
+ >
12558
+ <sds-icon name="map-pin" [size]="18" />
12559
+ </span>
12560
+ <span class="min-w-0 flex-1">
12561
+ <span class="block truncate font-semibold text-ink">{{
12562
+ primaryLine(row.suggestion)
12563
+ }}</span>
12564
+ @if (secondaryLine(row.suggestion); as second) {
12565
+ <span class="block truncate text-xs text-ink-soft">{{ second }}</span>
12566
+ }
12567
+ </span>
12568
+ </li>
12569
+ }
12570
+ </ul>
12571
+ } @else if (loading()) {
12572
+ <p
12573
+ role="status"
12574
+ data-sds-address-loading
12575
+ class="flex items-center justify-center gap-2 py-6 text-sm text-ink-soft"
12576
+ >
12577
+ <sds-icon name="loader-circle" [size]="16" class="animate-spin" />{{ searchingText() }}
12578
+ </p>
12579
+ } @else if (failed()) {
12580
+ <p
12581
+ role="alert"
12582
+ data-sds-address-error
12583
+ class="mx-1 my-1 flex items-center gap-2 rounded-control bg-accent-red-soft px-3 py-2.5 text-sm text-accent-red"
12584
+ >
12585
+ <sds-icon name="alert-circle" [size]="16" class="shrink-0" />{{ searchErrorText() }}
12586
+ </p>
12587
+ } @else if (showNoResults()) {
12588
+ <p role="status" data-sds-address-empty class="py-6 text-center text-sm text-ink-soft">
12589
+ {{ noResultsText() }}
12590
+ </p>
12591
+ }
12592
+ }
12593
+
12594
+ @if (extraTpl() && extraPosition() === 'below') {
12595
+ <ng-container
12596
+ [ngTemplateOutlet]="extraTpl()!.template"
12597
+ [ngTemplateOutletContext]="extraContext()"
12598
+ />
12599
+ }
12600
+ </ng-template>
12601
+ `,
12602
+ }]
12603
+ }], ctorParameters: () => [], propDecorators: { search: [{ type: i0.Input, args: [{ isSignal: true, alias: "search", required: true }] }], locate: [{ type: i0.Input, args: [{ isSignal: true, alias: "locate", required: false }] }], minChars: [{ type: i0.Input, args: [{ isSignal: true, alias: "minChars", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], inline: [{ type: i0.Input, args: [{ isSignal: true, alias: "inline", required: false }] }], extraPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "extraPosition", required: false }] }], showNoResults: [{ type: i0.Input, args: [{ isSignal: true, alias: "showNoResults", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], leadingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadingIcon", required: false }] }], useLocationText: [{ type: i0.Input, args: [{ isSignal: true, alias: "useLocationText", required: false }] }], locatingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "locatingText", required: false }] }], locateFailedText: [{ type: i0.Input, args: [{ isSignal: true, alias: "locateFailedText", required: false }] }], searchingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchingText", required: false }] }], noResultsText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noResultsText", required: false }] }], searchErrorText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchErrorText", required: false }] }], resultsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "resultsLabel", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], selected: [{ type: i0.Output, args: ["selected"] }], extraTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsAddressExtra), { isSignal: true }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], inputRef: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }] } });
12604
+
12605
+ const SIZES$1 = {
12606
+ sm: 'h-8 min-w-8 text-xs',
12607
+ md: 'h-9 min-w-9 text-sm',
12608
+ lg: 'h-11 min-w-11 text-base',
12609
+ };
12610
+ const ICON_SIZES = { sm: 15, md: 16, lg: 18 };
12611
+ const BTN_BASE = 'inline-flex items-center justify-center rounded-control px-1.5 font-medium ' +
12612
+ 'transition-colors select-none focus-visible:outline-none focus-visible:ring-2 ' +
12613
+ 'focus-visible:ring-brand-500 focus-visible:ring-offset-1';
12614
+ /**
12615
+ * Pagination control (mirrors PrimeNG's paginator). Drives a sliding window of
12616
+ * page links with first/prev/next/last navigation, an optional rows-per-page
12617
+ * dropdown (reuses <sds-select>), a current-page report and a jump-to-page input.
12618
+ *
12619
+ * State is exposed as two-way models: [first] (zero-based index of the first
12620
+ * record on the page) and [rows] (page size). A combined (pageChange) event
12621
+ * carries the full {@link SdsPaginatorState} on every change.
12622
+ *
12623
+ * @example
12624
+ * <sds-paginator
12625
+ * [totalRecords]="120"
12626
+ * [(first)]="first"
12627
+ * [(rows)]="rows"
12628
+ * [rowsPerPageOptions]="[10, 20, 50]"
12629
+ * showCurrentPageReport
12630
+ * (pageChange)="load($event)"
12631
+ * />
12632
+ */
12633
+ class SdsPaginator {
12634
+ /** Total number of records to paginate. */
12635
+ totalRecords = input(0, ...(ngDevMode ? [{ debugName: "totalRecords", transform: numberAttribute }] : [{ transform: numberAttribute }]));
12636
+ /** Zero-based index of the first record on the current page (two-way). */
12637
+ first = model(0, ...(ngDevMode ? [{ debugName: "first" }] : []));
12638
+ /** Rows displayed per page (two-way). */
12639
+ rows = model(10, ...(ngDevMode ? [{ debugName: "rows" }] : []));
12640
+ /** When non-empty, renders a rows-per-page dropdown with these choices. */
12641
+ rowsPerPageOptions = input([], ...(ngDevMode ? [{ debugName: "rowsPerPageOptions" }] : []));
12642
+ /** Maximum count of numbered page links shown in the sliding window. */
12643
+ pageLinkSize = input(5, ...(ngDevMode ? [{ debugName: "pageLinkSize", transform: numberAttribute }] : [{ transform: numberAttribute }]));
12644
+ showFirstLastIcon = input(true, ...(ngDevMode ? [{ debugName: "showFirstLastIcon", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12645
+ showPageLinks = input(true, ...(ngDevMode ? [{ debugName: "showPageLinks", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12646
+ showCurrentPageReport = input(false, ...(ngDevMode ? [{ debugName: "showCurrentPageReport", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12647
+ /** Tokens: {first} {last} {totalRecords} {currentPage} {totalPages} {rows}. */
12648
+ currentPageReportTemplate = input('{first} – {last} of {totalRecords}', ...(ngDevMode ? [{ debugName: "currentPageReportTemplate" }] : []));
12649
+ showJumpToPageInput = input(false, ...(ngDevMode ? [{ debugName: "showJumpToPageInput", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12650
+ rowsPerPageLabel = input('Rows per page', ...(ngDevMode ? [{ debugName: "rowsPerPageLabel" }] : []));
12651
+ jumpToPageLabel = input('Go to', ...(ngDevMode ? [{ debugName: "jumpToPageLabel" }] : []));
12652
+ /**
12653
+ * Accessible name for the <nav> wrapping the page controls, and for the four icon-only
12654
+ * first/prev/next/last buttons. Those buttons have no text, so these strings are their ONLY
12655
+ * accessible names — the English defaults suit an English-first app, but an app serving more than
12656
+ * one language MUST pass translated values.
12657
+ */
12658
+ paginationLabel = input('Pagination', ...(ngDevMode ? [{ debugName: "paginationLabel" }] : []));
12659
+ firstPageLabel = input('First page', ...(ngDevMode ? [{ debugName: "firstPageLabel" }] : []));
12660
+ previousPageLabel = input('Previous page', ...(ngDevMode ? [{ debugName: "previousPageLabel" }] : []));
12661
+ nextPageLabel = input('Next page', ...(ngDevMode ? [{ debugName: "nextPageLabel" }] : []));
12662
+ lastPageLabel = input('Last page', ...(ngDevMode ? [{ debugName: "lastPageLabel" }] : []));
12663
+ /**
12664
+ * Accessible name of a numbered page link. `{page}` is replaced with the 1-based page number —
12665
+ * a token rather than a prefix because word order is not the same in every language.
12666
+ */
12667
+ pageLinkLabel = input('Page {page}', ...(ngDevMode ? [{ debugName: "pageLinkLabel" }] : []));
12668
+ /**
12669
+ * Accessible name of the jump-to-page field. Separate from the visible {@link jumpToPageLabel}
12670
+ * ("Go to") on purpose: the visible text is terse because it sits next to the field, while the
12671
+ * accessible name has to stand alone.
12672
+ */
12673
+ jumpToPageAriaLabel = input('Go to page', ...(ngDevMode ? [{ debugName: "jumpToPageAriaLabel" }] : []));
12674
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
12675
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12676
+ /** Fires after any page or page-size change with the full paginator state. */
12677
+ pageChange = output();
12678
+ pageCount = computed(() => Math.max(1, Math.ceil(this.totalRecords() / Math.max(1, this.rows()))), ...(ngDevMode ? [{ debugName: "pageCount" }] : []));
12679
+ page = computed(() => Math.min(this.pageCount() - 1, Math.floor(this.first() / Math.max(1, this.rows()))), ...(ngDevMode ? [{ debugName: "page" }] : []));
12680
+ firstDisabled = computed(() => this.disabled() || this.page() <= 0, ...(ngDevMode ? [{ debugName: "firstDisabled" }] : []));
12681
+ lastDisabled = computed(() => this.disabled() || this.page() >= this.pageCount() - 1, ...(ngDevMode ? [{ debugName: "lastDisabled" }] : []));
12682
+ iconSize = computed(() => ICON_SIZES[this.size()], ...(ngDevMode ? [{ debugName: "iconSize" }] : []));
12683
+ // Sliding window of zero-based page indices, centred on the current page.
12684
+ pageLinks = computed(() => {
12685
+ const pages = this.pageCount();
12686
+ const visible = Math.min(this.pageLinkSize(), pages);
12687
+ let start = Math.max(0, Math.ceil(this.page() - visible / 2));
12688
+ const end = Math.min(pages - 1, start + visible - 1);
12689
+ start = Math.max(0, start - (visible - (end - start + 1)));
12690
+ return Array.from({ length: end - start + 1 }, (_, i) => start + i);
12691
+ }, ...(ngDevMode ? [{ debugName: "pageLinks" }] : []));
12692
+ reportText = computed(() => {
12693
+ const total = this.totalRecords();
12694
+ const first = total === 0 ? 0 : this.first() + 1;
12695
+ const last = Math.min(this.first() + this.rows(), total);
12696
+ return this.currentPageReportTemplate()
12697
+ .replace('{first}', String(first))
12698
+ .replace('{last}', String(last))
12699
+ .replace('{totalRecords}', String(total))
12700
+ .replace('{currentPage}', String(this.page() + 1))
12701
+ .replace('{totalPages}', String(this.pageCount()))
12702
+ .replace('{rows}', String(this.rows()));
12703
+ }, ...(ngDevMode ? [{ debugName: "reportText" }] : []));
12704
+ hostClass = computed(() => {
12705
+ const base = 'flex flex-wrap items-center gap-x-4 gap-y-3';
12706
+ const justify = this.showCurrentPageReport() ? 'justify-between' : 'justify-center';
12707
+ return `${base} ${justify}`;
12708
+ }, ...(ngDevMode ? [{ debugName: "hostClass" }] : []));
12709
+ pageLinkAriaLabel(page) {
12710
+ return this.pageLinkLabel().replace('{page}', String(page + 1));
12711
+ }
12712
+ navClass(isDisabled) {
12713
+ const cls = `${BTN_BASE} ${SIZES$1[this.size()]} px-0`;
12714
+ return isDisabled
12715
+ ? `${cls} text-ink-faint opacity-50 cursor-not-allowed`
12716
+ : `${cls} text-ink-soft hover:bg-muted cursor-pointer`;
12717
+ }
12718
+ linkClass(active) {
12719
+ const cls = `${BTN_BASE} ${SIZES$1[this.size()]}`;
12720
+ if (this.disabled())
12721
+ return `${cls} text-ink-faint opacity-50 cursor-not-allowed`;
12722
+ return active
12723
+ ? `${cls} bg-brand-600 text-white hover:bg-brand-700 ${DARK_BRAND_FILL_HOVER} cursor-pointer`
12724
+ : `${cls} text-ink-soft hover:bg-muted cursor-pointer`;
12725
+ }
12726
+ firstPage() {
12727
+ this.changePage(0);
11768
12728
  }
11769
12729
  prevPage() {
11770
12730
  this.changePage(this.page() - 1);
@@ -14877,7 +15837,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
14877
15837
  }]
14878
15838
  }], propDecorators: { closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }] } });
14879
15839
 
14880
- const STORAGE_KEY = 'sds-theme';
15840
+ /** The localStorage key the mode is persisted under. Also read by the pre-paint boot script (see README). */
15841
+ const SDS_THEME_STORAGE_KEY = 'sds-theme';
15842
+ /** `true` for the three values {@link SdsThemeMode} accepts — for validating stored or URL-supplied input. */
15843
+ function isSdsThemeMode(value) {
15844
+ return value === 'system' || value === 'light' || value === 'dark';
15845
+ }
14881
15846
  /**
14882
15847
  * Light/dark theme state for the whole app. It owns exactly one thing: the presence of the `.dark`
14883
15848
  * class on the document root, which flips every design token defined under `.dark` in the theme CSS
@@ -14888,53 +15853,80 @@ const STORAGE_KEY = 'sds-theme';
14888
15853
  *
14889
15854
  * ```ts
14890
15855
  * private readonly theme = inject(SdsThemeService);
14891
- * this.theme.toggle(); // flip
15856
+ * this.theme.toggle(); // flip (to an explicit light/dark)
15857
+ * this.theme.setMode('system'); // follow the OS again
14892
15858
  * this.theme.isDark(); // read (a signal)
15859
+ * this.theme.mode(); // 'system' | 'light' | 'dark'
14893
15860
  * ```
14894
15861
  *
14895
- * Initial value: a previously stored choice wins; otherwise the OS `prefers-color-scheme` is honoured,
14896
- * so a first-time visitor already in dark at the OS level lands in dark. The choice is persisted to
14897
- * `localStorage` and re-applied on the next visit. Every browser access is guarded, so the service is
14898
- * safe under SSR / prerender (it simply no-ops without a DOM).
15862
+ * Two layers: {@link mode} is the choice (persisted to `localStorage` under `sds-theme`), {@link theme} is
15863
+ * what that resolves to. With no stored choice the mode is `system`, which tracks the OS setting live —
15864
+ * a first-time visitor in dark at the OS level lands in dark, and follows the OS if it flips. Every
15865
+ * browser access is guarded, so the service is safe under SSR / prerender (it simply no-ops without a DOM).
14899
15866
  */
14900
15867
  class SdsThemeService {
14901
15868
  document = inject(DOCUMENT);
14902
- theme = signal(this.initialTheme(), ...(ngDevMode ? [{ debugName: "theme" }] : []));
15869
+ /** The person's choice. */
15870
+ mode = signal(this.initialMode(), ...(ngDevMode ? [{ debugName: "mode" }] : []));
15871
+ /** The OS preference, kept live by a `matchMedia` listener. */
15872
+ systemDark = signal(this.queryDark()?.matches ?? false, ...(ngDevMode ? [{ debugName: "systemDark" }] : []));
15873
+ /** The theme painted: the explicit choice, or the OS one under `system`. */
15874
+ theme = computed(() => {
15875
+ const mode = this.mode();
15876
+ return mode === 'system' ? (this.systemDark() ? 'dark' : 'light') : mode;
15877
+ }, ...(ngDevMode ? [{ debugName: "theme" }] : []));
14903
15878
  isDark = computed(() => this.theme() === 'dark', ...(ngDevMode ? [{ debugName: "isDark" }] : []));
14904
15879
  constructor() {
14905
- // Keep the DOM class and the stored preference in lockstep with the signal — one effect, so any
14906
- // caller that sets the signal (toggle, setTheme, or a future settings screen) applies uniformly.
14907
- effect(() => this.apply(this.theme()));
15880
+ // Keep the DOM class and the stored choice in lockstep with the signals — one effect, so any caller
15881
+ // (toggle, setTheme, setMode, or the OS flipping under `system`) applies uniformly.
15882
+ effect(() => this.apply(this.theme(), this.mode()));
15883
+ const query = this.queryDark();
15884
+ if (query) {
15885
+ const onChange = (event) => this.systemDark.set(event.matches);
15886
+ query.addEventListener?.('change', onChange);
15887
+ inject(DestroyRef).onDestroy(() => query.removeEventListener?.('change', onChange));
15888
+ }
14908
15889
  }
14909
- /** Flip between light and dark. */
15890
+ /** Flip between light and dark (an explicit choice, leaving `system`). */
14910
15891
  toggle() {
14911
- this.theme.update((t) => (t === 'light' ? 'dark' : 'light'));
15892
+ this.mode.set(this.theme() === 'light' ? 'dark' : 'light');
14912
15893
  }
14913
15894
  /** Set an explicit theme. */
14914
15895
  setTheme(theme) {
14915
- this.theme.set(theme);
15896
+ this.mode.set(theme);
15897
+ }
15898
+ /** Set the choice, `system` included. */
15899
+ setMode(mode) {
15900
+ this.mode.set(mode);
14916
15901
  }
14917
- initialTheme() {
15902
+ initialMode() {
14918
15903
  try {
14919
- const stored = this.readStorage()?.getItem(STORAGE_KEY);
14920
- if (stored === 'light' || stored === 'dark')
15904
+ const stored = this.readStorage()?.getItem(SDS_THEME_STORAGE_KEY);
15905
+ if (isSdsThemeMode(stored))
14921
15906
  return stored;
14922
- const win = this.document.defaultView;
14923
- if (win?.matchMedia('(prefers-color-scheme: dark)').matches)
14924
- return 'dark';
14925
15907
  }
14926
15908
  catch {
14927
- /* no storage / no matchMedia (SSR, privacy mode) — fall through to the light default */
15909
+ /* no storage (SSR, privacy mode) — fall through to following the OS */
15910
+ }
15911
+ return 'system';
15912
+ }
15913
+ queryDark() {
15914
+ try {
15915
+ return this.document.defaultView?.matchMedia?.('(prefers-color-scheme: dark)') ?? null;
15916
+ }
15917
+ catch {
15918
+ return null;
14928
15919
  }
14929
- return 'light';
14930
15920
  }
14931
- apply(theme) {
15921
+ apply(theme, mode) {
14932
15922
  const root = this.document.documentElement;
14933
15923
  root.classList.toggle('dark', theme === 'dark');
14934
15924
  // Let the UA paint form controls / scrollbars to match.
14935
15925
  root.style.colorScheme = theme;
14936
15926
  try {
14937
- this.readStorage()?.setItem(STORAGE_KEY, theme);
15927
+ // The CHOICE is stored, not the resolved theme — `system` must stay `system`, or the first visit
15928
+ // would freeze whatever the OS happened to be set to.
15929
+ this.readStorage()?.setItem(SDS_THEME_STORAGE_KEY, mode);
14938
15930
  }
14939
15931
  catch {
14940
15932
  /* storage unavailable — the class is still applied; only persistence is skipped */
@@ -15024,6 +16016,69 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
15024
16016
  }]
15025
16017
  }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], darkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "darkLabel", required: false }] }], lightLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lightLabel", required: false }] }] } });
15026
16018
 
16019
+ const SDS_UI_PREF_LANG_PARAM = 'lang';
16020
+ const SDS_UI_PREF_THEME_PARAM = 'theme';
16021
+ const ABSOLUTE = /^[a-z][a-z0-9+.-]*:/i;
16022
+ const PLACEHOLDER_ORIGIN = 'http://sds.invalid';
16023
+ /**
16024
+ * `url` with `lang` / `theme` set on its query (overwriting any present), the fragment kept. Accepts an
16025
+ * absolute URL or a root-relative path (returned root-relative). Invalid themes are dropped, not sent.
16026
+ *
16027
+ * ```ts
16028
+ * appendUiPrefs('https://auth.example/continue#token=abc', { lang: 'fr', theme: 'dark' })
16029
+ * // → 'https://auth.example/continue?lang=fr&theme=dark#token=abc'
16030
+ * ```
16031
+ */
16032
+ function appendUiPrefs(url, prefs) {
16033
+ const absolute = ABSOLUTE.test(url);
16034
+ let parsed;
16035
+ try {
16036
+ parsed = new URL(url, PLACEHOLDER_ORIGIN);
16037
+ }
16038
+ catch {
16039
+ return url;
16040
+ }
16041
+ const lang = prefs.lang?.trim();
16042
+ if (lang) {
16043
+ parsed.searchParams.set(SDS_UI_PREF_LANG_PARAM, lang);
16044
+ }
16045
+ if (isSdsThemeMode(prefs.theme)) {
16046
+ parsed.searchParams.set(SDS_UI_PREF_THEME_PARAM, prefs.theme);
16047
+ }
16048
+ return absolute ? parsed.toString() : parsed.pathname + parsed.search + parsed.hash;
16049
+ }
16050
+ /**
16051
+ * The prefs carried by a query string (`location.search`), validated: `lang` only if it is one of
16052
+ * `supportedLangs`, `theme` only if it is a {@link SdsThemeMode}. Anything else reads as absent.
16053
+ */
16054
+ function readUiPrefs(search, supportedLangs) {
16055
+ const params = new URLSearchParams(search);
16056
+ const lang = params.get(SDS_UI_PREF_LANG_PARAM);
16057
+ const theme = params.get(SDS_UI_PREF_THEME_PARAM);
16058
+ return {
16059
+ lang: lang && supportedLangs.includes(lang) ? lang : null,
16060
+ theme: isSdsThemeMode(theme) ? theme : null,
16061
+ };
16062
+ }
16063
+ /** `url` without the `lang` / `theme` params (everything else, fragment included, untouched). */
16064
+ function stripUiPrefs(url) {
16065
+ const absolute = ABSOLUTE.test(url);
16066
+ let parsed;
16067
+ try {
16068
+ parsed = new URL(url, PLACEHOLDER_ORIGIN);
16069
+ }
16070
+ catch {
16071
+ return url;
16072
+ }
16073
+ if (!parsed.searchParams.has(SDS_UI_PREF_LANG_PARAM) &&
16074
+ !parsed.searchParams.has(SDS_UI_PREF_THEME_PARAM)) {
16075
+ return url;
16076
+ }
16077
+ parsed.searchParams.delete(SDS_UI_PREF_LANG_PARAM);
16078
+ parsed.searchParams.delete(SDS_UI_PREF_THEME_PARAM);
16079
+ return absolute ? parsed.toString() : parsed.pathname + parsed.search + parsed.hash;
16080
+ }
16081
+
15027
16082
  /**
15028
16083
  * Shared collapse state for the sidebar and its nav items.
15029
16084
  *
@@ -17362,5 +18417,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
17362
18417
  * Generated bundle index. Do not edit.
17363
18418
  */
17364
18419
 
17365
- export { SDS_ALLERGENS, SDS_ALLERGEN_ICONS, SDS_ALLERGEN_LABELS_EN, SDS_ICONS, SDS_PHONE_COUNTRIES, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAllergenIcon, SdsAvatar, SdsBadge, SdsBulkBar, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsChecklistRow, SdsChip, SdsChipGroup, SdsDatePicker, SdsFileDrop, SdsIcon, SdsImageCropper, SdsInput, SdsInputNumber, SdsLanguageSwitcher, SdsLocalizedInput, SdsMenu, SdsMenuItem, SdsMenuTrigger, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsPaginator, SdsPhoneInput, SdsRadio, SdsScore, SdsSearchCommand, SdsSegmented, SdsSelect, SdsSelectEmpty, SdsSelectFooter, SdsSelectOption, SdsShell, SdsSidebar, SdsSortableHandle, SdsSortableItem, SdsSortableList, SdsStatusTile, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableEmpty, SdsTableHeader, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsThemeService, SdsThemeToggle, SdsTimeInput, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SdsTriStateChip, SdsUserMenu, SidebarStore, addDays, addMonths, addMonthsKeepDay, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isoDay, pad2, sdsFileMatchesAccept, sdsFormatBytes, sdsFormatNational, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth };
18420
+ export { SDS_ALLERGENS, SDS_ALLERGEN_ICONS, SDS_ALLERGEN_LABELS_EN, SDS_ICONS, SDS_PHONE_COUNTRIES, SDS_THEME_STORAGE_KEY, SDS_UI_PREF_LANG_PARAM, SDS_UI_PREF_THEME_PARAM, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAddressAutocomplete, SdsAddressExtra, SdsAllergenIcon, SdsAvatar, SdsBadge, SdsBulkBar, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsChecklistRow, SdsChip, SdsChipGroup, SdsDatePicker, SdsFileDrop, SdsIcon, SdsImageCropper, SdsInput, SdsInputNumber, SdsLanguageSwitcher, SdsLocalizedInput, SdsMenu, SdsMenuItem, SdsMenuTrigger, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsPaginator, SdsPhoneInput, SdsRadio, SdsScore, SdsSearchCommand, SdsSegmented, SdsSelect, SdsSelectEmpty, SdsSelectFooter, SdsSelectOption, SdsShell, SdsSidebar, SdsSortableHandle, SdsSortableItem, SdsSortableList, SdsStatusTile, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableEmpty, SdsTableHeader, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsThemeService, SdsThemeToggle, SdsTimeInput, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SdsTriStateChip, SdsUserMenu, SidebarStore, addDays, addMonths, addMonthsKeepDay, appendUiPrefs, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isSdsThemeMode, isoDay, pad2, readUiPrefs, sdsFileMatchesAccept, sdsFormatBytes, sdsFormatNational, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth, stripUiPrefs };
17366
18421
  //# sourceMappingURL=besaitech-ng-design-system.mjs.map