@praxisui/tabs 9.0.66 → 9.0.67

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.
package/README.md CHANGED
@@ -4,6 +4,19 @@
4
4
 
5
5
  Use this package for UI composition. The host still owns route-level state, domain persistence, widget data providers and authorization.
6
6
 
7
+ ### Resizing a tab container
8
+
9
+ In paginated `group` and `nav` headers, changing the container width reveals the
10
+ active label without moving keyboard focus or emitting a selection event. This
11
+ also applies to widget resizing without a window resize, in LTR and RTL. A label
12
+ wider than the available strip is aligned to its leading edge; it cannot be shown
13
+ in full. Explicitly disabled pagination is respected.
14
+
15
+ The runtime observes the header size and preserves Material's scrolling animation.
16
+ The group adapter accesses Material's internal tab header because `MatTabGroup`
17
+ has no focus-neutral reveal operation. Keep the group/nav, LTR/RTL resize regression
18
+ tests when upgrading Material; consumers must not replicate this bridge.
19
+
7
20
  ## LLM Recommendation Guide
8
21
 
9
22
  - Recommend when: users need metadata-driven tab groups or nav tabs with editable tab content, visibility, order, icons, and runtime authoring.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-06T19:09:46.917Z",
3
+ "generatedAt": "2026-09-07T00:54:55.301Z",
4
4
  "packageName": "@praxisui/tabs",
