@besaitech/ng-design-system 0.0.54 → 0.0.56

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.
@@ -119,6 +119,10 @@ const SDS_ICONS = {
119
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
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
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
+ // A photo that failed or is missing (sds-avatar fallbacks, photo trays).
123
+ 'image-off': '<line x1="2" x2="22" y1="2" y2="22"/><path d="M10.41 10.41a2 2 0 1 1-2.83-2.83"/><line x1="13.5" x2="6" y1="13.5" y2="21"/><line x1="18" x2="21" y1="12" y2="15"/><path d="M3.59 3.59A1.99 1.99 0 0 0 3 5v14a2 2 0 0 0 2 2h14c.55 0 1.052-.22 1.41-.59"/><path d="M21 15V5a2 2 0 0 0-2-2H9"/>',
124
+ // Fork and knife: the placeholder of a menu item without a photo.
125
+ 'utensils': '<path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2"/><path d="M7 2v20"/><path d="M21 15V2a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7"/>',
122
126
  '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
127
  '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
128
  '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"/>',
@@ -218,7 +222,7 @@ const VARIANTS$2 = {
218
222
  'dark:hover:bg-white/10 dark:active:bg-white/15',
219
223
  danger: 'bg-danger text-white shadow-sm hover:brightness-95 active:brightness-90',
220
224
  };
221
- const SIZES$b = {
225
+ const SIZES$c = {
222
226
  sm: 'h-8 px-3 text-sm',
223
227
  md: 'h-10 px-4 text-sm',
224
228
  lg: 'h-12 px-5 text-base',
@@ -278,7 +282,7 @@ class SdsButton {
278
282
  BASE,
279
283
  this.shape() === 'pill' ? 'rounded-full' : 'rounded-control',
280
284
  VARIANTS$2[this.variant()],
281
- SIZES$b[size],
285
+ SIZES$c[size],
282
286
  this.iconOnly() ? ICON_ONLY[size] : '',
283
287
  this.block() ? 'w-full' : '',
284
288
  ]
@@ -330,8 +334,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
330
334
  `, 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"] }]
331
335
  }], 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 }] }] } });
332
336
 
333
- let nextId$l = 0;
334
- const SIZES$a = {
337
+ let nextId$n = 0;
338
+ const SIZES$b = {
335
339
  sm: 'h-9 text-sm',
336
340
  md: 'h-10 text-sm',
337
341
  lg: 'h-12 text-base',
@@ -343,7 +347,7 @@ const SIZES$a = {
343
347
  * sidebar search field).
344
348
  */
345
349
  class SdsInput {
346
- inputId = `sds-input-${nextId$l++}`;
350
+ inputId = `sds-input-${nextId$n++}`;
347
351
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
348
352
  /** Accessible name used when no visible [label] is provided. */
349
353
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -427,7 +431,7 @@ class SdsInput {
427
431
  return `${left} ${right}`;
428
432
  }, ...(ngDevMode ? [{ debugName: "fieldPadding" }] : []));
429
433
  wrapperClasses = computed(() => {
430
- const parts = [SIZES$a[this.size()], this.pill() ? 'rounded-full' : 'rounded-control'];
434
+ const parts = [SIZES$b[this.size()], this.pill() ? 'rounded-full' : 'rounded-control'];
431
435
  if (this.isDisabled())
432
436
  parts.push('opacity-60 cursor-not-allowed');
433
437
  if (this.filled()) {
@@ -675,7 +679,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
675
679
  `, 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"] }]
676
680
  }], 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"] }] } });
677
681
 
678
- let nextId$k = 0;
682
+ let nextId$m = 0;
679
683
  const HEIGHTS = {
680
684
  sm: 'h-9',
681
685
  md: 'h-10',
@@ -703,7 +707,7 @@ const TEXT = {
703
707
  * with "." ("1.234" in nl-BE) stays a thousands separator.
704
708
  */
705
709
  class SdsInputNumber {
706
- inputId = `sds-input-number-${nextId$k++}`;
710
+ inputId = `sds-input-number-${nextId$m++}`;
707
711
  fieldRef = viewChild('field', ...(ngDevMode ? [{ debugName: "fieldRef" }] : []));
708
712
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
709
713
  /** Accessible name used when no visible [label] is provided. */
@@ -1470,108 +1474,123 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
1470
1474
  }]
1471
1475
  }], ctorParameters: () => [], propDecorators: { fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedby", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], currencyDisplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "currencyDisplay", required: false }] }], useGrouping: [{ type: i0.Input, args: [{ isSignal: true, alias: "useGrouping", required: false }] }], minFractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "minFractionDigits", required: false }] }], maxFractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFractionDigits", required: false }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], suffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffix", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], showButtons: [{ type: i0.Input, args: [{ isSignal: true, alias: "showButtons", required: false }] }], buttonLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonLayout", required: false }] }], incrementIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "incrementIcon", required: false }] }], decrementIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "decrementIcon", required: false }] }], allowEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowEmpty", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], incrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "incrementLabel", required: false }] }], decrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "decrementLabel", required: false }] }] } });
1472
1476
 
