@besaitech/ng-design-system 0.0.52 → 0.0.53

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, computed, ChangeDetectionStrategy, Component, booleanAttribute, isDevMode, effect, numberAttribute, output, signal, forwardRef, viewChild, viewChildren, afterRenderEffect, model, ElementRef, Injector, afterNextRender, TemplateRef, Directive, contentChild, Injectable, contentChildren, untracked, DestroyRef } from '@angular/core';
2
+ import { inject, input, computed, ChangeDetectionStrategy, Component, booleanAttribute, isDevMode, effect, numberAttribute, output, signal, forwardRef, viewChild, viewChildren, afterRenderEffect, model, ElementRef, Directive, Injector, contentChild, untracked, afterNextRender, TemplateRef, linkedSignal, Injectable, contentChildren, DestroyRef } 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';
@@ -108,6 +108,22 @@ const SDS_ICONS = {
108
108
  'search-x': '<path d="m13.5 8.5-5 5"/><path d="m8.5 8.5 5 5"/><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
109
109
  command: '<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"/>',
110
110
  sparkles: '<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/>',
111
+ // Added for 0.0.53 (Lucide 24x24 stroke paths): status glyphs for sds-checklist-row / sds-score /
112
+ // sds-status-tile (distinct SHAPES per status, so status never rides on colour), the sds-menu trigger
113
+ // ellipses, the sds-sortable-list grip, sds-file-drop / sds-image-cropper glyphs, and common row actions.
114
+ 'triangle-alert': '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
115
+ 'octagon-x': '<path d="m15 9-6 6"/><path d="M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"/><path d="m9 9 6 6"/>',
116
+ 'ban': '<circle cx="12" cy="12" r="10"/><path d="m4.9 4.9 14.2 14.2"/>',
117
+ 'ellipsis': '<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>',
118
+ 'ellipsis-vertical': '<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/>',
119
+ 'grip-vertical': '<circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/>',
120
+ 'upload': '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/>',
121
+ 'image': '<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>',
122
+ 'zoom-in': '<circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="11" x2="11" y1="8" y2="14"/><line x1="8" x2="14" y1="11" y2="11"/>',
123
+ 'zoom-out': '<circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="8" x2="14" y1="11" y2="11"/>',
124
+ 'trash-2': '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/>',
125
+ 'clock': '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
126
+ 'pencil': '<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/>',
111
127
  // A question mark in a circle: the "may contain" mark on sds-tri-state-chip, and a generic help glyph.
112
128
  'circle-help': '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
113
129
  // Stock / inventory (the kitchen stock board, the sold-out toolkit). Original glyph: three stacked boxes.
@@ -314,7 +330,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
314
330
  `, 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"] }]
315
331
  }], 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 }] }] } });
316
332
 
317
- let nextId$g = 0;
333
+ let nextId$l = 0;
318
334
  const SIZES$a = {
319
335
  sm: 'h-9 text-sm',
320
336
  md: 'h-10 text-sm',
@@ -327,7 +343,7 @@ const SIZES$a = {
327
343
  * sidebar search field).
328
344
  */
329
345
  class SdsInput {
330
- inputId = `sds-input-${nextId$g++}`;
346
+ inputId = `sds-input-${nextId$l++}`;
331
347
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
332
348
  /** Accessible name used when no visible [label] is provided. */
333
349
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -660,7 +676,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
660
676
  `, 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"] }]
661
677
  }], 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 }] }], 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"] }] } });
662
678
 
663
- let nextId$f = 0;
679
+ let nextId$k = 0;
664
680
  const HEIGHTS = {
665
681
  sm: 'h-9',
666
682
  md: 'h-10',
@@ -683,7 +699,7 @@ const TEXT = {
683
699
  * {@link min}/{@link max} on blur and on every spinner step; ArrowUp / ArrowDown also step it.
684
700
  */
685
701
  class SdsInputNumber {
686
- inputId = `sds-input-number-${nextId$f++}`;
702
+ inputId = `sds-input-number-${nextId$k++}`;
687
703
  fieldRef = viewChild('field', ...(ngDevMode ? [{ debugName: "fieldRef" }] : []));
688
704
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
689
705
  /** Accessible name used when no visible [label] is provided. */
@@ -856,6 +872,20 @@ class SdsInputNumber {
856
872
  el.value = v === null ? '' : this.format(v);
857
873
  });
858
874
  }
875
+ /**
876
+ * Focus the field, painting the current value into it first (the live-format effect stands down while
877
+ * the field is focused, so a value written just before focusing would otherwise not show). With
878
+ * `select`, the text is selected — handy for an in-cell editor.
879
+ */
880
+ focus(options = {}) {
881
+ const el = this.fieldRef()?.nativeElement;
882
+ if (!el)
883
+ return;
884
+ this.paintCommitted();
885
+ el.focus();
886
+ if (options.select)
887
+ el.select();
888
+ }
859
889
  isActive(el) {
860
890
  return typeof document !== 'undefined' && document.activeElement === el;
861
891
  }
@@ -1390,7 +1420,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
1390
1420
  }]
1391
1421
  }], 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 }] }], 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 }] }] } });
1392
1422
 
1393
- let nextId$e = 0;
1423
+ let nextId$j = 0;
1394
1424
  const SIZES$9 = {
1395
1425
  sm: 'min-h-9 text-sm',
1396
1426
  md: 'min-h-10 text-sm',
@@ -1410,7 +1440,7 @@ const SIZES$9 = {
1410
1440
  * `<sds-localized-input [languages]="['fr','nl','en']" [(ngModel)]="title" />`.
1411
1441
  */
1412
1442
  class SdsLocalizedInput {
1413
- inputId = `sds-localized-input-${nextId$e++}`;
1443
+ inputId = `sds-localized-input-${nextId$j++}`;
1414
1444
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
1415
1445
  hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
1416
1446
  placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
@@ -1666,7 +1696,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
1666
1696
  }]
1667
1697
  }], 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 }] }] } });
1668
1698
 
1669
- let nextId$d = 0;
1699
+ let nextId$i = 0;
1670
1700
  const SIZES$8 = {
1671
1701
  md: 'h-12 w-11 text-lg',
1672
1702
  lg: 'h-14 w-12 text-xl',
@@ -1683,7 +1713,7 @@ const SIZES$8 = {
1683
1713
  * first box and have it land correctly.
1684
1714
  */
1685
1715
  class SdsOtpInput {
1686
- groupId = `sds-otp-${nextId$d++}`;
1716
+ groupId = `sds-otp-${nextId$i++}`;
1687
1717
  /** How many boxes / characters the code has. */
1688
1718
  length = input(6, ...(ngDevMode ? [{ debugName: "length", transform: numberAttribute }] : [{ transform: numberAttribute }]));
1689
1719
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
@@ -1983,7 +2013,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
1983
2013
  }]
1984
2014
  }], 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 }] }] } });
1985
2015
 
1986
- let nextId$c = 0;
2016
+ let nextId$h = 0;
1987
2017
  const SIZES$7 = {
1988
2018
  sm: 'text-sm px-3 py-2',
1989
2019
  md: 'text-sm px-3.5 py-2.5',
@@ -1995,7 +2025,7 @@ const SIZES$7 = {
1995
2025
  * [(ngModel)].
1996
2026
  */
1997
2027
  class SdsTextarea {
1998
- fieldId = `sds-textarea-${nextId$c++}`;
2028
+ fieldId = `sds-textarea-${nextId$h++}`;
1999
2029
  field = viewChild('field', ...(ngDevMode ? [{ debugName: "field" }] : []));
2000
2030
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
2001
2031
  /** Accessible name used when no visible [label] is provided. */
@@ -2248,7 +2278,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
2248
2278
  }]
2249
2279
  }], 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 }] }], resize: [{ type: i0.Input, args: [{ isSignal: true, alias: "resize", required: false }] }] } });
2250
2280
 
2251
- let nextId$b = 0;
2281
+ let nextId$g = 0;
2252
2282
  const SIZES$6 = {
2253
2283
  sm: { track: 'h-5 w-9', knob: 'h-4 w-4', on: 'translate-x-4', icon: 11 },
2254
2284
  md: { track: 'h-6 w-11', knob: 'h-5 w-5', on: 'translate-x-5', icon: 13 },
@@ -2261,7 +2291,7 @@ const SIZES$6 = {
2261
2291
  * states.
2262
2292
  */
2263
2293
  class SdsToggleSwitch {
2264
- fieldId = `sds-toggle-switch-${nextId$b++}`;
2294
+ fieldId = `sds-toggle-switch-${nextId$g++}`;
2265
2295
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
2266
2296
  /** Accessible name used when no visible [label] is provided. */
2267
2297
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -2505,7 +2535,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
2505
2535
  }]
2506
2536
  }], 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 }] }] } });
2507
2537
 
2508
- let nextId$a = 0;
2538
+ let nextId$f = 0;
2509
2539
  /**
2510
2540
  * Boolean checkbox. Implements ControlValueAccessor — use with reactive forms
2511
2541
  * or `[(ngModel)]`. Renders a styled box (brand-filled with a check when on)
@@ -2517,7 +2547,7 @@ let nextId$a = 0;
2517
2547
  * <sds-checkbox label="Fusion automatique" [(ngModel)]="autoMerge" />
2518
2548
  */
2519
2549
  class SdsCheckbox {
2520
- id = `sds-checkbox-${nextId$a++}`;
2550
+ id = `sds-checkbox-${nextId$f++}`;
2521
2551
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
2522
2552
  /** Accessible name used when no visible [label] is provided. */
2523
2553
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -2651,7 +2681,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
2651
2681
  }]
2652
2682
  }], 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 }] }] } });
2653
2683
 
2654
- let nextId$9 = 0;
2684
+ let nextId$e = 0;
2655
2685
  /**
2656
2686
  * Mounted radios by group name. A pick updates the rest of its group, because a native radio says
2657
2687
  * nothing when the browser unchecks it — without this a reactive form would leave the old choice drawn.
@@ -2677,7 +2707,7 @@ const groups = new Map();
2677
2707
  * <sds-radio name="size" value="large" label="Grand" [(ngModel)]="size" />
2678
2708
  */