5
- "packageVersion": "9.0.66",
5
+ "packageVersion": "9.0.67",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 1,
@@ -1,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, Input, Inject, Component, ChangeDetectorRef, effect, EventEmitter, signal, ViewChild, Output, ChangeDetectionStrategy, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { inject, Input, Inject, Component, ChangeDetectorRef, effect, EventEmitter, signal, ElementRef, afterEveryRender, ViewChild, Output, ChangeDetectionStrategy, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
3
  import { ActivatedRoute } from '@angular/router';
4
4
  import * as i1$1 from '@angular/common';
5
5
  import { CommonModule } from '@angular/common';
6
6
  import * as i3 from '@angular/material/tabs';
7
- import { MatTabsModule, MatTabGroup } from '@angular/material/tabs';
7
+ import { MatTabsModule, MatTabGroup, MatTabNav } from '@angular/material/tabs';
8
8
  import * as i4$1 from '@angular/material/icon';
9
9
  import { MatIconModule } from '@angular/material/icon';
10
10
  import * as i10 from '@angular/material/tooltip';
@@ -3184,6 +3184,15 @@ class PraxisTabs {
3184
3184
  controlledSelectedIndex;
3185
3185
  destroyed = false;
3186
3186
  groupComponent;
3187
+ navComponent;
3188
+ hostElement = inject(ElementRef);
3189
+ headerResizeObserver;
3190
+ observedHeader;
3191
+ headerResizeFrame;
3192
+ observedHeaderWidth = -1;
3193
+ observeHeaderAfterRender = afterEveryRender({
3194
+ read: () => this.observeHeaderResize(),
3195
+ });
3187
3196
  destroy$ = new Subject();
3188
3197
  widgetDefinitionCache = new WeakMap();
3189
3198
  generatedContentForm = new FormGroup({});
@@ -3214,6 +3223,10 @@ class PraxisTabs {
3214
3223
  }
3215
3224
  ngOnDestroy() {
3216
3225
  this.destroyed = true;
3226
+ this.headerResizeObserver?.disconnect();
3227
+ if (this.headerResizeFrame !== undefined) {
3228
+ this.hostElement.nativeElement.ownerDocument.defaultView?.cancelAnimationFrame(this.headerResizeFrame);
3229
+ }
3217
3230
  this.assistantSessions.removeContextSession(this.buildAiAssistantContextSnapshot().identity);
3218
3231
  this.aiAssistantStateSubscription?.unsubscribe();
3219
3232
  this.destroy$.next();
@@ -3222,6 +3235,65 @@ class PraxisTabs {
3222
3235
  isNavMode() {
3223
3236
  return !!this.config?.nav && !!this.config?.nav?.links?.length;
3224
3237
  }
3238
+ observeHeaderResize() {
3239
+ const header = this.hostElement.nativeElement.querySelector(':scope > .praxis-tabs-root > mat-tab-group > mat-tab-header, :scope > .praxis-tabs-root > nav[mat-tab-nav-bar]') ?? undefined;
3240
+ if (header === this.observedHeader || typeof ResizeObserver === 'undefined')
3241
+ return;
3242
+ this.headerResizeObserver?.disconnect();
3243
+ this.observedHeader = header;
3244
+ this.observedHeaderWidth = -1;
3245
+ if (!header)
3246
+ return;
3247
+ this.headerResizeObserver = new ResizeObserver(() => {
3248
+ const width = header.clientWidth;
3249
+ if (width === this.observedHeaderWidth)
3250
+ return;
3251
+ this.observedHeaderWidth = width;
3252
+ const view = header.ownerDocument.defaultView;
3253
+ if (!view)
3254
+ return;
3255
+ if (this.headerResizeFrame !== undefined)
3256
+ view.cancelAnimationFrame(this.headerResizeFrame);
3257
+ this.headerResizeFrame = view.requestAnimationFrame(() => {
3258
+ this.headerResizeFrame = undefined;
3259
+ if (!this.destroyed && header === this.observedHeader && width > 0) {
3260
+ this.revealActiveHeaderLabel(header);
3261
+ }
3262
+ });
3263
+ });
3264
+ this.headerResizeObserver.observe(header);
3265
+ }
3266
+ revealActiveHeaderLabel(element) {
3267
+ // Material adapter boundary: MatTabGroup exposes no focus-neutral reveal API.
3268
+ // Keep access to its header here; do not change selectedIndex or focusIndex.
3269
+ const header = this.isNavMode() ? this.navComponent : this.groupComponent?._tabHeader;
3270
+ if (!header || header.disablePagination)
3271
+ return;
3272
+ header.updatePagination();
3273
+ this.cdr.detectChanges(); // Material paginator controls must occupy their final width.
3274
+ const viewport = element.querySelector('.mat-mdc-tab-label-container, .mat-mdc-tab-link-container');
3275
+ const active = element.querySelector('[role="tab"][aria-selected="true"]');
3276
+ if (!viewport || !active)
3277
+ return;
3278
+ const rtl = element.ownerDocument.defaultView?.getComputedStyle(element).direction === 'rtl';
3279
+ const labelContainer = active.offsetParent;
3280
+ if (rtl && !labelContainer)
3281
+ return;
3282
+ // Offset geometry excludes the in-flight transform animation. Reading painted
3283
+ // rectangles here would apply an intermediate animation delta a second time.
3284
+ const end = rtl ? labelContainer.offsetWidth - active.offsetLeft
3285
+ : active.offsetLeft + active.offsetWidth;
3286
+ const start = end - active.offsetWidth;
3287
+ const before = header.scrollDistance;
3288
+ const after = before + viewport.clientWidth;
3289
+ const delta = start < before ? start - before
3290
+ : end > after ? Math.min(end - after, start - before) : 0;
3291
+ if (Math.abs(delta) < 1)
3292
+ return;
3293
+ header.scrollDistance += delta;
3294
+ header.updatePagination();
3295
+ this.cdr.markForCheck();
3296
+ }
3225
3297
  effectiveAnimationDuration() {
3226
3298
  const reduce = this.config?.accessibility?.reduceMotion;
3227
3299
  if (reduce)
@@ -4314,7 +4386,7 @@ class PraxisTabs {
4314
4386
  return JSON.parse(JSON.stringify(config));
4315
4387
  }
4316
4388
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisTabs, deps: [], target: i0.ɵɵFactoryTarget.Component });
4317
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisTabs, isStandalone: true, selector: "praxis-tabs", inputs: { config: "config", tabsId: "tabsId", componentInstanceId: "componentInstanceId", configPersistenceStrategy: "configPersistenceStrategy", selectedIndex: "selectedIndex", enableCustomization: "enableCustomization", form: "form", context: "context" }, outputs: { animationDone: "animationDone", focusChange: "focusChange", selectedIndexChange: "selectedIndexChange", selectedTabChange: "selectedTabChange", indexFocused: "indexFocused", selectFocusedIndex: "selectFocusedIndex", configChange: "configChange", widgetEvent: "widgetEvent" }, providers: [providePraxisI18nConfig(PRAXIS_TABS_I18N_CONFIG)], viewQueries: [{ propertyName: "groupComponent", first: true, predicate: MatTabGroup, descendants: true }], usesOnChanges: true, ngImport: i0, template: `
4389
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisTabs, isStandalone: true, selector: "praxis-tabs", inputs: { config: "config", tabsId: "tabsId", componentInstanceId: "componentInstanceId", configPersistenceStrategy: "configPersistenceStrategy", selectedIndex: "selectedIndex", enableCustomization: "enableCustomization", form: "form", context: "context" }, outputs: { animationDone: "animationDone", focusChange: "focusChange", selectedIndexChange: "selectedIndexChange", selectedTabChange: "selectedTabChange", indexFocused: "indexFocused", selectFocusedIndex: "selectFocusedIndex", configChange: "configChange", widgetEvent: "widgetEvent" }, providers: [providePraxisI18nConfig(PRAXIS_TABS_I18N_CONFIG)], viewQueries: [{ propertyName: "groupComponent", first: true, predicate: MatTabGroup, descendants: true }, { propertyName: "navComponent", first: true, predicate: MatTabNav, descendants: true }], usesOnChanges: true, ngImport: i0, template: `
4318
4390
  <div
4319
4391
  class="praxis-tabs-root"
4320
4392
  [class.density-compact]="config?.appearance?.density === 'compact'"
@@ -4981,6 +5053,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4981
5053
  }], groupComponent: [{
4982
5054
  type: ViewChild,
4983
5055
  args: [MatTabGroup]
5056
+ }], navComponent: [{
5057
+ type: ViewChild,
5058
+ args: [MatTabNav]
4984
5059
  }] } });