1473
- let nextId$j = 0;
1474
- const SIZES$9 = {
1475
- sm: 'min-h-9 text-sm',
1476
- md: 'min-h-10 text-sm',
1477
- lg: 'min-h-12 text-base',
1477
+ let nextId$l = 0;
1478
+ const SIZES$a = {
1479
+ sm: 'h-9 text-sm',
1480
+ md: 'h-10 text-sm',
1481
+ lg: 'h-12 text-base',
1478
1482
  };
1483
+ const pad2$1 = (n) => String(n).padStart(2, '0');
1484
+ /** Parse `HH:mm` or `HH:mm:ss` (the seconds are ignored). Anything else is null. */
1485
+ function parseTime(value) {
1486
+ if (typeof value !== 'string')
1487
+ return null;
1488
+ const match = /^(\d{1,2}):(\d{2})(?::\d{2}(?:\.\d+)?)?$/.exec(value.trim());
1489
+ if (!match)
1490
+ return null;
1491
+ const h = Number(match[1]);
1492
+ const m = Number(match[2]);
1493
+ if (h > 23 || m > 59)
1494
+ return null;
1495
+ return { h, m };
1496
+ }
1479
1497
  /**
1480
- * One language-aware text field for editing a translated value (a {@link SdsLocalizedValue} — a title,
1481
- * label or description in several languages). Only one language is edited at a time; a compact switcher
1482
- * sitting **inside the field** flips which translation you are editing, and a dot marks the languages that
1483
- * already have content, so a partially-translated value is obvious at a glance.
1498
+ * Time-of-day field, 24-hour, whatever the browser's locale: two segments (hours : minutes) in one
1499
+ * framed field, the value being a `"HH:mm"` string (or null) — the shape of a backend `LocalTime`.
1500
+ * Implements ControlValueAccessor: use `formControlName` or `[(ngModel)]`.
1484
1501
  *
1485
- * The active language defaults to {@link currentLanguage} when it is one of {@link languages}, otherwise the
1486
- * first accepted language — so the field "opens on the language you are working in". Empty translations are
1487
- * pruned from the emitted value, keeping the stored map clean.
1502
+ * Unlike a native `<input type="time">` it never switches to a 12-hour clock with AM/PM, and it is themed
1503
+ * like every other field (label, hint, error, sizes, `filled`, dark mode).
1488
1504
  *
1489
- * Implements ControlValueAccessor over the map — use with reactive forms or `[(ngModel)]`:
1490
- * `<sds-localized-input [languages]="['fr','nl','en']" [(ngModel)]="title" />`.
1505
+ * Keyboard: type digits (the hours move on to the minutes by themselves after two digits, or one digit
1506
+ * above 2); `:`/`.`/→ jump to the minutes, ← or Backspace on an empty minute go back; ↑/↓ step the hours
1507
+ * by 1 and the minutes by [minuteStep] (wrapping, never carrying into the hour). Each segment is a
1508
+ * `spinbutton` with its own accessible name. An hour typed without minutes gets `:00` when the field is
1509
+ * left; emptying both segments sets null.
1510
+ *
1511
+ * @example
1512
+ * <sds-time-input label="Opens at" formControlName="start" [minuteStep]="15" />
1513
+ *
1514
+ * @example
1515
+ * <sds-time-input [ariaLabel]="'Closes at'" [ngModel]="end()" (ngModelChange)="end.set($event)" clearable />
1491
1516
  */
1492
- class SdsLocalizedInput {
1493
- inputId = `sds-localized-input-${nextId$j++}`;
1517
+ class SdsTimeInput {
1518
+ fieldId = `sds-time-input-${nextId$l++}`;
1494
1519
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
1520
+ /** Accessible name of the field when there is no visible [label]. */
1521
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
1495
1522
  hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
1496
- placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
1497
- /** The accepted language codes, in display order (e.g. `['fr','nl','en']`). */
1498
- languages = input([], ...(ngDevMode ? [{ debugName: "languages" }] : []));
1499
- /** Language to pre-select; used when it is one of {@link languages}, else the first accepted language. */
1500
- currentLanguage = input(null, ...(ngDevMode ? [{ debugName: "currentLanguage" }] : []));
1501
- multiline = input(false, ...(ngDevMode ? [{ debugName: "multiline", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
1502
- rows = input(3, ...(ngDevMode ? [{ debugName: "rows", transform: numberAttribute }] : [{ transform: numberAttribute }]));
1523
+ error = input('', ...(ngDevMode ? [{ debugName: "error" }] : []));
1503
1524
  size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
1525
+ /** `filled` is borderless on a muted ground, like sds-input's `filled`. */
1526
+ appearance = input('default', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
1504
1527
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
1505
- /**
1506
- * Accessible name for the in-field language switcher, and the "no translation yet" title suffix on each
1507
- * language button. English defaults; translate them in a multi-language app (the buttons are
1508
- * letter-only, so the title carries their meaning).
1509
- */
1510
- languageSwitcherLabel = input('Language', ...(ngDevMode ? [{ debugName: "languageSwitcherLabel" }] : []));
1511
- untranslatedLabel = input('not translated yet', ...(ngDevMode ? [{ debugName: "untranslatedLabel" }] : []));
1512
- valueMap = signal({}, ...(ngDevMode ? [{ debugName: "valueMap" }] : []));
1513
- /** Language chosen via the switcher; null means "follow currentLanguage / the first language". */
1514
- manual = signal(null, ...(ngDevMode ? [{ debugName: "manual" }] : []));
1528
+ readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
1529
+ /** Show a clear button (sets null) while there is a value. */
1530
+ clearable = input(false, ...(ngDevMode ? [{ debugName: "clearable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
1531
+ /** Leading clock icon. */
1532
+ showIcon = input(true, ...(ngDevMode ? [{ debugName: "showIcon", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
1533
+ /** Minutes moved by ↑/↓ on the minutes segment (snaps to the step first). Default 1. */
1534
+ minuteStep = input(1, ...(ngDevMode ? [{ debugName: "minuteStep", transform: numberAttribute }] : [{ transform: numberAttribute }]));
1535
+ /** Emit `HH:mm:ss` (seconds always `00`) instead of `HH:mm`. */
1536
+ withSeconds = input(false, ...(ngDevMode ? [{ debugName: "withSeconds", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
1537
+ /** Placeholder of each empty segment. */
1538
+ placeholderText = input('--', ...(ngDevMode ? [{ debugName: "placeholderText" }] : []));
1539
+ /** Accessible name of the hours segment. English default — translate it. */
1540
+ hoursLabel = input('Hours', ...(ngDevMode ? [{ debugName: "hoursLabel" }] : []));
1541
+ /** Accessible name of the minutes segment. English default — translate it. */
1542
+ minutesLabel = input('Minutes', ...(ngDevMode ? [{ debugName: "minutesLabel" }] : []));
1543
+ /** Accessible name of the clear button. English default — translate it. */
1544
+ clearLabel = input('Clear time', ...(ngDevMode ? [{ debugName: "clearLabel" }] : []));
1545
+ hoursEl = viewChild.required('hoursEl');
1546
+ minutesEl = viewChild.required('minutesEl');
1547
+ hours = signal(null, ...(ngDevMode ? [{ debugName: "hours" }] : []));
1548
+ minutes = signal(null, ...(ngDevMode ? [{ debugName: "minutes" }] : []));
1549
+ /** Raw text of a segment while it is being typed (null = show the committed value). */
1550
+ hoursDraft = signal(null, ...(ngDevMode ? [{ debugName: "hoursDraft" }] : []));
1551
+ minutesDraft = signal(null, ...(ngDevMode ? [{ debugName: "minutesDraft" }] : []));
1515
1552
  cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
1516
1553
  isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
1554
+ hasValue = computed(() => this.hours() !== null || this.minutes() !== null, ...(ngDevMode ? [{ debugName: "hasValue" }] : []));
1555
+ hoursText = computed(() => this.hoursDraft() ?? (this.hours() === null ? '' : pad2$1(this.hours())), ...(ngDevMode ? [{ debugName: "hoursText" }] : []));
1556
+ minutesText = computed(() => this.minutesDraft() ?? (this.minutes() === null ? '' : pad2$1(this.minutes())), ...(ngDevMode ? [{ debugName: "minutesText" }] : []));
1557
+ describedBy = computed(() => {
1558
+ if (this.error())
1559
+ return `${this.fieldId}-err`;
1560
+ if (this.hint())
1561
+ return `${this.fieldId}-hint`;
1562
+ return null;
1563
+ }, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
1517
1564
  wrapperClasses = computed(() => {
1518
- const parts = [SIZES$9[this.size()]];
1519
- parts.push(this.isDisabled()
1520
- ? 'border-border opacity-60 cursor-not-allowed'
1521
- : 'border-border focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25');
1522
- return parts.join(' ');
1523
- }, ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
1524
- /** The language currently being edited. */
1525
- activeLang = computed(() => {
1526
- const langs = this.languages();
1527
- if (!langs.length) {
1528
- return '';
1529
- }
1530
- const manual = this.manual();
1531
- if (manual && langs.includes(manual)) {
1532
- return manual;
1533
- }
1534
- const preferred = this.currentLanguage();
1535
- return preferred && langs.includes(preferred) ? preferred : langs[0];
1536
- }, ...(ngDevMode ? [{ debugName: "activeLang" }] : []));
1537
- current = computed(() => this.valueMap()[this.activeLang()] ?? '', ...(ngDevMode ? [{ debugName: "current" }] : []));
1538
- filled(lang) {
1539
- const text = this.valueMap()[lang];
1540
- return !!text && text.trim().length > 0;
1541
- }
1542
- fieldPlaceholder() {
1543
- const base = this.placeholder();
1544
- const lang = this.activeLang().toUpperCase();
1545
- if (base) {
1546
- return lang ? `${base} (${lang})` : base;
1547
- }
1548
- return lang ? `(${lang})` : '';
1549
- }
1550
- select(lang) {
1551
- this.manual.set(lang);
1552
- this.onTouched();
1553
- }
1554
- write(event) {
1555
- const lang = this.activeLang();
1556
- if (!lang) {
1557
- return;
1558
- }
1559
- const text = event.target.value;
1560
- const next = { ...this.valueMap() };
1561
- if (text) {
1562
- next[lang] = text;
1565
+ const parts = [SIZES$a[this.size()], 'rounded-control'];
1566
+ if (this.isDisabled())
1567
+ parts.push('opacity-60 cursor-not-allowed');
1568
+ if (this.appearance() === 'filled') {
1569
+ parts.push('border-transparent bg-muted');
1570
+ parts.push(this.error()
1571
+ ? 'border-danger focus-within:ring-2 focus-within:ring-danger/30'
1572
+ : 'focus-within:ring-2 focus-within:ring-brand-500/25');
1563
1573
  }
1564
1574
  else {
1565
- delete next[lang];
1575
+ parts.push('bg-surface');
1576
+ parts.push(this.error()
1577
+ ? 'border-danger focus-within:border-danger focus-within:ring-2 focus-within:ring-danger/25'
1578
+ : 'border-border focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25');
1566
1579
  }
1567
- this.valueMap.set(next);
1568
- this.onChange(next);
1580
+ return parts.join(' ');
1581
+ }, ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
1582
+ pad(n) {
1583
+ return pad2$1(n);
1569
1584
  }
1570
1585
  // --- ControlValueAccessor -------------------------------------------------
1571
1586
  onChange = () => { };
1572
1587
  onTouched = () => { };
1573
1588
  writeValue(value) {
1574
- this.valueMap.set(value ? { ...value } : {});
1589
+ const t = parseTime(value);
1590
+ this.hours.set(t ? t.h : null);
1591
+ this.minutes.set(t ? t.m : null);
1592
+ this.hoursDraft.set(null);
1593
+ this.minutesDraft.set(null);
1575
1594
  }
1576
1595
  registerOnChange(fn) {
1577
1596
  this.onChange = fn;
@@ -1582,44 +1601,599 @@ class SdsLocalizedInput {
1582
1601
  setDisabledState(isDisabled) {
1583
1602
  this.cvaDisabled.set(isDisabled);
1584
1603
  }
1585
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsLocalizedInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
1586
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsLocalizedInput, isStandalone: true, selector: "sds-localized-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, languages: { classPropertyName: "languages", publicName: "languages", isSignal: true, isRequired: false, transformFunction: null }, currentLanguage: { classPropertyName: "currentLanguage", publicName: "currentLanguage", isSignal: true, isRequired: false, transformFunction: null }, multiline: { classPropertyName: "multiline", publicName: "multiline", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, languageSwitcherLabel: { classPropertyName: "languageSwitcherLabel", publicName: "languageSwitcherLabel", isSignal: true, isRequired: false, transformFunction: null }, untranslatedLabel: { classPropertyName: "untranslatedLabel", publicName: "untranslatedLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, providers: [
1587
- {
1588
- provide: NG_VALUE_ACCESSOR,
1589
- useExisting: forwardRef(() => SdsLocalizedInput),
1590
- multi: true,
1591
- },
1592
- ], ngImport: i0, template: `
1604
+ // --- typing ---------------------------------------------------------------
1605
+ selectAll(event) {
1606
+ const el = event.target;
1607
+ queueMicrotask(() => el.select());
1608
+ }
1609
+ /** Clicking the frame (icon, padding) focuses the hours, like clicking a native field. */
1610
+ onFrameClick(event) {
1611
+ if (this.isDisabled())
1612
+ return;
1613
+ const target = event.target;
1614
+ if (target.tagName !== 'INPUT' && !target.closest('button'))
1615
+ this.hoursEl().nativeElement.focus();
1616
+ }
1617
+ onHoursInput(event) {
1618
+ const el = event.target;
1619
+ const text = el.value;
1620
+ // A pasted/autofilled full time ("09:45", "9.45", "9h45") fills both segments.
1621
+ if (/[:.h]/i.test(text)) {
1622
+ const t = parseTime(text.replace(/[.h]/i, ':'));
1623
+ if (t) {
1624
+ this.commitHours(t.h);
1625
+ this.commitMinutes(t.m);
1626
+ this.syncSegments();
1627
+ this.focusMinutes();
1628
+ return;
1629
+ }
1630
+ }
1631
+ const digits = text.replace(/\D/g, '');
1632
+ if (digits === '') {
1633
+ el.value = '';
1634
+ this.hoursDraft.set('');
1635
+ this.hours.set(null);
1636
+ this.emitIfComplete();
1637
+ return;
1638
+ }
1639
+ // How many leading digits make the hour: "9" is 09 (no hour starts with 3-9 and has a second digit);
1640
+ // "1"/"2" wait for a second digit; "25" is 2 then 5 minutes. Whatever follows spills into minutes, so
1641
+ // typing "945" in one go (or a fast/IME/autofill burst) still reads as 09:45.
1642
+ let hourLength;
1643
+ if (Number(digits[0]) > 2)
1644
+ hourLength = 1;
1645
+ else if (digits.length === 1)
1646
+ hourLength = 0;
1647
+ else
1648
+ hourLength = Number(digits.slice(0, 2)) <= 23 ? 2 : 1;
1649
+ if (hourLength === 0) {
1650
+ el.value = digits;
1651
+ this.hoursDraft.set(digits);
1652
+ return;
1653
+ }
1654
+ this.commitHours(Number(digits.slice(0, hourLength)));
1655
+ el.value = this.hoursText();
1656
+ const rest = digits.slice(hourLength, hourLength + 2);
1657
+ if (rest)
1658
+ this.takeMinuteDigits(rest);
1659
+ this.focusMinutes();
1660
+ }
1661
+ onMinutesInput(event) {
1662
+ const el = event.target;
1663
+ const digits = el.value.replace(/\D/g, '').slice(0, 2);
1664
+ if (digits === '') {
1665
+ el.value = '';
1666
+ this.minutesDraft.set('');
1667
+ this.minutes.set(null);
1668
+ this.emitIfComplete();
1669
+ return;
1670
+ }
1671
+ this.takeMinuteDigits(digits);
1672
+ el.value = this.minutesText();
1673
+ }
1674
+ /** "7" (no minute starts with 6-9 and has a second digit) and any 2 digits commit; "0"-"5" wait. */
1675
+ takeMinuteDigits(digits) {
1676
+ if (digits.length >= 2 || Number(digits) > 5) {
1677
+ this.commitMinutes(Math.min(59, Number(digits.slice(0, 2))));
1678
+ }
1679
+ else {
1680
+ this.minutesDraft.set(digits);
1681
+ }
1682
+ this.minutesEl().nativeElement.value = this.minutesText();
1683
+ }
1684
+ /** Force both inputs to show the committed values (a binding does not repaint an unchanged value). */
1685
+ syncSegments() {
1686
+ this.hoursEl().nativeElement.value = this.hoursText();
1687
+ this.minutesEl().nativeElement.value = this.minutesText();
1688
+ }
1689
+ onHoursKeydown(event) {
1690
+ if (this.readonly())
1691
+ return;
1692
+ const el = event.target;
1693
+ switch (event.key) {
1694
+ case 'ArrowUp':
1695
+ case 'ArrowDown': {
1696
+ event.preventDefault();
1697
+ const delta = event.key === 'ArrowUp' ? 1 : -1;
1698
+ const current = this.draftNumber(this.hoursDraft()) ?? this.hours();
1699
+ const next = current === null ? (delta > 0 ? 0 : 23) : (current + delta + 24) % 24;
1700
+ this.commitHours(next);
1701
+ this.syncSegments();
1702
+ break;
1703
+ }
1704
+ case ':':
1705
+ case '.':
1706
+ case 'h':
1707
+ event.preventDefault();
1708
+ this.flushHours();
1709
+ this.syncSegments();
1710
+ this.focusMinutes();
1711
+ break;
1712
+ case 'ArrowRight':
1713
+ if (el.selectionStart === el.value.length) {
1714
+ event.preventDefault();
1715
+ this.flushHours();
1716
+ this.focusMinutes();
1717
+ }
1718
+ break;
1719
+ }
1720
+ }
1721
+ onMinutesKeydown(event) {
1722
+ if (this.readonly())
1723
+ return;
1724
+ const el = event.target;
1725
+ switch (event.key) {
1726
+ case 'ArrowUp':
1727
+ case 'ArrowDown': {
1728
+ event.preventDefault();
1729
+ const step = Math.max(1, Math.min(60, this.minuteStep()));
1730
+ const current = this.draftNumber(this.minutesDraft()) ?? this.minutes();
1731
+ let next;
1732
+ if (current === null) {
1733
+ next = event.key === 'ArrowUp' ? 0 : 60 - step;
1734
+ }
1735
+ else if (event.key === 'ArrowUp') {
1736
+ next = (Math.floor(current / step) + 1) * step;
1737
+ }
1738
+ else {
1739
+ next = current % step === 0 ? current - step : Math.floor(current / step) * step;
1740
+ }
1741
+ this.commitMinutes(((next % 60) + 60) % 60);
1742
+ this.syncSegments();
1743
+ break;
1744
+ }
1745
+ case 'Backspace':
1746
+ if (el.value === '') {
1747
+ event.preventDefault();
1748
+ this.focusHours();
1749
+ }
1750
+ break;
1751
+ case 'ArrowLeft':
1752
+ if (el.selectionStart === 0 && el.selectionEnd === 0) {
1753
+ event.preventDefault();
1754
+ this.flushMinutes();
1755
+ this.focusHours();
1756
+ }
1757
+ break;
1758
+ }
1759
+ }
1760
+ /** Leaving the whole field completes a half-typed time and marks the control touched. */
1761
+ onFocusOut(event) {
1762
+ const next = event.relatedTarget;
1763
+ const group = event.currentTarget;
1764
+ if (next && group.contains(next))
1765
+ return;
1766
+ this.flushHours();
1767
+ this.flushMinutes();
1768
+ if (this.hours() !== null && this.minutes() === null)
1769
+ this.commitMinutes(0);
1770
+ else if (this.hours() === null && this.minutes() !== null)
1771
+ this.commitHours(0);
1772
+ this.syncSegments();
1773
+ this.onTouched();
1774
+ }
1775
+ clear() {
1776
+ this.hours.set(null);
1777
+ this.minutes.set(null);
1778
+ this.hoursDraft.set(null);
1779
+ this.minutesDraft.set(null);
1780
+ this.onChange(null);
1781
+ this.onTouched();
1782
+ }
1783
+ // --- internals ------------------------------------------------------------
1784
+ draftNumber(draft) {
1785
+ return draft === null || draft === '' ? null : Number(draft);
1786
+ }
1787
+ flushHours() {
1788
+ const d = this.hoursDraft();
1789
+ if (d !== null && d !== '')
1790
+ this.commitHours(Math.min(23, Number(d)));
1791
+ else
1792
+ this.hoursDraft.set(null);
1793
+ }
1794
+ flushMinutes() {
1795
+ const d = this.minutesDraft();
1796
+ if (d !== null && d !== '')
1797
+ this.commitMinutes(Math.min(59, Number(d)));
1798
+ else
1799
+ this.minutesDraft.set(null);
1800
+ }
1801
+ commitHours(h) {
1802
+ this.hours.set(h);
1803
+ this.hoursDraft.set(null);
1804
+ this.emitIfComplete();
1805
+ }
1806
+ commitMinutes(m) {
1807
+ this.minutes.set(m);
1808
+ this.minutesDraft.set(null);
1809
+ this.emitIfComplete();
1810
+ }
1811
+ /** Emits `HH:mm` once both segments hold a value, and null once both are empty. */
1812
+ emitIfComplete() {
1813
+ const h = this.hours();
1814
+ const m = this.minutes();
1815
+ if (h !== null && m !== null) {
1816
+ this.onChange(`${pad2$1(h)}:${pad2$1(m)}${this.withSeconds() ? ':00' : ''}`);
1817
+ }
1818
+ else if (h === null && m === null) {
1819
+ this.onChange(null);
1820
+ }
1821
+ }
1822
+ focusMinutes() {
1823
+ const el = this.minutesEl().nativeElement;
1824
+ el.focus();
1825
+ el.select();
1826
+ }
1827
+ focusHours() {
1828
+ const el = this.hoursEl().nativeElement;
1829
+ el.focus();
1830
+ el.select();
1831
+ }
1832
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTimeInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
1833
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsTimeInput, isStandalone: true, selector: "sds-time-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, showIcon: { classPropertyName: "showIcon", publicName: "showIcon", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null }, withSeconds: { classPropertyName: "withSeconds", publicName: "withSeconds", isSignal: true, isRequired: false, transformFunction: null }, placeholderText: { classPropertyName: "placeholderText", publicName: "placeholderText", isSignal: true, isRequired: false, transformFunction: null }, hoursLabel: { classPropertyName: "hoursLabel", publicName: "hoursLabel", isSignal: true, isRequired: false, transformFunction: null }, minutesLabel: { classPropertyName: "minutesLabel", publicName: "minutesLabel", isSignal: true, isRequired: false, transformFunction: null }, clearLabel: { classPropertyName: "clearLabel", publicName: "clearLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, providers: [
1834
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SdsTimeInput), multi: true },
1835
+ ], viewQueries: [{ propertyName: "hoursEl", first: true, predicate: ["hoursEl"], descendants: true, isSignal: true }, { propertyName: "minutesEl", first: true, predicate: ["minutesEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
1593
1836
  @if (label()) {
1594
- <label [for]="inputId" class="mb-1.5 block text-sm font-medium text-ink">{{ label() }}</label>
1837
+ <label
1838
+ [id]="fieldId + '-label'"
1839
+ [for]="fieldId + '-h'"
1840
+ class="mb-1.5 block text-sm font-medium text-ink"
1841
+ >
1842
+ {{ label() }}
1843
+ </label>
1595
1844
  }
1596
1845
 
1597
1846
  <div
1598
- class="relative flex items-stretch rounded-control border bg-surface text-ink transition-colors"
1847
+ role="group"
1848
+ [attr.aria-labelledby]="label() ? fieldId + '-label' : null"
1849
+ [attr.aria-label]="!label() && ariaLabel() ? ariaLabel() : null"
1850
+ [attr.aria-describedby]="describedBy()"
1851
+ class="relative flex items-center border transition-colors"
1599
1852
  [class]="wrapperClasses()"
1853
+ (focusout)="onFocusOut($event)"
1854
+ (click)="onFrameClick($event)"
1600
1855
  >
1601
- @if (multiline()) {
1602
- <textarea
1603
- [id]="inputId"
1604
- [rows]="rows()"
1605
- [placeholder]="fieldPlaceholder()"
1606
- [disabled]="isDisabled()"
1607
- [value]="current()"
1608
- (input)="write($event)"
1609
- (blur)="onTouched()"
1610
- class="min-w-0 flex-1 resize-y bg-transparent px-3.5 py-2 outline-none placeholder:text-current/50 disabled:cursor-not-allowed"
1611
- ></textarea>
1612
- } @else {
1856
+ @if (showIcon()) {
1857
+ <sds-icon
1858
+ name="clock"
1859
+ [size]="16"
1860
+ class="pointer-events-none ml-3 shrink-0 text-ink-faint"
1861
+ />
1862
+ }
1863
+ <div class="flex items-center tabular-nums" [class]="showIcon() ? 'pl-2' : 'pl-3.5'">
1613
1864
  <input
1614
- [id]="inputId"
1615
- [placeholder]="fieldPlaceholder()"
1865
+ #hoursEl
1866
+ [id]="fieldId + '-h'"
1867
+ type="text"
1868
+ inputmode="numeric"
1869
+ autocomplete="off"
1870
+ maxlength="5"
1871
+ role="spinbutton"
1872
+ aria-valuemin="0"
1873
+ aria-valuemax="23"
1874
+ [attr.aria-valuenow]="hours()"
1875
+ [attr.aria-valuetext]="hours() === null ? null : pad(hours()!)"
1876
+ [attr.aria-label]="hoursLabel()"
1877
+ [attr.aria-invalid]="error() ? 'true' : null"
1878
+ [attr.aria-describedby]="describedBy()"
1879
+ [value]="hoursText()"
1880
+ [placeholder]="placeholderText()"
1616
1881
  [disabled]="isDisabled()"
1617
- [value]="current()"
1618
- (input)="write($event)"
1619
- (blur)="onTouched()"
1620
- class="min-w-0 flex-1 bg-transparent px-3.5 outline-none placeholder:text-current/50 disabled:cursor-not-allowed"
1882
+ [readOnly]="readonly()"
1883
+ class="w-[2.6ch] bg-transparent text-center text-ink outline-none placeholder:text-ink-faint disabled:cursor-not-allowed"
1884
+ (focus)="selectAll($event)"
1885
+ (input)="onHoursInput($event)"
1886
+ (keydown)="onHoursKeydown($event)"
1621
1887
  />
1622
- }
1888
+ <span class="px-px text-ink-soft" aria-hidden="true">:</span>
1889
+ <input
1890
+ #minutesEl
1891
+ [id]="fieldId + '-m'"
1892
+ type="text"
1893
+ inputmode="numeric"
1894
+ autocomplete="off"
1895
+ maxlength="2"
1896
+ role="spinbutton"
1897
+ aria-valuemin="0"
1898
+ aria-valuemax="59"
1899
+ [attr.aria-valuenow]="minutes()"
1900
+ [attr.aria-valuetext]="minutes() === null ? null : pad(minutes()!)"
1901
+ [attr.aria-label]="minutesLabel()"
1902
+ [attr.aria-invalid]="error() ? 'true' : null"
1903
+ [attr.aria-describedby]="describedBy()"
1904
+ [value]="minutesText()"
1905
+ [placeholder]="placeholderText()"
1906
+ [disabled]="isDisabled()"
1907
+ [readOnly]="readonly()"
1908
+ class="w-[2.6ch] bg-transparent text-center text-ink outline-none placeholder:text-ink-faint disabled:cursor-not-allowed"
1909
+ (focus)="selectAll($event)"
1910
+ (input)="onMinutesInput($event)"
1911
+ (keydown)="onMinutesKeydown($event)"
1912
+ />
1913
+ </div>
1914
+
1915
+ @if (clearable() && hasValue() && !isDisabled() && !readonly()) {
1916
+ <button
1917
+ type="button"
1918
+ tabindex="-1"
1919
+ [attr.aria-label]="clearLabel()"
1920
+ class="ml-auto mr-2 inline-flex h-6 w-6 items-center justify-center rounded-full text-ink-faint hover:bg-black/5 hover:text-ink-soft dark:hover:bg-white/10"
1921
+ (click)="clear()"
1922
+ >
1923
+ <sds-icon name="x" [size]="15" />
1924
+ </button>
1925
+ }
1926
+ </div>
1927
+
1928
+ @if (error()) {
1929
+ <p [id]="fieldId + '-err'" class="mt-1.5 text-xs font-medium text-danger">{{ error() }}</p>
1930
+ } @else if (hint()) {
1931
+ <p [id]="fieldId + '-hint'" class="mt-1.5 text-xs text-ink-faint">{{ hint() }}</p>
1932
+ }
1933
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1934
+ }
1935
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTimeInput, decorators: [{
1936
+ type: Component,
1937
+ args: [{
1938
+ selector: 'sds-time-input',
1939
+ changeDetection: ChangeDetectionStrategy.OnPush,
1940
+ imports: [SdsIcon],
1941
+ providers: [
1942
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SdsTimeInput), multi: true },
1943
+ ],
1944
+ host: { class: 'block' },
1945
+ template: `
1946
+ @if (label()) {
1947
+ <label
1948
+ [id]="fieldId + '-label'"
1949
+ [for]="fieldId + '-h'"
1950
+ class="mb-1.5 block text-sm font-medium text-ink"
1951
+ >
1952
+ {{ label() }}
1953
+ </label>
1954
+ }
1955
+
1956
+ <div
1957
+ role="group"
1958
+ [attr.aria-labelledby]="label() ? fieldId + '-label' : null"
1959
+ [attr.aria-label]="!label() && ariaLabel() ? ariaLabel() : null"
1960
+ [attr.aria-describedby]="describedBy()"
1961
+ class="relative flex items-center border transition-colors"
1962
+ [class]="wrapperClasses()"
1963
+ (focusout)="onFocusOut($event)"
1964
+ (click)="onFrameClick($event)"
1965
+ >
1966
+ @if (showIcon()) {
1967
+ <sds-icon
1968
+ name="clock"
1969
+ [size]="16"
1970
+ class="pointer-events-none ml-3 shrink-0 text-ink-faint"
1971
+ />
1972
+ }
1973
+ <div class="flex items-center tabular-nums" [class]="showIcon() ? 'pl-2' : 'pl-3.5'">
1974
+ <input
1975
+ #hoursEl
1976
+ [id]="fieldId + '-h'"
1977
+ type="text"
1978
+ inputmode="numeric"
1979
+ autocomplete="off"
1980
+ maxlength="5"
1981
+ role="spinbutton"
1982
+ aria-valuemin="0"
1983
+ aria-valuemax="23"
1984
+ [attr.aria-valuenow]="hours()"
1985
+ [attr.aria-valuetext]="hours() === null ? null : pad(hours()!)"
1986
+ [attr.aria-label]="hoursLabel()"
1987
+ [attr.aria-invalid]="error() ? 'true' : null"
1988
+ [attr.aria-describedby]="describedBy()"
1989
+ [value]="hoursText()"
1990
+ [placeholder]="placeholderText()"
1991
+ [disabled]="isDisabled()"
1992
+ [readOnly]="readonly()"
1993
+ class="w-[2.6ch] bg-transparent text-center text-ink outline-none placeholder:text-ink-faint disabled:cursor-not-allowed"
1994
+ (focus)="selectAll($event)"
1995
+ (input)="onHoursInput($event)"
1996
+ (keydown)="onHoursKeydown($event)"
1997
+ />
1998
+ <span class="px-px text-ink-soft" aria-hidden="true">:</span>
1999
+ <input
2000
+ #minutesEl
2001
+ [id]="fieldId + '-m'"
2002
+ type="text"
2003
+ inputmode="numeric"
2004
+ autocomplete="off"
2005
+ maxlength="2"
2006
+ role="spinbutton"
2007
+ aria-valuemin="0"
2008
+ aria-valuemax="59"
2009
+ [attr.aria-valuenow]="minutes()"
2010
+ [attr.aria-valuetext]="minutes() === null ? null : pad(minutes()!)"
2011
+ [attr.aria-label]="minutesLabel()"
2012
+ [attr.aria-invalid]="error() ? 'true' : null"
2013
+ [attr.aria-describedby]="describedBy()"
2014
+ [value]="minutesText()"
2015
+ [placeholder]="placeholderText()"
2016
+ [disabled]="isDisabled()"
2017
+ [readOnly]="readonly()"
2018
+ class="w-[2.6ch] bg-transparent text-center text-ink outline-none placeholder:text-ink-faint disabled:cursor-not-allowed"
2019
+ (focus)="selectAll($event)"
2020
+ (input)="onMinutesInput($event)"
2021
+ (keydown)="onMinutesKeydown($event)"
2022
+ />
2023
+ </div>
2024
+
2025
+ @if (clearable() && hasValue() && !isDisabled() && !readonly()) {
2026
+ <button
2027
+ type="button"
2028
+ tabindex="-1"
2029
+ [attr.aria-label]="clearLabel()"
2030
+ class="ml-auto mr-2 inline-flex h-6 w-6 items-center justify-center rounded-full text-ink-faint hover:bg-black/5 hover:text-ink-soft dark:hover:bg-white/10"
2031
+ (click)="clear()"
2032
+ >
2033
+ <sds-icon name="x" [size]="15" />
2034
+ </button>
2035
+ }
2036
+ </div>
2037
+
2038
+ @if (error()) {
2039
+ <p [id]="fieldId + '-err'" class="mt-1.5 text-xs font-medium text-danger">{{ error() }}</p>
2040
+ } @else if (hint()) {
2041
+ <p [id]="fieldId + '-hint'" class="mt-1.5 text-xs text-ink-faint">{{ hint() }}</p>
2042
+ }
2043
+ `,
2044
+ }]
2045
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], showIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showIcon", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], withSeconds: [{ type: i0.Input, args: [{ isSignal: true, alias: "withSeconds", required: false }] }], placeholderText: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderText", required: false }] }], hoursLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoursLabel", required: false }] }], minutesLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "minutesLabel", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], hoursEl: [{ type: i0.ViewChild, args: ['hoursEl', { isSignal: true }] }], minutesEl: [{ type: i0.ViewChild, args: ['minutesEl', { isSignal: true }] }] } });
2046
+
2047
+ let nextId$k = 0;
2048
+ const SIZES$9 = {
2049
+ sm: 'min-h-9 text-sm',
2050
+ md: 'min-h-10 text-sm',
2051
+ lg: 'min-h-12 text-base',
2052
+ };
2053
+ /**
2054
+ * One language-aware text field for editing a translated value (a {@link SdsLocalizedValue} — a title,
2055
+ * label or description in several languages). Only one language is edited at a time; a compact switcher
2056
+ * sitting **inside the field** flips which translation you are editing, and a dot marks the languages that
2057
+ * already have content, so a partially-translated value is obvious at a glance.
2058
+ *
2059
+ * The active language defaults to {@link currentLanguage} when it is one of {@link languages}, otherwise the
2060
+ * first accepted language — so the field "opens on the language you are working in". Empty translations are
2061
+ * pruned from the emitted value, keeping the stored map clean.
2062
+ *
2063
+ * Implements ControlValueAccessor over the map — use with reactive forms or `[(ngModel)]`:
2064
+ * `<sds-localized-input [languages]="['fr','nl','en']" [(ngModel)]="title" />`.
2065
+ */
2066
+ class SdsLocalizedInput {
2067
+ inputId = `sds-localized-input-${nextId$k++}`;
2068
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
2069
+ hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
2070
+ placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
2071
+ /** The accepted language codes, in display order (e.g. `['fr','nl','en']`). */
2072
+ languages = input([], ...(ngDevMode ? [{ debugName: "languages" }] : []));
2073
+ /** Language to pre-select; used when it is one of {@link languages}, else the first accepted language. */
2074
+ currentLanguage = input(null, ...(ngDevMode ? [{ debugName: "currentLanguage" }] : []));
2075
+ multiline = input(false, ...(ngDevMode ? [{ debugName: "multiline", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
2076
+ rows = input(3, ...(ngDevMode ? [{ debugName: "rows", transform: numberAttribute }] : [{ transform: numberAttribute }]));
2077
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
2078
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
2079
+ /**
2080
+ * Accessible name for the in-field language switcher, and the "no translation yet" title suffix on each
2081
+ * language button. English defaults; translate them in a multi-language app (the buttons are
2082
+ * letter-only, so the title carries their meaning).
2083
+ */
2084
+ languageSwitcherLabel = input('Language', ...(ngDevMode ? [{ debugName: "languageSwitcherLabel" }] : []));
2085
+ untranslatedLabel = input('not translated yet', ...(ngDevMode ? [{ debugName: "untranslatedLabel" }] : []));
2086
+ valueMap = signal({}, ...(ngDevMode ? [{ debugName: "valueMap" }] : []));
2087
+ /** Language chosen via the switcher; null means "follow currentLanguage / the first language". */
2088
+ manual = signal(null, ...(ngDevMode ? [{ debugName: "manual" }] : []));
2089
+ cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
2090
+ isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
2091
+ wrapperClasses = computed(() => {
2092
+ const parts = [SIZES$9[this.size()]];
2093
+ parts.push(this.isDisabled()
2094
+ ? 'border-border opacity-60 cursor-not-allowed'
2095
+ : 'border-border focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25');
2096
+ return parts.join(' ');
2097
+ }, ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
2098
+ /** The language currently being edited. */
2099
+ activeLang = computed(() => {
2100
+ const langs = this.languages();
2101
+ if (!langs.length) {
2102
+ return '';
2103
+ }
2104
+ const manual = this.manual();
2105
+ if (manual && langs.includes(manual)) {
2106
+ return manual;
2107
+ }
2108
+ const preferred = this.currentLanguage();
2109
+ return preferred && langs.includes(preferred) ? preferred : langs[0];
2110
+ }, ...(ngDevMode ? [{ debugName: "activeLang" }] : []));
2111
+ current = computed(() => this.valueMap()[this.activeLang()] ?? '', ...(ngDevMode ? [{ debugName: "current" }] : []));
2112
+ filled(lang) {
2113
+ const text = this.valueMap()[lang];
2114
+ return !!text && text.trim().length > 0;
2115
+ }
2116
+ fieldPlaceholder() {
2117
+ const base = this.placeholder();
2118
+ const lang = this.activeLang().toUpperCase();
2119
+ if (base) {
2120
+ return lang ? `${base} (${lang})` : base;
2121
+ }
2122
+ return lang ? `(${lang})` : '';
2123
+ }
2124
+ select(lang) {
2125
+ this.manual.set(lang);
2126
+ this.onTouched();
2127
+ }
2128
+ write(event) {
2129
+ const lang = this.activeLang();
2130
+ if (!lang) {
2131
+ return;
2132
+ }
2133
+ const text = event.target.value;
2134
+ const next = { ...this.valueMap() };
2135
+ if (text) {
2136
+ next[lang] = text;
2137
+ }
2138
+ else {
2139
+ delete next[lang];
2140
+ }
2141
+ this.valueMap.set(next);
2142
+ this.onChange(next);
2143
+ }
2144
+ // --- ControlValueAccessor -------------------------------------------------
2145
+ onChange = () => { };
2146
+ onTouched = () => { };
2147
+ writeValue(value) {
2148
+ this.valueMap.set(value ? { ...value } : {});
2149
+ }
2150
+ registerOnChange(fn) {
2151
+ this.onChange = fn;
2152
+ }
2153
+ registerOnTouched(fn) {
2154
+ this.onTouched = fn;
2155
+ }
2156
+ setDisabledState(isDisabled) {
2157
+ this.cvaDisabled.set(isDisabled);
2158
+ }
2159
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsLocalizedInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
2160
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsLocalizedInput, isStandalone: true, selector: "sds-localized-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, languages: { classPropertyName: "languages", publicName: "languages", isSignal: true, isRequired: false, transformFunction: null }, currentLanguage: { classPropertyName: "currentLanguage", publicName: "currentLanguage", isSignal: true, isRequired: false, transformFunction: null }, multiline: { classPropertyName: "multiline", publicName: "multiline", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, languageSwitcherLabel: { classPropertyName: "languageSwitcherLabel", publicName: "languageSwitcherLabel", isSignal: true, isRequired: false, transformFunction: null }, untranslatedLabel: { classPropertyName: "untranslatedLabel", publicName: "untranslatedLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, providers: [
2161
+ {
2162
+ provide: NG_VALUE_ACCESSOR,
2163
+ useExisting: forwardRef(() => SdsLocalizedInput),
2164
+ multi: true,
2165
+ },
2166
+ ], ngImport: i0, template: `
2167
+ @if (label()) {
2168
+ <label [for]="inputId" class="mb-1.5 block text-sm font-medium text-ink">{{ label() }}</label>
2169
+ }
2170
+
2171
+ <div
2172
+ class="relative flex items-stretch rounded-control border bg-surface text-ink transition-colors"
2173
+ [class]="wrapperClasses()"
2174
+ >
2175
+ @if (multiline()) {
2176
+ <textarea
2177
+ [id]="inputId"
2178
+ [rows]="rows()"
2179
+ [placeholder]="fieldPlaceholder()"
2180
+ [disabled]="isDisabled()"
2181
+ [value]="current()"
2182
+ (input)="write($event)"
2183
+ (blur)="onTouched()"
2184
+ class="min-w-0 flex-1 resize-y bg-transparent px-3.5 py-2 outline-none placeholder:text-current/50 disabled:cursor-not-allowed"
2185
+ ></textarea>
2186
+ } @else {
2187
+ <input
2188
+ [id]="inputId"
2189
+ [placeholder]="fieldPlaceholder()"
2190
+ [disabled]="isDisabled()"
2191
+ [value]="current()"
2192
+ (input)="write($event)"
2193
+ (blur)="onTouched()"
2194
+ class="min-w-0 flex-1 bg-transparent px-3.5 outline-none placeholder:text-current/50 disabled:cursor-not-allowed"
2195
+ />
2196
+ }
1623
2197
 
1624
2198
  @if (languages().length > 1) {
1625
2199
  <div
@@ -1746,7 +2320,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
1746
2320
  }]
1747
2321
  }], 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 }] }] } });
1748
2322
 
1749
- let nextId$i = 0;
2323
+ let nextId$j = 0;
1750
2324
  const SIZES$8 = {
1751
2325
  md: 'h-12 w-11 text-lg',
1752
2326
  lg: 'h-14 w-12 text-xl',
@@ -1763,7 +2337,7 @@ const SIZES$8 = {
1763
2337
  * first box and have it land correctly.
1764
2338
  */
1765
2339
  class SdsOtpInput {
1766
- groupId = `sds-otp-${nextId$i++}`;
2340
+ groupId = `sds-otp-${nextId$j++}`;
1767
2341
  /** How many boxes / characters the code has. */
1768
2342
  length = input(6, ...(ngDevMode ? [{ debugName: "length", transform: numberAttribute }] : [{ transform: numberAttribute }]));
1769
2343
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
@@ -2063,7 +2637,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
2063
2637
  }]
2064
2638
  }], 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 }] }] } });
2065
2639
 
2066
- let nextId$h = 0;
2640
+ let nextId$i = 0;
2067
2641
  const SIZES$7 = {
2068
2642
  sm: 'text-sm px-3 py-2',
2069
2643
  md: 'text-sm px-3.5 py-2.5',
@@ -2075,7 +2649,7 @@ const SIZES$7 = {
2075
2649
  * [(ngModel)].
2076
2650
  */
2077
2651
  class SdsTextarea {
2078
- fieldId = `sds-textarea-${nextId$h++}`;
2652
+ fieldId = `sds-textarea-${nextId$i++}`;
2079
2653
  field = viewChild('field', ...(ngDevMode ? [{ debugName: "field" }] : []));
2080
2654
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
2081
2655
  /** Accessible name used when no visible [label] is provided. */
@@ -2335,7 +2909,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
2335
2909
  }]
2336
2910
  }], ctorParameters: () => [], propDecorators: { field: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], autoResize: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoResize", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], showCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCount", required: false }] }], countLimitLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "countLimitLabel", required: false }] }], countLabelTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "countLabelTemplate", required: false }] }], resize: [{ type: i0.Input, args: [{ isSignal: true, alias: "resize", required: false }] }] } });
