@besaitech/ng-design-system 0.0.63 → 0.0.65

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