mn-angular-lib 1.0.137 → 1.0.138

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.
@@ -10542,6 +10542,12 @@ const DEFAULT_SKELETON_TAB_COUNT = 3;
10542
10542
  class MnTabComponent {
10543
10543
  /** The horizontally-scrolling wrapper the edge fade is painted onto. */
10544
10544
  scrollContainer;
10545
+ /** The tab row; queried for the active tab so the indicator can measure it. */
10546
+ tabList;
10547
+ /** The shared underline that slides to the active tab. */
10548
+ indicator;
10549
+ /** Pending indicator remeasure, cancelled on destroy so a post-destroy frame can't read a detached ref. */
10550
+ indicatorFrame;
10545
10551
  /** How far the fade reaches in from each overflowing edge. */
10546
10552
  static FADE = '2rem';
10547
10553
  /** Data source containing tab items and default active index. */
@@ -10605,14 +10611,23 @@ class MnTabComponent {
10605
10611
  const el = this.scrollContainer?.nativeElement;
10606
10612
  if (!el)
10607
10613
  return;
10608
- this.resizeObserver = new ResizeObserver(() => this.updateEdgeFades());
10614
+ this.resizeObserver = new ResizeObserver(() => {
10615
+ this.updateEdgeFades();
10616
+ // Tabs may have reflowed (viewport change, justified widths); snap the
10617
+ // indicator to the new geometry — animating a resize tick reads as jank.
10618
+ this.updateIndicator(false);
10619
+ });
10609
10620
  this.resizeObserver.observe(el);
10610
10621
  if (el.firstElementChild)
10611
10622
  this.resizeObserver.observe(el.firstElementChild);
10612
10623
  this.updateEdgeFades();
10624
+ // Place the indicator on the default tab without a slide-in from zero.
10625
+ this.updateIndicator(false);
10613
10626
  }
10614
10627
  ngOnDestroy() {
10615
10628
  this.resizeObserver?.disconnect();
10629
+ if (this.indicatorFrame !== undefined)
10630
+ cancelAnimationFrame(this.indicatorFrame);
10616
10631
  }
10617
10632
  /**
10618
10633
  * Paints a fade over whichever edge has tabs scrolled out of view — a soft
@@ -10652,6 +10667,52 @@ class MnTabComponent {
10652
10667
  item.onClick?.();
10653
10668
  this.currentActive = item;
10654
10669
  this.activeChange.emit(item);
10670
+ // Slide the underline to the new tab. Measure on the next frame, after
10671
+ // change detection has applied the active tab's `font-bold` (which widens
10672
+ // it) so the indicator lands on the final, bolded geometry.
10673
+ this.scheduleIndicator(true);
10674
+ }
10675
+ /**
10676
+ * Moves the shared underline to the active tab. When `animate` is false the
10677
+ * move is snapped (no slide) by disabling the transition for one reflow —
10678
+ * used on init, async selection and resize, where a slide would read as jank.
10679
+ * @param animate - Whether the move should slide (true) or snap (false).
10680
+ */
10681
+ updateIndicator(animate) {
10682
+ const bar = this.indicator?.nativeElement;
10683
+ const list = this.tabList?.nativeElement;
10684
+ if (!bar || !list)
10685
+ return;
10686
+ const active = list.querySelector('[role="tab"][aria-selected="true"]');
10687
+ if (!active) {
10688
+ bar.style.opacity = '0';
10689
+ return;
10690
+ }
10691
+ if (!animate)
10692
+ bar.style.transition = 'none';
10693
+ bar.style.opacity = '1';
10694
+ bar.style.width = `${active.offsetWidth}px`;
10695
+ bar.style.transform = `translateX(${active.offsetLeft}px)`;
10696
+ if (!animate) {
10697
+ // Force a reflow so the snapped values apply before the transition is
10698
+ // restored, then hand animation back to the CSS class.
10699
+ void bar.offsetWidth;
10700
+ bar.style.transition = '';
10701
+ }
10702
+ }
10703
+ /**
10704
+ * Remeasures the indicator on the next animation frame, so the read happens
10705
+ * after layout reflects the latest active-tab classes. Coalesces bursts and
10706
+ * is cancellable on destroy.
10707
+ * @param animate - Whether the resulting move should slide.
10708
+ */
10709
+ scheduleIndicator(animate) {
10710
+ if (this.indicatorFrame !== undefined)
10711
+ cancelAnimationFrame(this.indicatorFrame);
10712
+ this.indicatorFrame = requestAnimationFrame(() => {
10713
+ this.indicatorFrame = undefined;
10714
+ this.updateIndicator(animate);
10715
+ });
10655
10716
  }
10656
10717
  /**
10657
10718
  * Returns the resolved badge value for a tab item, supporting both plain numbers and Signal<number>.
@@ -10670,7 +10731,10 @@ class MnTabComponent {
10670
10731
  syncActiveTab() {
10671
10732
  const items = this.dataSource?.items;
10672
10733
  if (!items || items.length === 0) {
10673
- this.currentActive = undefined;
10734
+ if (this.currentActive !== undefined) {
10735
+ this.currentActive = undefined;
10736
+ this.scheduleIndicator(false);
10737
+ }
10674
10738
  return;
10675
10739
  }
10676
10740
  if (this.currentActive && items.includes(this.currentActive)) {
@@ -10679,16 +10743,24 @@ class MnTabComponent {
10679
10743
  const defaultIndex = this.dataSource.defaultActive;
10680
10744
  const index = defaultIndex >= 0 && defaultIndex < items.length ? defaultIndex : 0;
10681
10745
  this.currentActive = items[index];
10746
+ // Selection resolved from data (not a user click): snap, don't slide.
10747
+ this.scheduleIndicator(false);
10682
10748
  }
10683
10749
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10684
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTabComponent, isStandalone: true, selector: "mn-tab", inputs: { dataSource: "dataSource", scrollable: "scrollable", justified: "justified" }, outputs: { activeChange: "activeChange" }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }], ngImport: i0, template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n role=\"tablist\"\n class=\"tabs flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.border-primary]=\"currentActive === item\"\n [class.border-transparent]=\"currentActive !== item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: MnBadge, selector: "span[mnBadge]", inputs: ["data"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "pipe", type: MnTranslatePipe, name: "mnTranslate" }] });
10750
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTabComponent, isStandalone: true, selector: "mn-tab", inputs: { dataSource: "dataSource", scrollable: "scrollable", justified: "justified" }, outputs: { activeChange: "activeChange" }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }, { propertyName: "tabList", first: true, predicate: ["tabList"], descendants: true }, { propertyName: "indicator", first: true, predicate: ["indicator"], descendants: true }], ngImport: i0, template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n #tabList\n role=\"tablist\"\n class=\"tabs relative flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n <!--\n Shared sliding indicator: a single underline that travels to the active\n tab, rather than each tab flipping its own border on/off (which snaps).\n Pinned to left-0/top-auto so offsetLeft maps 1:1 regardless of the\n flex justify-content or the justified full-width layout; position and\n width are measured and set from TS. Sits on the same baseline as the\n hover ::after so hover \u2192 select reads as one continuous underline.\n -->\n <div\n #indicator\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute left-0 top-auto bottom-0 h-[2px] w-0 bg-primary opacity-0 transition-[transform,width] duration-300 ease-out motion-reduce:transition-none\"\n ></div>\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 border-transparent cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: MnBadge, selector: "span[mnBadge]", inputs: ["data"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "pipe", type: MnTranslatePipe, name: "mnTranslate" }] });
10685
10751
  }
10686
10752
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTabComponent, decorators: [{
10687
10753
  type: Component,
10688
- args: [{ selector: 'mn-tab', standalone: true, imports: [MnTranslatePipe, CommonModule, MnBadge, MnSkeleton], template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n role=\"tablist\"\n class=\"tabs flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.border-primary]=\"currentActive === item\"\n [class.border-transparent]=\"currentActive !== item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n" }]
10754
+ args: [{ selector: 'mn-tab', standalone: true, imports: [MnTranslatePipe, CommonModule, MnBadge, MnSkeleton], template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n #tabList\n role=\"tablist\"\n class=\"tabs relative flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n <!--\n Shared sliding indicator: a single underline that travels to the active\n tab, rather than each tab flipping its own border on/off (which snaps).\n Pinned to left-0/top-auto so offsetLeft maps 1:1 regardless of the\n flex justify-content or the justified full-width layout; position and\n width are measured and set from TS. Sits on the same baseline as the\n hover ::after so hover \u2192 select reads as one continuous underline.\n -->\n <div\n #indicator\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute left-0 top-auto bottom-0 h-[2px] w-0 bg-primary opacity-0 transition-[transform,width] duration-300 ease-out motion-reduce:transition-none\"\n ></div>\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 border-transparent cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n" }]
10689
10755
  }], propDecorators: { scrollContainer: [{
10690
10756
  type: ViewChild,
10691
10757
  args: ['scrollContainer']
10758
+ }], tabList: [{
10759
+ type: ViewChild,
10760
+ args: ['tabList']
10761
+ }], indicator: [{
10762
+ type: ViewChild,
10763
+ args: ['indicator']
10692
10764
  }], dataSource: [{
10693
10765
  type: Input
10694
10766
  }], scrollable: [{