4985
5060
 
4986
5061
  class PraxisTabsWidgetConfigEditor {
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@praxisui/tabs",
3
- "version": "9.0.66",
3
+ "version": "9.0.67",
4
4
  "description": "Configurable tabs (group and nav) for Praxis UI with metadata-driven content and runtime editor.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
7
7
  "@angular/core": "^21.0.0",
8
8
  "@angular/material": "^21.0.0",
9
9
  "@angular/cdk": "^21.0.0",
10
- "@praxisui/core": "^9.0.66",
11
- "@praxisui/dynamic-fields": "^9.0.66",
12
- "@praxisui/settings-panel": "^9.0.66",
10
+ "@praxisui/core": "^9.0.67",
11
+ "@praxisui/dynamic-fields": "^9.0.67",
12
+ "@praxisui/settings-panel": "^9.0.67",
13
13
  "@angular/forms": "^21.0.0",
14
14
  "@angular/router": "^21.0.0",
15
- "@praxisui/ai": "^9.0.66",
15
+ "@praxisui/ai": "^9.0.67",
16
16
  "rxjs": "~7.8.0"
17
17
  },
18
18
  "dependencies": {
@@ -208,6 +208,13 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
208
208
  private controlledSelectedIndex?;
209
209
  private destroyed;
210
210
  private groupComponent?;
211
+ private navComponent?;
212
+ private readonly hostElement;
213
+ private headerResizeObserver?;
214
+ private observedHeader?;
215
+ private headerResizeFrame?;
216
+ private observedHeaderWidth;
217
+ private readonly observeHeaderAfterRender;
211
218
  private readonly destroy$;
212
219
  private widgetDefinitionCache;
213
220
  private readonly generatedContentForm;
@@ -215,6 +222,8 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
215
222
  ngOnChanges(changes: SimpleChanges): void;
216
223
  ngOnDestroy(): void;
217
224
  isNavMode(): boolean;
225
+ private observeHeaderResize;
226
+ private revealActiveHeaderLabel;
218
227
  effectiveAnimationDuration(): string;
219
228
  getNavActive(i: number): boolean;
220
229
  protected visibleNavLinkEntries(): Array<{