@besaitech/ng-design-system 0.0.62 → 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,21 +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" }] : []));
13254
- listClass = computed(() => {
13255
- const base = 'relative flex border-b border-border';
13256
- return this.store.scrollable() ? `${base} overflow-x-auto` : base;
13257
- }, ...(ngDevMode ? [{ debugName: "listClass" }] : []));
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" }] : []));
13290
+ /**
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.
13294
+ */
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
+ }
13258
13389
  onKeydown(event) {
13259
13390
  const keys = ['ArrowRight', 'ArrowLeft', 'Home', 'End'];
13260
13391
  if (!keys.includes(event.key))
13261
13392
  return;
13262
- 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);
13263
13394
  if (!tabs.length)
13264
13395
  return;
13265
13396
  event.preventDefault();
@@ -13284,33 +13415,141 @@ class SdsTabList {
13284
13415
  el.click(); // automatic activation
13285
13416
  }
13286
13417
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTabList, deps: [], target: i0.ɵɵFactoryTarget.Component });
13287
- 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 });
13288
13472
  }
13289
13473
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTabList, decorators: [{
13290
13474
  type: Component,
13291
- args: [{
13292
- selector: 'sds-tab-list',
13293
- changeDetection: ChangeDetectionStrategy.OnPush,
13294
- host: {
13295
- role: 'tablist',
13296
- '[class]': 'listClass()',
13297
- '[attr.aria-label]': 'ariaLabel() || null',
13298
- '[attr.aria-labelledby]': 'ariaLabelledby() || null',
13299
- '(keydown)': 'onKeydown($event)',
13300
- },
13301
- template: `<ng-content />`,
13302
- }]
13303
- }], 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 }] }] } });
13304
13530
 
13305
13531
  /**
13306
- * A single tab header (role="tab"). Project the label and, optionally, an
13307
- * <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`.
13308
13535
  */
13309
13536
  class SdsTab {
13310
13537
  store = inject(SdsTabsStore);
13311
13538
  value = input.required(...(ngDevMode ? [{ debugName: "value" }] : []));
13312
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" }] : []));
13313
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
+ }
13314
13553
  ngOnInit() {
13315
13554
  this.store.registerTab(this.value());
13316
13555
  }
@@ -13322,19 +13561,36 @@ class SdsTab {
13322
13561
  return;
13323
13562
  this.store.select(this.value());
13324
13563
  }
13325
- tabClass() {
13326
- const base = 'relative -mb-px inline-flex items-center gap-2 whitespace-nowrap border-b-2 px-4 py-2.5 ' +
13327
- 'text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 ' +
13328
- 'focus-visible:ring-brand-500 focus-visible:ring-inset';
13329
- if (this.disabled()) {
13330
- return `${base} border-transparent text-ink-faint opacity-50 cursor-not-allowed`;
13331
- }
13332
- return this.active()
13333
- ? `${base} border-brand-600 text-brand-700 cursor-pointer`
13334
- : `${base} border-transparent text-ink-soft hover:border-border hover:text-ink cursor-pointer`;
13335
- }
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" }] : []));
13336
13592
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTab, deps: [], target: i0.ɵɵFactoryTarget.Component });
13337
- 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: `
13338
13594
  <button
13339
13595
  type="button"
13340
13596
  role="tab"
@@ -13343,19 +13599,27 @@ class SdsTab {
13343
13599
  [attr.aria-controls]="store.hasPanel(value()) ? store.panelId(value()) : null"
13344
13600
  [attr.aria-disabled]="disabled() ? 'true' : null"
13345
13601
  [attr.tabindex]="active() && !disabled() ? 0 : -1"
13602
+ [attr.data-sds-tab]="''"
13346
13603
  [disabled]="disabled()"
13347
13604
  [class]="tabClass()"
13348
13605
  (click)="select()"
13349
13606
  >
13350
- <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
+ }
13351
13614
  </button>
13352
- `, 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 });
13353
13616
  }
13354
13617
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTab, decorators: [{
13355
13618
  type: Component,
13356
13619
  args: [{
13357
13620
  selector: 'sds-tab',
13358
13621
  changeDetection: ChangeDetectionStrategy.OnPush,
13622
+ imports: [SdsIcon],
13359
13623
  host: { class: 'contents' },
13360
13624
  template: `
13361
13625
  <button
@@ -13366,15 +13630,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13366
13630
  [attr.aria-controls]="store.hasPanel(value()) ? store.panelId(value()) : null"
13367
13631
  [attr.aria-disabled]="disabled() ? 'true' : null"
13368
13632
  [attr.tabindex]="active() && !disabled() ? 0 : -1"
13633
+ [attr.data-sds-tab]="''"
13369
13634
  [disabled]="disabled()"
13370
13635
  [class]="tabClass()"
13371
13636
  (click)="select()"
13372
13637
  >
13373
- <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
+ }
13374
13645
  </button>
13375
13646
  `,
13376
13647
  }]
13377
- }], 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 }] }] } });
13378
13649
 
13379
13650
  /** Wrapper for the <sds-tab-panel> content blocks of a tab set. */
13380
13651
  class SdsTabPanels {