2337
2911
 
2338
- let nextId$g = 0;
2912
+ let nextId$h = 0;
2339
2913
  const SIZES$6 = {
2340
2914
  sm: { track: 'h-5 w-9', knob: 'h-4 w-4', on: 'translate-x-4', icon: 11 },
2341
2915
  md: { track: 'h-6 w-11', knob: 'h-5 w-5', on: 'translate-x-5', icon: 13 },
@@ -2348,7 +2922,7 @@ const SIZES$6 = {
2348
2922
  * states.
2349
2923
  */
2350
2924
  class SdsToggleSwitch {
2351
- fieldId = `sds-toggle-switch-${nextId$g++}`;
2925
+ fieldId = `sds-toggle-switch-${nextId$h++}`;
2352
2926
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
2353
2927
  /** Accessible name used when no visible [label] is provided. */
2354
2928
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -2592,7 +3166,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
2592
3166
  }]
2593
3167
  }], 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 }] }] } });
2594
3168
 
2595
- let nextId$f = 0;
3169
+ let nextId$g = 0;
2596
3170
  /**
2597
3171
  * Boolean checkbox. Implements ControlValueAccessor — use with reactive forms
2598
3172
  * or `[(ngModel)]`. Renders a styled box (brand-filled with a check when on)
@@ -2607,7 +3181,7 @@ let nextId$f = 0;
2607
3181
  * <sds-checkbox label="Auto-merge" [(ngModel)]="autoMerge" />
2608
3182
  */
