@besaitech/ng-design-system 0.0.63 → 0.0.64

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.
@@ -13109,6 +13109,8 @@ class SdsTabsStore {
13109
13109
  id = `sds-tabs-${nextId$3++}`;
13110
13110
  /** Whether the tab list scrolls horizontally (mirrors [scrollable]). */
13111
13111
  scrollable = signal(false, ...(ngDevMode ? [{ debugName: "scrollable" }] : []));
13112
+ /** Header style (mirrors [variant]). */
13113
+ variant = signal('underline', ...(ngDevMode ? [{ debugName: "variant" }] : []));
13112
13114
  /** Controlled mode (mirrors [controlled]): a click only REQUESTS a tab, the host sets [value]. */
13113
13115
  controlled = signal(false, ...(ngDevMode ? [{ debugName: "controlled" }] : []));
13114
13116
  /** Called with every tab the user picks (wired to the host's (tabSelect) output). */
@@ -13120,6 +13122,8 @@ class SdsTabsStore {
13120
13122
  connect(value) {
13121
13123
  this.value = value;
13122
13124
  }
13125
+ /** Bumped when a tab's size may have changed (label, icon or count updated), so the indicator re-measures. */
13126
+ layoutVersion = signal(0, ...(ngDevMode ? [{ debugName: "layoutVersion" }] : []));
13123
13127
  activeValue() {
13124
13128
  return this.value();
13125
13129
  }
@@ -13175,6 +13179,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13175
13179
  * `?tab=` query param — or when switching can be refused (an unsaved-changes guard): the tab never
13176
13180
  * shows as selected before the host agrees. `(tabSelect)` also fires in the default mode.
13177
13181
  *
13182
+ * **Two looks** (`variant`): `underline` (default) for a page's sections — a brand underline slides to the active
13183
+ * tab, inactive tabs get a soft hover; `segmented` for a compact switch between 2–4 views (drawers, cards) — a raised
13184
+ * thumb slides between the options. Either can carry an icon (`[icon]`) and a count (`[count]`) per tab. A
13185
+ * `scrollable` underline list that overflows fades its edges and shows arrow buttons, and keeps the active tab in view.
13186
+ *
13178
13187
  * **Panels are optional.** A tab set whose content is rendered by the host (an `@switch` on the value, a
13179
13188
  * router outlet) can omit <sds-tab-panels>; a tab then carries no `aria-controls` to a missing panel.
13180
13189
  *
@@ -13191,6 +13200,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13191
13200
  * </sds-tabs>
13192
13201
  *
13193
13202
  * @example
13203
+ * <!-- icons + counts; a compact switch in a drawer -->
13204
+ * <sds-tabs scrollable [(value)]="tab">
13205
+ * <sds-tab-list ariaLabel="Orders">
13206
+ * <sds-tab value="active" icon="clipboard-list" [count]="activeCount()">Active</sds-tab>
13207
+ * <sds-tab value="history" icon="clock">History</sds-tab>
13208
+ * </sds-tab-list>
13209
+ * </sds-tabs>
13210
+ * <sds-tabs variant="segmented" [(value)]="view">
13211
+ * <sds-tab-list class="w-full">…</sds-tab-list>
13212
+ * </sds-tabs>
13213
+ *
13214
+ * @example
13194
13215
  * <!-- driven by a query param, content rendered by the host -->
13195
13216
  * <sds-tabs controlled [value]="activeTab()" (tabSelect)="selectTab($event)">
13196
13217
  * <sds-tab-list>
@@ -13204,8 +13225,10 @@ class SdsTabs {
13204
13225
  store = inject(SdsTabsStore);
13205
13226
  /** Value of the active tab (two-way). */
13206
13227
  value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
13207
- /** Let the tab list scroll horizontally instead of overflowing. */
13228
+ /** Let the tab list scroll horizontally instead of overflowing (edge fades + arrow buttons when it does). */
13208
13229
  scrollable = input(false, ...(ngDevMode ? [{ debugName: "scrollable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
13230
+ /** `underline` (page sections, default) or `segmented` (a compact switch between 2–4 views). */
13231
+ variant = input('underline', ...(ngDevMode ? [{ debugName: "variant" }] : []));
13209
13232
  /**
13210
13233
  * Controlled mode: a user pick only emits (tabSelect); the host decides by setting [value]. See the
13211
13234
  * class note.
@@ -13217,10 +13240,11 @@ class SdsTabs {
13217
13240
  this.store.connect(this.value);
13218
13241
  this.store.onSelect = (value) => this.tabSelect.emit(value);
13219
13242
  effect(() => this.store.scrollable.set(this.scrollable()));
13243
+ effect(() => this.store.variant.set(this.variant()));
13220
13244
  effect(() => this.store.controlled.set(this.controlled()));
13221
13245
  }
13222
13246
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTabs, deps: [], target: i0.ɵɵFactoryTarget.Component });
13223
- 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 });
13247
+ 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 }, variant: { classPropertyName: "variant", publicName: "variant", 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 });
13224
13248
  }
13225
13249
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTabs, decorators: [{
13226
13250
  type: Component,
@@ -13231,12 +13255,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13231
13255
  host: { class: 'block' },
13232
13256
  template: `<ng-content />`,
13233
13257
  }]
13234
- }], 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"] }] } });
13258
+ }], 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 }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], controlled: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlled", required: false }] }], tabSelect: [{ type: i0.Output, args: ["tabSelect"] }] } });
13235
13259
 
13236
13260
  /**
13237
- * Container (role="tablist") for the <sds-tab> headers. Renders the bottom rule
13238
- * and handles roving-focus keyboard navigation (Arrow keys, Home, End) with
13239
- * automatic activation.
13261
+ * Container (role="tablist") for the <sds-tab> headers: draws the active-tab indicator and handles roving-focus
13262
+ * keyboard navigation (Arrow keys, Home, End) with automatic activation.
13263
+ *
13264
+ * **Underline** (default): a hairline rule with a 3px brand underline that slides to the active tab. When the tab set
13265
+ * is `scrollable` and the tabs don't fit, the hidden side fades out and an arrow button scrolls by a page; picking a
13266
+ * tab (click or keyboard) scrolls it into view. **Segmented**: a tinted track with a raised thumb that slides under the
13267
+ * active option; put `class="w-full"` on the list to stretch the options evenly.
13240
13268
  *
13241
13269
  * Name the tab set with [ariaLabel] (or [ariaLabelledby] pointing at a visible heading) when the page has
13242
13270
  * more than one, or when the tabs alone do not say what they switch.
@@ -13245,26 +13273,124 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13245
13273
  * <sds-tab-list [ariaLabel]="'Order status'">…</sds-tab-list>
13246
13274
  */
13247
13275
  class SdsTabList {
13248
- host = inject(ElementRef);
13249
13276
  store = inject(SdsTabsStore);
13277
+ scroller = viewChild.required('scroller');
13250
13278
  /** Accessible name of the tablist. */
13251
13279
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
13252
13280
  /** Id of a visible element that names the tablist (takes precedence over [ariaLabel] for AT). */
13253
13281
  ariaLabelledby = input('', ...(ngDevMode ? [{ debugName: "ariaLabelledby" }] : []));
13282
+ segmented = computed(() => this.store.variant() === 'segmented', ...(ngDevMode ? [{ debugName: "segmented" }] : []));
13283
+ indicator = signal(null, ...(ngDevMode ? [{ debugName: "indicator" }] : []));
13284
+ /** Off for the first placement, so the indicator doesn't fly in from the left on load. */
13285
+ animate = signal(false, ...(ngDevMode ? [{ debugName: "animate" }] : []));
13286
+ canScrollStart = signal(false, ...(ngDevMode ? [{ debugName: "canScrollStart" }] : []));
13287
+ canScrollEnd = signal(false, ...(ngDevMode ? [{ debugName: "canScrollEnd" }] : []));
13288
+ arrows = computed(() => this.store.scrollable() && !this.segmented(), ...(ngDevMode ? [{ debugName: "arrows" }] : []));
13289
+ hostClass = computed(() => this.segmented() ? 'relative inline-flex max-w-full' : 'relative block', ...(ngDevMode ? [{ debugName: "hostClass" }] : []));
13254
13290
  /**
13255
- * Scrollable: `overflow-x: auto` turns the other axis to `auto` too (CSS), so the tabs' 1px overlap of a
13256
- * border-bottom rule (`-mb-px`, so the active underline covers it) made the list 1px taller than its box and
13257
- * showed a vertical scrollbar. There the rule is an inset shadow instead — painted under the tabs, so the active
13258
- * underline still covers it with no overlap — and vertical overflow is hidden.
13291
+ * The scroller is the tablist. Underline: the rule is an inset shadow, painted under the tabs, so nothing overflows
13292
+ * vertically — `overflow-x: auto` turns the other axis to `auto` too (CSS), and a 1px overflow showed a vertical
13293
+ * scrollbar. Segmented: a tinted track (ink at 5%, so it reads on any surface) around the options.
13259
13294
  */
13260
- listClass = computed(() => this.store.scrollable()
13261
- ? 'relative flex overflow-x-auto overflow-y-hidden shadow-[inset_0_-1px_0_var(--color-border)]'
13262
- : 'relative flex border-b border-border', ...(ngDevMode ? [{ debugName: "listClass" }] : []));
13295
+ scrollerClass = computed(() => {
13296
+ const scroll = this.store.scrollable()
13297
+ ? 'overflow-x-auto overflow-y-hidden overscroll-x-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
13298
+ : '';
13299
+ return this.segmented()
13300
+ ? `relative flex w-full gap-1 rounded-control bg-ink/5 p-1 ${scroll}`
13301
+ : `relative flex gap-1 shadow-[inset_0_-1px_0_var(--color-border)] ${scroll}`;
13302
+ }, ...(ngDevMode ? [{ debugName: "scrollerClass" }] : []));
13303
+ /** Fade the side(s) with hidden tabs, so the overflow reads as "more this way". */
13304
+ fadeMask = computed(() => {
13305
+ if (!this.arrows())
13306
+ return null;
13307
+ const start = this.canScrollStart();
13308
+ const end = this.canScrollEnd();
13309
+ if (!start && !end)
13310
+ return null;
13311
+ return `linear-gradient(to right, ${start ? 'transparent, black 48px' : 'black'}, ${end ? 'black calc(100% - 48px), transparent' : 'black'})`;
13312
+ }, ...(ngDevMode ? [{ debugName: "fadeMask" }] : []));
13313
+ constructor() {
13314
+ // Place the indicator after every render that may move the active tab (selection, labels, counts, variant).
13315
+ afterRenderEffect(() => {
13316
+ this.store.activeValue();
13317
+ this.store.layoutVersion();
13318
+ this.store.variant();
13319
+ this.place();
13320
+ });
13321
+ // Fonts loading, a parent resizing or labels translating all change tab widths without a signal.
13322
+ const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(() => this.place(false));
13323
+ afterRenderEffect(() => {
13324
+ const el = this.scroller().nativeElement;
13325
+ if (!observer)
13326
+ return;
13327
+ observer.disconnect();
13328
+ observer.observe(el);
13329
+ el.querySelectorAll('[data-sds-tab]').forEach((tab) => observer.observe(tab));
13330
+ });
13331
+ inject(DestroyRef).onDestroy(() => observer?.disconnect());
13332
+ }
13333
+ /** Measure the active tab and move the indicator there; keep it in view when the selection changed. */
13334
+ place(reveal = true) {
13335
+ const scroller = this.scroller().nativeElement;
13336
+ const tab = scroller.querySelector('[role="tab"][aria-selected="true"]');
13337
+ this.measureScroll();
13338
+ if (!tab) {
13339
+ this.indicator.set(null);
13340
+ return;
13341
+ }
13342
+ // Underline: as wide as the label area (the tab's padding is the hover pill's, not the underline's).
13343
+ const inset = this.segmented() ? 0 : 12;
13344
+ const box = { left: tab.offsetLeft + inset, width: Math.max(tab.offsetWidth - inset * 2, 12) };
13345
+ const prev = this.indicator();
13346
+ if (prev && prev.left === box.left && prev.width === box.width)
13347
+ return;
13348
+ this.indicator.set(box);
13349
+ if (prev) {
13350
+ this.animate.set(true);
13351
+ }
13352
+ else {
13353
+ // First placement: no transition, then enable it for the moves that follow.
13354
+ requestAnimationFrame(() => this.animate.set(true));
13355
+ }
13356
+ if (reveal)
13357
+ this.reveal(tab);
13358
+ }
13359
+ /** Scroll just enough to show the active tab clear of the edge fades (never scrolls the page vertically). */
13360
+ reveal(tab) {
13361
+ const scroller = this.scroller().nativeElement;
13362
+ if (scroller.scrollWidth <= scroller.clientWidth)
13363
+ return;
13364
+ const margin = 48;
13365
+ const start = tab.offsetLeft - margin;
13366
+ const end = tab.offsetLeft + tab.offsetWidth + margin - scroller.clientWidth;
13367
+ if (scroller.scrollLeft > start) {
13368
+ scroller.scrollTo({ left: Math.max(start, 0), behavior: this.smooth() });
13369
+ }
13370
+ else if (scroller.scrollLeft < end) {
13371
+ scroller.scrollTo({ left: end, behavior: this.smooth() });
13372
+ }
13373
+ }
13374
+ measureScroll() {
13375
+ const el = this.scroller().nativeElement;
13376
+ const max = el.scrollWidth - el.clientWidth;
13377
+ this.canScrollStart.set(el.scrollLeft > 1);
13378
+ this.canScrollEnd.set(max > 1 && el.scrollLeft < max - 1);
13379
+ }
13380
+ scrollByPage(direction) {
13381
+ const el = this.scroller().nativeElement;
13382
+ el.scrollBy({ left: direction * el.clientWidth * 0.75, behavior: this.smooth() });
13383
+ }
13384
+ smooth() {
13385
+ return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches
13386
+ ? 'auto'
13387
+ : 'smooth';
13388
+ }
13263
13389
  onKeydown(event) {
13264
13390
  const keys = ['ArrowRight', 'ArrowLeft', 'Home', 'End'];
13265
13391
  if (!keys.includes(event.key))
13266
13392
  return;
13267
- const tabs = Array.from(this.host.nativeElement.querySelectorAll('[role="tab"]')).filter((b) => !b.disabled);
13393
+ const tabs = Array.from(this.scroller().nativeElement.querySelectorAll('[role="tab"]')).filter((b) => !b.disabled);
13268
13394
  if (!tabs.length)
13269
13395
  return;
13270
13396
  event.preventDefault();
@@ -13289,33 +13415,141 @@ class SdsTabList {
13289
13415
  el.click(); // automatic activation
13290
13416
  }
13291
13417
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTabList, deps: [], target: i0.ɵɵFactoryTarget.Component });
13292
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.25", type: SdsTabList, isStandalone: true, selector: "sds-tab-list", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "ariaLabelledby", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tablist" }, listeners: { "keydown": "onKeydown($event)" }, properties: { "class": "listClass()", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "ariaLabelledby() || null" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
13418
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsTabList, isStandalone: true, selector: "sds-tab-list", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "ariaLabelledby", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClass()" } }, viewQueries: [{ propertyName: "scroller", first: true, predicate: ["scroller"], descendants: true, isSignal: true }], ngImport: i0, template: `
13419
+ @if (arrows() && canScrollStart()) {
13420
+ <button
13421
+ type="button"
13422
+ tabindex="-1"
13423
+ aria-hidden="true"
13424
+ class="absolute left-0 top-1 z-20 flex h-9 w-8 items-center justify-center rounded-control bg-surface text-ink-soft shadow-sm ring-1 ring-border transition-colors hover:text-ink"
13425
+ (click)="scrollByPage(-1)"
13426
+ >
13427
+ <sds-icon name="chevron-left" [size]="16" />
13428
+ </button>
13429
+ }
13430
+ <div
13431
+ #scroller
13432
+ role="tablist"
13433
+ [attr.aria-label]="ariaLabel() || null"
13434
+ [attr.aria-labelledby]="ariaLabelledby() || null"
13435
+ [class]="scrollerClass()"
13436
+ [style.mask-image]="fadeMask()"
13437
+ (keydown)="onKeydown($event)"
13438
+ (scroll)="measureScroll()"
13439
+ >
13440
+ @if (segmented() && indicator(); as box) {
13441
+ <span
13442
+ aria-hidden="true"
13443
+ class="pointer-events-none absolute bottom-1 top-1 rounded-[calc(var(--radius-control)-2px)] bg-surface shadow-sm ring-1 ring-border dark:bg-ink/15 dark:ring-transparent"
13444
+ [class.sds-tabs-animate]="animate()"
13445
+ [style.left.px]="box.left"
13446
+ [style.width.px]="box.width"
13447
+ ></span>
13448
+ }
13449
+ <ng-content />
13450
+ @if (!segmented() && indicator(); as box) {
13451
+ <span
13452
+ aria-hidden="true"
13453
+ class="pointer-events-none absolute bottom-0 h-[3px] rounded-t-full bg-brand-600"
13454
+ [class.sds-tabs-animate]="animate()"
13455
+ [style.left.px]="box.left"
13456
+ [style.width.px]="box.width"
13457
+ ></span>
13458
+ }
13459
+ </div>
13460
+ @if (arrows() && canScrollEnd()) {
13461
+ <button
13462
+ type="button"
13463
+ tabindex="-1"
13464
+ aria-hidden="true"
13465
+ class="absolute right-0 top-1 z-20 flex h-9 w-8 items-center justify-center rounded-control bg-surface text-ink-soft shadow-sm ring-1 ring-border transition-colors hover:text-ink"
13466
+ (click)="scrollByPage(1)"
13467
+ >
13468
+ <sds-icon name="chevron-right" [size]="16" />
13469
+ </button>
13470
+ }
13471
+ `, isInline: true, styles: [".sds-tabs-animate{transition:left .26s cubic-bezier(.2,.8,.2,1),width .26s cubic-bezier(.2,.8,.2,1)}@media(prefers-reduced-motion:reduce){.sds-tabs-animate{transition:none}}\n"], dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth", "filled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13293
13472
  }
13294
13473
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTabList, decorators: [{
13295
13474
  type: Component,
13296
- args: [{
13297
- selector: 'sds-tab-list',
13298
- changeDetection: ChangeDetectionStrategy.OnPush,
13299
- host: {
13300
- role: 'tablist',
13301
- '[class]': 'listClass()',
13302
- '[attr.aria-label]': 'ariaLabel() || null',
13303
- '[attr.aria-labelledby]': 'ariaLabelledby() || null',
13304
- '(keydown)': 'onKeydown($event)',
13305
- },
13306
- template: `<ng-content />`,
13307
- }]
13308
- }], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }] } });
13475
+ args: [{ selector: 'sds-tab-list', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SdsIcon], host: { '[class]': 'hostClass()' }, template: `
13476
+ @if (arrows() && canScrollStart()) {
13477
+ <button
13478
+ type="button"
13479
+ tabindex="-1"
13480
+ aria-hidden="true"
13481
+ class="absolute left-0 top-1 z-20 flex h-9 w-8 items-center justify-center rounded-control bg-surface text-ink-soft shadow-sm ring-1 ring-border transition-colors hover:text-ink"
13482
+ (click)="scrollByPage(-1)"
13483
+ >
13484
+ <sds-icon name="chevron-left" [size]="16" />
13485
+ </button>
13486
+ }
13487
+ <div
13488
+ #scroller
13489
+ role="tablist"
13490
+ [attr.aria-label]="ariaLabel() || null"
13491
+ [attr.aria-labelledby]="ariaLabelledby() || null"
13492
+ [class]="scrollerClass()"
13493
+ [style.mask-image]="fadeMask()"
13494
+ (keydown)="onKeydown($event)"
13495
+ (scroll)="measureScroll()"
13496
+ >
13497
+ @if (segmented() && indicator(); as box) {
13498
+ <span
13499
+ aria-hidden="true"
13500
+ class="pointer-events-none absolute bottom-1 top-1 rounded-[calc(var(--radius-control)-2px)] bg-surface shadow-sm ring-1 ring-border dark:bg-ink/15 dark:ring-transparent"
13501
+ [class.sds-tabs-animate]="animate()"
13502
+ [style.left.px]="box.left"
13503
+ [style.width.px]="box.width"
13504
+ ></span>
13505
+ }
13506
+ <ng-content />
13507
+ @if (!segmented() && indicator(); as box) {
13508
+ <span
13509
+ aria-hidden="true"
13510
+ class="pointer-events-none absolute bottom-0 h-[3px] rounded-t-full bg-brand-600"
13511
+ [class.sds-tabs-animate]="animate()"
13512
+ [style.left.px]="box.left"
13513
+ [style.width.px]="box.width"
13514
+ ></span>
13515
+ }
13516
+ </div>
13517
+ @if (arrows() && canScrollEnd()) {
13518
+ <button
13519
+ type="button"
13520
+ tabindex="-1"
13521
+ aria-hidden="true"
13522
+ class="absolute right-0 top-1 z-20 flex h-9 w-8 items-center justify-center rounded-control bg-surface text-ink-soft shadow-sm ring-1 ring-border transition-colors hover:text-ink"
13523
+ (click)="scrollByPage(1)"
13524
+ >
13525
+ <sds-icon name="chevron-right" [size]="16" />
13526
+ </button>
13527
+ }
13528
+ `, styles: [".sds-tabs-animate{transition:left .26s cubic-bezier(.2,.8,.2,1),width .26s cubic-bezier(.2,.8,.2,1)}@media(prefers-reduced-motion:reduce){.sds-tabs-animate{transition:none}}\n"] }]
13529
+ }], ctorParameters: () => [], propDecorators: { scroller: [{ type: i0.ViewChild, args: ['scroller', { isSignal: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }] } });
13309
13530
 
13310
13531
  /**
13311
- * A single tab header (role="tab"). Project the label and, optionally, an
13312
- * <sds-icon> for an icon + text header. Set [disabled] to make it unselectable.
13532
+ * A single tab header (role="tab"). Project the label; optionally give it an [icon] (a registry name, drawn before
13533
+ * the label) and a [count] (a small badge after it — e.g. how many orders wait in that tab; `null` hides it, `0`
13534
+ * shows it). Set [disabled] to make it unselectable. Its look follows the tab set's `variant`.
13313
13535
  */
13314
13536
  class SdsTab {
13315
13537
  store = inject(SdsTabsStore);
13316
13538
  value = input.required(...(ngDevMode ? [{ debugName: "value" }] : []));
13317
13539
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
13540
+ /** An icon from the library registry, drawn before the label. */
13541
+ icon = input(null, ...(ngDevMode ? [{ debugName: "icon" }] : []));
13542
+ /** A count badge after the label (`null` / unset: none; `0` is shown). */
13543
+ count = input(null, ...(ngDevMode ? [{ debugName: "count" }] : []));
13318
13544
  active = computed(() => this.store.isActive(this.value()), ...(ngDevMode ? [{ debugName: "active" }] : []));
13545
+ constructor() {
13546
+ // A new count or icon changes the tab's width: let the list move its indicator.
13547
+ effect(() => {
13548
+ this.count();
13549
+ this.icon();
13550
+ untracked(() => this.store.layoutVersion.update((v) => v + 1));
13551
+ });
13552
+ }
13319
13553
  ngOnInit() {
13320
13554
  this.store.registerTab(this.value());
13321
13555
  }
@@ -13327,21 +13561,36 @@ class SdsTab {
13327
13561
  return;
13328
13562
  this.store.select(this.value());
13329
13563
  }
13330
- tabClass() {
13331
- // Overlaps the list's border-bottom rule by 1px; a scrollable list draws its rule inside instead (see SdsTabList).
13332
- const offset = this.store.scrollable() ? '' : '-mb-px ';
13333
- const base = `relative ${offset}inline-flex items-center gap-2 whitespace-nowrap border-b-2 px-4 py-2.5 ` +
13334
- 'text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 ' +
13335
- 'focus-visible:ring-brand-500 focus-visible:ring-inset';
13336
- if (this.disabled()) {
13337
- return `${base} border-transparent text-ink-faint opacity-50 cursor-not-allowed`;
13338
- }
13339
- return this.active()
13340
- ? `${base} border-brand-600 text-brand-700 cursor-pointer`
13341
- : `${base} border-transparent text-ink-soft hover:border-border hover:text-ink cursor-pointer`;
13342
- }
13564
+ tabClass = computed(() => {
13565
+ const focus = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-0';
13566
+ const state = this.disabled()
13567
+ ? 'cursor-not-allowed text-ink-faint opacity-50'
13568
+ : this.active()
13569
+ ? 'cursor-pointer text-ink'
13570
+ : 'cursor-pointer text-ink-soft hover:text-ink';
13571
+ if (this.store.variant() === 'segmented') {
13572
+ // Sits on the thumb the list slides under the active tab (z-10 keeps the label above it). Sized to its label,
13573
+ // sharing any extra width when the list is stretched (class="w-full").
13574
+ return ('relative z-10 inline-flex min-w-0 flex-auto items-center justify-center gap-2 whitespace-nowrap ' +
13575
+ `rounded-[calc(var(--radius-control)-2px)] px-3.5 py-1.5 text-sm font-medium transition-colors ${focus} ${state}`);
13576
+ }
13577
+ // Underline: a soft hover pill above the rule; the list draws the sliding underline under the active tab.
13578
+ const hover = this.disabled() || this.active() ? '' : 'hover:bg-ink/5';
13579
+ return ('relative inline-flex shrink-0 items-center gap-2 whitespace-nowrap rounded-control px-3 py-2 mt-1 mb-2 ' +
13580
+ `text-sm font-medium transition-colors ${focus} ${hover} ${state}`);
13581
+ }, ...(ngDevMode ? [{ debugName: "tabClass" }] : []));
13582
+ /** Underline: the active tab's icon takes the brand colour, like its underline. Segmented: the label's colour. */
13583
+ iconClass = computed(() => this.active() && !this.disabled() && this.store.variant() === 'underline'
13584
+ ? 'shrink-0 text-brand-600'
13585
+ : 'shrink-0', ...(ngDevMode ? [{ debugName: "iconClass" }] : []));
13586
+ countClass = computed(() => {
13587
+ const base = 'inline-flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-xs font-semibold tabular-nums transition-colors';
13588
+ return this.active() && !this.disabled()
13589
+ ? `${base} bg-brand-100 text-brand-700`
13590
+ : `${base} bg-ink/[0.07] text-ink-soft`;
13591
+ }, ...(ngDevMode ? [{ debugName: "countClass" }] : []));
13343
13592
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTab, deps: [], target: i0.ɵɵFactoryTarget.Component });
13344
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.25", type: SdsTab, isStandalone: true, selector: "sds-tab", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
13593
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsTab, isStandalone: true, selector: "sds-tab", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", 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 } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
13345
13594
  <button
13346
13595
  type="button"
13347
13596
  role="tab"
@@ -13350,19 +13599,27 @@ class SdsTab {
13350
13599
  [attr.aria-controls]="store.hasPanel(value()) ? store.panelId(value()) : null"
13351
13600
  [attr.aria-disabled]="disabled() ? 'true' : null"
13352
13601
  [attr.tabindex]="active() && !disabled() ? 0 : -1"
13602
+ [attr.data-sds-tab]="''"
13353
13603
  [disabled]="disabled()"
13354
13604
  [class]="tabClass()"
13355
13605
  (click)="select()"
13356
13606
  >
13357
- <ng-content />
13607
+ @if (icon(); as name) {
13608
+ <sds-icon [name]="name" [size]="16" [class]="iconClass()" aria-hidden="true" />
13609
+ }
13610
+ <span class="truncate"><ng-content /></span>
13611
+ @if (count() !== null && count() !== undefined) {
13612
+ <span [class]="countClass()">{{ count() }}</span>
13613
+ }
13358
13614
  </button>
13359
- `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
13615
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth", "filled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13360
13616
  }
13361
13617
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTab, decorators: [{
13362
13618
  type: Component,
13363
13619
  args: [{
13364
13620
  selector: 'sds-tab',
13365
13621
  changeDetection: ChangeDetectionStrategy.OnPush,
13622
+ imports: [SdsIcon],
13366
13623
  host: { class: 'contents' },
13367
13624
  template: `
13368
13625
  <button
@@ -13373,15 +13630,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13373
13630
  [attr.aria-controls]="store.hasPanel(value()) ? store.panelId(value()) : null"
13374
13631
  [attr.aria-disabled]="disabled() ? 'true' : null"
13375
13632
  [attr.tabindex]="active() && !disabled() ? 0 : -1"
13633
+ [attr.data-sds-tab]="''"
13376
13634
  [disabled]="disabled()"
13377
13635
  [class]="tabClass()"
13378
13636
  (click)="select()"
13379
13637
  >
13380
- <ng-content />
13638
+ @if (icon(); as name) {
13639
+ <sds-icon [name]="name" [size]="16" [class]="iconClass()" aria-hidden="true" />
13640
+ }
13641
+ <span class="truncate"><ng-content /></span>
13642
+ @if (count() !== null && count() !== undefined) {
13643
+ <span [class]="countClass()">{{ count() }}</span>
13644
+ }
13381
13645
  </button>
13382
13646
  `,
13383
13647
  }]
13384
- }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
13648
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }] } });
13385
13649
 
13386
13650
  /** Wrapper for the <sds-tab-panel> content blocks of a tab set. */
13387
13651
  class SdsTabPanels {