2679
2709
  class SdsRadio {
2680
- id = `sds-radio-${nextId$9++}`;
2710
+ id = `sds-radio-${nextId$e++}`;
2681
2711
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
2682
2712
  /** Accessible name used when no visible [label] is provided. */
2683
2713
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -3163,7 +3193,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
3163
3193
 
3164
3194
  /** Activation order: none → contains → may-contain → none. */
3165
3195
  const ORDER = ['none', 'contains', 'may-contain'];
3166
- let nextId$8 = 0;
3196
+ let nextId$d = 0;
3167
3197
  /**
3168
3198
  * A chip that cycles through three states — `none`, `contains`, `may-contain` — built for the 14-tile
3169
3199
  * allergen declaration grid, but generic (any "no / yes / maybe" declaration).
@@ -3183,7 +3213,7 @@ let nextId$8 = 0;
3183
3213
  * containsLabel="Contains" mayContainLabel="May contain" noneLabel="Not present" />
3184
3214
  */
3185
3215
  class SdsTriStateChip {
3186
- chipId = `sds-tri-state-chip-${nextId$8++}`;
3216
+ chipId = `sds-tri-state-chip-${nextId$d++}`;
3187
3217
  /** Current state. Two-way: `[(value)]`. */
3188
3218
  value = model('none', ...(ngDevMode ? [{ debugName: "value" }] : []));
3189
3219
  /** Visible name (e.g. the allergen). Required — the chip has no other accessible name. */
@@ -3314,96 +3344,2875 @@ class SdsTriStateChip {
3314
3344
  <!-- The state is part of the accessible name ("Milk, may contain"), visible caption or not. -->
3315
3345
  <span class="sr-only">, {{ stateText() }}</span>
3316
3346
 
3317
- <!-- Non-colour mark: nothing / a check / a question mark. The slot keeps its width in every
3318
- state so the chip never shifts when cycled. -->
3319
- <span
3320
- class="grid shrink-0 place-items-center"
3321
- [class]="size() === 'sm' ? 'h-4 w-4' : 'h-5 w-5'"
3322
- aria-hidden="true"
3347
+ <!-- Non-colour mark: nothing / a check / a question mark. The slot keeps its width in every
3348
+ state so the chip never shifts when cycled. -->
3349
+ <span
3350
+ class="grid shrink-0 place-items-center"
3351
+ [class]="size() === 'sm' ? 'h-4 w-4' : 'h-5 w-5'"
3352
+ aria-hidden="true"
3353
+ >
3354
+ @switch (value()) {
3355
+ @case ('contains') {
3356
+ <sds-icon name="check" [size]="size() === 'sm' ? 14 : 18" [strokeWidth]="3" />
3357
+ }
3358
+ @case ('may-contain') {
3359
+ <sds-icon name="circle-help" [size]="size() === 'sm' ? 15 : 19" />
3360
+ }
3361
+ }
3362
+ </span>
3363
+ </button>
3364
+
3365
+ @if (hint()) {
3366
+ <span [id]="chipId + '-hint'" class="sr-only">{{ hint() }}</span>
3367
+ }
3368
+ <span class="sr-only" aria-live="polite" aria-atomic="true">{{ announcement() }}</span>
3369
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3370
+ }
3371
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTriStateChip, decorators: [{
3372
+ type: Component,
3373
+ args: [{
3374
+ selector: 'sds-tri-state-chip',
3375
+ changeDetection: ChangeDetectionStrategy.OnPush,
3376
+ imports: [SdsIcon],
3377
+ host: { class: 'inline-flex min-w-0' },
3378
+ template: `
3379
+ <button
3380
+ type="button"
3381
+ [id]="chipId"
3382
+ [class]="chipClass()"
3383
+ [disabled]="disabled()"
3384
+ [attr.data-state]="value()"
3385
+ [attr.aria-describedby]="hint() ? chipId + '-hint' : null"
3386
+ (click)="onClick($event)"
3387
+ (keydown)="onKeydown($event)"
3388
+ >
3389
+ @if (icon()) {
3390
+ <sds-icon [name]="icon()!" [size]="size() === 'sm' ? 18 : 22" class="shrink-0" />
3391
+ }
3392
+
3393
+ <span class="flex min-w-0 flex-1 flex-col text-left leading-tight">
3394
+ <!-- Up to two lines, so "Cereals containing gluten" stays readable in a narrow grid cell. -->
3395
+ <span
3396
+ class="line-clamp-2 break-words font-semibold"
3397
+ [class]="size() === 'sm' ? 'text-xs' : 'text-sm'"
3398
+ >{{ label() }}</span
3399
+ >
3400
+ @if (showState()) {
3401
+ <span
3402
+ class="truncate text-[11px] font-medium"
3403
+ [class]="captionClass()"
3404
+ aria-hidden="true"
3405
+ >{{ stateText() }}</span
3406
+ >
3407
+ }
3408
+ </span>
3409
+ <!-- The state is part of the accessible name ("Milk, may contain"), visible caption or not. -->
3410
+ <span class="sr-only">, {{ stateText() }}</span>
3411
+
3412
+ <!-- Non-colour mark: nothing / a check / a question mark. The slot keeps its width in every
3413
+ state so the chip never shifts when cycled. -->
3414
+ <span
3415
+ class="grid shrink-0 place-items-center"
3416
+ [class]="size() === 'sm' ? 'h-4 w-4' : 'h-5 w-5'"
3417
+ aria-hidden="true"
3418
+ >
3419
+ @switch (value()) {
3420
+ @case ('contains') {
3421
+ <sds-icon name="check" [size]="size() === 'sm' ? 14 : 18" [strokeWidth]="3" />
3422
+ }
3423
+ @case ('may-contain') {
3424
+ <sds-icon name="circle-help" [size]="size() === 'sm' ? 15 : 19" />
3425
+ }
3426
+ }
3427
+ </span>
3428
+ </button>
3429
+
3430
+ @if (hint()) {
3431
+ <span [id]="chipId + '-hint'" class="sr-only">{{ hint() }}</span>
3432
+ }
3433
+ <span class="sr-only" aria-live="polite" aria-atomic="true">{{ announcement() }}</span>
3434
+ `,
3435
+ }]
3436
+ }], 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 }] }] } });
3437
+
3438
+ let nextId$c = 0;
3439
+ /**
3440
+ * Segmented control: a small set of mutually exclusive options shown side by side (e.g. "Pick exactly /
3441
+ * at least / up to"). Radio-group semantics: `role="radiogroup"` with `role="radio"` segments, one tab
3442
+ * stop, and the arrow keys (with Home/End) move AND select, like native radios. Disabled segments are
3443
+ * skipped.
3444
+ *
3445
+ * The value is a two-way `model` — bind `[(value)]`. Give the group a name with [ariaLabel] (or
3446
+ * [ariaLabelledby]); with [iconOnly] each segment keeps its label as its accessible name.
3447
+ *
3448
+ * @example
3449
+ * <sds-segmented ariaLabel="Rule" [options]="[{ value: 'exactly', label: 'Exactly' }, { value: 'upTo', label: 'Up to' }]"
3450
+ * [(value)]="rule" />
3451
+ */
3452
+ class SdsSegmented {
3453
+ host = inject(ElementRef);
3454
+ baseId = `sds-segmented-${nextId$c++}`;
3455
+ /** Options: `{ value, label, icon?, disabled? }` objects, or bare strings/numbers. */
3456
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
3457
+ /** Selected value. Two-way: `[(value)]`. */
3458
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
3459
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
3460
+ /** Accessible name of the group. */
3461
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
3462
+ /** Id of an element that names the group (alternative to [ariaLabel]). */
3463
+ ariaLabelledby = input('', ...(ngDevMode ? [{ debugName: "ariaLabelledby" }] : []));
3464
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
3465
+ /** Stretch to the container width, segments sharing it equally. */
3466
+ fullWidth = input(false, ...(ngDevMode ? [{ debugName: "fullWidth", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
3467
+ /** Show icons only; labels stay as accessible names and tooltips. */
3468
+ iconOnly = input(false, ...(ngDevMode ? [{ debugName: "iconOnly", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
3469
+ segments = computed(() => this.options().map((o) => o !== null && typeof o === 'object'
3470
+ ? { value: o.value, label: o.label, icon: o.icon ?? null, disabled: !!o.disabled }
3471
+ : { value: o, label: String(o), icon: null, disabled: false }), ...(ngDevMode ? [{ debugName: "segments" }] : []));
3472
+ selectedIndex = computed(() => this.segments().findIndex((s) => s.value === this.value()), ...(ngDevMode ? [{ debugName: "selectedIndex" }] : []));
3473
+ /** Roving tab stop: the selected segment, else the first enabled one. */
3474
+ tabStop = computed(() => {
3475
+ const sel = this.selectedIndex();
3476
+ if (sel >= 0 && !this.segments()[sel].disabled)
3477
+ return sel;
3478
+ return this.segments().findIndex((s) => !s.disabled);
3479
+ }, ...(ngDevMode ? [{ debugName: "tabStop" }] : []));
3480
+ isSelected(seg) {
3481
+ return seg.value === this.value();
3482
+ }
3483
+ segmentId(i) {
3484
+ return `${this.baseId}-${i}`;
3485
+ }
3486
+ segmentClass(seg) {
3487
+ const sm = this.size() === 'sm';
3488
+ const base = 'inline-flex min-w-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] ' +
3489
+ 'font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 ' +
3490
+ 'disabled:cursor-not-allowed disabled:opacity-50 ' +
3491
+ (sm ? 'h-7 px-2.5 text-xs' : 'h-8 px-3.5 text-sm') +
3492
+ (this.fullWidth() ? ' flex-1' : '') +
3493
+ (this.iconOnly() ? (sm ? ' w-8 px-0' : ' w-9 px-0') : '');
3494
+ // Selected: raised surface, ring and bold text — more than a tint, so it reads in greyscale too.
3495
+ return this.isSelected(seg)
3496
+ ? `${base} bg-surface font-semibold text-ink shadow-sm ring-1 ring-border dark:bg-border`
3497
+ : `${base} cursor-pointer text-ink-soft hover:bg-surface/60 hover:text-ink`;
3498
+ }
3499
+ choose(i) {
3500
+ const seg = this.segments()[i];
3501
+ if (!seg || seg.disabled || this.disabled())
3502
+ return;
3503
+ this.value.set(seg.value);
3504
+ }
3505
+ onKeydown(event) {
3506
+ if (this.disabled())
3507
+ return;
3508
+ const segs = this.segments();
3509
+ if (!segs.length)
3510
+ return;
3511
+ const current = Number(event.target.dataset['index'] ?? this.tabStop());
3512
+ let next = null;
3513
+ switch (event.key) {
3514
+ case 'ArrowRight':
3515
+ case 'ArrowDown':
3516
+ next = this.step(current, 1);
3517
+ break;
3518
+ case 'ArrowLeft':
3519
+ case 'ArrowUp':
3520
+ next = this.step(current, -1);
3521
+ break;
3522
+ case 'Home':
3523
+ next = segs.findIndex((s) => !s.disabled);
3524
+ break;
3525
+ case 'End':
3526
+ for (let i = segs.length - 1; i >= 0; i--) {
3527
+ if (!segs[i].disabled) {
3528
+ next = i;
3529
+ break;
3530
+ }
3531
+ }
3532
+ break;
3533
+ default:
3534
+ return;
3535
+ }
3536
+ event.preventDefault();
3537
+ if (next === null || next < 0)
3538
+ return;
3539
+ this.choose(next);
3540
+ this.host.nativeElement.querySelector(`#${this.segmentId(next)}`)?.focus();
3541
+ }
3542
+ step(from, delta) {
3543
+ const segs = this.segments();
3544
+ let i = from;
3545
+ for (let n = 0; n < segs.length; n++) {
3546
+ i = (i + delta + segs.length) % segs.length;
3547
+ if (!segs[i].disabled)
3548
+ return i;
3549
+ }
3550
+ return null;
3551
+ }
3552
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSegmented, deps: [], target: i0.ɵɵFactoryTarget.Component });
3553
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsSegmented, isStandalone: true, selector: "sds-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "ariaLabelledby", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, iconOnly: { classPropertyName: "iconOnly", publicName: "iconOnly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { properties: { "class.w-full": "fullWidth()" }, classAttribute: "inline-flex max-w-full" }, ngImport: i0, template: `
3554
+ <div
3555
+ role="radiogroup"
3556
+ [attr.aria-label]="ariaLabel() || null"
3557
+ [attr.aria-labelledby]="ariaLabelledby() || null"
3558
+ [attr.aria-disabled]="disabled() ? 'true' : null"
3559
+ class="flex max-w-full items-stretch gap-0.5 overflow-x-auto rounded-control border border-border bg-muted p-0.5"
3560
+ [class.w-full]="fullWidth()"
3561
+ (keydown)="onKeydown($event)"
3562
+ >
3563
+ @for (seg of segments(); track $index; let i = $index) {
3564
+ <button
3565
+ type="button"
3566
+ role="radio"
3567
+ [id]="segmentId(i)"
3568
+ [attr.aria-checked]="isSelected(seg)"
3569
+ [attr.aria-label]="iconOnly() ? seg.label : null"
3570
+ [attr.title]="iconOnly() ? seg.label : null"
3571
+ [attr.tabindex]="i === tabStop() ? 0 : -1"
3572
+ [attr.data-index]="i"
3573
+ [disabled]="disabled() || seg.disabled"
3574
+ [class]="segmentClass(seg)"
3575
+ (click)="choose(i)"
3576
+ >
3577
+ @if (seg.icon) {
3578
+ <sds-icon [name]="seg.icon" [size]="size() === 'sm' ? 15 : 17" class="shrink-0" />
3579
+ }
3580
+ @if (!iconOnly()) {
3581
+ <span class="truncate">{{ seg.label }}</span>
3582
+ }
3583
+ </button>
3584
+ }
3585
+ </div>
3586
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3587
+ }
3588
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSegmented, decorators: [{
3589
+ type: Component,
3590
+ args: [{
3591
+ selector: 'sds-segmented',
3592
+ changeDetection: ChangeDetectionStrategy.OnPush,
3593
+ imports: [SdsIcon],
3594
+ host: { class: 'inline-flex max-w-full', '[class.w-full]': 'fullWidth()' },
3595
+ template: `
3596
+ <div
3597
+ role="radiogroup"
3598
+ [attr.aria-label]="ariaLabel() || null"
3599
+ [attr.aria-labelledby]="ariaLabelledby() || null"
3600
+ [attr.aria-disabled]="disabled() ? 'true' : null"
3601
+ class="flex max-w-full items-stretch gap-0.5 overflow-x-auto rounded-control border border-border bg-muted p-0.5"
3602
+ [class.w-full]="fullWidth()"
3603
+ (keydown)="onKeydown($event)"
3604
+ >
3605
+ @for (seg of segments(); track $index; let i = $index) {
3606
+ <button
3607
+ type="button"
3608
+ role="radio"
3609
+ [id]="segmentId(i)"
3610
+ [attr.aria-checked]="isSelected(seg)"
3611
+ [attr.aria-label]="iconOnly() ? seg.label : null"
3612
+ [attr.title]="iconOnly() ? seg.label : null"
3613
+ [attr.tabindex]="i === tabStop() ? 0 : -1"
3614
+ [attr.data-index]="i"
3615
+ [disabled]="disabled() || seg.disabled"
3616
+ [class]="segmentClass(seg)"
3617
+ (click)="choose(i)"
3618
+ >
3619
+ @if (seg.icon) {
3620
+ <sds-icon [name]="seg.icon" [size]="size() === 'sm' ? 15 : 17" class="shrink-0" />
3621
+ }
3622
+ @if (!iconOnly()) {
3623
+ <span class="truncate">{{ seg.label }}</span>
3624
+ }
3625
+ </button>
3626
+ }
3627
+ </div>
3628
+ `,
3629
+ }]
3630
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], fullWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullWidth", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }] } });
3631
+
3632
+ /**
3633
+ * Each status has its own glyph SHAPE (circle-check, triangle, octagon, circle-i) as well as its own
3634
+ * tint, so the row still reads in greyscale or for a colour-blind reader. `label` is the English
3635
+ * default of the spoken status; override it with [statusLabel].
3636
+ */
3637
+ const STATUS = {
3638
+ done: { icon: 'check-circle', tile: 'bg-accent-green-soft text-accent-green', label: 'Done' },
3639
+ warning: { icon: 'triangle-alert', tile: 'bg-accent-amber-soft text-accent-amber', label: 'Warning' },
3640
+ blocker: { icon: 'octagon-x', tile: 'bg-accent-red-soft text-accent-red', label: 'Blocking' },
3641
+ info: { icon: 'info', tile: 'bg-muted text-ink-soft', label: 'Info' },
3642
+ };
3643
+ /**
3644
+ * One line of a readiness checklist or health report: a status glyph, a title, an optional
3645
+ * description, an optional count pill and an action (a projected `[sdsChecklistAction]` element, or
3646
+ * the built-in button from [actionLabel] that emits `(action)`).
3647
+ *
3648
+ * The status is announced as text before the title ("Blocking: 3 items have no price"), never by
3649
+ * colour alone. Put rows in a list for screen readers: `<ul role="list">` with each row wrapped in
3650
+ * an `<li>`, or give the container `role="list"` and each row `role="listitem"`.
3651
+ *
3652
+ * @example
3653
+ * <sds-checklist-row status="blocker" title="3 items have no price" description="Customers can't order them."
3654
+ * [count]="3" actionLabel="Fix" (action)="openFilter('no-price')" />
3655
+ */
3656
+ class SdsChecklistRow {
3657
+ status = input('info', ...(ngDevMode ? [{ debugName: "status" }] : []));
3658
+ title = input.required(...(ngDevMode ? [{ debugName: "title" }] : []));
3659
+ description = input('', ...(ngDevMode ? [{ debugName: "description" }] : []));
3660
+ /** Optional number of affected things, shown as a pill after the title. Null hides it. */
3661
+ count = input(null, ...(ngDevMode ? [{ debugName: "count" }] : []));
3662
+ /**
3663
+ * Accessible text for the count pill; `{count}` is replaced. Default `'{count} items'` — pass a
3664
+ * translated one.
3665
+ */
3666
+ countLabel = input('{count} items', ...(ngDevMode ? [{ debugName: "countLabel" }] : []));
3667
+ /** Spoken status prefix. Defaults per status: Done · Warning · Blocking · Info (English). */
3668
+ statusLabel = input('', ...(ngDevMode ? [{ debugName: "statusLabel" }] : []));
3669
+ /** Label of the built-in action button. Empty hides it (project `[sdsChecklistAction]` instead). */
3670
+ actionLabel = input('', ...(ngDevMode ? [{ debugName: "actionLabel" }] : []));
3671
+ /**
3672
+ * Full accessible name for the action button when its visible label is terse ("Fix") — e.g.
3673
+ * "Fix: 3 items have no price".
3674
+ */
3675
+ actionAriaLabel = input('', ...(ngDevMode ? [{ debugName: "actionAriaLabel" }] : []));
3676
+ /** Strike the title through when the row is done. */
3677
+ strikeDone = input(false, ...(ngDevMode ? [{ debugName: "strikeDone", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
3678
+ /** Emitted when the built-in action button is pressed. */
3679
+ action = output();
3680
+ style = computed(() => STATUS[this.status()] ?? STATUS.info, ...(ngDevMode ? [{ debugName: "style" }] : []));
3681
+ resolvedStatusLabel = computed(() => this.statusLabel() || this.style().label, ...(ngDevMode ? [{ debugName: "resolvedStatusLabel" }] : []));
3682
+ countText = computed(() => this.countLabel().replace('{count}', String(this.count() ?? '')), ...(ngDevMode ? [{ debugName: "countText" }] : []));
3683
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsChecklistRow, deps: [], target: i0.ɵɵFactoryTarget.Component });
3684
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsChecklistRow, isStandalone: true, selector: "sds-checklist-row", inputs: { status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, countLabel: { classPropertyName: "countLabel", publicName: "countLabel", isSignal: true, isRequired: false, transformFunction: null }, statusLabel: { classPropertyName: "statusLabel", publicName: "statusLabel", isSignal: true, isRequired: false, transformFunction: null }, actionLabel: { classPropertyName: "actionLabel", publicName: "actionLabel", isSignal: true, isRequired: false, transformFunction: null }, actionAriaLabel: { classPropertyName: "actionAriaLabel", publicName: "actionAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, strikeDone: { classPropertyName: "strikeDone", publicName: "strikeDone", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { action: "action" }, host: { classAttribute: "block" }, ngImport: i0, template: `
3685
+ <div
3686
+ class="flex items-start gap-3 rounded-control px-3 py-3"
3687
+ [class.opacity-80]="status() === 'done'"
3688
+ [attr.data-status]="status()"
3689
+ >
3690
+ <span
3691
+ class="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-full"
3692
+ [class]="style().tile"
3693
+ aria-hidden="true"
3694
+ >
3695
+ <sds-icon [name]="style().icon" [size]="18" />
3696
+ </span>
3697
+
3698
+ <div class="min-w-0 flex-1">
3699
+ <p class="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm font-semibold text-ink">
3700
+ <span class="sr-only">{{ resolvedStatusLabel() }}:</span>
3701
+ <span [class.line-through]="status() === 'done' && strikeDone()">{{ title() }}</span>
3702
+ @if (count() !== null) {
3703
+ <span
3704
+ class="inline-flex min-w-6 items-center justify-center rounded-full border border-border bg-muted px-1.5 text-xs font-semibold tabular-nums text-ink-soft"
3705
+ aria-hidden="true"
3706
+ >{{ count() }}</span
3707
+ >
3708
+ <span class="sr-only">({{ countText() }})</span>
3709
+ }
3710
+ </p>
3711
+ @if (description()) {
3712
+ <p class="mt-0.5 text-sm text-ink-soft">{{ description() }}</p>
3713
+ }
3714
+ <ng-content />
3715
+ </div>
3716
+
3717
+ <div class="flex shrink-0 items-center gap-2 self-center">
3718
+ <ng-content select="[sdsChecklistAction]" />
3719
+ @if (actionLabel()) {
3720
+ <button
3721
+ type="button"
3722
+ class="inline-flex h-9 items-center gap-1.5 rounded-control border border-border bg-surface px-3 text-sm font-semibold text-ink transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
3723
+ [attr.aria-label]="actionAriaLabel() || null"
3724
+ (click)="action.emit()"
3725
+ >
3726
+ {{ actionLabel() }}
3727
+ <sds-icon name="chevron-right" [size]="16" class="text-ink-faint" />
3728
+ </button>
3729
+ }
3730
+ </div>
3731
+ </div>
3732
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3733
+ }
3734
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsChecklistRow, decorators: [{
3735
+ type: Component,
3736
+ args: [{
3737
+ selector: 'sds-checklist-row',
3738
+ changeDetection: ChangeDetectionStrategy.OnPush,
3739
+ imports: [SdsIcon],
3740
+ host: { class: 'block' },
3741
+ template: `
3742
+ <div
3743
+ class="flex items-start gap-3 rounded-control px-3 py-3"
3744
+ [class.opacity-80]="status() === 'done'"
3745
+ [attr.data-status]="status()"
3746
+ >
3747
+ <span
3748
+ class="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-full"
3749
+ [class]="style().tile"
3750
+ aria-hidden="true"
3751
+ >
3752
+ <sds-icon [name]="style().icon" [size]="18" />
3753
+ </span>
3754
+
3755
+ <div class="min-w-0 flex-1">
3756
+ <p class="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm font-semibold text-ink">
3757
+ <span class="sr-only">{{ resolvedStatusLabel() }}:</span>
3758
+ <span [class.line-through]="status() === 'done' && strikeDone()">{{ title() }}</span>
3759
+ @if (count() !== null) {
3760
+ <span
3761
+ class="inline-flex min-w-6 items-center justify-center rounded-full border border-border bg-muted px-1.5 text-xs font-semibold tabular-nums text-ink-soft"
3762
+ aria-hidden="true"
3763
+ >{{ count() }}</span
3764
+ >
3765
+ <span class="sr-only">({{ countText() }})</span>
3766
+ }
3767
+ </p>
3768
+ @if (description()) {
3769
+ <p class="mt-0.5 text-sm text-ink-soft">{{ description() }}</p>
3770
+ }
3771
+ <ng-content />
3772
+ </div>
3773
+
3774
+ <div class="flex shrink-0 items-center gap-2 self-center">
3775
+ <ng-content select="[sdsChecklistAction]" />
3776
+ @if (actionLabel()) {
3777
+ <button
3778
+ type="button"
3779
+ class="inline-flex h-9 items-center gap-1.5 rounded-control border border-border bg-surface px-3 text-sm font-semibold text-ink transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
3780
+ [attr.aria-label]="actionAriaLabel() || null"
3781
+ (click)="action.emit()"
3782
+ >
3783
+ {{ actionLabel() }}
3784
+ <sds-icon name="chevron-right" [size]="16" class="text-ink-faint" />
3785
+ </button>
3786
+ }
3787
+ </div>
3788
+ </div>
3789
+ `,
3790
+ }]
3791
+ }], propDecorators: { status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], countLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "countLabel", required: false }] }], statusLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "statusLabel", required: false }] }], actionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionLabel", required: false }] }], actionAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionAriaLabel", required: false }] }], strikeDone: [{ type: i0.Input, args: [{ isSignal: true, alias: "strikeDone", required: false }] }], action: [{ type: i0.Output, args: ["action"] }] } });
3792
+
3793
+ const TIERS = {
3794
+ good: { icon: 'check-circle', text: 'text-accent-green', stroke: 'stroke-success', fill: 'bg-success' },
3795
+ fair: { icon: 'triangle-alert', text: 'text-accent-amber', stroke: 'stroke-warning', fill: 'bg-warning' },
3796
+ poor: { icon: 'octagon-x', text: 'text-accent-red', stroke: 'stroke-danger', fill: 'bg-danger' },
3797
+ };
3798
+ const RING = {
3799
+ sm: { box: 48, stroke: 5, value: 'text-sm', max: 'text-[9px]' },
3800
+ md: { box: 72, stroke: 7, value: 'text-xl', max: 'text-[10px]' },
3801
+ lg: { box: 96, stroke: 9, value: 'text-3xl', max: 'text-xs' },
3802
+ };
3803
+ const BAR_H = { sm: 'h-1.5', md: 'h-2', lg: 'h-3' };
3804
+ /**
3805
+ * A 0–100 score as a ring or a bar, tinted by tier (poor · fair · good). The tier is also written out
3806
+ * in words with its own glyph, and the whole thing is a `role="meter"` with a spoken value text — so
3807
+ * the score is never conveyed by colour alone.
3808
+ *
3809
+ * @example
3810
+ * <sds-score [value]="64" label="Menu health" />
3811
+ * <sds-score [value]="health()" variant="bar" label="Menu health" goodLabel="Great" />
3812
+ */
3813
+ class SdsScore {
3814
+ /** The score, 0–100 (clamped; non-numbers read as 0). */
3815
+ value = input(0, ...(ngDevMode ? [{ debugName: "value", transform: numberAttribute }] : [{ transform: numberAttribute }]));
3816
+ variant = input('ring', ...(ngDevMode ? [{ debugName: "variant" }] : []));
3817
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
3818
+ /** Visible caption and the meter's accessible name, e.g. "Menu health". */
3819
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
3820
+ /** Show the tier in words (with its glyph). */
3821
+ showTier = input(true, ...(ngDevMode ? [{ debugName: "showTier", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
3822
+ /** Lowest value that counts as `fair`. */
3823
+ fairFrom = input(50, ...(ngDevMode ? [{ debugName: "fairFrom", transform: numberAttribute }] : [{ transform: numberAttribute }]));
3824
+ /** Lowest value that counts as `good`. */
3825
+ goodFrom = input(80, ...(ngDevMode ? [{ debugName: "goodFrom", transform: numberAttribute }] : [{ transform: numberAttribute }]));
3826
+ poorLabel = input('Needs work', ...(ngDevMode ? [{ debugName: "poorLabel" }] : []));
3827
+ fairLabel = input('Fair', ...(ngDevMode ? [{ debugName: "fairLabel" }] : []));
3828
+ goodLabel = input('Good', ...(ngDevMode ? [{ debugName: "goodLabel" }] : []));
3829
+ /** Spoken value; tokens `{value}` and `{tier}`. */
3830
+ valueTextTemplate = input('{value} out of 100, {tier}', ...(ngDevMode ? [{ debugName: "valueTextTemplate" }] : []));
3831
+ clamped = computed(() => {
3832
+ const v = this.value();
3833
+ return Number.isFinite(v) ? Math.round(Math.min(100, Math.max(0, v))) : 0;
3834
+ }, ...(ngDevMode ? [{ debugName: "clamped" }] : []));
3835
+ tier = computed(() => {
3836
+ const v = this.clamped();
3837
+ if (v >= this.goodFrom())
3838
+ return 'good';
3839
+ if (v >= this.fairFrom())
3840
+ return 'fair';
3841
+ return 'poor';
3842
+ }, ...(ngDevMode ? [{ debugName: "tier" }] : []));
3843
+ tierStyle = computed(() => TIERS[this.tier()], ...(ngDevMode ? [{ debugName: "tierStyle" }] : []));
3844
+ tierText = computed(() => this.tier() === 'good' ? this.goodLabel() : this.tier() === 'fair' ? this.fairLabel() : this.poorLabel(), ...(ngDevMode ? [{ debugName: "tierText" }] : []));
3845
+ valueText = computed(() => this.valueTextTemplate()
3846
+ .replace('{value}', String(this.clamped()))
3847
+ .replace('{tier}', this.tierText()), ...(ngDevMode ? [{ debugName: "valueText" }] : []));
3848
+ ring = computed(() => RING[this.size()], ...(ngDevMode ? [{ debugName: "ring" }] : []));
3849
+ radius = computed(() => (this.ring().box - this.ring().stroke) / 2, ...(ngDevMode ? [{ debugName: "radius" }] : []));
3850
+ circumference = computed(() => 2 * Math.PI * this.radius(), ...(ngDevMode ? [{ debugName: "circumference" }] : []));
3851
+ dashOffset = computed(() => this.circumference() * (1 - this.clamped() / 100), ...(ngDevMode ? [{ debugName: "dashOffset" }] : []));
3852
+ barHeight = computed(() => BAR_H[this.size()], ...(ngDevMode ? [{ debugName: "barHeight" }] : []));
3853
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsScore, deps: [], target: i0.ɵɵFactoryTarget.Component });
3854
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsScore, isStandalone: true, selector: "sds-score", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, showTier: { classPropertyName: "showTier", publicName: "showTier", isSignal: true, isRequired: false, transformFunction: null }, fairFrom: { classPropertyName: "fairFrom", publicName: "fairFrom", isSignal: true, isRequired: false, transformFunction: null }, goodFrom: { classPropertyName: "goodFrom", publicName: "goodFrom", isSignal: true, isRequired: false, transformFunction: null }, poorLabel: { classPropertyName: "poorLabel", publicName: "poorLabel", isSignal: true, isRequired: false, transformFunction: null }, fairLabel: { classPropertyName: "fairLabel", publicName: "fairLabel", isSignal: true, isRequired: false, transformFunction: null }, goodLabel: { classPropertyName: "goodLabel", publicName: "goodLabel", isSignal: true, isRequired: false, transformFunction: null }, valueTextTemplate: { classPropertyName: "valueTextTemplate", publicName: "valueTextTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, ngImport: i0, template: `
3855
+ <div
3856
+ role="meter"
3857
+ aria-valuemin="0"
3858
+ aria-valuemax="100"
3859
+ [attr.aria-valuenow]="clamped()"
3860
+ [attr.aria-valuetext]="valueText()"
3861
+ [attr.aria-label]="label() || null"
3862
+ [attr.data-tier]="tier()"
3863
+ >
3864
+ @if (variant() === 'ring') {
3865
+ <div class="flex items-center gap-3" aria-hidden="true">
3866
+ <div class="relative shrink-0" [style.width.px]="ring().box" [style.height.px]="ring().box">
3867
+ <svg [attr.viewBox]="'0 0 ' + ring().box + ' ' + ring().box" class="-rotate-90" width="100%" height="100%">
3868
+ <circle
3869
+ [attr.cx]="ring().box / 2"
3870
+ [attr.cy]="ring().box / 2"
3871
+ [attr.r]="radius()"
3872
+ fill="none"
3873
+ class="stroke-border"
3874
+ [attr.stroke-width]="ring().stroke"
3875
+ />
3876
+ <circle
3877
+ [attr.cx]="ring().box / 2"
3878
+ [attr.cy]="ring().box / 2"
3879
+ [attr.r]="radius()"
3880
+ fill="none"
3881
+ stroke-linecap="round"
3882
+ class="transition-[stroke-dashoffset] duration-500"
3883
+ [class]="tierStyle().stroke"
3884
+ [attr.stroke-width]="ring().stroke"
3885
+ [attr.stroke-dasharray]="circumference()"
3886
+ [attr.stroke-dashoffset]="dashOffset()"
3887
+ />
3888
+ </svg>
3889
+ <div class="absolute inset-0 flex flex-col items-center justify-center leading-none">
3890
+ <span class="font-extrabold tabular-nums text-ink" [class]="ring().value">{{ clamped() }}</span>
3891
+ @if (size() !== 'sm') {
3892
+ <span class="mt-0.5 font-medium text-ink-faint" [class]="ring().max">/100</span>
3893
+ }
3894
+ </div>
3895
+ </div>
3896
+ @if (label() || showTier()) {
3897
+ <div class="min-w-0">
3898
+ @if (label()) {
3899
+ <p class="truncate text-sm font-semibold text-ink">{{ label() }}</p>
3900
+ }
3901
+ @if (showTier()) {
3902
+ <p class="mt-0.5 flex items-center gap-1 text-sm font-medium" [class]="tierStyle().text">
3903
+ <sds-icon [name]="tierStyle().icon" [size]="15" />
3904
+ {{ tierText() }}
3905
+ </p>
3906
+ }
3907
+ </div>
3908
+ }
3909
+ </div>
3910
+ } @else {
3911
+ <div aria-hidden="true">
3912
+ <div class="mb-1.5 flex items-baseline justify-between gap-3">
3913
+ <span class="truncate text-sm font-semibold text-ink">{{ label() }}</span>
3914
+ <span class="flex shrink-0 items-center gap-1.5 text-sm">
3915
+ <span class="font-bold tabular-nums text-ink">{{ clamped() }}<span class="text-ink-faint">/100</span></span>
3916
+ @if (showTier()) {
3917
+ <span class="flex items-center gap-1 font-medium" [class]="tierStyle().text">
3918
+ <sds-icon [name]="tierStyle().icon" [size]="14" />
3919
+ {{ tierText() }}
3920
+ </span>
3921
+ }
3922
+ </span>
3923
+ </div>
3924
+ <div class="w-full overflow-hidden rounded-full bg-border" [class]="barHeight()">
3925
+ <div
3926
+ class="h-full rounded-full transition-[width] duration-500"
3927
+ [class]="tierStyle().fill"
3928
+ [style.width.%]="clamped()"
3929
+ ></div>
3930
+ </div>
3931
+ </div>
3932
+ }
3933
+ </div>
3934
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3935
+ }
3936
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsScore, decorators: [{
3937
+ type: Component,
3938
+ args: [{
3939
+ selector: 'sds-score',
3940
+ changeDetection: ChangeDetectionStrategy.OnPush,
3941
+ imports: [SdsIcon],
3942
+ host: { class: 'block' },
3943
+ template: `
3944
+ <div
3945
+ role="meter"
3946
+ aria-valuemin="0"
3947
+ aria-valuemax="100"
3948
+ [attr.aria-valuenow]="clamped()"
3949
+ [attr.aria-valuetext]="valueText()"
3950
+ [attr.aria-label]="label() || null"
3951
+ [attr.data-tier]="tier()"
3952
+ >
3953
+ @if (variant() === 'ring') {
3954
+ <div class="flex items-center gap-3" aria-hidden="true">
3955
+ <div class="relative shrink-0" [style.width.px]="ring().box" [style.height.px]="ring().box">
3956
+ <svg [attr.viewBox]="'0 0 ' + ring().box + ' ' + ring().box" class="-rotate-90" width="100%" height="100%">
3957
+ <circle
3958
+ [attr.cx]="ring().box / 2"
3959
+ [attr.cy]="ring().box / 2"
3960
+ [attr.r]="radius()"
3961
+ fill="none"
3962
+ class="stroke-border"
3963
+ [attr.stroke-width]="ring().stroke"
3964
+ />
3965
+ <circle
3966
+ [attr.cx]="ring().box / 2"
3967
+ [attr.cy]="ring().box / 2"
3968
+ [attr.r]="radius()"
3969
+ fill="none"
3970
+ stroke-linecap="round"
3971
+ class="transition-[stroke-dashoffset] duration-500"
3972
+ [class]="tierStyle().stroke"
3973
+ [attr.stroke-width]="ring().stroke"
3974
+ [attr.stroke-dasharray]="circumference()"
3975
+ [attr.stroke-dashoffset]="dashOffset()"
3976
+ />
3977
+ </svg>
3978
+ <div class="absolute inset-0 flex flex-col items-center justify-center leading-none">
3979
+ <span class="font-extrabold tabular-nums text-ink" [class]="ring().value">{{ clamped() }}</span>
3980
+ @if (size() !== 'sm') {
3981
+ <span class="mt-0.5 font-medium text-ink-faint" [class]="ring().max">/100</span>
3982
+ }
3983
+ </div>
3984
+ </div>
3985
+ @if (label() || showTier()) {
3986
+ <div class="min-w-0">
3987
+ @if (label()) {
3988
+ <p class="truncate text-sm font-semibold text-ink">{{ label() }}</p>
3989
+ }
3990
+ @if (showTier()) {
3991
+ <p class="mt-0.5 flex items-center gap-1 text-sm font-medium" [class]="tierStyle().text">
3992
+ <sds-icon [name]="tierStyle().icon" [size]="15" />
3993
+ {{ tierText() }}
3994
+ </p>
3995
+ }
3996
+ </div>
3997
+ }
3998
+ </div>
3999
+ } @else {
4000
+ <div aria-hidden="true">
4001
+ <div class="mb-1.5 flex items-baseline justify-between gap-3">
4002
+ <span class="truncate text-sm font-semibold text-ink">{{ label() }}</span>
4003
+ <span class="flex shrink-0 items-center gap-1.5 text-sm">
4004
+ <span class="font-bold tabular-nums text-ink">{{ clamped() }}<span class="text-ink-faint">/100</span></span>
4005
+ @if (showTier()) {
4006
+ <span class="flex items-center gap-1 font-medium" [class]="tierStyle().text">
4007
+ <sds-icon [name]="tierStyle().icon" [size]="14" />
4008
+ {{ tierText() }}
4009
+ </span>
4010
+ }
4011
+ </span>
4012
+ </div>
4013
+ <div class="w-full overflow-hidden rounded-full bg-border" [class]="barHeight()">
4014
+ <div
4015
+ class="h-full rounded-full transition-[width] duration-500"
4016
+ [class]="tierStyle().fill"
4017
+ [style.width.%]="clamped()"
4018
+ ></div>
4019
+ </div>
4020
+ </div>
4021
+ }
4022
+ </div>
4023
+ `,
4024
+ }]
4025
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], showTier: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTier", required: false }] }], fairFrom: [{ type: i0.Input, args: [{ isSignal: true, alias: "fairFrom", required: false }] }], goodFrom: [{ type: i0.Input, args: [{ isSignal: true, alias: "goodFrom", required: false }] }], poorLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "poorLabel", required: false }] }], fairLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "fairLabel", required: false }] }], goodLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "goodLabel", required: false }] }], valueTextTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueTextTemplate", required: false }] }] } });
4026
+
4027
+ const STATES = {
4028
+ available: {
4029
+ icon: 'check-circle',
4030
+ frame: 'border-border bg-surface',
4031
+ badge: 'bg-accent-green-soft text-accent-green',
4032
+ },
4033
+ 'sold-out': {
4034
+ icon: 'ban',
4035
+ frame: 'border-danger bg-accent-red-soft',
4036
+ badge: 'bg-danger text-white',
4037
+ },
4038
+ hidden: {
4039
+ icon: 'eye-off',
4040
+ frame: 'border-dashed border-ink-faint bg-muted',
4041
+ badge: 'bg-ink/10 text-ink-soft',
4042
+ },
4043
+ };
4044
+ /**
4045
+ * Touch-first square tile for a stock board: a big press target (the whole tile) with a title,
4046
+ * optional subtitle, optional photo and a state — available · sold out · hidden.
4047
+ *
4048
+ * The state is never colour-only: each has its own glyph and word in a badge (check / "Available",
4049
+ * a no-entry sign / "Sold out", a crossed eye / "Hidden"), sold-out tiles get a diagonal hatch and a
4050
+ * struck-through title, hidden tiles a dashed frame. Pressing gives a visible pressed state.
4051
+ *
4052
+ * The tile is one `<button>` that emits `(activate)`. Pass [selected] (true/false) to make it a toggle
4053
+ * for multi-select ("select all matches"): it then carries `aria-pressed` and a corner check. [busy]
4054
+ * shows a spinner and sets `aria-busy` while a save is in flight.
4055
+ *
4056
+ * @example
4057
+ * <sds-status-tile title="Bacon" subtitle="Extras" state="sold-out" note="Back at 22:00"
4058
+ * soldOutLabel="Sold out" (activate)="toggle(item)" />
4059
+ */
4060
+ class SdsStatusTile {
4061
+ title = input.required(...(ngDevMode ? [{ debugName: "title" }] : []));
4062
+ subtitle = input('', ...(ngDevMode ? [{ debugName: "subtitle" }] : []));
4063
+ /** Optional photo URL, shown across the top of the tile. */
4064
+ image = input(null, ...(ngDevMode ? [{ debugName: "image" }] : []));
4065
+ /** Alt text for the photo. Empty (default) marks it decorative — the title already names the tile. */
4066
+ imageAlt = input('', ...(ngDevMode ? [{ debugName: "imageAlt" }] : []));
4067
+ state = input('available', ...(ngDevMode ? [{ debugName: "state" }] : []));
4068
+ /** Short extra line next to the state, e.g. "Back at 22:00". */
4069
+ note = input('', ...(ngDevMode ? [{ debugName: "note" }] : []));
4070
+ /** `null` (default): a plain action tile. `true`/`false`: a toggle for multi-select (`aria-pressed`). */
4071
+ selected = input(null, ...(ngDevMode ? [{ debugName: "selected" }] : []));
4072
+ /** A save is in flight: spinner + `aria-busy`. */
4073
+ busy = input(false, ...(ngDevMode ? [{ debugName: "busy", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4074
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4075
+ availableLabel = input('Available', ...(ngDevMode ? [{ debugName: "availableLabel" }] : []));
4076
+ soldOutLabel = input('Sold out', ...(ngDevMode ? [{ debugName: "soldOutLabel" }] : []));
4077
+ hiddenLabel = input('Hidden', ...(ngDevMode ? [{ debugName: "hiddenLabel" }] : []));
4078
+ /** The tile was pressed (click, tap, Enter or Space). */
4079
+ activate = output();
4080
+ style = computed(() => STATES[this.state()] ?? STATES.available, ...(ngDevMode ? [{ debugName: "style" }] : []));
4081
+ stateText = computed(() => {
4082
+ switch (this.state()) {
4083
+ case 'sold-out':
4084
+ return this.soldOutLabel();
4085
+ case 'hidden':
4086
+ return this.hiddenLabel();
4087
+ default:
4088
+ return this.availableLabel();
4089
+ }
4090
+ }, ...(ngDevMode ? [{ debugName: "stateText" }] : []));
4091
+ /** Diagonal hatch on sold-out tiles — a pattern cue on top of the colour. */
4092
+ hatch = computed(() => this.state() === 'sold-out'
4093
+ ? 'repeating-linear-gradient(135deg, transparent 0 12px, color-mix(in srgb, var(--color-danger) 10%, transparent) 12px 24px)'
4094
+ : null, ...(ngDevMode ? [{ debugName: "hatch" }] : []));
4095
+ tileClass = computed(() => {
4096
+ const ring = this.selected()
4097
+ ? ' ring-2 ring-brand-500 ring-offset-2 ring-offset-canvas dark:ring-ink'
4098
+ : '';
4099
+ return ('relative flex aspect-square w-full min-h-32 cursor-pointer select-none flex-col overflow-hidden ' +
4100
+ 'rounded-card border-2 text-left shadow-sm transition-[transform,box-shadow] duration-100 ' +
4101
+ 'hover:shadow-md active:scale-[0.97] active:shadow-none focus-visible:outline-none ' +
4102
+ 'focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 ' +
4103
+ 'focus-visible:ring-offset-canvas disabled:cursor-not-allowed disabled:opacity-60 ' +
4104
+ 'touch-manipulation ' +
4105
+ this.style().frame +
4106
+ (this.state() === 'hidden' ? ' opacity-80' : '') +
4107
+ ring);
4108
+ }, ...(ngDevMode ? [{ debugName: "tileClass" }] : []));
4109
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsStatusTile, deps: [], target: i0.ɵɵFactoryTarget.Component });
4110
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsStatusTile, isStandalone: true, selector: "sds-status-tile", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, image: { classPropertyName: "image", publicName: "image", isSignal: true, isRequired: false, transformFunction: null }, imageAlt: { classPropertyName: "imageAlt", publicName: "imageAlt", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, note: { classPropertyName: "note", publicName: "note", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, busy: { classPropertyName: "busy", publicName: "busy", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, availableLabel: { classPropertyName: "availableLabel", publicName: "availableLabel", isSignal: true, isRequired: false, transformFunction: null }, soldOutLabel: { classPropertyName: "soldOutLabel", publicName: "soldOutLabel", isSignal: true, isRequired: false, transformFunction: null }, hiddenLabel: { classPropertyName: "hiddenLabel", publicName: "hiddenLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activate: "activate" }, host: { classAttribute: "block" }, ngImport: i0, template: `
4111
+ <button
4112
+ type="button"
4113
+ [class]="tileClass()"
4114
+ [disabled]="disabled()"
4115
+ [attr.aria-pressed]="selected() === null ? null : selected()"
4116
+ [attr.aria-busy]="busy() ? 'true' : null"
4117
+ [attr.data-state]="state()"
4118
+ [style.background-image]="hatch()"
4119
+ (click)="activate.emit()"
4120
+ >
4121
+ @if (image()) {
4122
+ <span class="relative block h-[45%] w-full shrink-0 overflow-hidden bg-muted">
4123
+ <img
4124
+ [src]="image()"
4125
+ [alt]="imageAlt()"
4126
+ class="h-full w-full object-cover"
4127
+ [class.grayscale]="state() !== 'available'"
4128
+ [class.opacity-60]="state() !== 'available'"
4129
+ loading="lazy"
4130
+ draggable="false"
4131
+ />
4132
+ </span>
4133
+ }
4134
+
4135
+ <span class="flex min-h-0 w-full flex-1 flex-col gap-1 p-3 text-left">
4136
+ <span
4137
+ class="line-clamp-2 break-words font-bold leading-tight text-ink"
4138
+ [class]="image() ? 'text-base' : 'text-lg'"
4139
+ [class.line-through]="state() === 'sold-out'"
4140
+ [class.decoration-2]="state() === 'sold-out'"
4141
+ >{{ title() }}</span
4142
+ >
4143
+ @if (subtitle()) {
4144
+ <span class="truncate text-xs text-ink-soft">{{ subtitle() }}</span>
4145
+ }
4146
+
4147
+ <span class="mt-auto flex flex-wrap items-center gap-1.5 pt-1">
4148
+ <span
4149
+ class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-bold"
4150
+ [class]="style().badge"
4151
+ >
4152
+ <sds-icon [name]="style().icon" [size]="14" [strokeWidth]="2.5" />
4153
+ {{ stateText() }}
4154
+ </span>
4155
+ @if (note()) {
4156
+ <span class="truncate text-xs font-medium text-ink-soft">{{ note() }}</span>
4157
+ }
4158
+ </span>
4159
+ </span>
4160
+
4161
+ @if (selected()) {
4162
+ <span
4163
+ class="absolute right-2 top-2 grid h-7 w-7 place-items-center rounded-full bg-brand-600 text-white shadow ring-2 ring-surface dark:bg-ink dark:text-surface"
4164
+ aria-hidden="true"
4165
+ >
4166
+ <sds-icon name="check" [size]="16" [strokeWidth]="3" />
4167
+ </span>
4168
+ } @else if (selected() === false) {
4169
+ <span
4170
+ class="absolute right-2 top-2 h-7 w-7 rounded-full border-2 border-ink-faint bg-surface/80"
4171
+ aria-hidden="true"
4172
+ ></span>
4173
+ }
4174
+
4175
+ @if (busy()) {
4176
+ <span class="absolute inset-0 grid place-items-center bg-surface/60" aria-hidden="true">
4177
+ <span class="h-7 w-7 animate-spin rounded-full border-2 border-brand-200 border-t-brand-600"></span>
4178
+ </span>
4179
+ }
4180
+ </button>
4181
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4182
+ }
4183
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsStatusTile, decorators: [{
4184
+ type: Component,
4185
+ args: [{
4186
+ selector: 'sds-status-tile',
4187
+ changeDetection: ChangeDetectionStrategy.OnPush,
4188
+ imports: [SdsIcon],
4189
+ host: { class: 'block' },
4190
+ template: `
4191
+ <button
4192
+ type="button"
4193
+ [class]="tileClass()"
4194
+ [disabled]="disabled()"
4195
+ [attr.aria-pressed]="selected() === null ? null : selected()"
4196
+ [attr.aria-busy]="busy() ? 'true' : null"
4197
+ [attr.data-state]="state()"
4198
+ [style.background-image]="hatch()"
4199
+ (click)="activate.emit()"
4200
+ >
4201
+ @if (image()) {
4202
+ <span class="relative block h-[45%] w-full shrink-0 overflow-hidden bg-muted">
4203
+ <img
4204
+ [src]="image()"
4205
+ [alt]="imageAlt()"
4206
+ class="h-full w-full object-cover"
4207
+ [class.grayscale]="state() !== 'available'"
4208
+ [class.opacity-60]="state() !== 'available'"
4209
+ loading="lazy"
4210
+ draggable="false"
4211
+ />
4212
+ </span>
4213
+ }
4214
+
4215
+ <span class="flex min-h-0 w-full flex-1 flex-col gap-1 p-3 text-left">
4216
+ <span
4217
+ class="line-clamp-2 break-words font-bold leading-tight text-ink"
4218
+ [class]="image() ? 'text-base' : 'text-lg'"
4219
+ [class.line-through]="state() === 'sold-out'"
4220
+ [class.decoration-2]="state() === 'sold-out'"
4221
+ >{{ title() }}</span
4222
+ >
4223
+ @if (subtitle()) {
4224
+ <span class="truncate text-xs text-ink-soft">{{ subtitle() }}</span>
4225
+ }
4226
+
4227
+ <span class="mt-auto flex flex-wrap items-center gap-1.5 pt-1">
4228
+ <span
4229
+ class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-bold"
4230
+ [class]="style().badge"
4231
+ >
4232
+ <sds-icon [name]="style().icon" [size]="14" [strokeWidth]="2.5" />
4233
+ {{ stateText() }}
4234
+ </span>
4235
+ @if (note()) {
4236
+ <span class="truncate text-xs font-medium text-ink-soft">{{ note() }}</span>
4237
+ }
4238
+ </span>
4239
+ </span>
4240
+
4241
+ @if (selected()) {
4242
+ <span
4243
+ class="absolute right-2 top-2 grid h-7 w-7 place-items-center rounded-full bg-brand-600 text-white shadow ring-2 ring-surface dark:bg-ink dark:text-surface"
4244
+ aria-hidden="true"
4245
+ >
4246
+ <sds-icon name="check" [size]="16" [strokeWidth]="3" />
4247
+ </span>
4248
+ } @else if (selected() === false) {
4249
+ <span
4250
+ class="absolute right-2 top-2 h-7 w-7 rounded-full border-2 border-ink-faint bg-surface/80"
4251
+ aria-hidden="true"
4252
+ ></span>
4253
+ }
4254
+
4255
+ @if (busy()) {
4256
+ <span class="absolute inset-0 grid place-items-center bg-surface/60" aria-hidden="true">
4257
+ <span class="h-7 w-7 animate-spin rounded-full border-2 border-brand-200 border-t-brand-600"></span>
4258
+ </span>
4259
+ }
4260
+ </button>
4261
+ `,
4262
+ }]
4263
+ }], 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"] }] } });
4264
+
4265
+ let nextId$b = 0;
4266
+ /**
4267
+ * Marks the element inside `<sds-menu>` that opens it. It may be a native button or a component that
4268
+ * renders one (e.g. `<sds-button sdsMenuTrigger>`): the menu wires `aria-haspopup`, `aria-expanded` and
4269
+ * `aria-controls` onto the first focusable element it finds there.
4270
+ */
4271
+ class SdsMenuTrigger {
4272
+ el = inject(ElementRef);
4273
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4274
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.25", type: SdsMenuTrigger, isStandalone: true, selector: "[sdsMenuTrigger]", ngImport: i0 });
4275
+ }
4276
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsMenuTrigger, decorators: [{
4277
+ type: Directive,
4278
+ args: [{
4279
+ selector: '[sdsMenuTrigger]',
4280
+ standalone: true,
4281
+ }]
4282
+ }] });
4283
+ /**
4284
+ * Action menu (menu button + popover). The panel is appended to `<body>` and positioned against the
4285
+ * trigger, flipping above it when there is no room below — so it is never clipped by a drawer, a dialog
4286
+ * or a scrolling table, and it sits above them (z 1100).
4287
+ *
4288
+ * Items come from [items]: actions (`{ label, description?, icon?, danger?, disabled? }`) and
4289
+ * `{ separator: true }` dividers. Activating one emits `(select)` with it and closes the menu.
4290
+ *
4291
+ * Trigger: project your own element marked `sdsMenuTrigger`, or let the menu render a compact icon
4292
+ * button (`triggerIcon`, default a horizontal ellipsis) named by [triggerLabel].
4293
+ *
4294
+ * Keyboard (WAI-ARIA menu button): Enter / Space / ArrowDown open on the first item, ArrowUp on the
4295
+ * last; ArrowUp/Down move (wrapping, skipping disabled items), Home/End jump, a letter jumps to the next
4296
+ * item starting with it, Enter/Space activate. **Escape closes only the menu** — the key event is
4297
+ * stopped, so an enclosing drawer or dialog stays open — and focus returns to the trigger. Tab closes it
4298
+ * too. A click outside closes it.
4299
+ *
4300
+ * @example
4301
+ * <sds-menu [items]="[{ id: 'sold', label: 'Mark sold out', icon: 'ban' }, { separator: true },
4302
+ * { id: 'del', label: 'Delete', icon: 'trash-2', danger: true }]"
4303
+ * triggerLabel="Item actions" (select)="onAction($event)" />
4304
+ *
4305
+ * @example
4306
+ * <sds-menu [items]="durations" (select)="snooze($event)">
4307
+ * <sds-button sdsMenuTrigger variant="secondary">Mark sold out</sds-button>
4308
+ * </sds-menu>
4309
+ */
4310
+ class SdsMenu {
4311
+ host = inject(ElementRef);
4312
+ injector = inject(Injector);
4313
+ menuId = `sds-menu-${nextId$b++}`;
4314
+ triggerId = `${this.menuId}-trigger`;
4315
+ /** Actions and separators, in order. */
4316
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
4317
+ /** Open state. Two-way: `[(open)]` — set it to open the menu from code. */
4318
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
4319
+ /** Which edge of the trigger the panel lines up with. */
4320
+ align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : []));
4321
+ /** Accessible name of the built-in icon trigger (required when there is no [triggerText]). */
4322
+ triggerLabel = input('More actions', ...(ngDevMode ? [{ debugName: "triggerLabel" }] : []));
4323
+ /** Icon of the built-in trigger. */
4324
+ triggerIcon = input('ellipsis', ...(ngDevMode ? [{ debugName: "triggerIcon" }] : []));
4325
+ /** Optional visible text on the built-in trigger (then it is its name). */
4326
+ triggerText = input('', ...(ngDevMode ? [{ debugName: "triggerText" }] : []));
4327
+ /** Accessible name of the menu itself; defaults to the trigger's name. */
4328
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
4329
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4330
+ /** Keep the menu open after an item is activated (e.g. toggles). */
4331
+ keepOpen = input(false, ...(ngDevMode ? [{ debugName: "keepOpen", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4332
+ /** An action was activated. */
4333
+ select = output();
4334
+ projectedTrigger = contentChild(SdsMenuTrigger, ...(ngDevMode ? [{ debugName: "projectedTrigger" }] : []));
4335
+ panelEl = null;
4336
+ /** Which item gets focus when the panel opens: 'first' | 'last' | 'none'. */
4337
+ focusOnOpen = 'first';
4338
+ typeBuffer = '';
4339
+ typeTimer;
4340
+ enabledIndexes = computed(() => this.items()
4341
+ .map((e, i) => (this.isSeparator(e) || e.disabled ? -1 : i))
4342
+ .filter((i) => i >= 0), ...(ngDevMode ? [{ debugName: "enabledIndexes" }] : []));
4343
+ constructor() {
4344
+ // Wire ARIA onto a projected trigger's focusable element (the built-in one binds it in the template).
4345
+ effect(() => {
4346
+ const isOpen = this.open();
4347
+ const trigger = this.projectedTrigger();
4348
+ if (!trigger)
4349
+ return;
4350
+ const el = this.findFocusable(trigger.el.nativeElement);
4351
+ if (!el)
4352
+ return;
4353
+ if (!el.id)
4354
+ el.id = this.triggerId;
4355
+ el.setAttribute('aria-haspopup', 'menu');
4356
+ el.setAttribute('aria-expanded', String(isOpen));
4357
+ if (isOpen)
4358
+ el.setAttribute('aria-controls', this.menuId);
4359
+ else
4360
+ el.removeAttribute('aria-controls');
4361
+ });
4362
+ // Open/close side effects: move the panel to <body>, position it, focus an item; clean up on close.
4363
+ effect(() => {
4364
+ if (this.open()) {
4365
+ untracked(() => afterNextRender(() => this.afterOpen(), { injector: this.injector }));
4366
+ }
4367
+ else {
4368
+ untracked(() => this.teardown());
4369
+ }
4370
+ });
4371
+ }
4372
+ ngOnDestroy() {
4373
+ clearTimeout(this.typeTimer);
4374
+ this.teardown();
4375
+ }
4376
+ // --- helpers ----------------------------------------------------------------
4377
+ isSeparator(entry) {
4378
+ return entry.separator === true;
4379
+ }
4380
+ descId(i) {
4381
+ return `${this.menuId}-desc-${i}`;
4382
+ }
4383
+ itemClass(entry) {
4384
+ const base = 'flex w-full items-start gap-3 px-3.5 py-2 text-left transition-colors outline-none ' +
4385
+ 'focus-visible:ring-2 focus-visible:ring-inset ';
4386
+ if (entry.disabled)
4387
+ return `${base} cursor-not-allowed text-ink-faint opacity-60`;
4388
+ return entry.danger
4389
+ ? `${base} cursor-pointer text-danger hover:bg-danger/10 focus:bg-danger/10 focus-visible:ring-danger/40`
4390
+ : `${base} cursor-pointer text-ink hover:bg-muted focus:bg-muted focus-visible:ring-brand-500/40`;
4391
+ }
4392
+ findFocusable(root) {
4393
+ if (root.matches('button, a[href], [tabindex]'))
4394
+ return root;
4395
+ return root.querySelector('button, a[href], [tabindex]:not([tabindex="-1"])');
4396
+ }
4397
+ triggerEl() {
4398
+ const projected = this.projectedTrigger();
4399
+ if (projected)
4400
+ return this.findFocusable(projected.el.nativeElement);
4401
+ return this.host.nativeElement.querySelector(`#${this.triggerId}`);
4402
+ }
4403
+ isTriggerDisabled() {
4404
+ const el = this.triggerEl();
4405
+ return this.disabled() || !!el?.disabled;
4406
+ }
4407
+ // --- open / close ---------------------------------------------------------
4408
+ show(focus) {
4409
+ if (this.isTriggerDisabled())
4410
+ return;
4411
+ this.focusOnOpen = focus;
4412
+ if (this.open()) {
4413
+ this.focusItemAt(focus);
4414
+ return;
4415
+ }
4416
+ this.open.set(true);
4417
+ }
4418
+ /** Close the menu; with `returnFocus`, focus goes back to the trigger. */
4419
+ close(returnFocus = false) {
4420
+ if (!this.open())
4421
+ return;
4422
+ this.open.set(false);
4423
+ this.teardown();
4424
+ if (returnFocus)
4425
+ this.triggerEl()?.focus();
4426
+ }
4427
+ afterOpen() {
4428
+ if (!this.open())
4429
+ return;
4430
+ this.panelEl = this.host.nativeElement.querySelector('[data-sds-menu-panel]');
4431
+ if (!this.panelEl)
4432
+ return;
4433
+ if (this.panelEl.parentElement !== document.body)
4434
+ document.body.appendChild(this.panelEl);
4435
+ // Name the menu after its trigger unless an explicit [ariaLabel] was given.
4436
+ const trigger = this.triggerEl();
4437
+ if (!this.ariaLabel() && trigger?.id)
4438
+ this.panelEl.setAttribute('aria-labelledby', trigger.id);
4439
+ this.position();
4440
+ this.addReflowListeners();
4441
+ this.focusItemAt(this.focusOnOpen);
4442
+ }
4443
+ teardown() {
4444
+ this.removeReflowListeners();
4445
+ this.panelEl?.remove();
4446
+ this.panelEl = null;
4447
+ }
4448
+ // --- trigger events -------------------------------------------------------
4449
+ onTriggerClick(event) {
4450
+ if (!event.target.closest('button, a, [tabindex]'))
4451
+ return;
4452
+ if (this.open())
4453
+ this.close(false);
4454
+ else
4455
+ this.show('first');
4456
+ }
4457
+ onTriggerKeydown(event) {
4458
+ if (event.key === 'ArrowDown') {
4459
+ event.preventDefault();
4460
+ this.show('first');
4461
+ }
4462
+ else if (event.key === 'ArrowUp') {
4463
+ event.preventDefault();
4464
+ this.show('last');
4465
+ }
4466
+ }
4467
+ // --- menu keyboard --------------------------------------------------------
4468
+ onMenuKeydown(event) {
4469
+ const idx = this.enabledIndexes();
4470
+ const current = Number(document.activeElement?.dataset?.['index'] ?? -1);
4471
+ const pos = idx.indexOf(current);
4472
+ switch (event.key) {
4473
+ case 'ArrowDown':
4474
+ event.preventDefault();
4475
+ if (idx.length)
4476
+ this.focusIndex(idx[(pos + 1 + idx.length) % idx.length]);
4477
+ return;
4478
+ case 'ArrowUp':
4479
+ event.preventDefault();
4480
+ if (idx.length)
4481
+ this.focusIndex(idx[pos <= 0 ? idx.length - 1 : pos - 1]);
4482
+ return;
4483
+ case 'Home':
4484
+ event.preventDefault();
4485
+ this.focusItemAt('first');
4486
+ return;
4487
+ case 'End':
4488
+ event.preventDefault();
4489
+ this.focusItemAt('last');
4490
+ return;
4491
+ case 'Escape':
4492
+ // Close ONLY the menu: stop the event so a drawer/dialog listening for Escape stays open.
4493
+ event.preventDefault();
4494
+ event.stopPropagation();
4495
+ event.stopImmediatePropagation();
4496
+ this.close(true);
4497
+ return;
4498
+ case 'Tab':
4499
+ event.preventDefault();
4500
+ this.close(true);
4501
+ return;
4502
+ case 'Enter':
4503
+ case ' ': {
4504
+ event.preventDefault();
4505
+ const entry = this.items()[current];
4506
+ if (entry && !this.isSeparator(entry))
4507
+ this.activate(entry);
4508
+ return;
4509
+ }
4510
+ default:
4511
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
4512
+ this.typeAhead(event.key, current);
4513
+ }
4514
+ }
4515
+ }
4516
+ typeAhead(char, current) {
4517
+ clearTimeout(this.typeTimer);
4518
+ this.typeBuffer += char.toLowerCase();
4519
+ // "d", "d", "d" cycles through the items starting with d; "de" narrows to "Delete".
4520
+ const repeated = [...this.typeBuffer].every((c) => c === this.typeBuffer[0]);
4521
+ const query = repeated ? this.typeBuffer[0] : this.typeBuffer;
4522
+ const items = this.items();
4523
+ const idx = this.enabledIndexes();
4524
+ const start = Math.max(0, idx.indexOf(current));
4525
+ const from = query.length === 1 ? start + 1 : start;
4526
+ const ordered = [...idx.slice(from), ...idx.slice(0, from)];
4527
+ const hit = ordered.find((i) => items[i].label.toLowerCase().startsWith(query));
4528
+ if (hit !== undefined)
4529
+ this.focusIndex(hit);
4530
+ this.typeTimer = setTimeout(() => (this.typeBuffer = ''), 600);
4531
+ }
4532
+ focusIndex(i) {
4533
+ this.panelEl?.querySelector(`[data-index="${i}"]`)?.focus();
4534
+ }
4535
+ focusItemAt(where) {
4536
+ const idx = this.enabledIndexes();
4537
+ if (!idx.length) {
4538
+ this.panelEl?.focus();
4539
+ return;
4540
+ }
4541
+ this.focusIndex(where === 'first' ? idx[0] : idx[idx.length - 1]);
4542
+ }
4543
+ activate(entry, event) {
4544
+ event?.stopPropagation();
4545
+ if (entry.disabled)
4546
+ return;
4547
+ this.select.emit(entry);
4548
+ if (!this.keepOpen())
4549
+ this.close(true);
4550
+ }
4551
+ // --- outside click ----------------------------------------------------------
4552
+ onDocumentClick(event) {
4553
+ if (!this.open())
4554
+ return;
4555
+ const target = event.target;
4556
+ if (this.host.nativeElement.contains(target))
4557
+ return;
4558
+ if (this.panelEl?.contains(target))
4559
+ return;
4560
+ this.close(false);
4561
+ }
4562
+ // --- positioning (body-appended, mirrors sds-select) ------------------------
4563
+ reflow = () => this.position();
4564
+ listening = false;
4565
+ addReflowListeners() {
4566
+ if (this.listening)
4567
+ return;
4568
+ window.addEventListener('scroll', this.reflow, true);
4569
+ window.addEventListener('resize', this.reflow);
4570
+ this.listening = true;
4571
+ }
4572
+ removeReflowListeners() {
4573
+ if (!this.listening)
4574
+ return;
4575
+ window.removeEventListener('scroll', this.reflow, true);
4576
+ window.removeEventListener('resize', this.reflow);
4577
+ this.listening = false;
4578
+ }
4579
+ position() {
4580
+ const trigger = this.triggerEl();
4581
+ const panel = this.panelEl;
4582
+ if (!trigger || !panel)
4583
+ return;
4584
+ const r = trigger.getBoundingClientRect();
4585
+ const pw = panel.offsetWidth;
4586
+ const ph = panel.offsetHeight;
4587
+ const gap = 4;
4588
+ const margin = 8;
4589
+ let top = r.bottom + gap;
4590
+ if (top + ph > window.innerHeight - margin && r.top - gap - ph > margin) {
4591
+ top = r.top - gap - ph;
4592
+ }
4593
+ top = Math.max(margin, Math.min(top, window.innerHeight - margin - ph));
4594
+ let left = this.align() === 'end' ? r.right - pw : r.left;
4595
+ left = Math.max(margin, Math.min(left, window.innerWidth - margin - pw));
4596
+ panel.style.top = `${Math.round(top)}px`;
4597
+ panel.style.left = `${Math.round(left)}px`;
4598
+ }
4599
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
4600
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsMenu, isStandalone: true, selector: "sds-menu", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, triggerLabel: { classPropertyName: "triggerLabel", publicName: "triggerLabel", isSignal: true, isRequired: false, transformFunction: null }, triggerIcon: { classPropertyName: "triggerIcon", publicName: "triggerIcon", isSignal: true, isRequired: false, transformFunction: null }, triggerText: { classPropertyName: "triggerText", publicName: "triggerText", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, keepOpen: { classPropertyName: "keepOpen", publicName: "keepOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", select: "select" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, classAttribute: "relative inline-flex" }, queries: [{ propertyName: "projectedTrigger", first: true, predicate: SdsMenuTrigger, descendants: true, isSignal: true }], ngImport: i0, template: `
4601
+ <span
4602
+ #slot
4603
+ class="inline-flex"
4604
+ (click)="onTriggerClick($event)"
4605
+ (keydown)="onTriggerKeydown($event)"
4606
+ >
4607
+ <ng-content select="[sdsMenuTrigger]" />
4608
+ @if (!projectedTrigger()) {
4609
+ <button
4610
+ type="button"
4611
+ [id]="triggerId"
4612
+ class="inline-flex h-9 min-w-9 items-center justify-center gap-1.5 rounded-control px-2 text-sm font-medium text-ink-soft transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:cursor-not-allowed disabled:opacity-50"
4613
+ [class.bg-muted]="open()"
4614
+ [attr.aria-label]="triggerText() ? null : triggerLabel()"
4615
+ [disabled]="disabled()"
4616
+ aria-haspopup="menu"
4617
+ [attr.aria-expanded]="open()"
4618
+ [attr.aria-controls]="open() ? menuId : null"
4619
+ >
4620
+ <sds-icon [name]="triggerIcon()" [size]="18" />
4621
+ @if (triggerText()) {
4622
+ <span>{{ triggerText() }}</span>
4623
+ }
4624
+ </button>
4625
+ }
4626
+ </span>
4627
+
4628
+ @if (open()) {
4629
+ <div
4630
+ data-sds-menu-panel
4631
+ role="menu"
4632
+ [id]="menuId"
4633
+ [attr.aria-label]="ariaLabel() || null"
4634
+ class="fixed z-[1100] min-w-52 max-w-80 overflow-y-auto rounded-card border border-border bg-surface py-1.5 shadow-xl outline-none animate-sds-menu"
4635
+ [style.max-height]="'min(24rem, calc(100vh - 16px))'"
4636
+ (keydown)="onMenuKeydown($event)"
4637
+ >
4638
+ @for (entry of items(); track $index; let i = $index) {
4639
+ @if (isSeparator(entry)) {
4640
+ <div role="separator" class="my-1.5 border-t border-border"></div>
4641
+ } @else {
4642
+ <button
4643
+ type="button"
4644
+ role="menuitem"
4645
+ tabindex="-1"
4646
+ [attr.data-index]="i"
4647
+ [attr.aria-disabled]="entry.disabled ? 'true' : null"
4648
+ [attr.aria-describedby]="entry.description ? descId(i) : null"
4649
+ [class]="itemClass(entry)"
4650
+ (click)="activate(entry, $event)"
4651
+ (mouseenter)="entry.disabled ? null : focusIndex(i)"
4652
+ >
4653
+ @if (entry.icon) {
4654
+ <sds-icon [name]="entry.icon" [size]="18" class="mt-px shrink-0" />
4655
+ }
4656
+ <span class="min-w-0 flex-1 text-left">
4657
+ <span class="block truncate text-sm font-medium">{{ entry.label }}</span>
4658
+ @if (entry.description) {
4659
+ <span
4660
+ [id]="descId(i)"
4661
+ class="mt-0.5 block text-xs"
4662
+ [class]="entry.danger ? 'text-danger/80' : 'text-ink-faint'"
4663
+ >{{ entry.description }}</span
4664
+ >
4665
+ }
4666
+ </span>
4667
+ </button>
4668
+ }
4669
+ }
4670
+ </div>
4671
+ }
4672
+ `, isInline: true, styles: [".animate-sds-menu{animation:sds-menu-pop .12s ease-out}@keyframes sds-menu-pop{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.animate-sds-menu{animation:none}}\n"], dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4673
+ }
4674
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsMenu, decorators: [{
4675
+ type: Component,
4676
+ args: [{ selector: 'sds-menu', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SdsIcon], host: {
4677
+ class: 'relative inline-flex',
4678
+ '(document:click)': 'onDocumentClick($event)',
4679
+ }, template: `
4680
+ <span
4681
+ #slot
4682
+ class="inline-flex"
4683
+ (click)="onTriggerClick($event)"
4684
+ (keydown)="onTriggerKeydown($event)"
4685
+ >
4686
+ <ng-content select="[sdsMenuTrigger]" />
4687
+ @if (!projectedTrigger()) {
4688
+ <button
4689
+ type="button"
4690
+ [id]="triggerId"
4691
+ class="inline-flex h-9 min-w-9 items-center justify-center gap-1.5 rounded-control px-2 text-sm font-medium text-ink-soft transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:cursor-not-allowed disabled:opacity-50"
4692
+ [class.bg-muted]="open()"
4693
+ [attr.aria-label]="triggerText() ? null : triggerLabel()"
4694
+ [disabled]="disabled()"
4695
+ aria-haspopup="menu"
4696
+ [attr.aria-expanded]="open()"
4697
+ [attr.aria-controls]="open() ? menuId : null"
4698
+ >
4699
+ <sds-icon [name]="triggerIcon()" [size]="18" />
4700
+ @if (triggerText()) {
4701
+ <span>{{ triggerText() }}</span>
4702
+ }
4703
+ </button>
4704
+ }
4705
+ </span>
4706
+
4707
+ @if (open()) {
4708
+ <div
4709
+ data-sds-menu-panel
4710
+ role="menu"
4711
+ [id]="menuId"
4712
+ [attr.aria-label]="ariaLabel() || null"
4713
+ class="fixed z-[1100] min-w-52 max-w-80 overflow-y-auto rounded-card border border-border bg-surface py-1.5 shadow-xl outline-none animate-sds-menu"
4714
+ [style.max-height]="'min(24rem, calc(100vh - 16px))'"
4715
+ (keydown)="onMenuKeydown($event)"
4716
+ >
4717
+ @for (entry of items(); track $index; let i = $index) {
4718
+ @if (isSeparator(entry)) {
4719
+ <div role="separator" class="my-1.5 border-t border-border"></div>
4720
+ } @else {
4721
+ <button
4722
+ type="button"
4723
+ role="menuitem"
4724
+ tabindex="-1"
4725
+ [attr.data-index]="i"
4726
+ [attr.aria-disabled]="entry.disabled ? 'true' : null"
4727
+ [attr.aria-describedby]="entry.description ? descId(i) : null"
4728
+ [class]="itemClass(entry)"
4729
+ (click)="activate(entry, $event)"
4730
+ (mouseenter)="entry.disabled ? null : focusIndex(i)"
4731
+ >
4732
+ @if (entry.icon) {
4733
+ <sds-icon [name]="entry.icon" [size]="18" class="mt-px shrink-0" />
4734
+ }
4735
+ <span class="min-w-0 flex-1 text-left">
4736
+ <span class="block truncate text-sm font-medium">{{ entry.label }}</span>
4737
+ @if (entry.description) {
4738
+ <span
4739
+ [id]="descId(i)"
4740
+ class="mt-0.5 block text-xs"
4741
+ [class]="entry.danger ? 'text-danger/80' : 'text-ink-faint'"
4742
+ >{{ entry.description }}</span
4743
+ >
4744
+ }
4745
+ </span>
4746
+ </button>
4747
+ }
4748
+ }
4749
+ </div>
4750
+ }
4751
+ `, styles: [".animate-sds-menu{animation:sds-menu-pop .12s ease-out}@keyframes sds-menu-pop{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.animate-sds-menu{animation:none}}\n"] }]
4752
+ }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], triggerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabel", required: false }] }], triggerIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerIcon", required: false }] }], triggerText: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerText", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], keepOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepOpen", required: false }] }], select: [{ type: i0.Output, args: ["select"] }], projectedTrigger: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsMenuTrigger), { isSignal: true }] }] } });
4753
+
4754
+ /**
4755
+ * Selection toolbar for bulk editing: "N selected", the actions the app projects, and a clear-selection
4756
+ * button. It shows only while something is selected (`count > 0`) and sticks to the bottom of its
4757
+ * scroll container by default, so it stays in reach while the list scrolls.
4758
+ *
4759
+ * A `role="toolbar"` named by [ariaLabel]; the count sits in a polite live region that exists even
4760
+ * while the bar is hidden, so "3 selected" is announced as the selection changes. Every visible and
4761
+ * spoken string is an input (English defaults).
4762
+ *
4763
+ * @example
4764
+ * <sds-bulk-bar [count]="selected().length" countLabel="{count} selected" (clear)="selected.set([])">
4765
+ * <sds-button size="sm" variant="secondary" (click)="bulkPrice()">Change price</sds-button>
4766
+ * <sds-button size="sm" variant="secondary" (click)="bulkSoldOut()">Mark sold out</sds-button>
4767
+ * </sds-bulk-bar>
4768
+ */
4769
+ class SdsBulkBar {
4770
+ /** Number of selected things. The bar is hidden at 0. */
4771
+ count = input(0, ...(ngDevMode ? [{ debugName: "count", transform: numberAttribute }] : [{ transform: numberAttribute }]));
4772
+ /** "{count} selected" — `{count}` is replaced. Pass a translated string. */
4773
+ countLabel = input('{count} selected', ...(ngDevMode ? [{ debugName: "countLabel" }] : []));
4774
+ /** Text of the clear-selection button. */
4775
+ clearLabel = input('Clear selection', ...(ngDevMode ? [{ debugName: "clearLabel" }] : []));
4776
+ /** Accessible name of the toolbar. */
4777
+ ariaLabel = input('Bulk actions', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
4778
+ position = input('bottom', ...(ngDevMode ? [{ debugName: "position" }] : []));
4779
+ /** The clear-selection button was pressed. */
4780
+ clear = output();
4781
+ countText = computed(() => this.countLabel().replace('{count}', String(this.count())), ...(ngDevMode ? [{ debugName: "countText" }] : []));
4782
+ /** The label without the number, shown next to the count pill ("selected"). */
4783
+ countWords = computed(() => this.countLabel().replace('{count}', '').replace(/\s+/g, ' ').trim(), ...(ngDevMode ? [{ debugName: "countWords" }] : []));
4784
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsBulkBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
4785
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsBulkBar, isStandalone: true, selector: "sds-bulk-bar", inputs: { count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, countLabel: { classPropertyName: "countLabel", publicName: "countLabel", isSignal: true, isRequired: false, transformFunction: null }, clearLabel: { classPropertyName: "clearLabel", publicName: "clearLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clear: "clear" }, host: { properties: { "class.sticky": "position() !== 'static' && count() > 0", "class.bottom-3": "position() === 'bottom'", "class.top-3": "position() === 'top'", "class.z-30": "position() !== 'static'" }, classAttribute: "block" }, ngImport: i0, template: `
4786
+ <!-- Always present so screen readers hear the count change, including back to zero. -->
4787
+ <span class="sr-only" aria-live="polite" aria-atomic="true">{{ count() > 0 ? countText() : '' }}</span>
4788
+
4789
+ @if (count() > 0) {
4790
+ <div
4791
+ role="toolbar"
4792
+ [attr.aria-label]="ariaLabel()"
4793
+ class="flex flex-wrap items-center gap-x-3 gap-y-2 rounded-card border border-border bg-surface px-3 py-2 shadow-xl animate-sds-bulk"
4794
+ >
4795
+ <span class="inline-flex items-center gap-2 pr-1 text-sm font-semibold text-ink">
4796
+ <span
4797
+ class="inline-flex h-6 min-w-6 items-center justify-center rounded-full bg-brand-600 px-1.5 text-xs font-bold tabular-nums text-white dark:bg-ink dark:text-surface"
4798
+ >{{ count() }}</span
4799
+ >
4800
+ {{ countWords() }}
4801
+ </span>
4802
+
4803
+ <span class="hidden h-6 w-px bg-border sm:block" aria-hidden="true"></span>
4804
+
4805
+ <div class="flex min-w-0 flex-1 flex-wrap items-center gap-2">
4806
+ <ng-content />
4807
+ </div>
4808
+
4809
+ <button
4810
+ type="button"
4811
+ class="ml-auto inline-flex h-9 items-center gap-1.5 rounded-control px-2.5 text-sm font-medium text-ink-soft transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
4812
+ (click)="clear.emit()"
4813
+ >
4814
+ <sds-icon name="x" [size]="16" />
4815
+ <span>{{ clearLabel() }}</span>
4816
+ </button>
4817
+ </div>
4818
+ }
4819
+ `, isInline: true, styles: [".animate-sds-bulk{animation:sds-bulk-in .16s ease-out}@keyframes sds-bulk-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.animate-sds-bulk{animation:none}}\n"], dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4820
+ }
4821
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsBulkBar, decorators: [{
4822
+ type: Component,
4823
+ args: [{ selector: 'sds-bulk-bar', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SdsIcon], host: {
4824
+ class: 'block',
4825
+ '[class.sticky]': "position() !== 'static' && count() > 0",
4826
+ '[class.bottom-3]': "position() === 'bottom'",
4827
+ '[class.top-3]': "position() === 'top'",
4828
+ '[class.z-30]': "position() !== 'static'",
4829
+ }, template: `
4830
+ <!-- Always present so screen readers hear the count change, including back to zero. -->
4831
+ <span class="sr-only" aria-live="polite" aria-atomic="true">{{ count() > 0 ? countText() : '' }}</span>
4832
+
4833
+ @if (count() > 0) {
4834
+ <div
4835
+ role="toolbar"
4836
+ [attr.aria-label]="ariaLabel()"
4837
+ class="flex flex-wrap items-center gap-x-3 gap-y-2 rounded-card border border-border bg-surface px-3 py-2 shadow-xl animate-sds-bulk"
4838
+ >
4839
+ <span class="inline-flex items-center gap-2 pr-1 text-sm font-semibold text-ink">
4840
+ <span
4841
+ class="inline-flex h-6 min-w-6 items-center justify-center rounded-full bg-brand-600 px-1.5 text-xs font-bold tabular-nums text-white dark:bg-ink dark:text-surface"
4842
+ >{{ count() }}</span
4843
+ >
4844
+ {{ countWords() }}
4845
+ </span>
4846
+
4847
+ <span class="hidden h-6 w-px bg-border sm:block" aria-hidden="true"></span>
4848
+
4849
+ <div class="flex min-w-0 flex-1 flex-wrap items-center gap-2">
4850
+ <ng-content />
4851
+ </div>
4852
+
4853
+ <button
4854
+ type="button"
4855
+ class="ml-auto inline-flex h-9 items-center gap-1.5 rounded-control px-2.5 text-sm font-medium text-ink-soft transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
4856
+ (click)="clear.emit()"
4857
+ >
4858
+ <sds-icon name="x" [size]="16" />
4859
+ <span>{{ clearLabel() }}</span>
4860
+ </button>
4861
+ </div>
4862
+ }
4863
+ `, styles: [".animate-sds-bulk{animation:sds-bulk-in .16s ease-out}@keyframes sds-bulk-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.animate-sds-bulk{animation:none}}\n"] }]
4864
+ }], propDecorators: { count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], countLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "countLabel", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], clear: [{ type: i0.Output, args: ["clear"] }] } });
4865
+
4866
+ /**
4867
+ * Template for one row's content inside `<sds-sortable-list>`.
4868
+ *
4869
+ * @example
4870
+ * <ng-template sdsSortableItem let-item let-i="index">{{ item.name }}</ng-template>
4871
+ */
4872
+ class SdsSortableItem {
4873
+ template = inject(TemplateRef);
4874
+ static ngTemplateContextGuard(_dir, _ctx) {
4875
+ return true;
4876
+ }
4877
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSortableItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4878
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.25", type: SdsSortableItem, isStandalone: true, selector: "ng-template[sdsSortableItem]", ngImport: i0 });
4879
+ }
4880
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSortableItem, decorators: [{
4881
+ type: Directive,
4882
+ args: [{
4883
+ selector: 'ng-template[sdsSortableItem]',
4884
+ standalone: true,
4885
+ }]
4886
+ }] });
4887
+ /**
4888
+ * Optional template for the drag handle's content (replaces the default grip icon). The handle itself
4889
+ * stays a library-owned button, so it keeps its accessible name and keyboard behaviour.
4890
+ */
4891
+ class SdsSortableHandle {
4892
+ template = inject(TemplateRef);
4893
+ static ngTemplateContextGuard(_dir, _ctx) {
4894
+ return true;
4895
+ }
4896
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSortableHandle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4897
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.25", type: SdsSortableHandle, isStandalone: true, selector: "ng-template[sdsSortableHandle]", ngImport: i0 });
4898
+ }
4899
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSortableHandle, decorators: [{
4900
+ type: Directive,
4901
+ args: [{
4902
+ selector: 'ng-template[sdsSortableHandle]',
4903
+ standalone: true,
4904
+ }]
4905
+ }] });
4906
+ let nextId$a = 0;
4907
+ /** Move one entry of `list` from `from` to `to`, returning a new array. */
4908
+ function moveItem(list, from, to) {
4909
+ const next = [...list];
4910
+ const [moved] = next.splice(from, 1);
4911
+ next.splice(to, 0, moved);
4912
+ return next;
4913
+ }
4914
+ /**
4915
+ * Accessible reorderable list, with no `@angular/cdk` dependency.
4916
+ *
4917
+ * - **Pointer** (mouse, pen, touch): drag an item by its handle. The handle has `touch-action: none`, so
4918
+ * a finger drags instead of scrolling the page; the rest of the row still scrolls normally.
4919
+ * - **Keyboard**: focus a handle, **Space/Enter** picks the item up, **↑/↓** (and Home/End) move it,
4920
+ * **Space/Enter** drops it, **Escape** cancels (the event is stopped, so an enclosing drawer stays open).
4921
+ * Moving focus away also cancels.
4922
+ * - **Screen readers**: every handle is named ("Reorder Burgers") and described by the instructions; an
4923
+ * assertive live region announces pick-up, each move ("moved to position 3 of 8"), the drop and a cancel.
4924
+ *
4925
+ * Nothing is committed until the drop: while dragging, the other rows slide to make room. The drop
4926
+ * updates the two-way `[(items)]` model and emits `(reorder)` with the old/new index and the new array.
4927
+ * Items for which [itemDisabled] returns true cannot be picked up (they still shift when others pass).
4928
+ *
4929
+ * Row content comes from `<ng-template sdsSortableItem let-item>`; without one the item's label is shown.
4930
+ * All spoken text is set through inputs (English defaults, `{item}` `{position}` `{total}` tokens).
4931
+ *
4932
+ * @example
4933
+ * <sds-sortable-list [(items)]="categories" dataKey="id" labelField="name" ariaLabel="Categories"
4934
+ * (reorder)="save($event)">
4935
+ * <ng-template sdsSortableItem let-cat>{{ cat.name }} <span>{{ cat.count }} items</span></ng-template>
4936
+ * </sds-sortable-list>
4937
+ */
4938
+ class SdsSortableList {
4939
+ host = inject(ElementRef);
4940
+ injector = inject(Injector);
4941
+ instructionsId = `sds-sortable-${nextId$a++}-help`;
4942
+ /** The items, in order. Two-way: `[(items)]` receives the new order on drop. */
4943
+ items = model([], ...(ngDevMode ? [{ debugName: "items" }] : []));
4944
+ /** Property used as a stable identity for rows (strongly recommended). */
4945
+ dataKey = input('', ...(ngDevMode ? [{ debugName: "dataKey" }] : []));
4946
+ /** Property holding the item's name, for the default row, the handle name and announcements. */
4947
+ labelField = input('label', ...(ngDevMode ? [{ debugName: "labelField" }] : []));
4948
+ /** Function returning the item's name; takes precedence over [labelField]. */
4949
+ itemLabel = input(null, ...(ngDevMode ? [{ debugName: "itemLabel" }] : []));
4950
+ /** Items for which this returns true cannot be picked up. */
4951
+ itemDisabled = input(null, ...(ngDevMode ? [{ debugName: "itemDisabled" }] : []));
4952
+ /** Disable reordering for the whole list. */
4953
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4954
+ /** `card` (default): bordered rows with a gap. `plain`: rows separated by dividers. */
4955
+ appearance = input('card', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
4956
+ /** Accessible name of the list. */
4957
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
4958
+ /** Name of each handle; `{item}` is the item's label. */
4959
+ handleLabel = input('Reorder {item}', ...(ngDevMode ? [{ debugName: "handleLabel" }] : []));
4960
+ /** Read after a handle's name (aria-describedby). */
4961
+ instructions = input('Press Space or Enter to pick up. Use the arrow keys to move, Space or Enter to drop, Escape to cancel.', ...(ngDevMode ? [{ debugName: "instructions" }] : []));
4962
+ pickedUpMessage = input('{item} picked up. Position {position} of {total}.', ...(ngDevMode ? [{ debugName: "pickedUpMessage" }] : []));
4963
+ movedMessage = input('{item} moved to position {position} of {total}.', ...(ngDevMode ? [{ debugName: "movedMessage" }] : []));
4964
+ droppedMessage = input('{item} dropped at position {position} of {total}.', ...(ngDevMode ? [{ debugName: "droppedMessage" }] : []));
4965
+ cancelledMessage = input('Reorder cancelled. {item} is back at position {position} of {total}.', ...(ngDevMode ? [{ debugName: "cancelledMessage" }] : []));
4966
+ /** A move was completed. */
4967
+ reorder = output();
4968
+ itemTpl = contentChild(SdsSortableItem, ...(ngDevMode ? [{ debugName: "itemTpl" }] : []));
4969
+ handleTpl = contentChild(SdsSortableHandle, ...(ngDevMode ? [{ debugName: "handleTpl" }] : []));
4970
+ /** Rendered order: follows [items] until a drop commits a new one. */
4971
+ working = linkedSignal(() => [...this.items()], ...(ngDevMode ? [{ debugName: "working" }] : []));
4972
+ // --- drag state -------------------------------------------------------------
4973
+ /** Index (in `working`) of the item being moved, or null. */
4974
+ active = signal(null, ...(ngDevMode ? [{ debugName: "active" }] : []));
4975
+ /** Where it would land if dropped now. */
4976
+ target = signal(null, ...(ngDevMode ? [{ debugName: "target" }] : []));
4977
+ mode = signal(null, ...(ngDevMode ? [{ debugName: "mode" }] : []));
4978
+ /** Pointer-mode vertical offset of the dragged row. */
4979
+ pointerDy = signal(0, ...(ngDevMode ? [{ debugName: "pointerDy" }] : []));
4980
+ /** No transitions for one frame while a drop re-renders the new order in place. */
4981
+ settling = signal(false, ...(ngDevMode ? [{ debugName: "settling" }] : []));
4982
+ liveMessage = signal('', ...(ngDevMode ? [{ debugName: "liveMessage" }] : []));
4983
+ /** Row boxes measured at drag start (top/height), plus the gap between rows. */
4984
+ rects = [];
4985
+ gap = 0;
4986
+ pointerId = null;
4987
+ startY = 0;
4988
+ /** Per-row offsets that open a gap where the moving row will land. */
4989
+ shifts = computed(() => {
4990
+ const a = this.active();
4991
+ const t = this.target();
4992
+ const n = this.working().length;
4993
+ const out = new Array(n).fill(0);
4994
+ if (a === null || t === null || !this.rects[a])
4995
+ return out;
4996
+ const step = this.rects[a].height + this.gap;
4997
+ for (let j = 0; j < n; j++) {
4998
+ if (j === a)
4999
+ continue;
5000
+ if (a < t && j > a && j <= t)
5001
+ out[j] = -step;
5002
+ else if (t < a && j >= t && j < a)
5003
+ out[j] = step;
5004
+ }
5005
+ return out;
5006
+ }, ...(ngDevMode ? [{ debugName: "shifts" }] : []));
5007
+ /** Offset of the moving row: follows the pointer, or jumps slot to slot with the keyboard. */
5008
+ activeOffset = computed(() => {
5009
+ const a = this.active();
5010
+ const t = this.target();
5011
+ if (a === null || t === null)
5012
+ return 0;
5013
+ if (this.mode() === 'pointer')
5014
+ return this.pointerDy();
5015
+ let d = 0;
5016
+ if (t > a)
5017
+ for (let j = a + 1; j <= t; j++)
5018
+ d += (this.rects[j]?.height ?? 0) + this.gap;
5019
+ else
5020
+ for (let j = t; j < a; j++)
5021
+ d -= (this.rects[j]?.height ?? 0) + this.gap;
5022
+ return d;
5023
+ }, ...(ngDevMode ? [{ debugName: "activeOffset" }] : []));
5024
+ // --- rendering helpers ------------------------------------------------------
5025
+ trackKey(item, index) {
5026
+ const k = this.dataKey();
5027
+ if (k && item && typeof item === 'object')
5028
+ return item[k] ?? index;
5029
+ return item ?? index;
5030
+ }
5031
+ labelOf(item) {
5032
+ const fn = this.itemLabel();
5033
+ if (fn)
5034
+ return fn(item);
5035
+ if (item && typeof item === 'object') {
5036
+ const v = item[this.labelField()];
5037
+ return v == null ? '' : String(v);
5038
+ }
5039
+ return String(item);
5040
+ }
5041
+ isItemDisabled(item) {
5042
+ return this.itemDisabled()?.(item) ?? false;
5043
+ }
5044
+ context(item, index) {
5045
+ return { $implicit: item, item, index, grabbed: this.active() === index };
5046
+ }
5047
+ handleText(item) {
5048
+ return this.handleLabel().replace('{item}', this.labelOf(item));
5049
+ }
5050
+ transformFor(i) {
5051
+ if (this.active() === null)
5052
+ return null;
5053
+ const y = i === this.active() ? this.activeOffset() : this.shifts()[i];
5054
+ return y ? `translate3d(0, ${y}px, 0)` : null;
5055
+ }
5056
+ rowClass(i) {
5057
+ const isActive = this.active() === i;
5058
+ const moving = this.active() !== null;
5059
+ const card = this.appearance() === 'card';
5060
+ const parts = ['relative flex items-center gap-2'];
5061
+ parts.push(card ? 'rounded-control border border-border bg-surface px-2 py-2' : 'bg-surface px-1 py-2');
5062
+ // Neighbours glide; the dragged row tracks the pointer 1:1 (no easing), and nothing animates
5063
+ // during the settle frame after a drop.
5064
+ if (moving && !this.settling() && !(isActive && this.mode() === 'pointer')) {
5065
+ parts.push('transition-transform duration-150 ease-out');
5066
+ }
5067
+ if (isActive)
5068
+ parts.push('z-10 shadow-lg ring-2 ring-brand-500');
5069
+ return parts.join(' ');
5070
+ }
5071
+ handleClass(item) {
5072
+ const base = 'grid h-9 w-8 shrink-0 place-items-center rounded-control text-ink-faint transition-colors ' +
5073
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 ' +
5074
+ 'disabled:cursor-not-allowed disabled:opacity-40 touch-none select-none';
5075
+ return this.disabled() || this.isItemDisabled(item)
5076
+ ? base
5077
+ : `${base} cursor-grab hover:bg-muted hover:text-ink active:cursor-grabbing`;
5078
+ }
5079
+ // --- keyboard ---------------------------------------------------------------
5080
+ onHandleKeydown(event, i) {
5081
+ if (this.mode() === 'pointer')
5082
+ return;
5083
+ const a = this.active();
5084
+ if (a === null) {
5085
+ if (event.key === ' ' || event.key === 'Enter') {
5086
+ event.preventDefault();
5087
+ this.begin(i, 'keyboard');
5088
+ this.announce(this.pickedUpMessage(), i);
5089
+ }
5090
+ return;
5091
+ }
5092
+ const last = this.working().length - 1;
5093
+ const t = this.target() ?? a;
5094
+ switch (event.key) {
5095
+ case 'ArrowUp':
5096
+ case 'ArrowLeft':
5097
+ event.preventDefault();
5098
+ this.keyMove(Math.max(0, t - 1));
5099
+ return;
5100
+ case 'ArrowDown':
5101
+ case 'ArrowRight':
5102
+ event.preventDefault();
5103
+ this.keyMove(Math.min(last, t + 1));
5104
+ return;
5105
+ case 'Home':
5106
+ event.preventDefault();
5107
+ this.keyMove(0);
5108
+ return;
5109
+ case 'End':
5110
+ event.preventDefault();
5111
+ this.keyMove(last);
5112
+ return;
5113
+ case ' ':
5114
+ case 'Enter':
5115
+ event.preventDefault();
5116
+ this.commit();
5117
+ return;
5118
+ case 'Escape':
5119
+ event.preventDefault();
5120
+ event.stopPropagation();
5121
+ this.cancel();
5122
+ return;
5123
+ case 'Tab':
5124
+ this.cancel();
5125
+ return;
5126
+ }
5127
+ }
5128
+ keyMove(to) {
5129
+ if (to === this.target())
5130
+ return;
5131
+ this.target.set(to);
5132
+ this.announce(this.movedMessage(), to);
5133
+ }
5134
+ /** Focus leaving the list while an item is picked up cancels the move. */
5135
+ onFocusOut(event) {
5136
+ if (this.mode() !== 'keyboard')
5137
+ return;
5138
+ const next = event.relatedTarget;
5139
+ if (next && this.host.nativeElement.contains(next))
5140
+ return;
5141
+ this.cancel();
5142
+ }
5143
+ // --- pointer ------------------------------------------------------------------
5144
+ onPointerDown(event, i) {
5145
+ if (this.active() !== null || event.button !== 0)
5146
+ return;
5147
+ const item = this.working()[i];
5148
+ if (this.disabled() || this.isItemDisabled(item))
5149
+ return;
5150
+ event.preventDefault();
5151
+ event.currentTarget.setPointerCapture?.(event.pointerId);
5152
+ event.currentTarget.focus({ preventScroll: true });
5153
+ this.pointerId = event.pointerId;
5154
+ this.startY = event.clientY;
5155
+ this.pointerDy.set(0);
5156
+ this.begin(i, 'pointer');
5157
+ }
5158
+ onPointerMove(event) {
5159
+ if (this.mode() !== 'pointer' || event.pointerId !== this.pointerId)
5160
+ return;
5161
+ const a = this.active();
5162
+ if (a === null)
5163
+ return;
5164
+ const dy = event.clientY - this.startY;
5165
+ this.pointerDy.set(dy);
5166
+ const center = this.rects[a].top + this.rects[a].height / 2 + dy;
5167
+ let t = 0;
5168
+ this.rects.forEach((r, j) => {
5169
+ if (j !== a && r.top + r.height / 2 < center)
5170
+ t++;
5171
+ });
5172
+ this.target.set(t);
5173
+ }
5174
+ onPointerUp(event) {
5175
+ if (this.mode() !== 'pointer' || event.pointerId !== this.pointerId)
5176
+ return;
5177
+ this.commit();
5178
+ }
5179
+ onPointerCancel() {
5180
+ if (this.mode() === 'pointer')
5181
+ this.cancel(false);
5182
+ }
5183
+ // --- lifecycle of a move -------------------------------------------------------
5184
+ begin(i, mode) {
5185
+ this.measure();
5186
+ this.active.set(i);
5187
+ this.target.set(i);
5188
+ this.mode.set(mode);
5189
+ }
5190
+ measure() {
5191
+ const rows = Array.from(this.host.nativeElement.querySelectorAll('li[data-sortable-index]'));
5192
+ this.rects = rows.map((r) => {
5193
+ const b = r.getBoundingClientRect();
5194
+ return { top: b.top, height: b.height };
5195
+ });
5196
+ this.gap =
5197
+ this.rects.length > 1
5198
+ ? Math.max(0, this.rects[1].top - (this.rects[0].top + this.rects[0].height))
5199
+ : 0;
5200
+ }
5201
+ commit() {
5202
+ const from = this.active();
5203
+ const to = this.target();
5204
+ const mode = this.mode();
5205
+ if (from === null || to === null)
5206
+ return this.reset();
5207
+ const item = this.working()[from];
5208
+ if (to === from) {
5209
+ this.reset();
5210
+ this.announce(this.droppedMessage(), to, item);
5211
+ if (mode === 'keyboard')
5212
+ this.focusHandle(to);
5213
+ return;
5214
+ }
5215
+ const next = moveItem(this.working(), from, to);
5216
+ // Swap order and clear the offsets in the same frame, with transitions off, so rows don't jump.
5217
+ this.settling.set(true);
5218
+ this.reset();
5219
+ this.working.set(next);
5220
+ this.items.set(next);
5221
+ this.reorder.emit({ previousIndex: from, currentIndex: to, item, items: next });
5222
+ this.announce(this.droppedMessage(), to, item);
5223
+ this.focusHandle(to);
5224
+ }
5225
+ /** Nothing was moved in the DOM while dragging, so focus is still where it should be. */
5226
+ cancel(announce = true) {
5227
+ const from = this.active();
5228
+ if (from === null)
5229
+ return;
5230
+ this.reset();
5231
+ if (announce)
5232
+ this.announce(this.cancelledMessage(), from);
5233
+ }
5234
+ reset() {
5235
+ this.active.set(null);
5236
+ this.target.set(null);
5237
+ this.mode.set(null);
5238
+ this.pointerDy.set(0);
5239
+ this.pointerId = null;
5240
+ }
5241
+ focusHandle(index) {
5242
+ afterNextRender(() => {
5243
+ this.settling.set(false);
5244
+ this.host.nativeElement
5245
+ .querySelector(`[data-sortable-handle="${index}"]`)
5246
+ ?.focus({ preventScroll: false });
5247
+ }, { injector: this.injector });
5248
+ }
5249
+ announce(template, position, item) {
5250
+ const it = item ?? this.working()[this.active() ?? position];
5251
+ const text = template
5252
+ .replace('{item}', it === undefined ? '' : this.labelOf(it))
5253
+ .replace('{position}', String(position + 1))
5254
+ .replace('{total}', String(this.working().length));
5255
+ // Re-set even when unchanged, so a repeated message is re-announced.
5256
+ this.liveMessage.set('');
5257
+ queueMicrotask(() => this.liveMessage.set(text));
5258
+ }
5259
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSortableList, deps: [], target: i0.ɵɵFactoryTarget.Component });
5260
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsSortableList, isStandalone: true, selector: "sds-sortable-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, dataKey: { classPropertyName: "dataKey", publicName: "dataKey", isSignal: true, isRequired: false, transformFunction: null }, labelField: { classPropertyName: "labelField", publicName: "labelField", isSignal: true, isRequired: false, transformFunction: null }, itemLabel: { classPropertyName: "itemLabel", publicName: "itemLabel", isSignal: true, isRequired: false, transformFunction: null }, itemDisabled: { classPropertyName: "itemDisabled", publicName: "itemDisabled", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, handleLabel: { classPropertyName: "handleLabel", publicName: "handleLabel", isSignal: true, isRequired: false, transformFunction: null }, instructions: { classPropertyName: "instructions", publicName: "instructions", isSignal: true, isRequired: false, transformFunction: null }, pickedUpMessage: { classPropertyName: "pickedUpMessage", publicName: "pickedUpMessage", isSignal: true, isRequired: false, transformFunction: null }, movedMessage: { classPropertyName: "movedMessage", publicName: "movedMessage", isSignal: true, isRequired: false, transformFunction: null }, droppedMessage: { classPropertyName: "droppedMessage", publicName: "droppedMessage", isSignal: true, isRequired: false, transformFunction: null }, cancelledMessage: { classPropertyName: "cancelledMessage", publicName: "cancelledMessage", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { items: "itemsChange", reorder: "reorder" }, host: { classAttribute: "block" }, queries: [{ propertyName: "itemTpl", first: true, predicate: SdsSortableItem, descendants: true, isSignal: true }, { propertyName: "handleTpl", first: true, predicate: SdsSortableHandle, descendants: true, isSignal: true }], ngImport: i0, template: `
5261
+ <ul
5262
+ role="list"
5263
+ [attr.aria-label]="ariaLabel() || null"
5264
+ class="flex flex-col"
5265
+ [class.gap-2]="appearance() === 'card'"
5266
+ [class.divide-y]="appearance() === 'plain'"
5267
+ [class.divide-border]="appearance() === 'plain'"
5268
+ (focusout)="onFocusOut($event)"
5269
+ >
5270
+ @for (item of working(); track trackKey(item, $index); let i = $index) {
5271
+ <li
5272
+ [attr.data-sortable-index]="i"
5273
+ [class]="rowClass(i)"
5274
+ [style.transform]="transformFor(i)"
5275
+ >
5276
+ <button
5277
+ type="button"
5278
+ [class]="handleClass(item)"
5279
+ [attr.aria-label]="handleText(item)"
5280
+ [attr.aria-describedby]="instructionsId"
5281
+ [attr.aria-pressed]="mode() === 'keyboard' && active() === i ? 'true' : 'false'"
5282
+ [attr.data-sortable-handle]="i"
5283
+ [disabled]="disabled() || isItemDisabled(item)"
5284
+ (keydown)="onHandleKeydown($event, i)"
5285
+ (pointerdown)="onPointerDown($event, i)"
5286
+ (pointermove)="onPointerMove($event)"
5287
+ (pointerup)="onPointerUp($event)"
5288
+ (pointercancel)="onPointerCancel()"
5289
+ >
5290
+ @if (handleTpl()) {
5291
+ <ng-container
5292
+ [ngTemplateOutlet]="handleTpl()!.template"
5293
+ [ngTemplateOutletContext]="context(item, i)"
5294
+ />
5295
+ } @else {
5296
+ <sds-icon name="grip-vertical" [size]="18" />
5297
+ }
5298
+ </button>
5299
+
5300
+ <div class="min-w-0 flex-1">
5301
+ @if (itemTpl()) {
5302
+ <ng-container
5303
+ [ngTemplateOutlet]="itemTpl()!.template"
5304
+ [ngTemplateOutletContext]="context(item, i)"
5305
+ />
5306
+ } @else {
5307
+ <span class="text-sm text-ink">{{ labelOf(item) }}</span>
5308
+ }
5309
+ </div>
5310
+ </li>
5311
+ }
5312
+ </ul>
5313
+
5314
+ <p [id]="instructionsId" class="sr-only">{{ instructions() }}</p>
5315
+ <div class="sr-only" aria-live="assertive" aria-atomic="true">{{ liveMessage() }}</div>
5316
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5317
+ }
5318
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSortableList, decorators: [{
5319
+ type: Component,
5320
+ args: [{
5321
+ selector: 'sds-sortable-list',
5322
+ changeDetection: ChangeDetectionStrategy.OnPush,
5323
+ imports: [NgTemplateOutlet, SdsIcon],
5324
+ host: { class: 'block' },
5325
+ template: `
5326
+ <ul
5327
+ role="list"
5328
+ [attr.aria-label]="ariaLabel() || null"
5329
+ class="flex flex-col"
5330
+ [class.gap-2]="appearance() === 'card'"
5331
+ [class.divide-y]="appearance() === 'plain'"
5332
+ [class.divide-border]="appearance() === 'plain'"
5333
+ (focusout)="onFocusOut($event)"
5334
+ >
5335
+ @for (item of working(); track trackKey(item, $index); let i = $index) {
5336
+ <li
5337
+ [attr.data-sortable-index]="i"
5338
+ [class]="rowClass(i)"
5339
+ [style.transform]="transformFor(i)"
5340
+ >
5341
+ <button
5342
+ type="button"
5343
+ [class]="handleClass(item)"
5344
+ [attr.aria-label]="handleText(item)"
5345
+ [attr.aria-describedby]="instructionsId"
5346
+ [attr.aria-pressed]="mode() === 'keyboard' && active() === i ? 'true' : 'false'"
5347
+ [attr.data-sortable-handle]="i"
5348
+ [disabled]="disabled() || isItemDisabled(item)"
5349
+ (keydown)="onHandleKeydown($event, i)"
5350
+ (pointerdown)="onPointerDown($event, i)"
5351
+ (pointermove)="onPointerMove($event)"
5352
+ (pointerup)="onPointerUp($event)"
5353
+ (pointercancel)="onPointerCancel()"
5354
+ >
5355
+ @if (handleTpl()) {
5356
+ <ng-container
5357
+ [ngTemplateOutlet]="handleTpl()!.template"
5358
+ [ngTemplateOutletContext]="context(item, i)"
5359
+ />
5360
+ } @else {
5361
+ <sds-icon name="grip-vertical" [size]="18" />
5362
+ }
5363
+ </button>
5364
+
5365
+ <div class="min-w-0 flex-1">
5366
+ @if (itemTpl()) {
5367
+ <ng-container
5368
+ [ngTemplateOutlet]="itemTpl()!.template"
5369
+ [ngTemplateOutletContext]="context(item, i)"
5370
+ />
5371
+ } @else {
5372
+ <span class="text-sm text-ink">{{ labelOf(item) }}</span>
5373
+ }
5374
+ </div>
5375
+ </li>
5376
+ }
5377
+ </ul>
5378
+
5379
+ <p [id]="instructionsId" class="sr-only">{{ instructions() }}</p>
5380
+ <div class="sr-only" aria-live="assertive" aria-atomic="true">{{ liveMessage() }}</div>
5381
+ `,
5382
+ }]
5383
+ }], 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 }] }], 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 }] }] } });
5384
+
5385
+ let nextId$9 = 0;
5386
+ /** "1.5 MB" style size (decimal units, like file managers show them), for messages. */
5387
+ function sdsFormatBytes(bytes) {
5388
+ if (!Number.isFinite(bytes) || bytes < 0)
5389
+ return '';
5390
+ if (bytes < 1000)
5391
+ return `${bytes} B`;
5392
+ const units = ['kB', 'MB', 'GB'];
5393
+ let v = bytes / 1000;
5394
+ let u = 0;
5395
+ while (v >= 1000 && u < units.length - 1) {
5396
+ v /= 1000;
5397
+ u++;
5398
+ }
5399
+ return `${v >= 10 || Number.isInteger(v) ? Math.round(v) : v.toFixed(1)} ${units[u]}`;
5400
+ }
5401
+ /** Does `file` match an `accept` string (".jpg,image/png,image/*")? Empty accepts everything. */
5402
+ function sdsFileMatchesAccept(file, accept) {
5403
+ const rules = accept
5404
+ .split(',')
5405
+ .map((r) => r.trim().toLowerCase())
5406
+ .filter(Boolean);
5407
+ if (!rules.length)
5408
+ return true;
5409
+ const name = file.name.toLowerCase();
5410
+ const type = (file.type || '').toLowerCase();
5411
+ return rules.some((rule) => {
5412
+ if (rule.startsWith('.'))
5413
+ return name.endsWith(rule);
5414
+ if (rule.endsWith('/*'))
5415
+ return type.startsWith(rule.slice(0, -1));
5416
+ return type === rule;
5417
+ });
5418
+ }
5419
+ /**
5420
+ * Drop zone plus a "Browse" button for picking files. Filters by [accept] (same syntax as the native
5421
+ * input: `.csv`, `image/*`, `image/png`), [maxSize] (bytes) and [maxFiles]; emits the accepted files as
5422
+ * `(files)` and the rest as `(rejected)` with a reason and a readable message, which is also shown
5423
+ * under the zone and announced.
5424
+ *
5425
+ * Keyboard and screen-reader users use the native Browse button (the zone itself is a pointer
5426
+ * affordance); clicking anywhere on the zone opens the picker too. All text is set through inputs with
5427
+ * English defaults.
5428
+ *
5429
+ * @example
5430
+ * <sds-file-drop accept="image/jpeg,image/png,image/webp" multiple [maxSize]="10_000_000"
5431
+ * label="Drop photos here" browseLabel="Choose photos"
5432
+ * (files)="upload($event)" (rejected)="onRejected($event)" />
5433
+ */
5434
+ class SdsFileDrop {
5435
+ zoneId = `sds-file-drop-${nextId$9++}`;
5436
+ fileInput = viewChild('fileInput', ...(ngDevMode ? [{ debugName: "fileInput" }] : []));
5437
+ /** Accepted types, native `accept` syntax. Empty = anything. */
5438
+ accept = input('', ...(ngDevMode ? [{ debugName: "accept" }] : []));
5439
+ multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
5440
+ /** Largest accepted file in bytes. Null = no limit. */
5441
+ maxSize = input(null, ...(ngDevMode ? [{ debugName: "maxSize" }] : []));
5442
+ /** Most files accepted in one pick/drop. Null = no limit (1 when not [multiple]). */
5443
+ maxFiles = input(null, ...(ngDevMode ? [{ debugName: "maxFiles" }] : []));
5444
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
5445
+ /** Icon in the zone at rest. */
5446
+ icon = input('upload', ...(ngDevMode ? [{ debugName: "icon" }] : []));
5447
+ label = input('Drag and drop files here', ...(ngDevMode ? [{ debugName: "label" }] : []));
5448
+ /** Shown while files are dragged over the zone. */
5449
+ dropLabel = input('Drop to add', ...(ngDevMode ? [{ debugName: "dropLabel" }] : []));
5450
+ browseLabel = input('Browse files', ...(ngDevMode ? [{ debugName: "browseLabel" }] : []));
5451
+ /** Extra line under the label, e.g. "JPEG or PNG, up to 10 MB". */
5452
+ hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
5453
+ /** Tokens: `{name}`. */
5454
+ wrongTypeMessage = input('{name}: this file type is not accepted.', ...(ngDevMode ? [{ debugName: "wrongTypeMessage" }] : []));
5455
+ /** Tokens: `{name}`, `{max}` (formatted size). */
5456
+ tooLargeMessage = input('{name}: larger than {max}.', ...(ngDevMode ? [{ debugName: "tooLargeMessage" }] : []));
5457
+ /** Tokens: `{name}`, `{max}` (file count). */
5458
+ tooManyMessage = input('{name}: only {max} file(s) can be added at once.', ...(ngDevMode ? [{ debugName: "tooManyMessage" }] : []));
5459
+ /** Files that passed every check, in pick/drop order. */
5460
+ files = output();
5461
+ /** Files that were turned away, with the reason. */
5462
+ rejected = output();
5463
+ dragging = signal(false, ...(ngDevMode ? [{ debugName: "dragging" }] : []));
5464
+ errors = signal([], ...(ngDevMode ? [{ debugName: "errors" }] : []));
5465
+ dragDepth = 0;
5466
+ describedBy = computed(() => {
5467
+ const ids = [`${this.zoneId}-label`];
5468
+ if (this.hint())
5469
+ ids.push(`${this.zoneId}-hint`);
5470
+ if (this.errors().length)
5471
+ ids.push(`${this.zoneId}-errors`);
5472
+ return ids.join(' ');
5473
+ }, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
5474
+ zoneClass = computed(() => {
5475
+ const base = 'flex flex-col items-center justify-center gap-2 rounded-card border-2 border-dashed px-6 py-8 ' +
5476
+ 'text-center transition-colors';
5477
+ if (this.disabled())
5478
+ return `${base} cursor-not-allowed border-border bg-muted opacity-60`;
5479
+ return this.dragging()
5480
+ ? `${base} cursor-copy border-brand-500 bg-brand-50 dark:bg-muted`
5481
+ : `${base} cursor-pointer border-border bg-muted/50 hover:border-ink-faint`;
5482
+ }, ...(ngDevMode ? [{ debugName: "zoneClass" }] : []));
5483
+ /** Open the native file picker. */
5484
+ browse(event) {
5485
+ event?.stopPropagation();
5486
+ if (this.disabled())
5487
+ return;
5488
+ this.fileInput()?.nativeElement.click();
5489
+ }
5490
+ onZoneClick(event) {
5491
+ if (event.target.closest('button, input'))
5492
+ return;
5493
+ this.browse();
5494
+ }
5495
+ onDragEnter(event) {
5496
+ if (this.disabled() || !this.hasFiles(event))
5497
+ return;
5498
+ event.preventDefault();
5499
+ this.dragDepth++;
5500
+ this.dragging.set(true);
5501
+ }
5502
+ onDragOver(event) {
5503
+ if (this.disabled() || !this.hasFiles(event))
5504
+ return;
5505
+ event.preventDefault();
5506
+ if (event.dataTransfer)
5507
+ event.dataTransfer.dropEffect = 'copy';
5508
+ }
5509
+ onDragLeave(event) {
5510
+ if (this.disabled() || !this.hasFiles(event))
5511
+ return;
5512
+ this.dragDepth = Math.max(0, this.dragDepth - 1);
5513
+ if (this.dragDepth === 0)
5514
+ this.dragging.set(false);
5515
+ }
5516
+ onDrop(event) {
5517
+ if (this.disabled())
5518
+ return;
5519
+ event.preventDefault();
5520
+ this.dragDepth = 0;
5521
+ this.dragging.set(false);
5522
+ this.handle(Array.from(event.dataTransfer?.files ?? []));
5523
+ }
5524
+ onInputChange(event) {
5525
+ const el = event.target;
5526
+ this.handle(Array.from(el.files ?? []));
5527
+ // Reset so picking the same file again still fires `change`.
5528
+ el.value = '';
5529
+ }
5530
+ hasFiles(event) {
5531
+ return Array.from(event.dataTransfer?.types ?? []).includes('Files');
5532
+ }
5533
+ handle(list) {
5534
+ if (!list.length)
5535
+ return;
5536
+ const accepted = [];
5537
+ const rejected = [];
5538
+ const max = this.maxFiles() ?? (this.multiple() ? null : 1);
5539
+ const maxSize = this.maxSize();
5540
+ for (const file of list) {
5541
+ if (!sdsFileMatchesAccept(file, this.accept())) {
5542
+ rejected.push({ file, reason: 'type', message: this.render(this.wrongTypeMessage(), file) });
5543
+ }
5544
+ else if (maxSize !== null && file.size > maxSize) {
5545
+ rejected.push({
5546
+ file,
5547
+ reason: 'size',
5548
+ message: this.render(this.tooLargeMessage(), file, sdsFormatBytes(maxSize)),
5549
+ });
5550
+ }
5551
+ else if (max !== null && accepted.length >= max) {
5552
+ rejected.push({
5553
+ file,
5554
+ reason: 'count',
5555
+ message: this.render(this.tooManyMessage(), file, String(max)),
5556
+ });
5557
+ }
5558
+ else {
5559
+ accepted.push(file);
5560
+ }
5561
+ }
5562
+ this.errors.set(rejected.map((r) => r.message));
5563
+ if (accepted.length)
5564
+ this.files.emit(accepted);
5565
+ if (rejected.length)
5566
+ this.rejected.emit(rejected);
5567
+ }
5568
+ render(template, file, max = '') {
5569
+ return template.replace('{name}', file.name).replace('{max}', max);
5570
+ }
5571
+ /** Clear the error messages under the zone. */
5572
+ clearErrors() {
5573
+ this.errors.set([]);
5574
+ }
5575
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsFileDrop, deps: [], target: i0.ɵɵFactoryTarget.Component });
5576
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsFileDrop, isStandalone: true, selector: "sds-file-drop", inputs: { accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, dropLabel: { classPropertyName: "dropLabel", publicName: "dropLabel", isSignal: true, isRequired: false, transformFunction: null }, browseLabel: { classPropertyName: "browseLabel", publicName: "browseLabel", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, wrongTypeMessage: { classPropertyName: "wrongTypeMessage", publicName: "wrongTypeMessage", isSignal: true, isRequired: false, transformFunction: null }, tooLargeMessage: { classPropertyName: "tooLargeMessage", publicName: "tooLargeMessage", isSignal: true, isRequired: false, transformFunction: null }, tooManyMessage: { classPropertyName: "tooManyMessage", publicName: "tooManyMessage", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { files: "files", rejected: "rejected" }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
5577
+ <div
5578
+ [class]="zoneClass()"
5579
+ [attr.aria-disabled]="disabled() ? 'true' : null"
5580
+ [attr.data-dragging]="dragging() ? 'true' : null"
5581
+ (click)="onZoneClick($event)"
5582
+ (dragenter)="onDragEnter($event)"
5583
+ (dragover)="onDragOver($event)"
5584
+ (dragleave)="onDragLeave($event)"
5585
+ (drop)="onDrop($event)"
5586
+ >
5587
+ <span
5588
+ class="grid h-11 w-11 place-items-center rounded-full bg-surface text-ink-soft shadow-sm ring-1 ring-border"
5589
+ aria-hidden="true"
5590
+ >
5591
+ <sds-icon [name]="dragging() ? 'upload' : icon()" [size]="22" />
5592
+ </span>
5593
+ <p class="text-sm font-semibold text-ink" [id]="zoneId + '-label'">
5594
+ {{ dragging() ? dropLabel() : label() }}
5595
+ </p>
5596
+ @if (hint()) {
5597
+ <p class="text-xs text-ink-faint" [id]="zoneId + '-hint'">{{ hint() }}</p>
5598
+ }
5599
+ <button
5600
+ type="button"
5601
+ class="mt-1 inline-flex h-9 items-center gap-1.5 rounded-control border border-border bg-surface px-3.5 text-sm font-semibold text-ink shadow-sm transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:cursor-not-allowed disabled:opacity-60"
5602
+ [disabled]="disabled()"
5603
+ [attr.aria-describedby]="describedBy()"
5604
+ (click)="browse($event)"
5605
+ >
5606
+ <sds-icon name="upload" [size]="16" />
5607
+ {{ browseLabel() }}
5608
+ </button>
5609
+ <input
5610
+ #fileInput
5611
+ type="file"
5612
+ class="sr-only"
5613
+ tabindex="-1"
5614
+ aria-hidden="true"
5615
+ [accept]="accept()"
5616
+ [multiple]="multiple()"
5617
+ [disabled]="disabled()"
5618
+ (change)="onInputChange($event)"
5619
+ />
5620
+ </div>
5621
+
5622
+ <div aria-live="polite" aria-atomic="true">
5623
+ @if (errors().length) {
5624
+ <ul [id]="zoneId + '-errors'" class="mt-2 space-y-1">
5625
+ @for (err of errors(); track $index) {
5626
+ <li class="flex items-start gap-1.5 text-xs font-medium text-danger">
5627
+ <sds-icon name="alert-circle" [size]="14" class="mt-px shrink-0" />
5628
+ <span>{{ err }}</span>
5629
+ </li>
5630
+ }
5631
+ </ul>
5632
+ }
5633
+ </div>
5634
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5635
+ }
5636
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsFileDrop, decorators: [{
5637
+ type: Component,
5638
+ args: [{
5639
+ selector: 'sds-file-drop',
5640
+ changeDetection: ChangeDetectionStrategy.OnPush,
5641
+ imports: [SdsIcon],
5642
+ host: { class: 'block' },
5643
+ template: `
5644
+ <div
5645
+ [class]="zoneClass()"
5646
+ [attr.aria-disabled]="disabled() ? 'true' : null"
5647
+ [attr.data-dragging]="dragging() ? 'true' : null"
5648
+ (click)="onZoneClick($event)"
5649
+ (dragenter)="onDragEnter($event)"
5650
+ (dragover)="onDragOver($event)"
5651
+ (dragleave)="onDragLeave($event)"
5652
+ (drop)="onDrop($event)"
5653
+ >
5654
+ <span
5655
+ class="grid h-11 w-11 place-items-center rounded-full bg-surface text-ink-soft shadow-sm ring-1 ring-border"
5656
+ aria-hidden="true"
5657
+ >
5658
+ <sds-icon [name]="dragging() ? 'upload' : icon()" [size]="22" />
5659
+ </span>
5660
+ <p class="text-sm font-semibold text-ink" [id]="zoneId + '-label'">
5661
+ {{ dragging() ? dropLabel() : label() }}
5662
+ </p>
5663
+ @if (hint()) {
5664
+ <p class="text-xs text-ink-faint" [id]="zoneId + '-hint'">{{ hint() }}</p>
5665
+ }
5666
+ <button
5667
+ type="button"
5668
+ class="mt-1 inline-flex h-9 items-center gap-1.5 rounded-control border border-border bg-surface px-3.5 text-sm font-semibold text-ink shadow-sm transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:cursor-not-allowed disabled:opacity-60"
5669
+ [disabled]="disabled()"
5670
+ [attr.aria-describedby]="describedBy()"
5671
+ (click)="browse($event)"
5672
+ >
5673
+ <sds-icon name="upload" [size]="16" />
5674
+ {{ browseLabel() }}
5675
+ </button>
5676
+ <input
5677
+ #fileInput
5678
+ type="file"
5679
+ class="sr-only"
5680
+ tabindex="-1"
5681
+ aria-hidden="true"
5682
+ [accept]="accept()"
5683
+ [multiple]="multiple()"
5684
+ [disabled]="disabled()"
5685
+ (change)="onInputChange($event)"
5686
+ />
5687
+ </div>
5688
+
5689
+ <div aria-live="polite" aria-atomic="true">
5690
+ @if (errors().length) {
5691
+ <ul [id]="zoneId + '-errors'" class="mt-2 space-y-1">
5692
+ @for (err of errors(); track $index) {
5693
+ <li class="flex items-start gap-1.5 text-xs font-medium text-danger">
5694
+ <sds-icon name="alert-circle" [size]="14" class="mt-px shrink-0" />
5695
+ <span>{{ err }}</span>
5696
+ </li>
5697
+ }
5698
+ </ul>
5699
+ }
5700
+ </div>
5701
+ `,
5702
+ }]
5703
+ }], 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 }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", 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 }] }], 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"] }] } });
5704
+
5705
+ let nextId$8 = 0;
5706
+ /**
5707
+ * Crops an image to a FIXED aspect ratio (e.g. the shop's card ratio): the frame is fixed and the image
5708
+ * moves and zooms behind it, always covering the frame, so every crop is valid.
5709
+ *
5710
+ * - Pointer: drag to move; wheel or pinch to zoom around the pointer; a zoom slider and ± buttons.
5711
+ * - Keyboard (focus the frame): arrow keys nudge the image (Shift = 10×), `+`/`-` zoom, `0` resets.
5712
+ * - `(cropChange)` emits the crop in the source image's natural pixels after every change;
5713
+ * {@link toBlob} renders it through a canvas (optionally resized) for upload.
5714
+ *
5715
+ * `src` takes a URL or a Blob/File (an object URL is managed for you). For a cross-origin URL the server
5716
+ * must send CORS headers and [crossOrigin] must be set, or the canvas is tainted and toBlob() rejects.
5717
+ *
5718
+ * @example
5719
+ * <sds-image-cropper [src]="file" [aspectRatio]="4 / 3" (cropChange)="crop = $event" #cropper />
5720
+ * <sds-button (click)="upload(await cropper.toBlob({ width: 1600 }))">Save</sds-button>
5721
+ */
5722
+ class SdsImageCropper {
5723
+ helpId = `sds-image-cropper-${nextId$8++}-help`;
5724
+ viewport = viewChild('viewport', ...(ngDevMode ? [{ debugName: "viewport" }] : []));
5725
+ /** Image to crop: a URL, or a Blob/File. */
5726
+ src = input(null, ...(ngDevMode ? [{ debugName: "src" }] : []));
5727
+ /** Frame width ÷ height, e.g. `1`, `4 / 3`, `16 / 9`. */
5728
+ aspectRatio = input(1, ...(ngDevMode ? [{ debugName: "aspectRatio", transform: numberAttribute }] : [{ transform: numberAttribute }]));
5729
+ /** Largest zoom factor relative to "cover". */
5730
+ maxZoom = input(4, ...(ngDevMode ? [{ debugName: "maxZoom", transform: numberAttribute }] : [{ transform: numberAttribute }]));
5731
+ /** Zoom change per button press / key press. */
5732
+ zoomStep = input(0.1, ...(ngDevMode ? [{ debugName: "zoomStep", transform: numberAttribute }] : [{ transform: numberAttribute }]));
5733
+ /** Visual mask only (the output is always the rectangle). */
5734
+ shape = input('rect', ...(ngDevMode ? [{ debugName: "shape" }] : []));
5735
+ /** `crossorigin` for URL sources, needed for toBlob() on a cross-origin image. */
5736
+ crossOrigin = input(null, ...(ngDevMode ? [{ debugName: "crossOrigin" }] : []));
5737
+ label = input('Crop area', ...(ngDevMode ? [{ debugName: "label" }] : []));
5738
+ roleDescription = input('image cropper', ...(ngDevMode ? [{ debugName: "roleDescription" }] : []));
5739
+ instructions = input('Drag, or use the arrow keys, to move the image. Use plus and minus to zoom, 0 to reset.', ...(ngDevMode ? [{ debugName: "instructions" }] : []));
5740
+ zoomLabel = input('Zoom', ...(ngDevMode ? [{ debugName: "zoomLabel" }] : []));
5741
+ zoomInLabel = input('Zoom in', ...(ngDevMode ? [{ debugName: "zoomInLabel" }] : []));
5742
+ zoomOutLabel = input('Zoom out', ...(ngDevMode ? [{ debugName: "zoomOutLabel" }] : []));
5743
+ /** The crop, in natural pixels, after every change. */
5744
+ cropChange = output();
5745
+ /** The image loaded; natural size. */
5746
+ imageLoad = output();
5747
+ /** The image failed to load. */
5748
+ imageError = output();
5749
+ objectUrl = signal(null, ...(ngDevMode ? [{ debugName: "objectUrl" }] : []));
5750
+ ownedUrl = null;
5751
+ imgEl = null;
5752
+ loaded = signal(false, ...(ngDevMode ? [{ debugName: "loaded" }] : []));
5753
+ naturalW = signal(0, ...(ngDevMode ? [{ debugName: "naturalW" }] : []));
5754
+ naturalH = signal(0, ...(ngDevMode ? [{ debugName: "naturalH" }] : []));
5755
+ /** Frame size in CSS px (tracked with a ResizeObserver). */
5756
+ frameW = signal(0, ...(ngDevMode ? [{ debugName: "frameW" }] : []));
5757
+ frameH = signal(0, ...(ngDevMode ? [{ debugName: "frameH" }] : []));
5758
+ zoom = signal(1, ...(ngDevMode ? [{ debugName: "zoom" }] : []));
5759
+ tx = signal(0, ...(ngDevMode ? [{ debugName: "tx" }] : []));
5760
+ ty = signal(0, ...(ngDevMode ? [{ debugName: "ty" }] : []));
5761
+ dragging = signal(false, ...(ngDevMode ? [{ debugName: "dragging" }] : []));
5762
+ /** Scale that makes the image just cover the frame (zoom 1). */
5763
+ coverScale = computed(() => {
5764
+ const w = this.naturalW();
5765
+ const h = this.naturalH();
5766
+ if (!w || !h || !this.frameW() || !this.frameH())
5767
+ return 1;
5768
+ return Math.max(this.frameW() / w, this.frameH() / h);
5769
+ }, ...(ngDevMode ? [{ debugName: "coverScale" }] : []));
5770
+ scale = computed(() => this.coverScale() * this.zoom(), ...(ngDevMode ? [{ debugName: "scale" }] : []));
5771
+ displayW = computed(() => this.naturalW() * this.scale(), ...(ngDevMode ? [{ debugName: "displayW" }] : []));
5772
+ displayH = computed(() => this.naturalH() * this.scale(), ...(ngDevMode ? [{ debugName: "displayH" }] : []));
5773
+ zoomText = computed(() => `${Math.round(this.zoom() * 100)}%`, ...(ngDevMode ? [{ debugName: "zoomText" }] : []));
5774
+ /** The current crop in natural pixels. */
5775
+ crop = computed(() => {
5776
+ const s = this.scale();
5777
+ if (!this.loaded() || !s)
5778
+ return { x: 0, y: 0, width: 0, height: 0 };
5779
+ const x = -this.tx() / s;
5780
+ const y = -this.ty() / s;
5781
+ const width = Math.min(this.naturalW(), this.frameW() / s);
5782
+ const height = Math.min(this.naturalH(), this.frameH() / s);
5783
+ return {
5784
+ x: Math.max(0, Math.round(x)),
5785
+ y: Math.max(0, Math.round(y)),
5786
+ width: Math.round(width),
5787
+ height: Math.round(height),
5788
+ };
5789
+ }, ...(ngDevMode ? [{ debugName: "crop" }] : []));
5790
+ resizeObserver = null;
5791
+ pointers = new Map();
5792
+ lastPinch = 0;
5793
+ constructor() {
5794
+ // Source → object URL (for Blobs), reset state.
5795
+ effect(() => {
5796
+ const src = this.src();
5797
+ untracked(() => {
5798
+ this.revoke();
5799
+ this.loaded.set(false);
5800
+ if (src instanceof Blob) {
5801
+ this.ownedUrl = URL.createObjectURL(src);
5802
+ this.objectUrl.set(this.ownedUrl);
5803
+ }
5804
+ else {
5805
+ this.objectUrl.set(src || null);
5806
+ }
5807
+ });
5808
+ });
5809
+ // Track the frame size; keep the image covering it (and the crop centre stable) on resize.
5810
+ effect((onCleanup) => {
5811
+ const el = this.viewport()?.nativeElement;
5812
+ if (!el || typeof ResizeObserver === 'undefined')
5813
+ return;
5814
+ const ro = new ResizeObserver(() => this.measureFrame());
5815
+ ro.observe(el);
5816
+ this.resizeObserver = ro;
5817
+ onCleanup(() => ro.disconnect());
5818
+ });
5819
+ }
5820
+ ngOnDestroy() {
5821
+ this.resizeObserver?.disconnect();
5822
+ this.revoke();
5823
+ }
5824
+ // --- loading -------------------------------------------------------------------
5825
+ onLoad(event) {
5826
+ const img = event.target;
5827
+ this.imgEl = img;
5828
+ this.naturalW.set(img.naturalWidth);
5829
+ this.naturalH.set(img.naturalHeight);
5830
+ this.measureFrame();
5831
+ this.loaded.set(true);
5832
+ this.reset();
5833
+ this.imageLoad.emit({ width: img.naturalWidth, height: img.naturalHeight });
5834
+ }
5835
+ onError() {
5836
+ this.loaded.set(false);
5837
+ this.imageError.emit();
5838
+ }
5839
+ revoke() {
5840
+ if (this.ownedUrl)
5841
+ URL.revokeObjectURL(this.ownedUrl);
5842
+ this.ownedUrl = null;
5843
+ }
5844
+ measureFrame() {
5845
+ const el = this.viewport()?.nativeElement;
5846
+ if (!el)
5847
+ return;
5848
+ const prevW = this.frameW();
5849
+ const w = el.clientWidth;
5850
+ const h = el.clientHeight || w / (this.aspectRatio() || 1);
5851
+ if (w === prevW && h === this.frameH())
5852
+ return;
5853
+ // Keep the same point of the image under the frame centre across a resize.
5854
+ const s = this.scale();
5855
+ const cx = s ? (this.frameW() / 2 - this.tx()) / s : 0;
5856
+ const cy = s ? (this.frameH() / 2 - this.ty()) / s : 0;
5857
+ this.frameW.set(w);
5858
+ this.frameH.set(h);
5859
+ if (this.loaded() && prevW) {
5860
+ const ns = this.scale();
5861
+ this.setOffset(w / 2 - cx * ns, h / 2 - cy * ns);
5862
+ this.emit();
5863
+ }
5864
+ }
5865
+ // --- geometry ------------------------------------------------------------------
5866
+ /** Centre the image at zoom 1. */
5867
+ reset() {
5868
+ this.zoom.set(1);
5869
+ this.setOffset((this.frameW() - this.displayW()) / 2, (this.frameH() - this.displayH()) / 2);
5870
+ this.emit();
5871
+ }
5872
+ setOffset(x, y) {
5873
+ const minX = Math.min(0, this.frameW() - this.displayW());
5874
+ const minY = Math.min(0, this.frameH() - this.displayH());
5875
+ this.tx.set(Math.min(0, Math.max(minX, x)));
5876
+ this.ty.set(Math.min(0, Math.max(minY, y)));
5877
+ }
5878
+ /** Zoom to `next`, keeping the frame point (px, py) fixed (defaults to the centre). */
5879
+ zoomTo(next, px = this.frameW() / 2, py = this.frameH() / 2) {
5880
+ const z = Math.min(this.maxZoom(), Math.max(1, next));
5881
+ const s = this.scale();
5882
+ const ix = (px - this.tx()) / s;
5883
+ const iy = (py - this.ty()) / s;
5884
+ this.zoom.set(z);
5885
+ const ns = this.scale();
5886
+ this.setOffset(px - ix * ns, py - iy * ns);
5887
+ this.emit();
5888
+ }
5889
+ /** Zoom in (+) or out (−) by `delta`. */
5890
+ zoomBy(delta) {
5891
+ if (!this.loaded())
5892
+ return;
5893
+ this.zoomTo(this.zoom() + delta);
5894
+ }
5895
+ emit() {
5896
+ if (this.loaded())
5897
+ this.cropChange.emit(this.crop());
5898
+ }
5899
+ // --- pointer ---------------------------------------------------------------------
5900
+ onPointerDown(event) {
5901
+ if (!this.loaded())
5902
+ return;
5903
+ event.currentTarget.setPointerCapture?.(event.pointerId);
5904
+ this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
5905
+ this.dragging.set(true);
5906
+ if (this.pointers.size === 2)
5907
+ this.lastPinch = this.pinchDistance();
5908
+ }
5909
+ onPointerMove(event) {
5910
+ const prev = this.pointers.get(event.pointerId);
5911
+ if (!prev)
5912
+ return;
5913
+ const cur = { x: event.clientX, y: event.clientY };
5914
+ this.pointers.set(event.pointerId, cur);
5915
+ if (this.pointers.size >= 2) {
5916
+ const d = this.pinchDistance();
5917
+ if (this.lastPinch > 0) {
5918
+ const rect = this.viewport().nativeElement.getBoundingClientRect();
5919
+ const pts = [...this.pointers.values()];
5920
+ const mx = (pts[0].x + pts[1].x) / 2 - rect.left;
5921
+ const my = (pts[0].y + pts[1].y) / 2 - rect.top;
5922
+ this.zoomTo(this.zoom() * (d / this.lastPinch), mx, my);
5923
+ }
5924
+ this.lastPinch = d;
5925
+ return;
5926
+ }
5927
+ this.setOffset(this.tx() + (cur.x - prev.x), this.ty() + (cur.y - prev.y));
5928
+ }
5929
+ onPointerUp(event) {
5930
+ if (!this.pointers.delete(event.pointerId))
5931
+ return;
5932
+ if (this.pointers.size < 2)
5933
+ this.lastPinch = 0;
5934
+ if (this.pointers.size === 0) {
5935
+ this.dragging.set(false);
5936
+ this.emit();
5937
+ }
5938
+ }
5939
+ pinchDistance() {
5940
+ const [a, b] = [...this.pointers.values()];
5941
+ return a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0;
5942
+ }
5943
+ onWheel(event) {
5944
+ if (!this.loaded())
5945
+ return;
5946
+ event.preventDefault();
5947
+ const rect = this.viewport().nativeElement.getBoundingClientRect();
5948
+ const factor = Math.exp(-event.deltaY * 0.0015);
5949
+ this.zoomTo(this.zoom() * factor, event.clientX - rect.left, event.clientY - rect.top);
5950
+ }
5951
+ onSlider(event) {
5952
+ this.zoomTo(Number(event.target.value));
5953
+ }
5954
+ // --- keyboard ---------------------------------------------------------------------
5955
+ onKeydown(event) {
5956
+ if (!this.loaded())
5957
+ return;
5958
+ const step = event.shiftKey ? 10 : 1;
5959
+ let handled = true;
5960
+ switch (event.key) {
5961
+ // Arrow keys move the IMAGE in that direction under the fixed frame.
5962
+ case 'ArrowLeft':
5963
+ this.setOffset(this.tx() - step, this.ty());
5964
+ break;
5965
+ case 'ArrowRight':
5966
+ this.setOffset(this.tx() + step, this.ty());
5967
+ break;
5968
+ case 'ArrowUp':
5969
+ this.setOffset(this.tx(), this.ty() - step);
5970
+ break;
5971
+ case 'ArrowDown':
5972
+ this.setOffset(this.tx(), this.ty() + step);
5973
+ break;
5974
+ case '+':
5975
+ case '=':
5976
+ this.zoomBy(this.zoomStep());
5977
+ return event.preventDefault();
5978
+ case '-':
5979
+ case '_':
5980
+ this.zoomBy(-this.zoomStep());
5981
+ return event.preventDefault();
5982
+ case '0':
5983
+ this.reset();
5984
+ return event.preventDefault();
5985
+ default:
5986
+ handled = false;
5987
+ }
5988
+ if (handled) {
5989
+ event.preventDefault();
5990
+ this.emit();
5991
+ }
5992
+ }
5993
+ // --- output -------------------------------------------------------------------------
5994
+ /**
5995
+ * Render the current crop to a canvas (natural resolution, or scaled to `width`). Returns null
5996
+ * before an image has loaded.
5997
+ */
5998
+ toCanvas(width) {
5999
+ const img = this.imgEl;
6000
+ const c = this.crop();
6001
+ if (!img || !this.loaded() || !c.width || !c.height)
6002
+ return null;
6003
+ const outW = Math.max(1, Math.round(width ?? c.width));
6004
+ const outH = Math.max(1, Math.round(outW / (this.aspectRatio() || c.width / c.height)));
6005
+ const canvas = document.createElement('canvas');
6006
+ canvas.width = outW;
6007
+ canvas.height = outH;
6008
+ const ctx = canvas.getContext('2d');
6009
+ if (!ctx)
6010
+ return null;
6011
+ ctx.imageSmoothingQuality = 'high';
6012
+ ctx.drawImage(img, c.x, c.y, c.width, c.height, 0, 0, outW, outH);
6013
+ return canvas;
6014
+ }
6015
+ /** Encode the current crop as a Blob (JPEG 0.9 by default). */
6016
+ toBlob(options = {}) {
6017
+ const canvas = this.toCanvas(options.width);
6018
+ if (!canvas)
6019
+ return Promise.resolve(null);
6020
+ return new Promise((resolve, reject) => {
6021
+ try {
6022
+ canvas.toBlob((b) => resolve(b), options.type ?? 'image/jpeg', options.quality ?? 0.9);
6023
+ }
6024
+ catch (e) {
6025
+ reject(e);
6026
+ }
6027
+ });
6028
+ }
6029
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsImageCropper, deps: [], target: i0.ɵɵFactoryTarget.Component });
6030
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsImageCropper, isStandalone: true, selector: "sds-image-cropper", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, maxZoom: { classPropertyName: "maxZoom", publicName: "maxZoom", isSignal: true, isRequired: false, transformFunction: null }, zoomStep: { classPropertyName: "zoomStep", publicName: "zoomStep", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, crossOrigin: { classPropertyName: "crossOrigin", publicName: "crossOrigin", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, roleDescription: { classPropertyName: "roleDescription", publicName: "roleDescription", isSignal: true, isRequired: false, transformFunction: null }, instructions: { classPropertyName: "instructions", publicName: "instructions", isSignal: true, isRequired: false, transformFunction: null }, zoomLabel: { classPropertyName: "zoomLabel", publicName: "zoomLabel", isSignal: true, isRequired: false, transformFunction: null }, zoomInLabel: { classPropertyName: "zoomInLabel", publicName: "zoomInLabel", isSignal: true, isRequired: false, transformFunction: null }, zoomOutLabel: { classPropertyName: "zoomOutLabel", publicName: "zoomOutLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cropChange: "cropChange", imageLoad: "imageLoad", imageError: "imageError" }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "viewport", first: true, predicate: ["viewport"], descendants: true, isSignal: true }], ngImport: i0, template: `
6031
+ <div
6032
+ #viewport
6033
+ class="relative w-full touch-none select-none overflow-hidden rounded-card bg-ink/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 focus-visible:ring-offset-surface"
6034
+ [class.cursor-grab]="loaded() && !dragging()"
6035
+ [class.cursor-grabbing]="dragging()"
6036
+ [style.aspect-ratio]="aspectRatio()"
6037
+ role="application"
6038
+ [attr.aria-roledescription]="roleDescription()"
6039
+ [attr.aria-label]="label()"
6040
+ [attr.aria-describedby]="helpId"
6041
+ tabindex="0"
6042
+ (pointerdown)="onPointerDown($event)"
6043
+ (pointermove)="onPointerMove($event)"
6044
+ (pointerup)="onPointerUp($event)"
6045
+ (pointercancel)="onPointerUp($event)"
6046
+ (wheel)="onWheel($event)"
6047
+ (keydown)="onKeydown($event)"
6048
+ >
6049
+ @if (objectUrl()) {
6050
+ <img
6051
+ [src]="objectUrl()"
6052
+ [attr.crossorigin]="crossOrigin()"
6053
+ alt=""
6054
+ draggable="false"
6055
+ class="pointer-events-none absolute left-0 top-0 max-w-none origin-top-left"
6056
+ [class.invisible]="!loaded()"
6057
+ [style.width.px]="displayW()"
6058
+ [style.height.px]="displayH()"
6059
+ [style.transform]="'translate3d(' + tx() + 'px,' + ty() + 'px,0)'"
6060
+ (load)="onLoad($event)"
6061
+ (error)="onError()"
6062
+ />
6063
+ }
6064
+ @if (shape() === 'round') {
6065
+ <div
6066
+ class="pointer-events-none absolute inset-0 rounded-full shadow-[0_0_0_9999px_rgba(0,0,0,0.45)]"
6067
+ aria-hidden="true"
6068
+ ></div>
6069
+ }
6070
+ <!-- Rule-of-thirds guides. -->
6071
+ <div class="pointer-events-none absolute inset-0" aria-hidden="true">
6072
+ <div class="absolute inset-y-0 left-1/3 w-px bg-white/30"></div>
6073
+ <div class="absolute inset-y-0 left-2/3 w-px bg-white/30"></div>
6074
+ <div class="absolute inset-x-0 top-1/3 h-px bg-white/30"></div>
6075
+ <div class="absolute inset-x-0 top-2/3 h-px bg-white/30"></div>
6076
+ </div>
6077
+ @if (!loaded()) {
6078
+ <div class="absolute inset-0 grid place-items-center text-white/70" aria-hidden="true">
6079
+ <sds-icon name="image" [size]="32" />
6080
+ </div>
6081
+ }
6082
+ </div>
6083
+ <p [id]="helpId" class="sr-only">{{ instructions() }}</p>
6084
+
6085
+ <div class="mt-3 flex items-center gap-2">
6086
+ <button
6087
+ type="button"
6088
+ class="grid h-9 w-9 shrink-0 place-items-center rounded-control text-ink-soft hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-40"
6089
+ [attr.aria-label]="zoomOutLabel()"
6090
+ [disabled]="!loaded() || zoom() <= 1"
6091
+ (click)="zoomBy(-zoomStep())"
3323
6092
  >
3324
- @switch (value()) {
3325
- @case ('contains') {
3326
- <sds-icon name="check" [size]="size() === 'sm' ? 14 : 18" [strokeWidth]="3" />
3327
- }
3328
- @case ('may-contain') {
3329
- <sds-icon name="circle-help" [size]="size() === 'sm' ? 15 : 19" />
3330
- }
3331
- }
3332
- </span>
3333
- </button>
3334
-
3335
- @if (hint()) {
3336
- <span [id]="chipId + '-hint'" class="sr-only">{{ hint() }}</span>
3337
- }
3338
- <span class="sr-only" aria-live="polite" aria-atomic="true">{{ announcement() }}</span>
6093
+ <sds-icon name="zoom-out" [size]="18" />
6094
+ </button>
6095
+ <input
6096
+ type="range"
6097
+ class="h-2 min-w-0 flex-1 cursor-pointer accent-brand-600"
6098
+ min="1"
6099
+ [max]="maxZoom()"
6100
+ step="0.01"
6101
+ [value]="zoom()"
6102
+ [disabled]="!loaded()"
6103
+ [attr.aria-label]="zoomLabel()"
6104
+ [attr.aria-valuetext]="zoomText()"
6105
+ (input)="onSlider($event)"
6106
+ />
6107
+ <button
6108
+ type="button"
6109
+ class="grid h-9 w-9 shrink-0 place-items-center rounded-control text-ink-soft hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-40"
6110
+ [attr.aria-label]="zoomInLabel()"
6111
+ [disabled]="!loaded() || zoom() >= maxZoom()"
6112
+ (click)="zoomBy(zoomStep())"
6113
+ >
6114
+ <sds-icon name="zoom-in" [size]="18" />
6115
+ </button>
6116
+ </div>
3339
6117
  `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3340
6118
  }
3341
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTriStateChip, decorators: [{
6119
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsImageCropper, decorators: [{
3342
6120
  type: Component,
3343
6121
  args: [{
3344
- selector: 'sds-tri-state-chip',
6122
+ selector: 'sds-image-cropper',
3345
6123
  changeDetection: ChangeDetectionStrategy.OnPush,
3346
6124
  imports: [SdsIcon],
3347
- host: { class: 'inline-flex min-w-0' },
6125
+ host: { class: 'block' },
3348
6126
  template: `
3349
- <button
3350
- type="button"
3351
- [id]="chipId"
3352
- [class]="chipClass()"
3353
- [disabled]="disabled()"
3354
- [attr.data-state]="value()"
3355
- [attr.aria-describedby]="hint() ? chipId + '-hint' : null"
3356
- (click)="onClick($event)"
6127
+ <div
6128
+ #viewport
6129
+ class="relative w-full touch-none select-none overflow-hidden rounded-card bg-ink/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 focus-visible:ring-offset-surface"
6130
+ [class.cursor-grab]="loaded() && !dragging()"
6131
+ [class.cursor-grabbing]="dragging()"
6132
+ [style.aspect-ratio]="aspectRatio()"
6133
+ role="application"
6134
+ [attr.aria-roledescription]="roleDescription()"
6135
+ [attr.aria-label]="label()"
6136
+ [attr.aria-describedby]="helpId"
6137
+ tabindex="0"
6138
+ (pointerdown)="onPointerDown($event)"
6139
+ (pointermove)="onPointerMove($event)"
6140
+ (pointerup)="onPointerUp($event)"
6141
+ (pointercancel)="onPointerUp($event)"
6142
+ (wheel)="onWheel($event)"
3357
6143
  (keydown)="onKeydown($event)"
3358
6144
  >
3359
- @if (icon()) {
3360
- <sds-icon [name]="icon()!" [size]="size() === 'sm' ? 18 : 22" class="shrink-0" />
6145
+ @if (objectUrl()) {
6146
+ <img
6147
+ [src]="objectUrl()"
6148
+ [attr.crossorigin]="crossOrigin()"
6149
+ alt=""
6150
+ draggable="false"
6151
+ class="pointer-events-none absolute left-0 top-0 max-w-none origin-top-left"
6152
+ [class.invisible]="!loaded()"
6153
+ [style.width.px]="displayW()"
6154
+ [style.height.px]="displayH()"
6155
+ [style.transform]="'translate3d(' + tx() + 'px,' + ty() + 'px,0)'"
6156
+ (load)="onLoad($event)"
6157
+ (error)="onError()"
6158
+ />
3361
6159
  }
6160
+ @if (shape() === 'round') {
6161
+ <div
6162
+ class="pointer-events-none absolute inset-0 rounded-full shadow-[0_0_0_9999px_rgba(0,0,0,0.45)]"
6163
+ aria-hidden="true"
6164
+ ></div>
6165
+ }
6166
+ <!-- Rule-of-thirds guides. -->
6167
+ <div class="pointer-events-none absolute inset-0" aria-hidden="true">
6168
+ <div class="absolute inset-y-0 left-1/3 w-px bg-white/30"></div>
6169
+ <div class="absolute inset-y-0 left-2/3 w-px bg-white/30"></div>
6170
+ <div class="absolute inset-x-0 top-1/3 h-px bg-white/30"></div>
6171
+ <div class="absolute inset-x-0 top-2/3 h-px bg-white/30"></div>
6172
+ </div>
6173
+ @if (!loaded()) {
6174
+ <div class="absolute inset-0 grid place-items-center text-white/70" aria-hidden="true">
6175
+ <sds-icon name="image" [size]="32" />
6176
+ </div>
6177
+ }
6178
+ </div>
6179
+ <p [id]="helpId" class="sr-only">{{ instructions() }}</p>
3362
6180
 
3363
- <span class="flex min-w-0 flex-1 flex-col text-left leading-tight">
3364
- <!-- Up to two lines, so "Cereals containing gluten" stays readable in a narrow grid cell. -->
3365
- <span
3366
- class="line-clamp-2 break-words font-semibold"
3367
- [class]="size() === 'sm' ? 'text-xs' : 'text-sm'"
3368
- >{{ label() }}</span
3369
- >
3370
- @if (showState()) {
3371
- <span
3372
- class="truncate text-[11px] font-medium"
3373
- [class]="captionClass()"
3374
- aria-hidden="true"
3375
- >{{ stateText() }}</span
3376
- >
3377
- }
3378
- </span>
3379
- <!-- The state is part of the accessible name ("Milk, may contain"), visible caption or not. -->
3380
- <span class="sr-only">, {{ stateText() }}</span>
3381
-
3382
- <!-- Non-colour mark: nothing / a check / a question mark. The slot keeps its width in every
3383
- state so the chip never shifts when cycled. -->
3384
- <span
3385
- class="grid shrink-0 place-items-center"
3386
- [class]="size() === 'sm' ? 'h-4 w-4' : 'h-5 w-5'"
3387
- aria-hidden="true"
6181
+ <div class="mt-3 flex items-center gap-2">
6182
+ <button
6183
+ type="button"
6184
+ class="grid h-9 w-9 shrink-0 place-items-center rounded-control text-ink-soft hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-40"
6185
+ [attr.aria-label]="zoomOutLabel()"
6186
+ [disabled]="!loaded() || zoom() <= 1"
6187
+ (click)="zoomBy(-zoomStep())"
3388
6188
  >
3389
- @switch (value()) {
3390
- @case ('contains') {
3391
- <sds-icon name="check" [size]="size() === 'sm' ? 14 : 18" [strokeWidth]="3" />
3392
- }
3393
- @case ('may-contain') {
3394
- <sds-icon name="circle-help" [size]="size() === 'sm' ? 15 : 19" />
3395
- }
3396
- }
3397
- </span>
3398
- </button>
3399
-
3400
- @if (hint()) {
3401
- <span [id]="chipId + '-hint'" class="sr-only">{{ hint() }}</span>
3402
- }
3403
- <span class="sr-only" aria-live="polite" aria-atomic="true">{{ announcement() }}</span>
6189
+ <sds-icon name="zoom-out" [size]="18" />
6190
+ </button>
6191
+ <input
6192
+ type="range"
6193
+ class="h-2 min-w-0 flex-1 cursor-pointer accent-brand-600"
6194
+ min="1"
6195
+ [max]="maxZoom()"
6196
+ step="0.01"
6197
+ [value]="zoom()"
6198
+ [disabled]="!loaded()"
6199
+ [attr.aria-label]="zoomLabel()"
6200
+ [attr.aria-valuetext]="zoomText()"
6201
+ (input)="onSlider($event)"
6202
+ />
6203
+ <button
6204
+ type="button"
6205
+ class="grid h-9 w-9 shrink-0 place-items-center rounded-control text-ink-soft hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-40"
6206
+ [attr.aria-label]="zoomInLabel()"
6207
+ [disabled]="!loaded() || zoom() >= maxZoom()"
6208
+ (click)="zoomBy(zoomStep())"
6209
+ >
6210
+ <sds-icon name="zoom-in" [size]="18" />
6211
+ </button>
6212
+ </div>
3404
6213
  `,
3405
6214
  }]
3406
- }], 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 }] }] } });
6215
+ }], ctorParameters: () => [], propDecorators: { viewport: [{ type: i0.ViewChild, args: ['viewport', { isSignal: true }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], maxZoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxZoom", required: false }] }], zoomStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomStep", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], crossOrigin: [{ type: i0.Input, args: [{ isSignal: true, alias: "crossOrigin", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], roleDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "roleDescription", required: false }] }], instructions: [{ type: i0.Input, args: [{ isSignal: true, alias: "instructions", required: false }] }], zoomLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomLabel", required: false }] }], zoomInLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomInLabel", required: false }] }], zoomOutLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomOutLabel", required: false }] }], cropChange: [{ type: i0.Output, args: ["cropChange"] }], imageLoad: [{ type: i0.Output, args: ["imageLoad"] }], imageError: [{ type: i0.Output, args: ["imageError"] }] } });
3407
6216
 
3408
6217
  /** Small, dependency-free date helpers (day-granularity, local time). */
3409
6218
  function startOfDay(d) {
@@ -8242,6 +11051,16 @@ let nextId$1 = 0;
8242
11051
  * every filtered row regardless of pagination (this is an intentional
8243
11052
  * deviation from PrimeNG's per-page select-all).
8244
11053
  *
11054
+ * **Editable grid (opt-in).** Set `editable` and give columns an `editor` (`text` · `number` ·
11055
+ * `currency` · `select`). The table becomes an ARIA grid with one tab stop: arrow keys move between
11056
+ * cells (Home/End, Ctrl+Home/End), Enter or F2 edits (typing a character starts editing with it),
11057
+ * Enter commits and moves down (Shift+Enter up), Tab / Shift+Tab commit and move to the next/previous
11058
+ * editable cell, Escape cancels (and is stopped, so an enclosing drawer stays open), clicking elsewhere
11059
+ * commits. Each changed commit emits `(cellCommit)` — the host applies it to its data (the table never
11060
+ * mutates rows) — and marks the cell dirty until {@link clearDirty}. Errors from `[cellErrors]` show
11061
+ * under the cell with `aria-invalid`. Number/currency cells use sds-input-number (live, locale-aware
11062
+ * formatting: "11,9" → "€11.90"). Everything else — sort, paging, selection — is unchanged.
11063
+ *
8245
11064
  * @example
8246
11065
  * <sds-table [value]="staff" [columns]="cols" dataKey="id"
8247
11066
  * sortable paginator [rows]="10" globalFilter
@@ -8250,6 +11069,11 @@ let nextId$1 = 0;
8250
11069
  * <sds-badge [variant]="row.actif ? 'success' : 'neutral'">{{ value }}</sds-badge>
8251
11070
  * </ng-template>
8252
11071
  * </sds-table>
11072
+ *
11073
+ * @example
11074
+ * <sds-table [value]="drafts()" [columns]="[{ field: 'name', header: 'Name', editor: 'text' },
11075
+ * { field: 'price', header: 'Price', editor: 'currency', align: 'right' }]"
11076
+ * dataKey="id" editable [cellErrors]="errors()" (cellCommit)="apply($event)" />
8253
11077
  */
8254
11078
  class SdsTable {
8255
11079
  tableId = `sds-table-${nextId$1++}`;
@@ -8308,6 +11132,16 @@ class SdsTable {
8308
11132
  resizableColumns = input(false, ...(ngDevMode ? [{ debugName: "resizableColumns", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
8309
11133
  /** Drag a column header to reorder columns. */
8310
11134
  reorderableColumns = input(false, ...(ngDevMode ? [{ debugName: "reorderableColumns", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11135
+ /** Turn on the editable grid (columns opt in with `editor`). Off: the table is unchanged. */
11136
+ editable = input(false, ...(ngDevMode ? [{ debugName: "editable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11137
+ /** Whether a single click (default) or a double click starts editing an editable cell. */
11138
+ editOn = input('click', ...(ngDevMode ? [{ debugName: "editOn" }] : []));
11139
+ /** Per-cell error messages, keyed by row key then field (see {@link SdsTableCellErrors}). */
11140
+ cellErrors = input({}, ...(ngDevMode ? [{ debugName: "cellErrors" }] : []));
11141
+ /** Default locale for number/currency cells (a column's `locale` wins). Empty = the browser's. */
11142
+ locale = input('', ...(ngDevMode ? [{ debugName: "locale" }] : []));
11143
+ /** Spoken marker of an edited (dirty) cell. */
11144
+ dirtyLabel = input('edited', ...(ngDevMode ? [{ debugName: "dirtyLabel" }] : []));
8311
11145
  // --- outputs --------------------------------------------------------------
8312
11146
  sortChange = output();
8313
11147
  pageChange = output();
@@ -8321,6 +11155,8 @@ class SdsTable {
8321
11155
  rowCollapse = output();
8322
11156
  /** Emitted after a drag-reorder with the moved field and its old/new index. */
8323
11157
  colReorder = output();
11158
+ /** Emitted when an edited cell is committed with a CHANGED value. Apply it to your data. */
11159
+ cellCommit = output();
8324
11160
  // --- custom cell templates ------------------------------------------------
8325
11161
  cellTemplates = contentChildren(SdsTableCell, ...(ngDevMode ? [{ debugName: "cellTemplates" }] : []));
8326
11162
  templateMap = computed(() => {
@@ -8573,7 +11409,7 @@ class SdsTable {
8573
11409
  }
8574
11410
  /** Enter/Space on a (single-mode) focusable row toggles its selection. */
8575
11411
  onRowKeydown(event, row) {
8576
- if (this.selectionMode() !== 'single' || this.loading())
11412
+ if (this.selectionMode() !== 'single' || this.loading() || this.editable())
8577
11413
  return;
8578
11414
  event.preventDefault();
8579
11415
  this.onRowClick(row);
@@ -8609,6 +11445,7 @@ class SdsTable {
8609
11445
  }
8610
11446
  // --- paginator ------------------------------------------------------------
8611
11447
  onPageChange(state) {
11448
+ this.editing.set(null);
8612
11449
  this.pageChange.emit(state);
8613
11450
  if (this.lazy())
8614
11451
  this.emitLazy();
@@ -8721,6 +11558,408 @@ class SdsTable {
8721
11558
  return row[k];
8722
11559
  return index;
8723
11560
  }
11561
+ // --- editable grid ----------------------------------------------------------
11562
+ host = inject(ElementRef);
11563
+ injector = inject(Injector);
11564
+ numberEditor = viewChild(SdsInputNumber, ...(ngDevMode ? [{ debugName: "numberEditor" }] : []));
11565
+ /** The roving tab stop (row index in the displayed page, column index). */
11566
+ activeCell = signal({ r: 0, c: 0 }, ...(ngDevMode ? [{ debugName: "activeCell" }] : []));
11567
+ /** The cell being edited, if any. */
11568
+ editing = signal(null, ...(ngDevMode ? [{ debugName: "editing" }] : []));
11569
+ /** The editor's working value. */
11570
+ draft = signal(null, ...(ngDevMode ? [{ debugName: "draft" }] : []));
11571
+ /** Edited cells: row key → fields. */
11572
+ dirty = signal(new Map(), ...(ngDevMode ? [{ debugName: "dirty" }] : []));
11573
+ /** Set while the table moves focus itself, so the editor's blur doesn't commit twice. */
11574
+ suppressBlur = false;
11575
+ /** Active cell clamped to the current page and columns. */
11576
+ tabStop = computed(() => {
11577
+ const rows = this.displayRows().length;
11578
+ const cols = this.effectiveColumns().length;
11579
+ const { r, c } = this.activeCell();
11580
+ return { r: Math.max(0, Math.min(r, rows - 1)), c: Math.max(0, Math.min(c, cols - 1)) };
11581
+ }, ...(ngDevMode ? [{ debugName: "tabStop" }] : []));
11582
+ draftText = computed(() => {
11583
+ const d = this.draft();
11584
+ return d == null ? '' : String(d);
11585
+ }, ...(ngDevMode ? [{ debugName: "draftText" }] : []));
11586
+ isTabStop(r, c) {
11587
+ const t = this.tabStop();
11588
+ return t.r === r && t.c === c;
11589
+ }
11590
+ isEditing(r, c) {
11591
+ const e = this.editing();
11592
+ return !!e && e.r === r && e.c === c;
11593
+ }
11594
+ isCellEditable(row, col) {
11595
+ if (!this.editable() || !col.editor || this.loading())
11596
+ return false;
11597
+ const rule = col.editable;
11598
+ if (rule === undefined)
11599
+ return true;
11600
+ return typeof rule === 'function' ? !!rule(row) : rule;
11601
+ }
11602
+ /** Row key used by dirty tracking, errors and commit events. */
11603
+ rowKeyOf(row) {
11604
+ if (this.dataKey() && row && typeof row === 'object')
11605
+ return row[this.dataKey()];
11606
+ const idx = this.value().indexOf(row);
11607
+ return idx >= 0 ? idx : row;
11608
+ }
11609
+ cellErrorFor(row, field) {
11610
+ const byRow = this.cellErrors()?.[String(this.rowKeyOf(row))];
11611
+ return (byRow && byRow[field]) || null;
11612
+ }
11613
+ cellErrorId(r, c) {
11614
+ return `${this.tableId}-err-${r}-${c}`;
11615
+ }
11616
+ isDirty(row, field) {
11617
+ return this.dirty().get(this.rowKeyOf(row))?.has(field) ?? false;
11618
+ }
11619
+ /** Forget the dirty marks — all of them, or one row's (by row key). */
11620
+ clearDirty(rowKey) {
11621
+ if (rowKey === undefined) {
11622
+ this.dirty.set(new Map());
11623
+ return;
11624
+ }
11625
+ const next = new Map(this.dirty());
11626
+ next.delete(rowKey);
11627
+ this.dirty.set(next);
11628
+ }
11629
+ /** Display text of a cell in view mode: `format`, else the editor's formatting, else the raw value. */
11630
+ displayCell(row, col) {
11631
+ const v = this.getValue(row, col.field);
11632
+ if (col.format)
11633
+ return col.format(v, row);
11634
+ if (v == null || v === '')
11635
+ return '';
11636
+ if ((col.editor === 'currency' || col.editor === 'number') && typeof v === 'number') {
11637
+ try {
11638
+ const opts = col.editor === 'currency'
11639
+ ? { style: 'currency', currency: col.currency ?? 'EUR' }
11640
+ : { style: 'decimal' };
11641
+ if (col.minFractionDigits != null)
11642
+ opts.minimumFractionDigits = col.minFractionDigits;
11643
+ if (col.maxFractionDigits != null)
11644
+ opts.maximumFractionDigits = col.maxFractionDigits;
11645
+ return new Intl.NumberFormat(col.locale ?? (this.locale() || undefined), opts).format(v);
11646
+ }
11647
+ catch {
11648
+ return String(v);
11649
+ }
11650
+ }
11651
+ if (col.editor === 'select') {
11652
+ return col.options?.find((o) => o.value === v)?.label ?? String(v);
11653
+ }
11654
+ return String(v);
11655
+ }
11656
+ gridCellClass(row, col, r, c) {
11657
+ const align = ALIGN[col.align ?? 'left'];
11658
+ const editing = this.isEditing(r, c);
11659
+ // While editing, the cell trades its padding for the editor's own, so the row height barely moves.
11660
+ const pad = editing ? 'px-1 py-1 text-sm' : SIZES[this.size()].cell;
11661
+ const parts = [
11662
+ pad,
11663
+ align,
11664
+ 'relative text-ink align-middle focus:outline-none focus:ring-2 focus:ring-inset focus:ring-brand-500',
11665
+ ];
11666
+ if (!editing && this.isCellEditable(row, col))
11667
+ parts.push('cursor-text hover:bg-muted/60');
11668
+ if (this.cellErrorFor(row, col.field))
11669
+ parts.push('bg-accent-red-soft/50 shadow-[inset_0_0_0_1px_var(--color-danger)]');
11670
+ return parts.join(' ');
11671
+ }
11672
+ onCellFocus(r, c) {
11673
+ const a = this.activeCell();
11674
+ if (a.r !== r || a.c !== c)
11675
+ this.activeCell.set({ r, c });
11676
+ }
11677
+ onCellClick(event, r, c) {
11678
+ if (this.isEditing(r, c)) {
11679
+ event.stopPropagation();
11680
+ return;
11681
+ }
11682
+ this.activeCell.set({ r, c });
11683
+ const row = this.displayRows()[r];
11684
+ const col = this.effectiveColumns()[c];
11685
+ if (this.editOn() === 'click' && row && col && this.isCellEditable(row, col)) {
11686
+ // Keep a click that edits a cell from also toggling single-row selection.
11687
+ event.stopPropagation();
11688
+ this.startEdit(r, c);
11689
+ }
11690
+ }
11691
+ onCellDblClick(event, r, c) {
11692
+ if (this.editOn() !== 'dblclick' || this.isEditing(r, c))
11693
+ return;
11694
+ const row = this.displayRows()[r];
11695
+ const col = this.effectiveColumns()[c];
11696
+ if (row && col && this.isCellEditable(row, col)) {
11697
+ event.stopPropagation();
11698
+ this.startEdit(r, c);
11699
+ }
11700
+ }
11701
+ /**
11702
+ * Start editing a cell of the displayed page (e.g. the first cell of a row you just added).
11703
+ * `field` names the column. Returns false when the cell isn't editable.
11704
+ */
11705
+ editCell(rowIndex, field) {
11706
+ const c = this.effectiveColumns().findIndex((col) => col.field === field);
11707
+ const row = this.displayRows()[rowIndex];
11708
+ const col = this.effectiveColumns()[c];
11709
+ if (c < 0 || !row || !this.isCellEditable(row, col))
11710
+ return false;
11711
+ this.activeCell.set({ r: rowIndex, c });
11712
+ this.startEdit(rowIndex, c);
11713
+ return true;
11714
+ }
11715
+ startEdit(r, c, initialChar) {
11716
+ const row = this.displayRows()[r];
11717
+ const col = this.effectiveColumns()[c];
11718
+ if (!row || !col || !this.isCellEditable(row, col))
11719
+ return;
11720
+ if (this.editing())
11721
+ this.commitEdit('none', false);
11722
+ const current = this.getValue(row, col.field);
11723
+ const numeric = col.editor === 'number' || col.editor === 'currency';
11724
+ this.draft.set(initialChar !== undefined && !numeric ? initialChar : initialChar !== undefined ? null : current);
11725
+ this.activeCell.set({ r, c });
11726
+ this.editing.set({ r, c });
11727
+ afterNextRender(() => this.focusEditor(col, initialChar), { injector: this.injector });
11728
+ }
11729
+ focusEditor(col, initialChar) {
11730
+ const e = this.editing();
11731
+ if (!e)
11732
+ return;
11733
+ const td = this.cellEl(e.r, e.c);
11734
+ if (!td)
11735
+ return;
11736
+ if (col.editor === 'number' || col.editor === 'currency') {
11737
+ const cmp = this.numberEditor();
11738
+ if (!cmp)
11739
+ return;
11740
+ const d = this.draft();
11741
+ cmp.writeValue(typeof d === 'number' ? d : null);
11742
+ cmp.registerOnChange((v) => this.draft.set(v));
11743
+ cmp.focus({ select: initialChar === undefined });
11744
+ if (initialChar !== undefined) {
11745
+ const field = td.querySelector('input');
11746
+ if (field) {
11747
+ field.value = initialChar;
11748
+ field.dispatchEvent(new Event('input', { bubbles: true }));
11749
+ }
11750
+ }
11751
+ return;
11752
+ }
11753
+ const el = td.querySelector('[data-sds-cell-editor]');
11754
+ el?.focus();
11755
+ if (el instanceof HTMLInputElement) {
11756
+ if (initialChar === undefined)
11757
+ el.select();
11758
+ else
11759
+ el.setSelectionRange(el.value.length, el.value.length);
11760
+ }
11761
+ }
11762
+ onTextEditorInput(event) {
11763
+ this.draft.set(event.target.value);
11764
+ }
11765
+ onSelectEditorChange(event, col) {
11766
+ const idx = Number(event.target.value);
11767
+ const opt = col.options?.[idx];
11768
+ this.draft.set(opt ? opt.value : null);
11769
+ }
11770
+ /** Focus left the editor (a click elsewhere): commit, but don't pull focus back. */
11771
+ onEditorBlur() {
11772
+ if (this.suppressBlur || !this.editing())
11773
+ return;
11774
+ // A focus move INSIDE the editor (sds-input-number's own buttons) is not a blur of the cell.
11775
+ setTimeout(() => {
11776
+ const e = this.editing();
11777
+ if (!e || this.suppressBlur)
11778
+ return;
11779
+ const td = this.cellEl(e.r, e.c);
11780
+ if (td && td.contains(document.activeElement) && document.activeElement !== td)
11781
+ return;
11782
+ this.commitEdit('none', false);
11783
+ });
11784
+ }
11785
+ /**
11786
+ * Commit the edit. `move` picks the next active cell; `refocus` puts focus back on the grid (not when
11787
+ * the user clicked away).
11788
+ */
11789
+ commitEdit(move, refocus = true) {
11790
+ const e = this.editing();
11791
+ if (!e)
11792
+ return;
11793
+ // Move focus to the cell first: the number editor clamps/tidies its value on blur.
11794
+ this.suppressBlur = true;
11795
+ if (refocus)
11796
+ this.cellEl(e.r, e.c)?.focus();
11797
+ this.suppressBlur = false;
11798
+ const row = this.displayRows()[e.r];
11799
+ const col = this.effectiveColumns()[e.c];
11800
+ this.editing.set(null);
11801
+ if (row && col) {
11802
+ let value = this.draft();
11803
+ if (col.editor === 'text' || col.editor === undefined)
11804
+ value = value == null ? '' : String(value);
11805
+ const previousValue = this.getValue(row, col.field);
11806
+ const same = value === previousValue ||
11807
+ ((value === '' || value == null) && (previousValue === '' || previousValue == null));
11808
+ if (!same) {
11809
+ const key = this.rowKeyOf(row);
11810
+ const next = new Map(this.dirty());
11811
+ next.set(key, new Set([...(next.get(key) ?? []), col.field]));
11812
+ this.dirty.set(next);
11813
+ this.cellCommit.emit({
11814
+ row,
11815
+ field: col.field,
11816
+ value,
11817
+ previousValue,
11818
+ rowKey: key,
11819
+ rowIndex: this.value().indexOf(row),
11820
+ });
11821
+ }
11822
+ }
11823
+ let target = { r: e.r, c: e.c };
11824
+ if (move === 'down')
11825
+ target = { r: Math.min(e.r + 1, this.displayRows().length - 1), c: e.c };
11826
+ else if (move === 'up')
11827
+ target = { r: Math.max(0, e.r - 1), c: e.c };
11828
+ else if (move === 'next' || move === 'prev')
11829
+ target = this.nextEditable(e.r, e.c, move === 'next' ? 1 : -1) ?? target;
11830
+ this.activeCell.set(target);
11831
+ if (refocus) {
11832
+ if (move === 'next' || move === 'prev') {
11833
+ // Tab keeps the spreadsheet rhythm: land in the next editable cell already editing.
11834
+ if (target.r !== e.r || target.c !== e.c)
11835
+ this.startEdit(target.r, target.c);
11836
+ else
11837
+ this.focusCellSoon(target.r, target.c);
11838
+ }
11839
+ else {
11840
+ this.focusCellSoon(target.r, target.c);
11841
+ }
11842
+ }
11843
+ }
11844
+ cancelEdit() {
11845
+ const e = this.editing();
11846
+ if (!e)
11847
+ return;
11848
+ this.suppressBlur = true;
11849
+ this.cellEl(e.r, e.c)?.focus();
11850
+ this.suppressBlur = false;
11851
+ this.editing.set(null);
11852
+ this.focusCellSoon(e.r, e.c);
11853
+ }
11854
+ /** The next editable cell in reading order (wrapping rows), or null at the grid's edge. */
11855
+ nextEditable(r, c, dir) {
11856
+ const rows = this.displayRows();
11857
+ const cols = this.effectiveColumns();
11858
+ let rr = r;
11859
+ let cc = c;
11860
+ for (let n = 0; n < rows.length * cols.length; n++) {
11861
+ cc += dir;
11862
+ if (cc >= cols.length) {
11863
+ cc = 0;
11864
+ rr++;
11865
+ }
11866
+ else if (cc < 0) {
11867
+ cc = cols.length - 1;
11868
+ rr--;
11869
+ }
11870
+ if (rr < 0 || rr >= rows.length)
11871
+ return null;
11872
+ if (this.isCellEditable(rows[rr], cols[cc]))
11873
+ return { r: rr, c: cc };
11874
+ }
11875
+ return null;
11876
+ }
11877
+ cellEl(r, c) {
11878
+ return this.host.nativeElement.querySelector(`td[data-cell="${r}:${c}"]`);
11879
+ }
11880
+ focusCellSoon(r, c) {
11881
+ afterNextRender(() => this.cellEl(r, c)?.focus(), { injector: this.injector });
11882
+ }
11883
+ onGridKeydown(event) {
11884
+ if (!this.editable() || this.loading())
11885
+ return;
11886
+ const target = event.target;
11887
+ const e = this.editing();
11888
+ // --- editing: Enter / Tab / Escape drive the edit; everything else belongs to the editor.
11889
+ if (e && this.cellEl(e.r, e.c)?.contains(target) && target.tagName !== 'TD') {
11890
+ switch (event.key) {
11891
+ case 'Enter':
11892
+ event.preventDefault();
11893
+ this.commitEdit(event.shiftKey ? 'up' : 'down');
11894
+ return;
11895
+ case 'Tab': {
11896
+ const next = this.nextEditable(e.r, e.c, event.shiftKey ? -1 : 1);
11897
+ if (!next) {
11898
+ // Last editable cell: commit and let Tab leave the grid naturally.
11899
+ this.commitEdit('none', false);
11900
+ return;
11901
+ }
11902
+ event.preventDefault();
11903
+ this.commitEdit(event.shiftKey ? 'prev' : 'next');
11904
+ return;
11905
+ }
11906
+ case 'Escape':
11907
+ event.preventDefault();
11908
+ event.stopPropagation();
11909
+ this.cancelEdit();
11910
+ return;
11911
+ }
11912
+ return;
11913
+ }
11914
+ // --- navigation: only when a grid cell itself has focus.
11915
+ const td = target.closest('td[data-cell]');
11916
+ if (!td || td !== target)
11917
+ return;
11918
+ const [r, c] = (td.dataset['cell'] ?? '0:0').split(':').map(Number);
11919
+ const rows = this.displayRows().length;
11920
+ const cols = this.effectiveColumns().length;
11921
+ let next = null;
11922
+ switch (event.key) {
11923
+ case 'ArrowUp':
11924
+ next = { r: Math.max(0, r - 1), c };
11925
+ break;
11926
+ case 'ArrowDown':
11927
+ next = { r: Math.min(rows - 1, r + 1), c };
11928
+ break;
11929
+ case 'ArrowLeft':
11930
+ next = { r, c: Math.max(0, c - 1) };
11931
+ break;
11932
+ case 'ArrowRight':
11933
+ next = { r, c: Math.min(cols - 1, c + 1) };
11934
+ break;
11935
+ case 'Home':
11936
+ next = event.ctrlKey || event.metaKey ? { r: 0, c: 0 } : { r, c: 0 };
11937
+ break;
11938
+ case 'End':
11939
+ next = event.ctrlKey || event.metaKey ? { r: rows - 1, c: cols - 1 } : { r, c: cols - 1 };
11940
+ break;
11941
+ case 'Enter':
11942
+ case 'F2':
11943
+ event.preventDefault();
11944
+ this.startEdit(r, c);
11945
+ return;
11946
+ default: {
11947
+ const row = this.displayRows()[r];
11948
+ const col = this.effectiveColumns()[c];
11949
+ const printable = event.key.length === 1 && event.key !== ' ' && !event.ctrlKey && !event.metaKey && !event.altKey;
11950
+ if (printable && row && col && col.editor !== 'select' && this.isCellEditable(row, col)) {
11951
+ event.preventDefault();
11952
+ this.startEdit(r, c, event.key);
11953
+ }
11954
+ return;
11955
+ }
11956
+ }
11957
+ event.preventDefault();
11958
+ if (next) {
11959
+ this.activeCell.set(next);
11960
+ this.cellEl(next.r, next.c)?.focus();
11961
+ }
11962
+ }
8724
11963
  // --- styling --------------------------------------------------------------
8725
11964
  headCellClass(col) {
8726
11965
  const align = ALIGN[col?.align ?? 'left'];
@@ -8753,7 +11992,7 @@ class SdsTable {
8753
11992
  : 'border-border bg-surface';
8754
11993
  }
8755
11994
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
8756
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsTable, isStandalone: true, selector: "sds-table", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, dataKey: { classPropertyName: "dataKey", publicName: "dataKey", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, striped: { classPropertyName: "striped", publicName: "striped", isSignal: true, isRequired: false, transformFunction: null }, gridlines: { classPropertyName: "gridlines", publicName: "gridlines", isSignal: true, isRequired: false, transformFunction: null }, hoverable: { classPropertyName: "hoverable", publicName: "hoverable", isSignal: true, isRequired: false, transformFunction: null }, sortable: { classPropertyName: "sortable", publicName: "sortable", isSignal: true, isRequired: false, transformFunction: null }, paginator: { classPropertyName: "paginator", publicName: "paginator", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, rowsPerPageOptions: { classPropertyName: "rowsPerPageOptions", publicName: "rowsPerPageOptions", isSignal: true, isRequired: false, transformFunction: null }, showCurrentPageReport: { classPropertyName: "showCurrentPageReport", publicName: "showCurrentPageReport", isSignal: true, isRequired: false, transformFunction: null }, currentPageReportTemplate: { classPropertyName: "currentPageReportTemplate", publicName: "currentPageReportTemplate", isSignal: true, isRequired: false, transformFunction: null }, rowsPerPageLabel: { classPropertyName: "rowsPerPageLabel", publicName: "rowsPerPageLabel", isSignal: true, isRequired: false, transformFunction: null }, paginationLabel: { classPropertyName: "paginationLabel", publicName: "paginationLabel", isSignal: true, isRequired: false, transformFunction: null }, firstPageLabel: { classPropertyName: "firstPageLabel", publicName: "firstPageLabel", isSignal: true, isRequired: false, transformFunction: null }, previousPageLabel: { classPropertyName: "previousPageLabel", publicName: "previousPageLabel", isSignal: true, isRequired: false, transformFunction: null }, nextPageLabel: { classPropertyName: "nextPageLabel", publicName: "nextPageLabel", isSignal: true, isRequired: false, transformFunction: null }, lastPageLabel: { classPropertyName: "lastPageLabel", publicName: "lastPageLabel", isSignal: true, isRequired: false, transformFunction: null }, pageLinkLabel: { classPropertyName: "pageLinkLabel", publicName: "pageLinkLabel", isSignal: true, isRequired: false, transformFunction: null }, globalFilter: { classPropertyName: "globalFilter", publicName: "globalFilter", isSignal: true, isRequired: false, transformFunction: null }, globalFilterPlaceholder: { classPropertyName: "globalFilterPlaceholder", publicName: "globalFilterPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, globalFilterAriaLabel: { classPropertyName: "globalFilterAriaLabel", publicName: "globalFilterAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, globalFilterFields: { classPropertyName: "globalFilterFields", publicName: "globalFilterFields", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, caption: { classPropertyName: "caption", publicName: "caption", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, scrollHeight: { classPropertyName: "scrollHeight", publicName: "scrollHeight", isSignal: true, isRequired: false, transformFunction: null }, lazy: { classPropertyName: "lazy", publicName: "lazy", isSignal: true, isRequired: false, transformFunction: null }, totalRecords: { classPropertyName: "totalRecords", publicName: "totalRecords", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, resizableColumns: { classPropertyName: "resizableColumns", publicName: "resizableColumns", isSignal: true, isRequired: false, transformFunction: null }, reorderableColumns: { classPropertyName: "reorderableColumns", publicName: "reorderableColumns", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selection: "selectionChange", sortChange: "sortChange", pageChange: "pageChange", rowSelect: "rowSelect", rowUnselect: "rowUnselect", lazyLoad: "lazyLoad", rowClick: "rowClick", rowExpand: "rowExpand", rowCollapse: "rowCollapse", colReorder: "colReorder" }, host: { classAttribute: "block" }, queries: [{ propertyName: "cellTemplates", predicate: SdsTableCell, isSignal: true }, { propertyName: "rowExpansion", first: true, predicate: SdsTableRowExpansion, descendants: true, isSignal: true }], ngImport: i0, template: `
11995
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsTable, isStandalone: true, selector: "sds-table", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, dataKey: { classPropertyName: "dataKey", publicName: "dataKey", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, striped: { classPropertyName: "striped", publicName: "striped", isSignal: true, isRequired: false, transformFunction: null }, gridlines: { classPropertyName: "gridlines", publicName: "gridlines", isSignal: true, isRequired: false, transformFunction: null }, hoverable: { classPropertyName: "hoverable", publicName: "hoverable", isSignal: true, isRequired: false, transformFunction: null }, sortable: { classPropertyName: "sortable", publicName: "sortable", isSignal: true, isRequired: false, transformFunction: null }, paginator: { classPropertyName: "paginator", publicName: "paginator", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, rowsPerPageOptions: { classPropertyName: "rowsPerPageOptions", publicName: "rowsPerPageOptions", isSignal: true, isRequired: false, transformFunction: null }, showCurrentPageReport: { classPropertyName: "showCurrentPageReport", publicName: "showCurrentPageReport", isSignal: true, isRequired: false, transformFunction: null }, currentPageReportTemplate: { classPropertyName: "currentPageReportTemplate", publicName: "currentPageReportTemplate", isSignal: true, isRequired: false, transformFunction: null }, rowsPerPageLabel: { classPropertyName: "rowsPerPageLabel", publicName: "rowsPerPageLabel", isSignal: true, isRequired: false, transformFunction: null }, paginationLabel: { classPropertyName: "paginationLabel", publicName: "paginationLabel", isSignal: true, isRequired: false, transformFunction: null }, firstPageLabel: { classPropertyName: "firstPageLabel", publicName: "firstPageLabel", isSignal: true, isRequired: false, transformFunction: null }, previousPageLabel: { classPropertyName: "previousPageLabel", publicName: "previousPageLabel", isSignal: true, isRequired: false, transformFunction: null }, nextPageLabel: { classPropertyName: "nextPageLabel", publicName: "nextPageLabel", isSignal: true, isRequired: false, transformFunction: null }, lastPageLabel: { classPropertyName: "lastPageLabel", publicName: "lastPageLabel", isSignal: true, isRequired: false, transformFunction: null }, pageLinkLabel: { classPropertyName: "pageLinkLabel", publicName: "pageLinkLabel", isSignal: true, isRequired: false, transformFunction: null }, globalFilter: { classPropertyName: "globalFilter", publicName: "globalFilter", isSignal: true, isRequired: false, transformFunction: null }, globalFilterPlaceholder: { classPropertyName: "globalFilterPlaceholder", publicName: "globalFilterPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, globalFilterAriaLabel: { classPropertyName: "globalFilterAriaLabel", publicName: "globalFilterAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, globalFilterFields: { classPropertyName: "globalFilterFields", publicName: "globalFilterFields", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, caption: { classPropertyName: "caption", publicName: "caption", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, scrollHeight: { classPropertyName: "scrollHeight", publicName: "scrollHeight", isSignal: true, isRequired: false, transformFunction: null }, lazy: { classPropertyName: "lazy", publicName: "lazy", isSignal: true, isRequired: false, transformFunction: null }, totalRecords: { classPropertyName: "totalRecords", publicName: "totalRecords", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, resizableColumns: { classPropertyName: "resizableColumns", publicName: "resizableColumns", isSignal: true, isRequired: false, transformFunction: null }, reorderableColumns: { classPropertyName: "reorderableColumns", publicName: "reorderableColumns", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, editOn: { classPropertyName: "editOn", publicName: "editOn", isSignal: true, isRequired: false, transformFunction: null }, cellErrors: { classPropertyName: "cellErrors", publicName: "cellErrors", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, dirtyLabel: { classPropertyName: "dirtyLabel", publicName: "dirtyLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selection: "selectionChange", sortChange: "sortChange", pageChange: "pageChange", rowSelect: "rowSelect", rowUnselect: "rowUnselect", lazyLoad: "lazyLoad", rowClick: "rowClick", rowExpand: "rowExpand", rowCollapse: "rowCollapse", colReorder: "colReorder", cellCommit: "cellCommit" }, host: { classAttribute: "block" }, queries: [{ propertyName: "cellTemplates", predicate: SdsTableCell, isSignal: true }, { propertyName: "rowExpansion", first: true, predicate: SdsTableRowExpansion, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "numberEditor", first: true, predicate: SdsInputNumber, descendants: true, isSignal: true }], ngImport: i0, template: `
8757
11996
  <div class="rounded-card border border-border bg-surface overflow-hidden">
8758
11997
  <!-- Toolbar: caption + global filter -->
8759
11998
  @if (caption() || globalFilter()) {
@@ -8798,6 +12037,8 @@ class SdsTable {
8798
12037
  >
8799
12038
  <table
8800
12039
  class="w-full border-collapse text-left"
12040
+ [attr.role]="editable() ? 'grid' : null"
12041
+ (keydown)="onGridKeydown($event)"
8801
12042
  [attr.aria-busy]="loading() ? 'true' : null"
8802
12043
  [attr.aria-labelledby]="caption() ? tableId + '-caption' : null"
8803
12044
  [attr.aria-label]="caption() ? null : ariaLabel()"
@@ -8901,7 +12142,7 @@ class SdsTable {
8901
12142
  @for (row of displayRows(); track trackRow($index, row); let i = $index) {
8902
12143
  <tr
8903
12144
  [class]="rowClass(row, i)"
8904
- [attr.tabindex]="selectionMode() === 'single' && !loading() ? 0 : null"
12145
+ [attr.tabindex]="selectionMode() === 'single' && !loading() && !editable() ? 0 : null"
8905
12146
  [attr.aria-selected]="selectionMode() === 'single' ? (isRowSelected(row) ? 'true' : 'false') : null"
8906
12147
  (click)="onRowClick(row)"
8907
12148
  (keydown.enter)="onRowKeydown($event, row)"
@@ -8967,20 +12208,128 @@ class SdsTable {
8967
12208
  </td>
8968
12209
  }
8969
12210
 
8970
- @for (col of effectiveColumns(); track col.field) {
8971
- <td
8972
- [class]="cellClass(col)"
8973
- [style.width]="columnStyleWidth(col)"
8974
- >
8975
- @if (templateFor(col.field); as tpl) {
8976
- <ng-container
8977
- [ngTemplateOutlet]="tpl"
8978
- [ngTemplateOutletContext]="cellContext(row, col, i)"
8979
- />
8980
- } @else {
8981
- {{ display(row, col.field) }}
8982
- }
8983
- </td>
12211
+ @for (col of effectiveColumns(); track col.field; let ci = $index) {
12212
+ @if (!editable()) {
12213
+ <td
12214
+ [class]="cellClass(col)"
12215
+ [style.width]="columnStyleWidth(col)"
12216
+ >
12217
+ @if (templateFor(col.field); as tpl) {
12218
+ <ng-container
12219
+ [ngTemplateOutlet]="tpl"
12220
+ [ngTemplateOutletContext]="cellContext(row, col, i)"
12221
+ />
12222
+ } @else {
12223
+ {{ displayCell(row, col) }}
12224
+ }
12225
+ </td>
12226
+ } @else {
12227
+ <!-- Editable grid cell: one roving tab stop, arrow-key navigation, in-place editor. -->
12228
+ <td
12229
+ role="gridcell"
12230
+ [class]="gridCellClass(row, col, i, ci)"
12231
+ [style.width]="columnStyleWidth(col)"
12232
+ [attr.data-cell]="i + ':' + ci"
12233
+ [attr.tabindex]="isTabStop(i, ci) ? 0 : -1"
12234
+ [attr.aria-readonly]="isCellEditable(row, col) ? null : 'true'"
12235
+ [attr.aria-invalid]="cellErrorFor(row, col.field) ? 'true' : null"
12236
+ [attr.aria-describedby]="cellErrorFor(row, col.field) ? cellErrorId(i, ci) : null"
12237
+ (click)="onCellClick($event, i, ci)"
12238
+ (dblclick)="onCellDblClick($event, i, ci)"
12239
+ (focus)="onCellFocus(i, ci)"
12240
+ >
12241
+ @if (isEditing(i, ci)) {
12242
+ @switch (col.editor) {
12243
+ @case ('select') {
12244
+ <select
12245
+ data-sds-cell-editor
12246
+ class="h-9 w-full rounded-control border border-brand-500 bg-surface px-2 text-sm text-ink outline-none ring-2 ring-brand-500/25"
12247
+ [attr.aria-label]="col.header"
12248
+ (change)="onSelectEditorChange($event, col)"
12249
+ (blur)="onEditorBlur()"
12250
+ >
12251
+ @if (draft() === null || draft() === undefined) {
12252
+ <option value="" disabled selected>{{ col.placeholder ?? '' }}</option>
12253
+ }
12254
+ @for (opt of col.options ?? []; track $index; let oi = $index) {
12255
+ <option [value]="oi" [selected]="opt.value === draft()">{{ opt.label }}</option>
12256
+ }
12257
+ </select>
12258
+ }
12259
+ @case ('number') {
12260
+ <sds-input-number
12261
+ data-sds-cell-editor
12262
+ size="sm"
12263
+ mode="decimal"
12264
+ [locale]="col.locale ?? locale()"
12265
+ [min]="col.min ?? null"
12266
+ [max]="col.max ?? null"
12267
+ [step]="col.step ?? 1"
12268
+ [minFractionDigits]="col.minFractionDigits ?? null"
12269
+ [maxFractionDigits]="col.maxFractionDigits ?? null"
12270
+ [placeholder]="col.placeholder ?? ''"
12271
+ [ariaLabel]="col.header"
12272
+ (focusout)="onEditorBlur()"
12273
+ />
12274
+ }
12275
+ @case ('currency') {
12276
+ <sds-input-number
12277
+ data-sds-cell-editor
12278
+ size="sm"
12279
+ mode="currency"
12280
+ [currency]="col.currency ?? 'EUR'"
12281
+ [locale]="col.locale ?? locale()"
12282
+ [min]="col.min ?? null"
12283
+ [max]="col.max ?? null"
12284
+ [step]="col.step ?? 1"
12285
+ [minFractionDigits]="col.minFractionDigits ?? null"
12286
+ [maxFractionDigits]="col.maxFractionDigits ?? null"
12287
+ [placeholder]="col.placeholder ?? ''"
12288
+ [ariaLabel]="col.header"
12289
+ (focusout)="onEditorBlur()"
12290
+ />
12291
+ }
12292
+ @default {
12293
+ <input
12294
+ data-sds-cell-editor
12295
+ type="text"
12296
+ class="h-9 w-full rounded-control border border-brand-500 bg-surface px-2.5 text-sm text-ink outline-none ring-2 ring-brand-500/25 placeholder:text-ink-faint"
12297
+ [value]="draftText()"
12298
+ [placeholder]="col.placeholder ?? ''"
12299
+ [attr.aria-label]="col.header"
12300
+ (input)="onTextEditorInput($event)"
12301
+ (blur)="onEditorBlur()"
12302
+ />
12303
+ }
12304
+ }
12305
+ } @else {
12306
+ @if (templateFor(col.field); as tpl) {
12307
+ <ng-container
12308
+ [ngTemplateOutlet]="tpl"
12309
+ [ngTemplateOutletContext]="cellContext(row, col, i)"
12310
+ />
12311
+ } @else {
12312
+ {{ displayCell(row, col) }}
12313
+ }
12314
+ @if (isDirty(row, col.field)) {
12315
+ <span
12316
+ class="pointer-events-none absolute right-1 top-1 h-1.5 w-1.5 rounded-full bg-brand-500"
12317
+ aria-hidden="true"
12318
+ ></span>
12319
+ <span class="sr-only">({{ dirtyLabel() }})</span>
12320
+ }
12321
+ }
12322
+ @if (cellErrorFor(row, col.field); as message) {
12323
+ <span
12324
+ [id]="cellErrorId(i, ci)"
12325
+ class="mt-1 flex items-start gap-1 text-xs font-medium normal-case text-danger"
12326
+ >
12327
+ <sds-icon name="alert-circle" [size]="13" class="mt-px shrink-0" />
12328
+ <span>{{ message }}</span>
12329
+ </span>
12330
+ }
12331
+ </td>
12332
+ }
8984
12333
  }
8985
12334
  </tr>
8986
12335
 
@@ -9047,14 +12396,14 @@ class SdsTable {
9047
12396
  </div>
9048
12397
  }
9049
12398
  </div>
9050
- `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }, { kind: "component", type: SdsPaginator, selector: "sds-paginator", inputs: ["totalRecords", "first", "rows", "rowsPerPageOptions", "pageLinkSize", "showFirstLastIcon", "showPageLinks", "showCurrentPageReport", "currentPageReportTemplate", "showJumpToPageInput", "rowsPerPageLabel", "jumpToPageLabel", "paginationLabel", "firstPageLabel", "previousPageLabel", "nextPageLabel", "lastPageLabel", "pageLinkLabel", "jumpToPageAriaLabel", "size", "disabled"], outputs: ["firstChange", "rowsChange", "pageChange"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
12399
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }, { kind: "component", type: SdsPaginator, selector: "sds-paginator", inputs: ["totalRecords", "first", "rows", "rowsPerPageOptions", "pageLinkSize", "showFirstLastIcon", "showPageLinks", "showCurrentPageReport", "currentPageReportTemplate", "showJumpToPageInput", "rowsPerPageLabel", "jumpToPageLabel", "paginationLabel", "firstPageLabel", "previousPageLabel", "nextPageLabel", "lastPageLabel", "pageLinkLabel", "jumpToPageAriaLabel", "size", "disabled"], outputs: ["firstChange", "rowsChange", "pageChange"] }, { kind: "component", type: SdsInputNumber, selector: "sds-input-number", inputs: ["label", "ariaLabel", "placeholder", "hint", "error", "size", "appearance", "disabled", "readonly", "mode", "locale", "currency", "currencyDisplay", "useGrouping", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "min", "max", "step", "showButtons", "buttonLayout", "incrementIcon", "decrementIcon", "allowEmpty", "clearable", "clearLabel", "incrementLabel", "decrementLabel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9051
12400
  }
9052
12401
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTable, decorators: [{
9053
12402
  type: Component,
9054
12403
  args: [{
9055
12404
  selector: 'sds-table',
9056
12405
  changeDetection: ChangeDetectionStrategy.OnPush,
9057
- imports: [SdsIcon, SdsPaginator, NgTemplateOutlet],
12406
+ imports: [SdsIcon, SdsPaginator, SdsInputNumber, NgTemplateOutlet],
9058
12407
  host: { class: 'block' },
9059
12408
  template: `
9060
12409
  <div class="rounded-card border border-border bg-surface overflow-hidden">
@@ -9101,6 +12450,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9101
12450
  >
9102
12451
  <table
9103
12452
  class="w-full border-collapse text-left"
12453
+ [attr.role]="editable() ? 'grid' : null"
12454
+ (keydown)="onGridKeydown($event)"
9104
12455
  [attr.aria-busy]="loading() ? 'true' : null"
9105
12456
  [attr.aria-labelledby]="caption() ? tableId + '-caption' : null"
9106
12457
  [attr.aria-label]="caption() ? null : ariaLabel()"
@@ -9204,7 +12555,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9204
12555
  @for (row of displayRows(); track trackRow($index, row); let i = $index) {
9205
12556
  <tr
9206
12557
  [class]="rowClass(row, i)"
9207
- [attr.tabindex]="selectionMode() === 'single' && !loading() ? 0 : null"
12558
+ [attr.tabindex]="selectionMode() === 'single' && !loading() && !editable() ? 0 : null"
9208
12559
  [attr.aria-selected]="selectionMode() === 'single' ? (isRowSelected(row) ? 'true' : 'false') : null"
9209
12560
  (click)="onRowClick(row)"
9210
12561
  (keydown.enter)="onRowKeydown($event, row)"
@@ -9270,20 +12621,128 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9270
12621
  </td>
9271
12622
  }
9272
12623
 
9273
- @for (col of effectiveColumns(); track col.field) {
9274
- <td
9275
- [class]="cellClass(col)"
9276
- [style.width]="columnStyleWidth(col)"
9277
- >
9278
- @if (templateFor(col.field); as tpl) {
9279
- <ng-container
9280
- [ngTemplateOutlet]="tpl"
9281
- [ngTemplateOutletContext]="cellContext(row, col, i)"
9282
- />
9283
- } @else {
9284
- {{ display(row, col.field) }}
9285
- }
9286
- </td>
12624
+ @for (col of effectiveColumns(); track col.field; let ci = $index) {
12625
+ @if (!editable()) {
12626
+ <td
12627
+ [class]="cellClass(col)"
12628
+ [style.width]="columnStyleWidth(col)"
12629
+ >
12630
+ @if (templateFor(col.field); as tpl) {
12631
+ <ng-container
12632
+ [ngTemplateOutlet]="tpl"
12633
+ [ngTemplateOutletContext]="cellContext(row, col, i)"
12634
+ />
12635
+ } @else {
12636
+ {{ displayCell(row, col) }}
12637
+ }
12638
+ </td>
12639
+ } @else {
12640
+ <!-- Editable grid cell: one roving tab stop, arrow-key navigation, in-place editor. -->
12641
+ <td
12642
+ role="gridcell"
12643
+ [class]="gridCellClass(row, col, i, ci)"
12644
+ [style.width]="columnStyleWidth(col)"
12645
+ [attr.data-cell]="i + ':' + ci"
12646
+ [attr.tabindex]="isTabStop(i, ci) ? 0 : -1"
12647
+ [attr.aria-readonly]="isCellEditable(row, col) ? null : 'true'"
12648
+ [attr.aria-invalid]="cellErrorFor(row, col.field) ? 'true' : null"
12649
+ [attr.aria-describedby]="cellErrorFor(row, col.field) ? cellErrorId(i, ci) : null"
12650
+ (click)="onCellClick($event, i, ci)"
12651
+ (dblclick)="onCellDblClick($event, i, ci)"
12652
+ (focus)="onCellFocus(i, ci)"
12653
+ >
12654
+ @if (isEditing(i, ci)) {
12655
+ @switch (col.editor) {
12656
+ @case ('select') {
12657
+ <select
12658
+ data-sds-cell-editor
12659
+ class="h-9 w-full rounded-control border border-brand-500 bg-surface px-2 text-sm text-ink outline-none ring-2 ring-brand-500/25"
12660
+ [attr.aria-label]="col.header"
12661
+ (change)="onSelectEditorChange($event, col)"
12662
+ (blur)="onEditorBlur()"
12663
+ >
12664
+ @if (draft() === null || draft() === undefined) {
12665
+ <option value="" disabled selected>{{ col.placeholder ?? '' }}</option>
12666
+ }
12667
+ @for (opt of col.options ?? []; track $index; let oi = $index) {
12668
+ <option [value]="oi" [selected]="opt.value === draft()">{{ opt.label }}</option>
12669
+ }
12670
+ </select>
12671
+ }
12672
+ @case ('number') {
12673
+ <sds-input-number
12674
+ data-sds-cell-editor
12675
+ size="sm"
12676
+ mode="decimal"
12677
+ [locale]="col.locale ?? locale()"
12678
+ [min]="col.min ?? null"
12679
+ [max]="col.max ?? null"
12680
+ [step]="col.step ?? 1"
12681
+ [minFractionDigits]="col.minFractionDigits ?? null"
12682
+ [maxFractionDigits]="col.maxFractionDigits ?? null"
12683
+ [placeholder]="col.placeholder ?? ''"
12684
+ [ariaLabel]="col.header"
12685
+ (focusout)="onEditorBlur()"
12686
+ />
12687
+ }
12688
+ @case ('currency') {
12689
+ <sds-input-number
12690
+ data-sds-cell-editor
12691
+ size="sm"
12692
+ mode="currency"
12693
+ [currency]="col.currency ?? 'EUR'"
12694
+ [locale]="col.locale ?? locale()"
12695
+ [min]="col.min ?? null"
12696
+ [max]="col.max ?? null"
12697
+ [step]="col.step ?? 1"
12698
+ [minFractionDigits]="col.minFractionDigits ?? null"
12699
+ [maxFractionDigits]="col.maxFractionDigits ?? null"
12700
+ [placeholder]="col.placeholder ?? ''"
12701
+ [ariaLabel]="col.header"
12702
+ (focusout)="onEditorBlur()"
12703
+ />
12704
+ }
12705
+ @default {
12706
+ <input
12707
+ data-sds-cell-editor
12708
+ type="text"
12709
+ class="h-9 w-full rounded-control border border-brand-500 bg-surface px-2.5 text-sm text-ink outline-none ring-2 ring-brand-500/25 placeholder:text-ink-faint"
12710
+ [value]="draftText()"
12711
+ [placeholder]="col.placeholder ?? ''"
12712
+ [attr.aria-label]="col.header"
12713
+ (input)="onTextEditorInput($event)"
12714
+ (blur)="onEditorBlur()"
12715
+ />
12716
+ }
12717
+ }
12718
+ } @else {
12719
+ @if (templateFor(col.field); as tpl) {
12720
+ <ng-container
12721
+ [ngTemplateOutlet]="tpl"
12722
+ [ngTemplateOutletContext]="cellContext(row, col, i)"
12723
+ />
12724
+ } @else {
12725
+ {{ displayCell(row, col) }}
12726
+ }
12727
+ @if (isDirty(row, col.field)) {
12728
+ <span
12729
+ class="pointer-events-none absolute right-1 top-1 h-1.5 w-1.5 rounded-full bg-brand-500"
12730
+ aria-hidden="true"
12731
+ ></span>
12732
+ <span class="sr-only">({{ dirtyLabel() }})</span>
12733
+ }
12734
+ }
12735
+ @if (cellErrorFor(row, col.field); as message) {
12736
+ <span
12737
+ [id]="cellErrorId(i, ci)"
12738
+ class="mt-1 flex items-start gap-1 text-xs font-medium normal-case text-danger"
12739
+ >
12740
+ <sds-icon name="alert-circle" [size]="13" class="mt-px shrink-0" />
12741
+ <span>{{ message }}</span>
12742
+ </span>
12743
+ }
12744
+ </td>
12745
+ }
9287
12746
  }
9288
12747
  </tr>
9289
12748
 
@@ -9352,7 +12811,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9352
12811
  </div>
9353
12812
  `,
9354
12813
  }]
9355
- }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], dataKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataKey", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], striped: [{ type: i0.Input, args: [{ isSignal: true, alias: "striped", required: false }] }], gridlines: [{ type: i0.Input, args: [{ isSignal: true, alias: "gridlines", required: false }] }], hoverable: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverable", required: false }] }], sortable: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortable", required: false }] }], paginator: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginator", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], rowsPerPageOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageOptions", required: false }] }], showCurrentPageReport: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCurrentPageReport", required: false }] }], currentPageReportTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPageReportTemplate", required: false }] }], rowsPerPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageLabel", required: false }] }], paginationLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationLabel", required: false }] }], firstPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstPageLabel", required: false }] }], previousPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousPageLabel", required: false }] }], nextPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextPageLabel", required: false }] }], lastPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lastPageLabel", required: false }] }], pageLinkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageLinkLabel", required: false }] }], globalFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilter", required: false }] }], globalFilterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterPlaceholder", required: false }] }], globalFilterAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterAriaLabel", required: false }] }], globalFilterFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterFields", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }, { type: i0.Output, args: ["selectionChange"] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], caption: [{ type: i0.Input, args: [{ isSignal: true, alias: "caption", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], scrollHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollHeight", required: false }] }], lazy: [{ type: i0.Input, args: [{ isSignal: true, alias: "lazy", required: false }] }], totalRecords: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalRecords", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], resizableColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizableColumns", required: false }] }], reorderableColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderableColumns", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], rowSelect: [{ type: i0.Output, args: ["rowSelect"] }], rowUnselect: [{ type: i0.Output, args: ["rowUnselect"] }], lazyLoad: [{ type: i0.Output, args: ["lazyLoad"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowExpand: [{ type: i0.Output, args: ["rowExpand"] }], rowCollapse: [{ type: i0.Output, args: ["rowCollapse"] }], colReorder: [{ type: i0.Output, args: ["colReorder"] }], cellTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SdsTableCell), { isSignal: true }] }], rowExpansion: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsTableRowExpansion), { isSignal: true }] }] } });
12814
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], dataKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataKey", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], striped: [{ type: i0.Input, args: [{ isSignal: true, alias: "striped", required: false }] }], gridlines: [{ type: i0.Input, args: [{ isSignal: true, alias: "gridlines", required: false }] }], hoverable: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverable", required: false }] }], sortable: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortable", required: false }] }], paginator: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginator", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], rowsPerPageOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageOptions", required: false }] }], showCurrentPageReport: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCurrentPageReport", required: false }] }], currentPageReportTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPageReportTemplate", required: false }] }], rowsPerPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageLabel", required: false }] }], paginationLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationLabel", required: false }] }], firstPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstPageLabel", required: false }] }], previousPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousPageLabel", required: false }] }], nextPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextPageLabel", required: false }] }], lastPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lastPageLabel", required: false }] }], pageLinkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageLinkLabel", required: false }] }], globalFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilter", required: false }] }], globalFilterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterPlaceholder", required: false }] }], globalFilterAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterAriaLabel", required: false }] }], globalFilterFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterFields", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }, { type: i0.Output, args: ["selectionChange"] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], caption: [{ type: i0.Input, args: [{ isSignal: true, alias: "caption", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], scrollHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollHeight", required: false }] }], lazy: [{ type: i0.Input, args: [{ isSignal: true, alias: "lazy", required: false }] }], totalRecords: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalRecords", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], resizableColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizableColumns", required: false }] }], reorderableColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderableColumns", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }], editOn: [{ type: i0.Input, args: [{ isSignal: true, alias: "editOn", required: false }] }], cellErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellErrors", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], dirtyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirtyLabel", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], rowSelect: [{ type: i0.Output, args: ["rowSelect"] }], rowUnselect: [{ type: i0.Output, args: ["rowUnselect"] }], lazyLoad: [{ type: i0.Output, args: ["lazyLoad"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowExpand: [{ type: i0.Output, args: ["rowExpand"] }], rowCollapse: [{ type: i0.Output, args: ["rowCollapse"] }], colReorder: [{ type: i0.Output, args: ["colReorder"] }], cellCommit: [{ type: i0.Output, args: ["cellCommit"] }], cellTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SdsTableCell), { isSignal: true }] }], rowExpansion: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsTableRowExpansion), { isSignal: true }] }], numberEditor: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SdsInputNumber), { isSignal: true }] }] } });
9356
12815
 
9357
12816
  /**
9358
12817
  * A tiny transient-notification bus. Application code calls {@link success} / {@link error} / {@link info}
@@ -10579,6 +14038,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10579
14038
  *
10580
14039
  * @example
10581
14040
  * <sds-nav-item icon="home" label="Accueil" link="/" [exact]="true" />
14041
+ * <sds-nav-item icon="book-open" label="Menu" link="/menu" [badge]="3" badgeLabel="{count} issues" badgeTone="danger" />
10582
14042
  */
10583
14043
  class SdsNavItem {
10584
14044
  store = inject(SidebarStore);
@@ -10589,8 +14049,52 @@ class SdsNavItem {
10589
14049
  exact = input(false, ...(ngDevMode ? [{ debugName: "exact", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10590
14050
  /** Manual active state (combined with routerLinkActive when a link is set). */
10591
14051
  active = input(false, ...(ngDevMode ? [{ debugName: "active", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
14052
+ /**
14053
+ * Optional count shown as a pill after the label (e.g. menu issues to fix). Hidden when null or 0.
14054
+ * On the collapsed rail it sits on the icon's corner.
14055
+ */
14056
+ badge = input(null, ...(ngDevMode ? [{ debugName: "badge" }] : []));
14057
+ /** Spoken text for the badge; `{count}` is replaced. Pass a translated string. */
14058
+ badgeLabel = input('{count} pending', ...(ngDevMode ? [{ debugName: "badgeLabel" }] : []));
14059
+ /** Counts above this read as "max+" (e.g. 99+). */
14060
+ badgeMax = input(99, ...(ngDevMode ? [{ debugName: "badgeMax" }] : []));
14061
+ /** Badge colour. `danger` for things that block, `brand` (default) for a neutral count. */
14062
+ badgeTone = input('brand', ...(ngDevMode ? [{ debugName: "badgeTone" }] : []));
10592
14063
  select = output();
10593
14064
  collapsed = this.store.collapsed;
14065
+ /** The badge's spoken text, or null when there is nothing to show. */
14066
+ badgeText = computed(() => {
14067
+ const n = this.badge();
14068
+ if (n === null || n === undefined || !Number.isFinite(n) || n <= 0)
14069
+ return null;
14070
+ return this.badgeLabel().replace('{count}', String(n));
14071
+ }, ...(ngDevMode ? [{ debugName: "badgeText" }] : []));
14072
+ badgeDisplay = computed(() => {
14073
+ const n = this.badge() ?? 0;
14074
+ return n > this.badgeMax() ? `${this.badgeMax()}+` : String(n);
14075
+ }, ...(ngDevMode ? [{ debugName: "badgeDisplay" }] : []));
14076
+ /** Accessible name on the collapsed rail, where the visible label is hidden. */
14077
+ collapsedName = computed(() => {
14078
+ const text = this.badgeText();
14079
+ return text ? `${this.label()}, ${text}` : this.label();
14080
+ }, ...(ngDevMode ? [{ debugName: "collapsedName" }] : []));
14081
+ badgeClass = computed(() => {
14082
+ const tone = this.badgeTone() === 'danger'
14083
+ ? 'bg-danger text-white'
14084
+ : this.badgeTone() === 'neutral'
14085
+ ? 'bg-muted text-ink-soft border border-border'
14086
+ : this.store.appearance() === 'surface'
14087
+ ? 'bg-brand-600 text-white dark:bg-ink dark:text-surface'
14088
+ : 'bg-white text-brand-800';
14089
+ const base = 'ml-auto inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 ' +
14090
+ 'text-[11px] font-bold tabular-nums leading-none';
14091
+ // Collapsed desktop rail: a small pill pinned to the top-right of the centred icon.
14092
+ const collapsed = this.collapsed()
14093
+ ? ' lg:absolute lg:right-1.5 lg:top-0.5 lg:ml-0 lg:h-4 lg:min-w-4 lg:px-1 lg:text-[10px]' +
14094
+ (this.store.appearance() === 'surface' ? ' lg:ring-2 lg:ring-surface' : '')
14095
+ : '';
14096
+ return `${base} ${tone}${collapsed}`;
14097
+ }, ...(ngDevMode ? [{ debugName: "badgeClass" }] : []));
10594
14098
  itemClass(isActive) {
10595
14099
  // A `surface` rail is light: an active item is a brand-tinted pill with a left accent bar, brand
10596
14100
  // text and a soft shadow (matching the certicheck reference); inactive is muted ink that lifts on
@@ -10618,7 +14122,7 @@ class SdsNavItem {
10618
14122
  accentBarClass = computed(() => 'pointer-events-none absolute left-1 top-1/2 h-5 w-1 -translate-y-1/2 rounded-full ' +
10619
14123
  (this.store.appearance() === 'surface' ? 'bg-brand-500' : 'bg-white'), ...(ngDevMode ? [{ debugName: "accentBarClass" }] : []));
10620
14124
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
10621
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsNavItem, isStandalone: true, selector: "sds-nav-item", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null }, exact: { classPropertyName: "exact", publicName: "exact", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select" }, host: { classAttribute: "block" }, ngImport: i0, template: `
14125
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsNavItem, isStandalone: true, selector: "sds-nav-item", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null }, exact: { classPropertyName: "exact", publicName: "exact", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeLabel: { classPropertyName: "badgeLabel", publicName: "badgeLabel", isSignal: true, isRequired: false, transformFunction: null }, badgeMax: { classPropertyName: "badgeMax", publicName: "badgeMax", isSignal: true, isRequired: false, transformFunction: null }, badgeTone: { classPropertyName: "badgeTone", publicName: "badgeTone", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select" }, host: { classAttribute: "block" }, ngImport: i0, template: `
10622
14126
  @if (link() !== null) {
10623
14127
  <a
10624
14128
  [routerLink]="link()"
@@ -10626,8 +14130,8 @@ class SdsNavItem {
10626
14130
  #rla="routerLinkActive"
10627
14131
  [routerLinkActiveOptions]="{ exact: exact() }"
10628
14132
  [class]="itemClass(rla.isActive || active())"
10629
- [attr.title]="collapsed() ? label() : null"
10630
- [attr.aria-label]="collapsed() ? label() : null"
14133
+ [attr.title]="collapsed() ? collapsedName() : null"
14134
+ [attr.aria-label]="collapsed() ? collapsedName() : null"
10631
14135
  [attr.aria-current]="rla.isActive || active() ? 'page' : null"
10632
14136
  (click)="store.closeMobile()"
10633
14137
  >
@@ -10640,8 +14144,8 @@ class SdsNavItem {
10640
14144
  <button
10641
14145
  type="button"
10642
14146
  [class]="itemClass(active())"
10643
- [attr.title]="collapsed() ? label() : null"
10644
- [attr.aria-label]="collapsed() ? label() : null"
14147
+ [attr.title]="collapsed() ? collapsedName() : null"
14148
+ [attr.aria-label]="collapsed() ? collapsedName() : null"
10645
14149
  [attr.aria-current]="active() ? 'page' : null"
10646
14150
  (click)="select.emit(); store.closeMobile()"
10647
14151
  >
@@ -10658,6 +14162,12 @@ class SdsNavItem {
10658
14162
  }
10659
14163
  <!-- Label is hidden only on the collapsed desktop rail; always shown in the mobile drawer. -->
10660
14164
  <span class="truncate" [class]="collapsed() ? 'lg:hidden' : ''">{{ label() }}</span>
14165
+ @if (badgeText(); as text) {
14166
+ <!-- The number is drawn; the words are what is spoken ("Menu, 3 pending"). On the collapsed
14167
+ desktop rail it becomes a small corner pill on the icon. -->
14168
+ <span [class]="badgeClass()" aria-hidden="true">{{ badgeDisplay() }}</span>
14169
+ <span class="sr-only">, {{ text }}</span>
14170
+ }
10661
14171
  </ng-template>
10662
14172
  `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10663
14173
  }
@@ -10676,8 +14186,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10676
14186
  #rla="routerLinkActive"
10677
14187
  [routerLinkActiveOptions]="{ exact: exact() }"
10678
14188
  [class]="itemClass(rla.isActive || active())"
10679
- [attr.title]="collapsed() ? label() : null"
10680
- [attr.aria-label]="collapsed() ? label() : null"
14189
+ [attr.title]="collapsed() ? collapsedName() : null"
14190
+ [attr.aria-label]="collapsed() ? collapsedName() : null"
10681
14191
  [attr.aria-current]="rla.isActive || active() ? 'page' : null"
10682
14192
  (click)="store.closeMobile()"
10683
14193
  >
@@ -10690,8 +14200,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10690
14200
  <button
10691
14201
  type="button"
10692
14202
  [class]="itemClass(active())"
10693
- [attr.title]="collapsed() ? label() : null"
10694
- [attr.aria-label]="collapsed() ? label() : null"
14203
+ [attr.title]="collapsed() ? collapsedName() : null"
14204
+ [attr.aria-label]="collapsed() ? collapsedName() : null"
10695
14205
  [attr.aria-current]="active() ? 'page' : null"
10696
14206
  (click)="select.emit(); store.closeMobile()"
10697
14207
  >
@@ -10708,10 +14218,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10708
14218
  }
10709
14219
  <!-- Label is hidden only on the collapsed desktop rail; always shown in the mobile drawer. -->
10710
14220
  <span class="truncate" [class]="collapsed() ? 'lg:hidden' : ''">{{ label() }}</span>
14221
+ @if (badgeText(); as text) {
14222
+ <!-- The number is drawn; the words are what is spoken ("Menu, 3 pending"). On the collapsed
14223
+ desktop rail it becomes a small corner pill on the icon. -->
14224
+ <span [class]="badgeClass()" aria-hidden="true">{{ badgeDisplay() }}</span>
14225
+ <span class="sr-only">, {{ text }}</span>
14226
+ }
10711
14227
  </ng-template>
10712
14228
  `,
10713
14229
  }]
10714
- }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], link: [{ type: i0.Input, args: [{ isSignal: true, alias: "link", required: false }] }], exact: [{ type: i0.Input, args: [{ isSignal: true, alias: "exact", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], select: [{ type: i0.Output, args: ["select"] }] } });
14230
+ }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], link: [{ type: i0.Input, args: [{ isSignal: true, alias: "link", required: false }] }], exact: [{ type: i0.Input, args: [{ isSignal: true, alias: "exact", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], badgeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeLabel", required: false }] }], badgeMax: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeMax", required: false }] }], badgeTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeTone", required: false }] }], select: [{ type: i0.Output, args: ["select"] }] } });
10715
14231
 
10716
14232
  /**
10717
14233
  * The signed-in user's trigger and dropdown for the top bar: an avatar (with the name and email beside
@@ -11934,5 +15450,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
11934
15450
  * Generated bundle index. Do not edit.
11935
15451
  */
11936
15452
 
11937
- export { SDS_ALLERGENS, SDS_ALLERGEN_ICONS, SDS_ALLERGEN_LABELS_EN, SDS_ICONS, SDS_PHONE_COUNTRIES, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAllergenIcon, SdsAvatar, SdsBadge, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsDatePicker, SdsIcon, SdsInput, SdsInputNumber, SdsLanguageSwitcher, SdsLocalizedInput, SdsMenuItem, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsPaginator, SdsPhoneInput, SdsRadio, SdsSearchCommand, SdsSelect, SdsSelectEmpty, SdsSelectFooter, SdsSelectOption, SdsShell, SdsSidebar, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsThemeService, SdsThemeToggle, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SdsTriStateChip, SdsUserMenu, SidebarStore, addDays, addMonths, addMonthsKeepDay, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isoDay, pad2, sdsFormatNational, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth };
15453
+ 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, 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, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsThemeService, SdsThemeToggle, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SdsTriStateChip, SdsUserMenu, SidebarStore, addDays, addMonths, addMonthsKeepDay, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isoDay, pad2, sdsFileMatchesAccept, sdsFormatBytes, sdsFormatNational, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth };
11938
15454
  //# sourceMappingURL=besaitech-ng-design-system.mjs.map