2609
3183
  class SdsCheckbox {
2610
- id = `sds-checkbox-${nextId$f++}`;
3184
+ id = `sds-checkbox-${nextId$g++}`;
2611
3185
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
2612
3186
  /** Accessible name used when no visible [label] is provided. */
2613
3187
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -2755,7 +3329,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
2755
3329
  }]
2756
3330
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }] } });
2757
3331
 
2758
- let nextId$e = 0;
3332
+ let nextId$f = 0;
2759
3333
  /**
2760
3334
  * Mounted radios by group name. A pick updates the rest of its group, because a native radio says
2761
3335
  * nothing when the browser unchecks it — without this a reactive form would leave the old choice drawn.
@@ -2781,7 +3355,7 @@ const groups = new Map();
2781
3355
  * <sds-radio name="size" value="large" label="Grand" [(ngModel)]="size" />
2782
3356
  */
2783
3357
  class SdsRadio {
2784
- id = `sds-radio-${nextId$e++}`;
3358
+ id = `sds-radio-${nextId$f++}`;
2785
3359
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
2786
3360
  /** Accessible name used when no visible [label] is provided. */
2787
3361
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -2964,8 +3538,30 @@ const STATUS_COLOR = {
2964
3538
  busy: 'bg-danger',
2965
3539
  away: 'bg-warning',
2966
3540
  };
3541
+ const SHAPE = {
3542
+ circle: 'rounded-full',
3543
+ rounded: 'rounded-control',
3544
+ square: 'rounded-none',
3545
+ };
2967
3546
  /**
2968
- * Circular avatar showing an image or initials, with an optional status dot.
3547
+ * Avatar showing an image, initials or a placeholder icon, with an optional status dot.
3548
+ *
3549
+ * - `shape`: `circle` (default, people), `rounded` (things: a dish photo, a product, a logo) or `square`.
3550
+ * - `fallbackIcon`: shown instead of initials when there is no image (or it fails to load) — e.g.
3551
+ * `utensils` or `image` for a menu item without a photo. The placeholder is drawn on `placeholderClass`
3552
+ * (a muted tile by default), not on the initials' brand fill.
3553
+ * - A broken `src` falls back to the icon or the initials instead of showing the browser's broken-image
3554
+ * glyph; a new `src` retries.
3555
+ * - `decorative`: the avatar only repeats a name that is already written next to it (a list row, a select
3556
+ * option), so the image gets `alt=""` and the whole avatar is hidden from assistive technology.
3557
+ *
3558
+ * @example
3559
+ * <sds-avatar name="Ada Lovelace" [src]="user.photoUrl" status="online" />
3560
+ *
3561
+ * @example
3562
+ * <!-- a menu item in a list: rounded photo, dish placeholder when there is none -->
3563
+ * <sds-avatar shape="rounded" [size]="32" [src]="item.thumbnailUrl" [name]="item.name"
3564
+ * fallbackIcon="utensils" decorative />
2969
3565
  */
2970
3566
  class SdsAvatar {
2971
3567
  name = input('', ...(ngDevMode ? [{ debugName: "name" }] : []));
@@ -2973,11 +3569,51 @@ class SdsAvatar {
2973
3569
  src = input(null, ...(ngDevMode ? [{ debugName: "src" }] : []));
2974
3570
  size = input(36, ...(ngDevMode ? [{ debugName: "size" }] : []));
2975
3571
  status = input(null, ...(ngDevMode ? [{ debugName: "status" }] : []));
3572
+ /** `circle` (default), `rounded` (radius of a control) or `square`. */
3573
+ shape = input('circle', ...(ngDevMode ? [{ debugName: "shape" }] : []));
2976
3574
  /** Tailwind bg utility for the initials background. */
2977
3575
  bgClass = input('bg-brand-400', ...(ngDevMode ? [{ debugName: "bgClass" }] : []));
2978
- dotSize = computed(() => Math.max(8, Math.round(this.size() * 0.28)), ...(ngDevMode ? [{ debugName: "dotSize" }] : []));
2979
- statusColor = computed(() => this.status() ? STATUS_COLOR[this.status()] : '', ...(ngDevMode ? [{ debugName: "statusColor" }] : []));
2980
- resolvedInitials = computed(() => {
3576
+ /**
3577
+ * Icon shown instead of initials when there is no usable image (e.g. `utensils` or `image` for a dish).
3578
+ * Null (default) keeps the initials.
3579
+ */
3580
+ fallbackIcon = input(null, ...(ngDevMode ? [{ debugName: "fallbackIcon" }] : []));
3581
+ /** Classes of the placeholder tile behind {@link fallbackIcon}: background and icon colour. */
3582
+ placeholderClass = input('bg-muted text-ink-faint', ...(ngDevMode ? [{ debugName: "placeholderClass" }] : []));
3583
+ /**
3584
+ * The avatar repeats a name already visible beside it: `alt=""` and `aria-hidden`, so a screen reader
3585
+ * does not read the name twice.
3586
+ */
3587
+ decorative = input(false, ...(ngDevMode ? [{ debugName: "decorative", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
3588
+ /** Native lazy loading for the image (default on — avatars are often in long lists). */
3589
+ lazy = input(true, ...(ngDevMode ? [{ debugName: "lazy", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
3590
+ /** The `src` that failed to load. Any other `src` is tried (so a new photo URL retries). */
3591
+ failedSrc = signal(undefined, ...(ngDevMode ? [{ debugName: "failedSrc" }] : []));
3592
+ showImage = computed(() => {
3593
+ const src = this.src();
3594
+ return !!src && src !== this.failedSrc();
3595
+ }, ...(ngDevMode ? [{ debugName: "showImage" }] : []));
3596
+ shapeClass = computed(() => SHAPE[this.shape()], ...(ngDevMode ? [{ debugName: "shapeClass" }] : []));
3597
+ frameClass = computed(() => {
3598
+ const parts = [this.shapeClass()];
3599
+ if (this.showImage()) {
3600
+ parts.push('bg-muted');
3601
+ }
3602
+ else if (this.fallbackIcon()) {
3603
+ parts.push(this.placeholderClass());
3604
+ }
3605
+ else {
3606
+ parts.push(this.bgClass(), 'text-white');
3607
+ }
3608
+ // People get the soft ring the avatar always had; things get a hairline so a white plate on a white
3609
+ // page still has an edge.
3610
+ parts.push(this.shape() === 'circle' ? 'ring-2 ring-white/20' : 'ring-1 ring-inset ring-border');
3611
+ return parts.join(' ');
3612
+ }, ...(ngDevMode ? [{ debugName: "frameClass" }] : []));
3613
+ iconSize = computed(() => Math.max(12, Math.round(this.size() * 0.5)), ...(ngDevMode ? [{ debugName: "iconSize" }] : []));
3614
+ dotSize = computed(() => Math.max(8, Math.round(this.size() * 0.28)), ...(ngDevMode ? [{ debugName: "dotSize" }] : []));
3615
+ statusColor = computed(() => this.status() ? STATUS_COLOR[this.status()] : '', ...(ngDevMode ? [{ debugName: "statusColor" }] : []));
3616
+ resolvedInitials = computed(() => {
2981
3617
  if (this.initials())
2982
3618
  return this.initials().slice(0, 2).toUpperCase();
2983
3619
  const parts = this.name().trim().split(/\s+/).filter(Boolean);
@@ -2988,19 +3624,31 @@ class SdsAvatar {
2988
3624
  return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
2989
3625
  }, ...(ngDevMode ? [{ debugName: "resolvedInitials" }] : []));
2990
3626
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsAvatar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2991
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsAvatar, isStandalone: true, selector: "sds-avatar", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, initials: { classPropertyName: "initials", publicName: "initials", isSignal: true, isRequired: false, transformFunction: null }, src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, bgClass: { classPropertyName: "bgClass", publicName: "bgClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "inline-block shrink-0 align-middle" }, ngImport: i0, template: `
3627
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsAvatar, isStandalone: true, selector: "sds-avatar", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, initials: { classPropertyName: "initials", publicName: "initials", isSignal: true, isRequired: false, transformFunction: null }, src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, bgClass: { classPropertyName: "bgClass", publicName: "bgClass", isSignal: true, isRequired: false, transformFunction: null }, fallbackIcon: { classPropertyName: "fallbackIcon", publicName: "fallbackIcon", isSignal: true, isRequired: false, transformFunction: null }, placeholderClass: { classPropertyName: "placeholderClass", publicName: "placeholderClass", isSignal: true, isRequired: false, transformFunction: null }, decorative: { classPropertyName: "decorative", publicName: "decorative", isSignal: true, isRequired: false, transformFunction: null }, lazy: { classPropertyName: "lazy", publicName: "lazy", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.aria-hidden": "decorative() ? 'true' : null" }, classAttribute: "inline-block shrink-0 align-middle" }, ngImport: i0, template: `
2992
3628
  <div
2993
- class="relative grid place-items-center overflow-hidden rounded-full font-semibold text-white ring-2 ring-white/20"
2994
- [class]="bgClass()"
3629
+ class="relative grid place-items-center font-semibold"
3630
+ [class]="frameClass()"
2995
3631
  [style.width.px]="size()"
2996
3632
  [style.height.px]="size()"
2997
3633
  [style.fontSize.px]="size() * 0.4"
2998
3634
  >
2999
- @if (src()) {
3000
- <img [src]="src()" [alt]="name()" class="h-full w-full object-cover" />
3001
- } @else {
3002
- <span>{{ resolvedInitials() }}</span>
3003
- }
3635
+ <!-- The image and placeholder are clipped to the shape; the status dot is not (it straddles the edge). -->
3636
+ <div class="absolute inset-0 grid place-items-center overflow-hidden" [class]="shapeClass()">
3637
+ @if (showImage()) {
3638
+ <img
3639
+ [src]="src()"
3640
+ [alt]="decorative() ? '' : name()"
3641
+ [attr.loading]="lazy() ? 'lazy' : null"
3642
+ decoding="async"
3643
+ class="h-full w-full object-cover"
3644
+ (error)="failedSrc.set(src())"
3645
+ />
3646
+ } @else if (fallbackIcon()) {
3647
+ <sds-icon [name]="fallbackIcon()!" [size]="iconSize()" />
3648
+ } @else {
3649
+ <span>{{ resolvedInitials() }}</span>
3650
+ }
3651
+ </div>
3004
3652
 
3005
3653
  @if (status()) {
3006
3654
  <span
@@ -3011,27 +3659,43 @@ class SdsAvatar {
3011
3659
  ></span>
3012
3660
  }
3013
3661
  </div>
3014
- `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
3662
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3015
3663
  }
3016
3664
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsAvatar, decorators: [{
3017
3665
  type: Component,
3018
3666
  args: [{
3019
3667
  selector: 'sds-avatar',
3020
3668
  changeDetection: ChangeDetectionStrategy.OnPush,
3021
- host: { class: 'inline-block shrink-0 align-middle' },
3669
+ imports: [SdsIcon],
3670
+ host: {
3671
+ class: 'inline-block shrink-0 align-middle',
3672
+ '[attr.aria-hidden]': "decorative() ? 'true' : null",
3673
+ },
3022
3674
  template: `
3023
3675
  <div
3024
- class="relative grid place-items-center overflow-hidden rounded-full font-semibold text-white ring-2 ring-white/20"
3025
- [class]="bgClass()"
3676
+ class="relative grid place-items-center font-semibold"
3677
+ [class]="frameClass()"
3026
3678
  [style.width.px]="size()"
3027
3679
  [style.height.px]="size()"
3028
3680
  [style.fontSize.px]="size() * 0.4"
3029
3681
  >
3030
- @if (src()) {
3031
- <img [src]="src()" [alt]="name()" class="h-full w-full object-cover" />
3032
- } @else {
3033
- <span>{{ resolvedInitials() }}</span>
3034
- }
3682
+ <!-- The image and placeholder are clipped to the shape; the status dot is not (it straddles the edge). -->
3683
+ <div class="absolute inset-0 grid place-items-center overflow-hidden" [class]="shapeClass()">
3684
+ @if (showImage()) {
3685
+ <img
3686
+ [src]="src()"
3687
+ [alt]="decorative() ? '' : name()"
3688
+ [attr.loading]="lazy() ? 'lazy' : null"
3689
+ decoding="async"
3690
+ class="h-full w-full object-cover"
3691
+ (error)="failedSrc.set(src())"
3692
+ />
3693
+ } @else if (fallbackIcon()) {
3694
+ <sds-icon [name]="fallbackIcon()!" [size]="iconSize()" />
3695
+ } @else {
3696
+ <span>{{ resolvedInitials() }}</span>
3697
+ }
3698
+ </div>
3035
3699
 
3036
3700
  @if (status()) {
3037
3701
  <span
@@ -3044,7 +3708,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
3044
3708
  </div>
3045
3709
  `,
3046
3710
  }]
3047
- }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], initials: [{ type: i0.Input, args: [{ isSignal: true, alias: "initials", required: false }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], bgClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "bgClass", required: false }] }] } });
3711
+ }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], initials: [{ type: i0.Input, args: [{ isSignal: true, alias: "initials", required: false }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], bgClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "bgClass", required: false }] }], fallbackIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallbackIcon", required: false }] }], placeholderClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderClass", required: false }] }], decorative: [{ type: i0.Input, args: [{ isSignal: true, alias: "decorative", required: false }] }], lazy: [{ type: i0.Input, args: [{ isSignal: true, alias: "lazy", required: false }] }] } });
3048
3712
 
3049
3713
  const VARIANTS$1 = {
3050
3714
  neutral: 'bg-muted text-ink-soft border-border',
@@ -3267,7 +3931,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
3267
3931
 
3268
3932
  /** Activation order: none → contains → may-contain → none. */
3269
3933
  const ORDER = ['none', 'contains', 'may-contain'];
3270
- let nextId$d = 0;
3934
+ let nextId$e = 0;
3271
3935
  /**
3272
3936
  * A chip that cycles through three states — `none`, `contains`, `may-contain` — built for the 14-tile
3273
3937
  * allergen declaration grid, but generic (any "no / yes / maybe" declaration).
@@ -3287,7 +3951,7 @@ let nextId$d = 0;
3287
3951
  * containsLabel="Contains" mayContainLabel="May contain" noneLabel="Not present" />
3288
3952
  */
3289
3953
  class SdsTriStateChip {
3290
- chipId = `sds-tri-state-chip-${nextId$d++}`;
3954
+ chipId = `sds-tri-state-chip-${nextId$e++}`;
3291
3955
  /** Current state. Two-way: `[(value)]`. */
3292
3956
  value = model('none', ...(ngDevMode ? [{ debugName: "value" }] : []));
3293
3957
  /** Visible name (e.g. the allergen). Required — the chip has no other accessible name. */
@@ -3500,87 +4164,417 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
3500
4164
  }
3501
4165
  </span>
3502
4166
  </button>
3503
-
3504
- @if (hint()) {
3505
- <span [id]="chipId + '-hint'" class="sr-only">{{ hint() }}</span>
3506
- }
3507
- <span class="sr-only" aria-live="polite" aria-atomic="true">{{ announcement() }}</span>
4167
+
4168
+ @if (hint()) {
4169
+ <span [id]="chipId + '-hint'" class="sr-only">{{ hint() }}</span>
4170
+ }
4171
+ <span class="sr-only" aria-live="polite" aria-atomic="true">{{ announcement() }}</span>
4172
+ `,
4173
+ }]
4174
+ }], 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 }] }] } });
4175
+
4176
+ let nextId$d = 0;
4177
+ /**
4178
+ * Segmented control: a small set of mutually exclusive options shown side by side (e.g. "Pick exactly /
4179
+ * at least / up to"). Radio-group semantics: `role="radiogroup"` with `role="radio"` segments, one tab
4180
+ * stop, and the arrow keys (with Home/End) move AND select, like native radios. Disabled segments are
4181
+ * skipped.
4182
+ *
4183
+ * The value is a two-way `model` — bind `[(value)]`. Give the group a name with [ariaLabel] (or
4184
+ * [ariaLabelledby]); with [iconOnly] each segment keeps its label as its accessible name.
4185
+ *
4186
+ * @example
4187
+ * <sds-segmented ariaLabel="Rule" [options]="[{ value: 'exactly', label: 'Exactly' }, { value: 'upTo', label: 'Up to' }]"
4188
+ * [(value)]="rule" />
4189
+ */
4190
+ class SdsSegmented {
4191
+ host = inject(ElementRef);
4192
+ baseId = `sds-segmented-${nextId$d++}`;
4193
+ /** Options: `{ value, label, icon?, disabled? }` objects, or bare strings/numbers. */
4194
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
4195
+ /** Selected value. Two-way: `[(value)]`. */
4196
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
4197
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
4198
+ /** Accessible name of the group. */
4199
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
4200
+ /** Id of an element that names the group (alternative to [ariaLabel]). */
4201
+ ariaLabelledby = input('', ...(ngDevMode ? [{ debugName: "ariaLabelledby" }] : []));
4202
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4203
+ /** Stretch to the container width, segments sharing it equally. */
4204
+ fullWidth = input(false, ...(ngDevMode ? [{ debugName: "fullWidth", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4205
+ /** Show icons only; labels stay as accessible names and tooltips. */
4206
+ iconOnly = input(false, ...(ngDevMode ? [{ debugName: "iconOnly", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4207
+ segments = computed(() => this.options().map((o) => o !== null && typeof o === 'object'
4208
+ ? { value: o.value, label: o.label, icon: o.icon ?? null, disabled: !!o.disabled }
4209
+ : { value: o, label: String(o), icon: null, disabled: false }), ...(ngDevMode ? [{ debugName: "segments" }] : []));
4210
+ selectedIndex = computed(() => this.segments().findIndex((s) => s.value === this.value()), ...(ngDevMode ? [{ debugName: "selectedIndex" }] : []));
4211
+ /** Roving tab stop: the selected segment, else the first enabled one. */
4212
+ tabStop = computed(() => {
4213
+ const sel = this.selectedIndex();
4214
+ if (sel >= 0 && !this.segments()[sel].disabled)
4215
+ return sel;
4216
+ return this.segments().findIndex((s) => !s.disabled);
4217
+ }, ...(ngDevMode ? [{ debugName: "tabStop" }] : []));
4218
+ isSelected(seg) {
4219
+ return seg.value === this.value();
4220
+ }
4221
+ segmentId(i) {
4222
+ return `${this.baseId}-${i}`;
4223
+ }
4224
+ segmentClass(seg) {
4225
+ const sm = this.size() === 'sm';
4226
+ const base = 'inline-flex min-w-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] ' +
4227
+ 'font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 ' +
4228
+ 'disabled:cursor-not-allowed disabled:opacity-50 ' +
4229
+ (sm ? 'h-7 px-2.5 text-xs' : 'h-8 px-3.5 text-sm') +
4230
+ (this.fullWidth() ? ' flex-1' : '') +
4231
+ (this.iconOnly() ? (sm ? ' w-8 px-0' : ' w-9 px-0') : '');
4232
+ // Selected: raised surface, ring and bold text — more than a tint, so it reads in greyscale too.
4233
+ return this.isSelected(seg)
4234
+ ? `${base} bg-surface font-semibold text-ink shadow-sm ring-1 ring-border dark:bg-border`
4235
+ : `${base} cursor-pointer text-ink-soft hover:bg-surface/60 hover:text-ink`;
4236
+ }
4237
+ choose(i) {
4238
+ const seg = this.segments()[i];
4239
+ if (!seg || seg.disabled || this.disabled())
4240
+ return;
4241
+ this.value.set(seg.value);
4242
+ }
4243
+ onKeydown(event) {
4244
+ if (this.disabled())
4245
+ return;
4246
+ const segs = this.segments();
4247
+ if (!segs.length)
4248
+ return;
4249
+ const current = Number(event.target.dataset['index'] ?? this.tabStop());
4250
+ let next = null;
4251
+ switch (event.key) {
4252
+ case 'ArrowRight':
4253
+ case 'ArrowDown':
4254
+ next = this.step(current, 1);
4255
+ break;
4256
+ case 'ArrowLeft':
4257
+ case 'ArrowUp':
4258
+ next = this.step(current, -1);
4259
+ break;
4260
+ case 'Home':
4261
+ next = segs.findIndex((s) => !s.disabled);
4262
+ break;
4263
+ case 'End':
4264
+ for (let i = segs.length - 1; i >= 0; i--) {
4265
+ if (!segs[i].disabled) {
4266
+ next = i;
4267
+ break;
4268
+ }
4269
+ }
4270
+ break;
4271
+ default:
4272
+ return;
4273
+ }
4274
+ event.preventDefault();
4275
+ if (next === null || next < 0)
4276
+ return;
4277
+ this.choose(next);
4278
+ this.host.nativeElement.querySelector(`#${this.segmentId(next)}`)?.focus();
4279
+ }
4280
+ step(from, delta) {
4281
+ const segs = this.segments();
4282
+ let i = from;
4283
+ for (let n = 0; n < segs.length; n++) {
4284
+ i = (i + delta + segs.length) % segs.length;
4285
+ if (!segs[i].disabled)
4286
+ return i;
4287
+ }
4288
+ return null;
4289
+ }
4290
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSegmented, deps: [], target: i0.ɵɵFactoryTarget.Component });
4291
+ 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: `
4292
+ <div
4293
+ role="radiogroup"
4294
+ [attr.aria-label]="ariaLabel() || null"
4295
+ [attr.aria-labelledby]="ariaLabelledby() || null"
4296
+ [attr.aria-disabled]="disabled() ? 'true' : null"
4297
+ class="flex max-w-full items-stretch gap-0.5 overflow-x-auto rounded-control border border-border bg-muted p-0.5"
4298
+ [class.w-full]="fullWidth()"
4299
+ (keydown)="onKeydown($event)"
4300
+ >
4301
+ @for (seg of segments(); track $index; let i = $index) {
4302
+ <button
4303
+ type="button"
4304
+ role="radio"
4305
+ [id]="segmentId(i)"
4306
+ [attr.aria-checked]="isSelected(seg)"
4307
+ [attr.aria-label]="iconOnly() ? seg.label : null"
4308
+ [attr.title]="iconOnly() ? seg.label : null"
4309
+ [attr.tabindex]="i === tabStop() ? 0 : -1"
4310
+ [attr.data-index]="i"
4311
+ [disabled]="disabled() || seg.disabled"
4312
+ [class]="segmentClass(seg)"
4313
+ (click)="choose(i)"
4314
+ >
4315
+ @if (seg.icon) {
4316
+ <sds-icon [name]="seg.icon" [size]="size() === 'sm' ? 15 : 17" class="shrink-0" />
4317
+ }
4318
+ @if (!iconOnly()) {
4319
+ <span class="truncate">{{ seg.label }}</span>
4320
+ }
4321
+ </button>
4322
+ }
4323
+ </div>
4324
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4325
+ }
4326
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSegmented, decorators: [{
4327
+ type: Component,
4328
+ args: [{
4329
+ selector: 'sds-segmented',
4330
+ changeDetection: ChangeDetectionStrategy.OnPush,
4331
+ imports: [SdsIcon],
4332
+ host: { class: 'inline-flex max-w-full', '[class.w-full]': 'fullWidth()' },
4333
+ template: `
4334
+ <div
4335
+ role="radiogroup"
4336
+ [attr.aria-label]="ariaLabel() || null"
4337
+ [attr.aria-labelledby]="ariaLabelledby() || null"
4338
+ [attr.aria-disabled]="disabled() ? 'true' : null"
4339
+ class="flex max-w-full items-stretch gap-0.5 overflow-x-auto rounded-control border border-border bg-muted p-0.5"
4340
+ [class.w-full]="fullWidth()"
4341
+ (keydown)="onKeydown($event)"
4342
+ >
4343
+ @for (seg of segments(); track $index; let i = $index) {
4344
+ <button
4345
+ type="button"
4346
+ role="radio"
4347
+ [id]="segmentId(i)"
4348
+ [attr.aria-checked]="isSelected(seg)"
4349
+ [attr.aria-label]="iconOnly() ? seg.label : null"
4350
+ [attr.title]="iconOnly() ? seg.label : null"
4351
+ [attr.tabindex]="i === tabStop() ? 0 : -1"
4352
+ [attr.data-index]="i"
4353
+ [disabled]="disabled() || seg.disabled"
4354
+ [class]="segmentClass(seg)"
4355
+ (click)="choose(i)"
4356
+ >
4357
+ @if (seg.icon) {
4358
+ <sds-icon [name]="seg.icon" [size]="size() === 'sm' ? 15 : 17" class="shrink-0" />
4359
+ }
4360
+ @if (!iconOnly()) {
4361
+ <span class="truncate">{{ seg.label }}</span>
4362
+ }
4363
+ </button>
4364
+ }
4365
+ </div>
4366
+ `,
4367
+ }]
4368
+ }], 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 }] }] } });
4369
+
4370
+ const SIZE = {
4371
+ sm: 'h-7 gap-1 px-2.5 text-xs',
4372
+ md: 'h-8 gap-1.5 px-3 text-sm',
4373
+ };
4374
+ /**
4375
+ * Chip look shared by sds-chip and sds-chip-group. Selected is more than a tint — a brand border, bolder
4376
+ * text and (by default) a check — so it reads in greyscale too. In dark mode the brand text/tint are
4377
+ * swapped for ink/muted (the library's rule for a monochrome brand's dark ramp).
4378
+ */
4379
+ function chipClass(size, selected) {
4380
+ const base = 'inline-flex shrink-0 select-none items-center whitespace-nowrap rounded-full border font-medium ' +
4381
+ 'transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 ' +
4382
+ 'focus-visible:ring-offset-1 focus-visible:ring-offset-surface disabled:cursor-not-allowed disabled:opacity-50 ' +
4383
+ SIZE[size];
4384
+ return selected
4385
+ ? `${base} cursor-pointer border-brand-600 bg-brand-50 font-semibold text-brand-700 ` +
4386
+ 'dark:border-ink-soft dark:bg-muted dark:text-ink'
4387
+ : `${base} cursor-pointer border-border bg-surface text-ink-soft hover:border-ink-faint hover:text-ink`;
4388
+ }
4389
+ const COUNT_CLASS = 'inline-flex min-w-5 items-center justify-center rounded-full bg-ink/5 px-1.5 text-[11px] font-semibold tabular-nums dark:bg-white/10';
4390
+ /**
4391
+ * A single on/off filter chip (`aria-pressed`), e.g. "Sold out only" or "With comments". Two-way
4392
+ * `[(selected)]`. For a set of chips — one or several picked — use `sds-chip-group`.
4393
+ *
4394
+ * @example
4395
+ * <sds-chip label="Sold out only" icon="ban" [(selected)]="soldOutOnly" />
4396
+ */
4397
+ class SdsChip {
4398
+ /** Visible text — also the chip's accessible name. */
4399
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
4400
+ /** On/off. Two-way: `[(selected)]`. */
4401
+ selected = model(false, ...(ngDevMode ? [{ debugName: "selected" }] : []));
4402
+ icon = input(null, ...(ngDevMode ? [{ debugName: "icon" }] : []));
4403
+ count = input(null, ...(ngDevMode ? [{ debugName: "count" }] : []));
4404
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
4405
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4406
+ /** Replace the icon with a check while selected. */
4407
+ showCheck = input(true, ...(ngDevMode ? [{ debugName: "showCheck", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4408
+ countClass = COUNT_CLASS;
4409
+ classes = computed(() => chipClass(this.size(), this.selected()), ...(ngDevMode ? [{ debugName: "classes" }] : []));
4410
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsChip, deps: [], target: i0.ɵɵFactoryTarget.Component });
4411
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsChip, isStandalone: true, selector: "sds-chip", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, showCheck: { classPropertyName: "showCheck", publicName: "showCheck", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selectedChange" }, host: { classAttribute: "inline-flex" }, ngImport: i0, template: `
4412
+ <button
4413
+ type="button"
4414
+ [attr.aria-pressed]="selected()"
4415
+ [disabled]="disabled()"
4416
+ [class]="classes()"
4417
+ (click)="selected.set(!selected())"
4418
+ >
4419
+ @if (selected() && showCheck()) {
4420
+ <sds-icon
4421
+ name="check"
4422
+ [size]="size() === 'sm' ? 12 : 14"
4423
+ [strokeWidth]="3"
4424
+ class="shrink-0"
4425
+ />
4426
+ } @else if (icon()) {
4427
+ <sds-icon [name]="icon()!" [size]="size() === 'sm' ? 13 : 15" class="shrink-0" />
4428
+ }
4429
+ <span class="truncate">{{ label() }}</span>
4430
+ @if (count() !== null && count() !== undefined) {
4431
+ <span [class]="countClass">{{ count() }}</span>
4432
+ }
4433
+ </button>
4434
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4435
+ }
4436
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsChip, decorators: [{
4437
+ type: Component,
4438
+ args: [{
4439
+ selector: 'sds-chip',
4440
+ changeDetection: ChangeDetectionStrategy.OnPush,
4441
+ imports: [SdsIcon],
4442
+ host: { class: 'inline-flex' },
4443
+ template: `
4444
+ <button
4445
+ type="button"
4446
+ [attr.aria-pressed]="selected()"
4447
+ [disabled]="disabled()"
4448
+ [class]="classes()"
4449
+ (click)="selected.set(!selected())"
4450
+ >
4451
+ @if (selected() && showCheck()) {
4452
+ <sds-icon
4453
+ name="check"
4454
+ [size]="size() === 'sm' ? 12 : 14"
4455
+ [strokeWidth]="3"
4456
+ class="shrink-0"
4457
+ />
4458
+ } @else if (icon()) {
4459
+ <sds-icon [name]="icon()!" [size]="size() === 'sm' ? 13 : 15" class="shrink-0" />
4460
+ }
4461
+ <span class="truncate">{{ label() }}</span>
4462
+ @if (count() !== null && count() !== undefined) {
4463
+ <span [class]="countClass">{{ count() }}</span>
4464
+ }
4465
+ </button>
3508
4466
  `,
3509
4467
  }]
3510
- }], 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 }] }] } });
3511
-
4468
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }, { type: i0.Output, args: ["selectedChange"] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showCheck: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCheck", required: false }] }] } });
3512
4469
  let nextId$c = 0;
3513
4470
  /**
3514
- * Segmented control: a small set of mutually exclusive options shown side by side (e.g. "Pick exactly /
3515
- * at least / up to"). Radio-group semantics: `role="radiogroup"` with `role="radio"` segments, one tab
3516
- * stop, and the arrow keys (with Home/End) move AND select, like native radios. Disabled segments are
3517
- * skipped.
4471
+ * A set of chips: pick ONE (a filter row such as "All · Missing photo · Hidden", VAT presets, a date
4472
+ * range) or, with `multiple`, SEVERAL (categories of an item, days of the week, dietary tags). Unlike
4473
+ * sds-segmented (a compact joined bar), chips wrap onto several lines and can carry a count.
3518
4474
  *
3519
- * The value is a two-way `model` — bind `[(value)]`. Give the group a name with [ariaLabel] (or
3520
- * [ariaLabelledby]); with [iconOnly] each segment keeps its label as its accessible name.
4475
+ * Value: two-way `[(value)]`, or a form control (`formControlName` / `[(ngModel)]` —
4476
+ * ControlValueAccessor). Single mode holds the picked value (or null); `multiple` holds an array, in the
4477
+ * order of the options.
4478
+ *
4479
+ * Accessibility: single mode is a `radiogroup` (one tab stop, arrows/Home/End move and select, like
4480
+ * native radios); `multiple` is a `group` of `aria-pressed` toggle buttons. Name it with [ariaLabel] or
4481
+ * [ariaLabelledby].
3521
4482
  *
3522
4483
  * @example
3523
- * <sds-segmented ariaLabel="Rule" [options]="[{ value: 'exactly', label: 'Exactly' }, { value: 'upTo', label: 'Up to' }]"
3524
- * [(value)]="rule" />
4484
+ * <sds-chip-group ariaLabel="Filter" [options]="filters()" [(value)]="issue" />
4485
+ * <!-- filters = [{ value: null, label: 'All', count: 34 }, { value: 'NO_PHOTO', label: 'Missing photo', count: 5 }] -->
4486
+ *
4487
+ * @example
4488
+ * <sds-chip-group ariaLabel="Categories" multiple [options]="categories()" formControlName="categoryIds" />
3525
4489
  */
3526
- class SdsSegmented {
4490
+ class SdsChipGroup {
3527
4491
  host = inject(ElementRef);
3528
- baseId = `sds-segmented-${nextId$c++}`;
3529
- /** Options: `{ value, label, icon?, disabled? }` objects, or bare strings/numbers. */
4492
+ baseId = `sds-chip-group-${nextId$c++}`;
4493
+ /** Options: `{ value, label, icon?, count?, disabled? }` objects, or bare strings/numbers. */
3530
4494
  options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
3531
- /** Selected value. Two-way: `[(value)]`. */
4495
+ /** Picked value (single) or values (multiple). Two-way: `[(value)]`. */
3532
4496
  value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
4497
+ /** Allow several chips at once; the value becomes an array. */
4498
+ multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4499
+ /** Single mode: clicking the picked chip again clears the value to null. */
4500
+ deselectable = input(false, ...(ngDevMode ? [{ debugName: "deselectable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
3533
4501
  size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
3534
- /** Accessible name of the group. */
3535
4502
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
3536
- /** Id of an element that names the group (alternative to [ariaLabel]). */
3537
4503
  ariaLabelledby = input('', ...(ngDevMode ? [{ debugName: "ariaLabelledby" }] : []));
3538
4504
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
3539
- /** Stretch to the container width, segments sharing it equally. */
3540
- fullWidth = input(false, ...(ngDevMode ? [{ debugName: "fullWidth", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
3541
- /** Show icons only; labels stay as accessible names and tooltips. */
3542
- iconOnly = input(false, ...(ngDevMode ? [{ debugName: "iconOnly", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
3543
- segments = computed(() => this.options().map((o) => o !== null && typeof o === 'object'
3544
- ? { value: o.value, label: o.label, icon: o.icon ?? null, disabled: !!o.disabled }
3545
- : { value: o, label: String(o), icon: null, disabled: false }), ...(ngDevMode ? [{ debugName: "segments" }] : []));
3546
- selectedIndex = computed(() => this.segments().findIndex((s) => s.value === this.value()), ...(ngDevMode ? [{ debugName: "selectedIndex" }] : []));
3547
- /** Roving tab stop: the selected segment, else the first enabled one. */
4505
+ /** Wrap onto several lines (default) or scroll horizontally on one line. */
4506
+ wrap = input(true, ...(ngDevMode ? [{ debugName: "wrap", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4507
+ /** `multiple`: a check replaces the icon on picked chips. */
4508
+ showCheck = input(true, ...(ngDevMode ? [{ debugName: "showCheck", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4509
+ countClass = COUNT_CLASS;
4510
+ cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
4511
+ isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
4512
+ chips = computed(() => this.options().map((o) => o !== null && typeof o === 'object'
4513
+ ? {
4514
+ value: o.value,
4515
+ label: o.label,
4516
+ icon: o.icon ?? null,
4517
+ count: o.count ?? null,
4518
+ disabled: !!o.disabled,
4519
+ }
4520
+ : { value: o, label: String(o), icon: null, count: null, disabled: false }), ...(ngDevMode ? [{ debugName: "chips" }] : []));
4521
+ selectedValues = computed(() => {
4522
+ const v = this.value();
4523
+ if (this.multiple())
4524
+ return Array.isArray(v) ? v : [];
4525
+ return [v];
4526
+ }, ...(ngDevMode ? [{ debugName: "selectedValues" }] : []));
4527
+ /** Single mode's roving tab stop: the picked chip, else the first enabled one. */
3548
4528
  tabStop = computed(() => {
3549
- const sel = this.selectedIndex();
3550
- if (sel >= 0 && !this.segments()[sel].disabled)
4529
+ const chips = this.chips();
4530
+ const sel = chips.findIndex((c) => this.isSelected(c));
4531
+ if (sel >= 0 && !chips[sel].disabled)
3551
4532
  return sel;
3552
- return this.segments().findIndex((s) => !s.disabled);
4533
+ return chips.findIndex((c) => !c.disabled);
3553
4534
  }, ...(ngDevMode ? [{ debugName: "tabStop" }] : []));
3554
- isSelected(seg) {
3555
- return seg.value === this.value();
4535
+ isSelected(chip) {
4536
+ return this.selectedValues().includes(chip.value);
3556
4537
  }
3557
- segmentId(i) {
4538
+ chipId(i) {
3558
4539
  return `${this.baseId}-${i}`;
3559
4540
  }
3560
- segmentClass(seg) {
3561
- const sm = this.size() === 'sm';
3562
- const base = 'inline-flex min-w-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] ' +
3563
- 'font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 ' +
3564
- 'disabled:cursor-not-allowed disabled:opacity-50 ' +
3565
- (sm ? 'h-7 px-2.5 text-xs' : 'h-8 px-3.5 text-sm') +
3566
- (this.fullWidth() ? ' flex-1' : '') +
3567
- (this.iconOnly() ? (sm ? ' w-8 px-0' : ' w-9 px-0') : '');
3568
- // Selected: raised surface, ring and bold text — more than a tint, so it reads in greyscale too.
3569
- return this.isSelected(seg)
3570
- ? `${base} bg-surface font-semibold text-ink shadow-sm ring-1 ring-border dark:bg-border`
3571
- : `${base} cursor-pointer text-ink-soft hover:bg-surface/60 hover:text-ink`;
4541
+ classFor(chip) {
4542
+ return chipClass(this.size(), this.isSelected(chip));
3572
4543
  }
3573
- choose(i) {
3574
- const seg = this.segments()[i];
3575
- if (!seg || seg.disabled || this.disabled())
4544
+ toggle(i) {
4545
+ const chip = this.chips()[i];
4546
+ if (!chip || chip.disabled || this.isDisabled())
3576
4547
  return;
3577
- this.value.set(seg.value);
4548
+ let next;
4549
+ if (this.multiple()) {
4550
+ const picked = new Set(this.selectedValues());
4551
+ if (picked.has(chip.value))
4552
+ picked.delete(chip.value);
4553
+ else
4554
+ picked.add(chip.value);
4555
+ // Keep the options' order, whatever the click order.
4556
+ next = this.chips()
4557
+ .map((c) => c.value)
4558
+ .filter((v) => picked.has(v));
4559
+ }
4560
+ else {
4561
+ if (this.isSelected(chip)) {
4562
+ if (!this.deselectable())
4563
+ return;
4564
+ next = null;
4565
+ }
4566
+ else {
4567
+ next = chip.value;
4568
+ }
4569
+ }
4570
+ this.value.set(next);
4571
+ this.onChange(next);
3578
4572
  }
3579
4573
  onKeydown(event) {
3580
- if (this.disabled())
4574
+ if (this.multiple() || this.isDisabled())
3581
4575
  return;
3582
- const segs = this.segments();
3583
- if (!segs.length)
4576
+ const chips = this.chips();
4577
+ if (!chips.length)
3584
4578
  return;
3585
4579
  const current = Number(event.target.dataset['index'] ?? this.tabStop());
3586
4580
  let next = null;
@@ -3594,11 +4588,11 @@ class SdsSegmented {
3594
4588
  next = this.step(current, -1);
3595
4589
  break;
3596
4590
  case 'Home':
3597
- next = segs.findIndex((s) => !s.disabled);
4591
+ next = chips.findIndex((c) => !c.disabled);
3598
4592
  break;
3599
4593
  case 'End':
3600
- for (let i = segs.length - 1; i >= 0; i--) {
3601
- if (!segs[i].disabled) {
4594
+ for (let i = chips.length - 1; i >= 0; i--) {
4595
+ if (!chips[i].disabled) {
3602
4596
  next = i;
3603
4597
  break;
3604
4598
  }
@@ -3610,98 +4604,136 @@ class SdsSegmented {
3610
4604
  event.preventDefault();
3611
4605
  if (next === null || next < 0)
3612
4606
  return;
3613
- this.choose(next);
3614
- this.host.nativeElement.querySelector(`#${this.segmentId(next)}`)?.focus();
4607
+ if (!this.isSelected(chips[next]))
4608
+ this.toggle(next);
4609
+ this.host.nativeElement.querySelector(`#${this.chipId(next)}`)?.focus();
3615
4610
  }
3616
4611
  step(from, delta) {
3617
- const segs = this.segments();
4612
+ const chips = this.chips();
3618
4613
  let i = from;
3619
- for (let n = 0; n < segs.length; n++) {
3620
- i = (i + delta + segs.length) % segs.length;
3621
- if (!segs[i].disabled)
4614
+ for (let n = 0; n < chips.length; n++) {
4615
+ i = (i + delta + chips.length) % chips.length;
4616
+ if (!chips[i].disabled)
3622
4617
  return i;
3623
4618
  }
3624
4619
  return null;
3625
4620
  }
3626
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSegmented, deps: [], target: i0.ɵɵFactoryTarget.Component });
3627
- 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: `
4621
+ // --- ControlValueAccessor -------------------------------------------------
4622
+ onChange = () => { };
4623
+ onTouched = () => { };
4624
+ writeValue(value) {
4625
+ // Normalised on read (selectedValues), so a null written to a `multiple` group is simply "none".
4626
+ this.value.set(value ?? null);
4627
+ }
4628
+ registerOnChange(fn) {
4629
+ this.onChange = fn;
4630
+ }
4631
+ registerOnTouched(fn) {
4632
+ this.onTouched = fn;
4633
+ }
4634
+ setDisabledState(isDisabled) {
4635
+ this.cvaDisabled.set(isDisabled);
4636
+ }
4637
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsChipGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
4638
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsChipGroup, isStandalone: true, selector: "sds-chip-group", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, deselectable: { classPropertyName: "deselectable", publicName: "deselectable", 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 }, wrap: { classPropertyName: "wrap", publicName: "wrap", isSignal: true, isRequired: false, transformFunction: null }, showCheck: { classPropertyName: "showCheck", publicName: "showCheck", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { classAttribute: "block" }, providers: [
4639
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SdsChipGroup), multi: true },
4640
+ ], ngImport: i0, template: `
3628
4641
  <div
3629
- role="radiogroup"
4642
+ [attr.role]="multiple() ? 'group' : 'radiogroup'"
3630
4643
  [attr.aria-label]="ariaLabel() || null"
3631
4644
  [attr.aria-labelledby]="ariaLabelledby() || null"
3632
- [attr.aria-disabled]="disabled() ? 'true' : null"
3633
- class="flex max-w-full items-stretch gap-0.5 overflow-x-auto rounded-control border border-border bg-muted p-0.5"
3634
- [class.w-full]="fullWidth()"
4645
+ [attr.aria-disabled]="isDisabled() ? 'true' : null"
4646
+ class="flex gap-2"
4647
+ [class]="wrap() ? 'flex-wrap' : 'overflow-x-auto pb-1'"
3635
4648
  (keydown)="onKeydown($event)"
4649
+ (focusout)="onTouched()"
3636
4650
  >
3637
- @for (seg of segments(); track $index; let i = $index) {
4651
+ @for (chip of chips(); track $index; let i = $index) {
3638
4652
  <button
3639
4653
  type="button"
3640
- role="radio"
3641
- [id]="segmentId(i)"
3642
- [attr.aria-checked]="isSelected(seg)"
3643
- [attr.aria-label]="iconOnly() ? seg.label : null"
3644
- [attr.title]="iconOnly() ? seg.label : null"
3645
- [attr.tabindex]="i === tabStop() ? 0 : -1"
4654
+ [id]="chipId(i)"
4655
+ [attr.role]="multiple() ? null : 'radio'"
4656
+ [attr.aria-checked]="multiple() ? null : isSelected(chip)"
4657
+ [attr.aria-pressed]="multiple() ? isSelected(chip) : null"
4658
+ [attr.tabindex]="multiple() || i === tabStop() ? 0 : -1"
3646
4659
  [attr.data-index]="i"
3647
- [disabled]="disabled() || seg.disabled"
3648
- [class]="segmentClass(seg)"
3649
- (click)="choose(i)"
4660
+ [disabled]="isDisabled() || chip.disabled"
4661
+ [class]="classFor(chip)"
4662
+ (click)="toggle(i)"
3650
4663
  >
3651
- @if (seg.icon) {
3652
- <sds-icon [name]="seg.icon" [size]="size() === 'sm' ? 15 : 17" class="shrink-0" />
4664
+ @if (isSelected(chip) && multiple() && showCheck()) {
4665
+ <sds-icon
4666
+ name="check"
4667
+ [size]="size() === 'sm' ? 12 : 14"
4668
+ [strokeWidth]="3"
4669
+ class="shrink-0"
4670
+ />
4671
+ } @else if (chip.icon) {
4672
+ <sds-icon [name]="chip.icon" [size]="size() === 'sm' ? 13 : 15" class="shrink-0" />
3653
4673
  }
3654
- @if (!iconOnly()) {
3655
- <span class="truncate">{{ seg.label }}</span>
4674
+ <span class="truncate">{{ chip.label }}</span>
4675
+ @if (chip.count !== null) {
4676
+ <span [class]="countClass">{{ chip.count }}</span>
3656
4677
  }
3657
4678
  </button>
3658
4679
  }
3659
4680
  </div>
3660
4681
  `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3661
4682
  }
3662
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSegmented, decorators: [{
4683
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsChipGroup, decorators: [{
3663
4684
  type: Component,
3664
4685
  args: [{
3665
- selector: 'sds-segmented',
4686
+ selector: 'sds-chip-group',
3666
4687
  changeDetection: ChangeDetectionStrategy.OnPush,
3667
4688
  imports: [SdsIcon],
3668
- host: { class: 'inline-flex max-w-full', '[class.w-full]': 'fullWidth()' },
4689
+ providers: [
4690
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SdsChipGroup), multi: true },
4691
+ ],
4692
+ host: { class: 'block' },
3669
4693
  template: `
3670
4694
  <div
3671
- role="radiogroup"
4695
+ [attr.role]="multiple() ? 'group' : 'radiogroup'"
3672
4696
  [attr.aria-label]="ariaLabel() || null"
3673
4697
  [attr.aria-labelledby]="ariaLabelledby() || null"
3674
- [attr.aria-disabled]="disabled() ? 'true' : null"
3675
- class="flex max-w-full items-stretch gap-0.5 overflow-x-auto rounded-control border border-border bg-muted p-0.5"
3676
- [class.w-full]="fullWidth()"
4698
+ [attr.aria-disabled]="isDisabled() ? 'true' : null"
4699
+ class="flex gap-2"
4700
+ [class]="wrap() ? 'flex-wrap' : 'overflow-x-auto pb-1'"
3677
4701
  (keydown)="onKeydown($event)"
4702
+ (focusout)="onTouched()"
3678
4703
  >
3679
- @for (seg of segments(); track $index; let i = $index) {
4704
+ @for (chip of chips(); track $index; let i = $index) {
3680
4705
  <button
3681
4706
  type="button"
3682
- role="radio"
3683
- [id]="segmentId(i)"
3684
- [attr.aria-checked]="isSelected(seg)"
3685
- [attr.aria-label]="iconOnly() ? seg.label : null"
3686
- [attr.title]="iconOnly() ? seg.label : null"
3687
- [attr.tabindex]="i === tabStop() ? 0 : -1"
4707
+ [id]="chipId(i)"
4708
+ [attr.role]="multiple() ? null : 'radio'"
4709
+ [attr.aria-checked]="multiple() ? null : isSelected(chip)"
4710
+ [attr.aria-pressed]="multiple() ? isSelected(chip) : null"
4711
+ [attr.tabindex]="multiple() || i === tabStop() ? 0 : -1"
3688
4712
  [attr.data-index]="i"
3689
- [disabled]="disabled() || seg.disabled"
3690
- [class]="segmentClass(seg)"
3691
- (click)="choose(i)"
4713
+ [disabled]="isDisabled() || chip.disabled"
4714
+ [class]="classFor(chip)"
4715
+ (click)="toggle(i)"
3692
4716
  >
3693
- @if (seg.icon) {
3694
- <sds-icon [name]="seg.icon" [size]="size() === 'sm' ? 15 : 17" class="shrink-0" />
4717
+ @if (isSelected(chip) && multiple() && showCheck()) {
4718
+ <sds-icon
4719
+ name="check"
4720
+ [size]="size() === 'sm' ? 12 : 14"
4721
+ [strokeWidth]="3"
4722
+ class="shrink-0"
4723
+ />
4724
+ } @else if (chip.icon) {
4725
+ <sds-icon [name]="chip.icon" [size]="size() === 'sm' ? 13 : 15" class="shrink-0" />
3695
4726
  }
3696
- @if (!iconOnly()) {
3697
- <span class="truncate">{{ seg.label }}</span>
4727
+ <span class="truncate">{{ chip.label }}</span>
4728
+ @if (chip.count !== null) {
4729
+ <span [class]="countClass">{{ chip.count }}</span>
3698
4730
  }
3699
4731
  </button>
3700
4732
  }
3701
4733
  </div>
3702
4734
  `,
3703
4735
  }]
3704
- }], 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 }] }] } });
4736
+ }], 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"] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], deselectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "deselectable", required: false }] }], 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 }] }], wrap: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrap", required: false }] }], showCheck: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCheck", required: false }] }] } });
3705
4737
 
3706
4738
  /**
3707
4739
  * Each status has its own glyph SHAPE (circle-check, triangle, octagon, circle-i) as well as its own
@@ -5702,6 +6734,10 @@ function sdsFileMatchesAccept(file, accept) {
5702
6734
  * affordance); clicking anywhere on the zone opens the picker too. All text is set through inputs with
5703
6735
  * English defaults.
5704
6736
  *
6737
+ * `appearance="button"` drops the zone and renders only the Browse button (files can still be dropped
6738
+ * on it) — for a "Replace photo" / "Upload logo" / "Add document" action next to a preview, with the
6739
+ * same accept/size/count checks and error messages. `browse()` opens the picker from your own control.
6740
+ *
5705
6741
  * @example
5706
6742
  * <sds-file-drop accept="image/jpeg,image/png,image/webp" multiple [maxSize]="10_000_000"
5707
6743
  * label="Drop photos here" browseLabel="Choose photos"
@@ -5720,6 +6756,10 @@ class SdsFileDrop {
5720
6756
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
5721
6757
  /** Icon in the zone at rest. */
5722
6758
  icon = input('upload', ...(ngDevMode ? [{ debugName: "icon" }] : []));
6759
+ /** `zone` (default): the dashed drop zone with a Browse button. `button`: the Browse button alone. */
6760
+ appearance = input('zone', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
6761
+ /** `button` appearance: size of the button. */
6762
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
5723
6763
  label = input('Drag and drop files here', ...(ngDevMode ? [{ debugName: "label" }] : []));
5724
6764
  /** Shown while files are dragged over the zone. */
5725
6765
  dropLabel = input('Drop to add', ...(ngDevMode ? [{ debugName: "dropLabel" }] : []));
@@ -5756,6 +6796,14 @@ class SdsFileDrop {
5756
6796
  ? `${base} cursor-copy border-brand-500 bg-brand-50 dark:bg-muted`
5757
6797
  : `${base} cursor-pointer border-border bg-muted/50 hover:border-ink-faint`;
5758
6798
  }, ...(ngDevMode ? [{ debugName: "zoneClass" }] : []));
6799
+ buttonClass = computed(() => {
6800
+ const size = this.size() === 'sm' ? 'h-8 px-3' : 'h-9 px-3.5';
6801
+ const drag = this.dragging() ? ' border-brand-500 bg-brand-50 dark:bg-muted' : ' border-border bg-surface hover:bg-muted';
6802
+ return (`inline-flex ${size} items-center gap-1.5 rounded-control border text-sm font-semibold text-ink shadow-sm ` +
6803
+ 'transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 ' +
6804
+ 'disabled:cursor-not-allowed disabled:opacity-60' +
6805
+ drag);
6806
+ }, ...(ngDevMode ? [{ debugName: "buttonClass" }] : []));
5759
6807
  /** Open the native file picker. */
5760
6808
  browse(event) {
5761
6809
  event?.stopPropagation();
@@ -5849,7 +6897,35 @@ class SdsFileDrop {
5849
6897
  this.errors.set([]);
5850
6898
  }
5851
6899
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsFileDrop, deps: [], target: i0.ɵɵFactoryTarget.Component });
5852
- 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: `
6900
+ 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 }, appearance: { classPropertyName: "appearance", publicName: "appearance", 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 }, 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: `
6901
+ @if (appearance() === 'button') {
6902
+ <button
6903
+ type="button"
6904
+ [class]="buttonClass()"
6905
+ [disabled]="disabled()"
6906
+ [attr.aria-describedby]="errors().length ? zoneId + '-errors' : null"
6907
+ [attr.data-dragging]="dragging() ? 'true' : null"
6908
+ (click)="browse($event)"
6909
+ (dragenter)="onDragEnter($event)"
6910
+ (dragover)="onDragOver($event)"
6911
+ (dragleave)="onDragLeave($event)"
6912
+ (drop)="onDrop($event)"
6913
+ >
6914
+ <sds-icon [name]="dragging() ? 'upload' : icon()" [size]="16" />
6915
+ {{ dragging() ? dropLabel() : browseLabel() }}
6916
+ </button>
6917
+ <input
6918
+ #fileInput
6919
+ type="file"
6920
+ class="sr-only"
6921
+ tabindex="-1"
6922
+ aria-hidden="true"
6923
+ [accept]="accept()"
6924
+ [multiple]="multiple()"
6925
+ [disabled]="disabled()"
6926
+ (change)="onInputChange($event)"
6927
+ />
6928
+ } @else {
5853
6929
  <div
5854
6930
  [class]="zoneClass()"
5855
6931
  [attr.aria-disabled]="disabled() ? 'true' : null"
@@ -5894,6 +6970,7 @@ class SdsFileDrop {
5894
6970
  (change)="onInputChange($event)"
5895
6971
  />
5896
6972
  </div>
6973
+ }
5897
6974
 
5898
6975
  <div aria-live="polite" aria-atomic="true">
5899
6976
  @if (errors().length) {
@@ -5917,6 +6994,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
5917
6994
  imports: [SdsIcon],
5918
6995
  host: { class: 'block' },
5919
6996
  template: `
6997
+ @if (appearance() === 'button') {
6998
+ <button
6999
+ type="button"
7000
+ [class]="buttonClass()"
7001
+ [disabled]="disabled()"
7002
+ [attr.aria-describedby]="errors().length ? zoneId + '-errors' : null"
7003
+ [attr.data-dragging]="dragging() ? 'true' : null"
7004
+ (click)="browse($event)"
7005
+ (dragenter)="onDragEnter($event)"
7006
+ (dragover)="onDragOver($event)"
7007
+ (dragleave)="onDragLeave($event)"
7008
+ (drop)="onDrop($event)"
7009
+ >
7010
+ <sds-icon [name]="dragging() ? 'upload' : icon()" [size]="16" />
7011
+ {{ dragging() ? dropLabel() : browseLabel() }}
7012
+ </button>
7013
+ <input
7014
+ #fileInput
7015
+ type="file"
7016
+ class="sr-only"
7017
+ tabindex="-1"
7018
+ aria-hidden="true"
7019
+ [accept]="accept()"
7020
+ [multiple]="multiple()"
7021
+ [disabled]="disabled()"
7022
+ (change)="onInputChange($event)"
7023
+ />
7024
+ } @else {
5920
7025
  <div
5921
7026
  [class]="zoneClass()"
5922
7027
  [attr.aria-disabled]="disabled() ? 'true' : null"
@@ -5961,6 +7066,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
5961
7066
  (change)="onInputChange($event)"
5962
7067
  />
5963
7068
  </div>
7069
+ }
5964
7070
 
5965
7071
  <div aria-live="polite" aria-atomic="true">
5966
7072
  @if (errors().length) {
@@ -5976,7 +7082,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
5976
7082
  </div>
5977
7083
  `,
5978
7084
  }]
5979
- }], 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"] }] } });
7085
+ }], 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 }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], dropLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "dropLabel", required: false }] }], browseLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "browseLabel", required: false }] }], 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"] }] } });
5980
7086
 
5981
7087
  let nextId$8 = 0;
5982
7088
  /**
@@ -10727,8 +11833,13 @@ class SdsTabsStore {
10727
11833
  id = `sds-tabs-${nextId$3++}`;
10728
11834
  /** Whether the tab list scrolls horizontally (mirrors [scrollable]). */
10729
11835
  scrollable = signal(false, ...(ngDevMode ? [{ debugName: "scrollable" }] : []));
11836
+ /** Controlled mode (mirrors [controlled]): a click only REQUESTS a tab, the host sets [value]. */
11837
+ controlled = signal(false, ...(ngDevMode ? [{ debugName: "controlled" }] : []));
11838
+ /** Called with every tab the user picks (wired to the host's (tabSelect) output). */
11839
+ onSelect = () => { };
10730
11840
  value;
10731
11841
  registered = signal([], ...(ngDevMode ? [{ debugName: "registered" }] : []));
11842
+ panels = signal([], ...(ngDevMode ? [{ debugName: "panels" }] : []));
10732
11843
  /** Called once by <sds-tabs> to share its two-way `value` signal. */
10733
11844
  connect(value) {
10734
11845
  this.value = value;
@@ -10739,8 +11850,11 @@ class SdsTabsStore {
10739
11850
  isActive(value) {
10740
11851
  return this.value() === value;
10741
11852
  }
11853
+ /** A user pick. Uncontrolled: becomes the active tab. Controlled: only reported to the host. */
10742
11854
  select(value) {
10743
- this.value.set(value);
11855
+ this.onSelect(value);
11856
+ if (!this.controlled())
11857
+ this.value.set(value);
10744
11858
  }
10745
11859
  registerTab(value) {
10746
11860
  this.registered.update((list) => (list.includes(value) ? list : [...list, value]));
@@ -10751,6 +11865,16 @@ class SdsTabsStore {
10751
11865
  unregisterTab(value) {
10752
11866
  this.registered.update((list) => list.filter((v) => v !== value));
10753
11867
  }
11868
+ registerPanel(value) {
11869
+ this.panels.update((list) => (list.includes(value) ? list : [...list, value]));
11870
+ }
11871
+ unregisterPanel(value) {
11872
+ this.panels.update((list) => list.filter((v) => v !== value));
11873
+ }
11874
+ /** Whether an <sds-tab-panel> exists for this value (tabs whose content lives elsewhere have none). */
11875
+ hasPanel(value) {
11876
+ return this.panels().includes(value);
11877
+ }
10754
11878
  tabId(value) {
10755
11879
  return `${this.id}-tab-${value}`;
10756
11880
  }
@@ -10770,6 +11894,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10770
11894
  * <sds-tab-panel> [value]; it is two-way bindable via [(value)]. When omitted,
10771
11895
  * the first registered tab is selected.
10772
11896
  *
11897
+ * **Controlled tabs** (`controlled`): a click, arrow key or Home/End only emits `(tabSelect)`; the active
11898
+ * tab changes when the host changes `[value]`. Use it when the active tab lives somewhere else — a
11899
+ * `?tab=` query param — or when switching can be refused (an unsaved-changes guard): the tab never
11900
+ * shows as selected before the host agrees. `(tabSelect)` also fires in the default mode.
11901
+ *
11902
+ * **Panels are optional.** A tab set whose content is rendered by the host (an `@switch` on the value, a
11903
+ * router outlet) can omit <sds-tab-panels>; a tab then carries no `aria-controls` to a missing panel.
11904
+ *
10773
11905
  * @example
10774
11906
  * <sds-tabs [(value)]="active">
10775
11907
  * <sds-tab-list>
@@ -10781,6 +11913,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10781
11913
  * <sds-tab-panel value="1">…</sds-tab-panel>
10782
11914
  * </sds-tab-panels>
10783
11915
  * </sds-tabs>
11916
+ *
11917
+ * @example
11918
+ * <!-- driven by a query param, content rendered by the host -->
11919
+ * <sds-tabs controlled [value]="activeTab()" (tabSelect)="selectTab($event)">
11920
+ * <sds-tab-list>
11921
+ * @for (tab of tabs; track tab) {
11922
+ * <sds-tab [value]="tab">{{ 'menu.tabs.' + tab | translate }}</sds-tab>
11923
+ * }
11924
+ * </sds-tab-list>
11925
+ * </sds-tabs>
10784
11926
  */
10785
11927
  class SdsTabs {
10786
11928
  store = inject(SdsTabsStore);
@@ -10788,12 +11930,21 @@ class SdsTabs {
10788
11930
  value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
10789
11931
  /** Let the tab list scroll horizontally instead of overflowing. */
10790
11932
  scrollable = input(false, ...(ngDevMode ? [{ debugName: "scrollable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11933
+ /**
11934
+ * Controlled mode: a user pick only emits (tabSelect); the host decides by setting [value]. See the
11935
+ * class note.
11936
+ */
11937
+ controlled = input(false, ...(ngDevMode ? [{ debugName: "controlled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11938
+ /** Emits the value of the tab the user picked (in both modes). */
11939
+ tabSelect = output();
10791
11940
  constructor() {
10792
11941
  this.store.connect(this.value);
11942
+ this.store.onSelect = (value) => this.tabSelect.emit(value);
10793
11943
  effect(() => this.store.scrollable.set(this.scrollable()));
11944
+ effect(() => this.store.controlled.set(this.controlled()));
10794
11945
  }
10795
11946
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTabs, deps: [], target: i0.ɵɵFactoryTarget.Component });
10796
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.25", type: SdsTabs, isStandalone: true, selector: "sds-tabs", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, scrollable: { classPropertyName: "scrollable", publicName: "scrollable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { classAttribute: "block" }, providers: [SdsTabsStore], ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
11947
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.25", type: SdsTabs, isStandalone: true, selector: "sds-tabs", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, scrollable: { classPropertyName: "scrollable", publicName: "scrollable", isSignal: true, isRequired: false, transformFunction: null }, controlled: { classPropertyName: "controlled", publicName: "controlled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", tabSelect: "tabSelect" }, host: { classAttribute: "block" }, providers: [SdsTabsStore], ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
10797
11948
  }
10798
11949
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTabs, decorators: [{
10799
11950
  type: Component,
@@ -10804,7 +11955,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10804
11955
  host: { class: 'block' },
10805
11956
  template: `<ng-content />`,
10806
11957
  }]
10807
- }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], scrollable: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollable", required: false }] }] } });
11958
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], scrollable: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollable", required: false }] }], controlled: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlled", required: false }] }], tabSelect: [{ type: i0.Output, args: ["tabSelect"] }] } });
10808
11959
 
10809
11960
  /**
10810
11961
  * Container (role="tablist") for the <sds-tab> headers. Renders the bottom rule
@@ -10901,7 +12052,7 @@ class SdsTab {
10901
12052
  role="tab"
10902
12053
  [id]="store.tabId(value())"
10903
12054
  [attr.aria-selected]="active()"
10904
- [attr.aria-controls]="store.panelId(value())"
12055
+ [attr.aria-controls]="store.hasPanel(value()) ? store.panelId(value()) : null"
10905
12056
  [attr.aria-disabled]="disabled() ? 'true' : null"
10906
12057
  [attr.tabindex]="active() && !disabled() ? 0 : -1"
10907
12058
  [disabled]="disabled()"
@@ -10924,7 +12075,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10924
12075
  role="tab"
10925
12076
  [id]="store.tabId(value())"
10926
12077
  [attr.aria-selected]="active()"
10927
- [attr.aria-controls]="store.panelId(value())"
12078
+ [attr.aria-controls]="store.hasPanel(value()) ? store.panelId(value()) : null"
10928
12079
  [attr.aria-disabled]="disabled() ? 'true' : null"
10929
12080
  [attr.tabindex]="active() && !disabled() ? 0 : -1"
10930
12081
  [disabled]="disabled()"
@@ -10969,6 +12120,12 @@ class SdsTabPanel {
10969
12120
  store = inject(SdsTabsStore);
10970
12121
  value = input.required(...(ngDevMode ? [{ debugName: "value" }] : []));
10971
12122
  active = computed(() => this.store.isActive(this.value()), ...(ngDevMode ? [{ debugName: "active" }] : []));
12123
+ ngOnInit() {
12124
+ this.store.registerPanel(this.value());
12125
+ }
12126
+ ngOnDestroy() {
12127
+ this.store.unregisterPanel(this.value());
12128
+ }
10972
12129
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTabPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
10973
12130
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsTabPanel, isStandalone: true, selector: "sds-tab-panel", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, host: { classAttribute: "block" }, ngImport: i0, template: `
10974
12131
  @if (active()) {
@@ -11382,6 +12539,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
11382
12539
  }]
11383
12540
  }] });
11384
12541
 
12542
+ /**
12543
+ * Replaces the table's plain `emptyMessage` with rich content — an explanation and a call to action,
12544
+ * a "no results for …" line. Rendered in the full-width empty row, only when not `loading`.
12545
+ *
12546
+ * @example
12547
+ * <sds-table [value]="categories()" [columns]="cols">
12548
+ * <ng-template sdsTableEmpty>
12549
+ * <p>No categories yet.</p>
12550
+ * <sds-button size="sm" (click)="create()">Add a category</sds-button>
12551
+ * </ng-template>
12552
+ * </sds-table>
12553
+ */
12554
+ class SdsTableEmpty {
12555
+ /** The wrapped template, rendered by the host table via ngTemplateOutlet. */
12556
+ template = inject(TemplateRef);
12557
+ /** Lets the compiler type-check `let-*` bindings against SdsTableEmptyContext. */
12558
+ static ngTemplateContextGuard(_dir, _ctx) {
12559
+ return true;
12560
+ }
12561
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTableEmpty, deps: [], target: i0.ɵɵFactoryTarget.Directive });
12562
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.25", type: SdsTableEmpty, isStandalone: true, selector: "ng-template[sdsTableEmpty]", ngImport: i0 });
12563
+ }
12564
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTableEmpty, decorators: [{
12565
+ type: Directive,
12566
+ args: [{
12567
+ selector: 'ng-template[sdsTableEmpty]',
12568
+ standalone: true,
12569
+ }]
12570
+ }] });
12571
+
11385
12572
  const SIZES = {
11386
12573
  sm: { cell: 'px-3 py-2 text-sm', head: 'px-3 py-2' },
11387
12574
  md: { cell: 'px-4 py-2.5 text-sm', head: 'px-4 py-2.5' },
@@ -11413,6 +12600,11 @@ let nextId$1 = 0;
11413
12600
  * - **Custom headers:** `<ng-template sdsHeader="field">` replaces a column's header content (label and
11414
12601
  * sort button) — e.g. a "select all" checkbox above your own checkbox column. Its context carries the
11415
12602
  * filtered `rows` (all pages) and the displayed `pageRows` (see {@link SdsTableHeaderContext}).
12603
+ * - **Clickable rows:** `rowClickable` makes each row an action (pointer, tab stop, Enter/Space) that
12604
+ * emits `(rowClick)`; clicks on buttons, links and fields inside a cell are not row clicks, so no
12605
+ * `stopPropagation()` is needed. `[rowClass]` adds per-row classes (e.g. dim a blocked row).
12606
+ * - **Empty state:** `<ng-template sdsTableEmpty let-query>` replaces the plain `emptyMessage` (e.g. a
12607
+ * sentence and a "create" button); it is not shown while `loading`.
11416
12608
  * - **Labels:** every visible and spoken string is an input with an English default (paginator,
11417
12609
  * filter, selection, expansion, editor). An app serving more than one language passes translations.
11418
12610
  *
@@ -11484,6 +12676,14 @@ class SdsTable {
11484
12676
  selection = model(null, ...(ngDevMode ? [{ debugName: "selection" }] : []));
11485
12677
  loading = input(false, ...(ngDevMode ? [{ debugName: "loading", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
11486
12678
  emptyMessage = input('No data', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : []));
12679
+ /**
12680
+ * Rows are actions: a pointer cursor, a tab stop per row, and Enter/Space emit `(rowClick)` like a
12681
+ * click. Clicks that land on a control inside a cell (button, link, input, checkbox…) do not count as
12682
+ * a row click, so a row can open a drawer while its cells keep their own buttons.
12683
+ */
12684
+ rowClickable = input(false, ...(ngDevMode ? [{ debugName: "rowClickable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
12685
+ /** Extra classes per body row, e.g. `(row) => row.blocked ? 'opacity-60' : ''`. */
12686
+ rowClass = input(null, ...(ngDevMode ? [{ debugName: "rowClass" }] : []));
11487
12687
  caption = input('', ...(ngDevMode ? [{ debugName: "caption" }] : []));
11488
12688
  /** Fallback accessible name for the `<table>` when no `caption` is set. */
11489
12689
  ariaLabel = input('Data table', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -11574,6 +12774,11 @@ class SdsTable {
11574
12774
  expansionTemplate() {
11575
12775
  return this.rowExpansion()?.template ?? null;
11576
12776
  }
12777
+ // --- empty-state template -------------------------------------------------
12778
+ emptyTpl = contentChild(SdsTableEmpty, ...(ngDevMode ? [{ debugName: "emptyTpl" }] : []));
12779
+ emptyTemplate() {
12780
+ return this.emptyTpl()?.template ?? null;
12781
+ }
11577
12782
  // --- pipeline state -------------------------------------------------------
11578
12783
  filterText = signal('', ...(ngDevMode ? [{ debugName: "filterText" }] : []));
11579
12784
  sortField = signal(null, ...(ngDevMode ? [{ debugName: "sortField" }] : []));
@@ -11809,9 +13014,14 @@ class SdsTable {
11809
13014
  this.toggleRowSelection(row);
11810
13015
  }
11811
13016
  }
11812
- /** Enter/Space on a (single-mode) focusable row toggles its selection. */
13017
+ /** Enter/Space on a focusable row (single selection or `rowClickable`) acts like a click on it. */
11813
13018
  onRowKeydown(event, row) {
11814
- if (this.selectionMode() !== 'single' || this.loading() || this.editable())
13019
+ if (this.loading() || this.editable())
13020
+ return;
13021
+ if (this.selectionMode() !== 'single' && !this.rowClickable())
13022
+ return;
13023
+ // Only the row itself: Enter on a button inside a cell belongs to that button.
13024
+ if (event.target !== event.currentTarget)
11815
13025
  return;
11816
13026
  event.preventDefault();
11817
13027
  this.onRowClick(row);
@@ -11830,9 +13040,11 @@ class SdsTable {
11830
13040
  this.rowSelect.emit(row);
11831
13041
  }
11832
13042
  }
11833
- onRowClick(row) {
13043
+ onRowClick(row, event) {
11834
13044
  if (this.loading())
11835
13045
  return;
13046
+ if (event && this.fromInnerControl(event))
13047
+ return;
11836
13048
  this.rowClick.emit(row);
11837
13049
  if (this.selectionMode() === 'single') {
11838
13050
  if (this.equals(this.selection(), row)) {
@@ -11845,6 +13057,16 @@ class SdsTable {
11845
13057
  }
11846
13058
  }
11847
13059
  }
13060
+ /** Whether a click started on an interactive element inside the row (not on the row itself). */
13061
+ fromInnerControl(event) {
13062
+ const row = event.currentTarget;
13063
+ const target = event.target;
13064
+ if (!row || !target || !(target instanceof Element))
13065
+ return false;
13066
+ const control = target.closest('button, a[href], input, select, textarea, label, [contenteditable="true"], ' +
13067
+ '[role="button"], [role="checkbox"], [role="switch"], [role="radio"], [role="combobox"], [role="menuitem"]');
13068
+ return !!control && control !== row && row.contains(control);
13069
+ }
11848
13070
  // --- paginator ------------------------------------------------------------
11849
13071
  onPageChange(state) {
11850
13072
  this.editing.set(null);
@@ -12378,7 +13600,7 @@ class SdsTable {
12378
13600
  const align = ALIGN[col?.align ?? 'left'];
12379
13601
  return `${SIZES[this.size()].cell} ${align} text-ink align-middle`;
12380
13602
  }
12381
- rowClass(row, index) {
13603
+ bodyRowClass(row, index) {
12382
13604
  const parts = ['transition-colors'];
12383
13605
  if (this.gridlines())
12384
13606
  parts.push('divide-x divide-border');
@@ -12390,9 +13612,12 @@ class SdsTable {
12390
13612
  }
12391
13613
  if (this.hoverable() && !this.isRowSelected(row))
12392
13614
  parts.push('hover:bg-muted/60');
12393
- if (this.selectionMode() === 'single') {
13615
+ if (this.selectionMode() === 'single' || (this.rowClickable() && !this.editable())) {
12394
13616
  parts.push('cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500');
12395
13617
  }
13618
+ const extra = this.rowClass()?.(row, index);
13619
+ if (extra)
13620
+ parts.push(extra);
12396
13621
  return parts.join(' ');
12397
13622
  }
12398
13623
  checkboxClass(checked, indeterminate) {
@@ -12401,7 +13626,7 @@ class SdsTable {
12401
13626
  : 'border-border bg-surface';
12402
13627
  }
12403
13628
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
12404
- 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 }, selectPlaceholder: { classPropertyName: "selectPlaceholder", publicName: "selectPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, selectAllLabel: { classPropertyName: "selectAllLabel", publicName: "selectAllLabel", isSignal: true, isRequired: false, transformFunction: null }, selectRowLabel: { classPropertyName: "selectRowLabel", publicName: "selectRowLabel", isSignal: true, isRequired: false, transformFunction: null }, selectionColumnLabel: { classPropertyName: "selectionColumnLabel", publicName: "selectionColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, expandColumnLabel: { classPropertyName: "expandColumnLabel", publicName: "expandColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, expandRowLabel: { classPropertyName: "expandRowLabel", publicName: "expandRowLabel", isSignal: true, isRequired: false, transformFunction: null }, collapseRowLabel: { classPropertyName: "collapseRowLabel", publicName: "collapseRowLabel", 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: "headerTemplates", predicate: SdsTableHeader, 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: `
13629
+ 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 }, rowClickable: { classPropertyName: "rowClickable", publicName: "rowClickable", isSignal: true, isRequired: false, transformFunction: null }, rowClass: { classPropertyName: "rowClass", publicName: "rowClass", 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 }, selectPlaceholder: { classPropertyName: "selectPlaceholder", publicName: "selectPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, selectAllLabel: { classPropertyName: "selectAllLabel", publicName: "selectAllLabel", isSignal: true, isRequired: false, transformFunction: null }, selectRowLabel: { classPropertyName: "selectRowLabel", publicName: "selectRowLabel", isSignal: true, isRequired: false, transformFunction: null }, selectionColumnLabel: { classPropertyName: "selectionColumnLabel", publicName: "selectionColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, expandColumnLabel: { classPropertyName: "expandColumnLabel", publicName: "expandColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, expandRowLabel: { classPropertyName: "expandRowLabel", publicName: "expandRowLabel", isSignal: true, isRequired: false, transformFunction: null }, collapseRowLabel: { classPropertyName: "collapseRowLabel", publicName: "collapseRowLabel", 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: "headerTemplates", predicate: SdsTableHeader, isSignal: true }, { propertyName: "rowExpansion", first: true, predicate: SdsTableRowExpansion, descendants: true, isSignal: true }, { propertyName: "emptyTpl", first: true, predicate: SdsTableEmpty, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "numberEditor", first: true, predicate: SdsInputNumber, descendants: true, isSignal: true }], ngImport: i0, template: `
12405
13630
  <div class="rounded-card border border-border bg-surface overflow-hidden">
12406
13631
  <!-- Toolbar: caption + global filter -->
12407
13632
  @if (caption() || globalFilter()) {
@@ -12555,10 +13780,10 @@ class SdsTable {
12555
13780
  <tbody class="divide-y divide-border">
12556
13781
  @for (row of displayRows(); track trackRow($index, row); let i = $index) {
12557
13782
  <tr
12558
- [class]="rowClass(row, i)"
12559
- [attr.tabindex]="selectionMode() === 'single' && !loading() && !editable() ? 0 : null"
13783
+ [class]="bodyRowClass(row, i)"
13784
+ [attr.tabindex]="(selectionMode() === 'single' || rowClickable()) && !loading() && !editable() ? 0 : null"
12560
13785
  [attr.aria-selected]="selectionMode() === 'single' ? (isRowSelected(row) ? 'true' : 'false') : null"
12561
- (click)="onRowClick(row)"
13786
+ (click)="onRowClick(row, $event)"
12562
13787
  (keydown.enter)="onRowKeydown($event, row)"
12563
13788
  (keydown.space)="onRowKeydown($event, row)"
12564
13789
  >
@@ -12783,7 +14008,14 @@ class SdsTable {
12783
14008
  [attr.colspan]="totalColumnCount()"
12784
14009
  class="py-10 text-center text-ink-faint"
12785
14010
  >
12786
- {{ emptyMessage() }}
14011
+ @if (emptyTemplate(); as tpl) {
14012
+ <ng-container
14013
+ [ngTemplateOutlet]="tpl"
14014
+ [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
14015
+ />
14016
+ } @else {
14017
+ {{ emptyMessage() }}
14018
+ }
12787
14019
  </td>
12788
14020
  </tr>
12789
14021
  }
@@ -12990,10 +14222,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
12990
14222
  <tbody class="divide-y divide-border">
12991
14223
  @for (row of displayRows(); track trackRow($index, row); let i = $index) {
12992
14224
  <tr
12993
- [class]="rowClass(row, i)"
12994
- [attr.tabindex]="selectionMode() === 'single' && !loading() && !editable() ? 0 : null"
14225
+ [class]="bodyRowClass(row, i)"
14226
+ [attr.tabindex]="(selectionMode() === 'single' || rowClickable()) && !loading() && !editable() ? 0 : null"
12995
14227
  [attr.aria-selected]="selectionMode() === 'single' ? (isRowSelected(row) ? 'true' : 'false') : null"
12996
- (click)="onRowClick(row)"
14228
+ (click)="onRowClick(row, $event)"
12997
14229
  (keydown.enter)="onRowKeydown($event, row)"
12998
14230
  (keydown.space)="onRowKeydown($event, row)"
12999
14231
  >
@@ -13218,7 +14450,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13218
14450
  [attr.colspan]="totalColumnCount()"
13219
14451
  class="py-10 text-center text-ink-faint"
13220
14452
  >
13221
- {{ emptyMessage() }}
14453
+ @if (emptyTemplate(); as tpl) {
14454
+ <ng-container
14455
+ [ngTemplateOutlet]="tpl"
14456
+ [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
14457
+ />
14458
+ } @else {
14459
+ {{ emptyMessage() }}
14460
+ }
13222
14461
  </td>
13223
14462
  </tr>
13224
14463
  }
@@ -13264,7 +14503,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13264
14503
  </div>
13265
14504
  `,
13266
14505
  }]
13267
- }], 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 }] }], selectPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectPlaceholder", required: false }] }], selectAllLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectAllLabel", required: false }] }], selectRowLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectRowLabel", required: false }] }], selectionColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionColumnLabel", required: false }] }], expandColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandColumnLabel", required: false }] }], expandRowLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandRowLabel", required: false }] }], collapseRowLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseRowLabel", 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 }] }], headerTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SdsTableHeader), { isSignal: true }] }], rowExpansion: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsTableRowExpansion), { isSignal: true }] }], numberEditor: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SdsInputNumber), { isSignal: true }] }] } });
14506
+ }], 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 }] }], rowClickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowClickable", required: false }] }], rowClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowClass", 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 }] }], selectPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectPlaceholder", required: false }] }], selectAllLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectAllLabel", required: false }] }], selectRowLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectRowLabel", required: false }] }], selectionColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionColumnLabel", required: false }] }], expandColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandColumnLabel", required: false }] }], expandRowLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandRowLabel", required: false }] }], collapseRowLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseRowLabel", 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 }] }], headerTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SdsTableHeader), { isSignal: true }] }], rowExpansion: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsTableRowExpansion), { isSignal: true }] }], emptyTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsTableEmpty), { isSignal: true }] }], numberEditor: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SdsInputNumber), { isSignal: true }] }] } });
13268
14507
 
13269
14508
  /**
13270
14509
  * A tiny transient-notification bus. Application code calls {@link success} / {@link error} / {@link info}
@@ -14828,7 +16067,7 @@ class SdsUserMenu {
14828
16067
  </div>
14829
16068
  </div>
14830
16069
  }
14831
- `, isInline: true, styles: [".animate-sds-menu{animation:sds-menu-in .14s ease-out}@keyframes sds-menu-in{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.animate-sds-menu{animation:none}}\n"], dependencies: [{ kind: "component", type: SdsAvatar, selector: "sds-avatar", inputs: ["name", "initials", "src", "size", "status", "bgClass"] }, { kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
16070
+ `, isInline: true, styles: [".animate-sds-menu{animation:sds-menu-in .14s ease-out}@keyframes sds-menu-in{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.animate-sds-menu{animation:none}}\n"], dependencies: [{ kind: "component", type: SdsAvatar, selector: "sds-avatar", inputs: ["name", "initials", "src", "size", "status", "shape", "bgClass", "fallbackIcon", "placeholderClass", "decorative", "lazy"] }, { kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14832
16071
  }
14833
16072
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsUserMenu, decorators: [{
14834
16073
  type: Component,
@@ -15902,5 +17141,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
15902
17141
  * Generated bundle index. Do not edit.
15903
17142
  */
15904
17143
 
15905
- 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, SdsTableHeader, 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 };
17144
+ export { SDS_ALLERGENS, SDS_ALLERGEN_ICONS, SDS_ALLERGEN_LABELS_EN, SDS_ICONS, SDS_PHONE_COUNTRIES, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAllergenIcon, SdsAvatar, SdsBadge, SdsBulkBar, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsChecklistRow, SdsChip, SdsChipGroup, SdsDatePicker, SdsFileDrop, SdsIcon, SdsImageCropper, SdsInput, SdsInputNumber, SdsLanguageSwitcher, SdsLocalizedInput, SdsMenu, SdsMenuItem, SdsMenuTrigger, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsPaginator, SdsPhoneInput, SdsRadio, SdsScore, SdsSearchCommand, SdsSegmented, SdsSelect, SdsSelectEmpty, SdsSelectFooter, SdsSelectOption, SdsShell, SdsSidebar, SdsSortableHandle, SdsSortableItem, SdsSortableList, SdsStatusTile, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableEmpty, SdsTableHeader, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsThemeService, SdsThemeToggle, SdsTimeInput, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SdsTriStateChip, SdsUserMenu, SidebarStore, addDays, addMonths, addMonthsKeepDay, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isoDay, pad2, sdsFileMatchesAccept, sdsFormatBytes, sdsFormatNational, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth };
15906
17145
  //# sourceMappingURL=besaitech-ng-design-system.mjs.map