@ds-mo/ui 1.10.0 → 1.10.1

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.
Files changed (31) hide show
  1. package/dist/.build-stamp +1 -1
  2. package/dist/components/ds-app-shell.js +1 -1
  3. package/dist/components/ds-app-shell.js.map +1 -1
  4. package/dist/components/ds-bar-nav.js +1 -1
  5. package/dist/components/ds-bar-nav.js.map +1 -1
  6. package/dist/components/ds-panel-nav.js +1 -1
  7. package/dist/components/ds-panel-nav.js.map +1 -1
  8. package/dist/components/ds-panel-tools.js +1 -1
  9. package/dist/components/ds-panel-tools.js.map +1 -1
  10. package/dist/components/p-DN65lYYc.js +2 -0
  11. package/dist/components/p-DN65lYYc.js.map +1 -0
  12. package/dist/types/components/AppShell/AppShell.d.ts +12 -1
  13. package/dist/types/components/BarNav/BarNav.d.ts +10 -0
  14. package/dist/types/components/PanelNav/PanelNav.d.ts +4 -0
  15. package/dist/types/components/PanelTools/PanelTools.d.ts +4 -0
  16. package/dist/types/components.d.ts +16 -0
  17. package/dist/types/nav/chrome-transition.d.ts +8 -0
  18. package/dist/types/nav/index.d.ts +4 -0
  19. package/dist/types/nav/shell-chrome-metrics.d.ts +12 -0
  20. package/package.json +2 -2
  21. package/src/angular/proxies.ts +20 -2
  22. package/src/react/ds-panel-nav.ts +5 -1
  23. package/src/react/ds-panel-tools.ts +11 -3
  24. package/src/wc/components/AppShell/AppShell.tsx +106 -16
  25. package/src/wc/components/BarNav/BarNav.tsx +91 -2
  26. package/src/wc/components/PanelNav/PanelNav.tsx +10 -0
  27. package/src/wc/components/PanelTools/PanelTools.tsx +10 -0
  28. package/src/wc/components.d.ts +16 -0
  29. package/src/wc/nav/chrome-transition.ts +10 -0
  30. package/src/wc/nav/index.ts +13 -0
  31. package/src/wc/nav/shell-chrome-metrics.ts +49 -0
@@ -1,5 +1,16 @@
1
1
  import { Component, Prop, Element, Watch, h, Host } from '@stencil/core';
2
2
  import type { NavChromeStyle } from '../../nav/nav-chrome';
3
+ import {
4
+ CHROME_TRANSITION_END,
5
+ CHROME_TRANSITION_START,
6
+ type ChromeTransitionDetail,
7
+ } from '../../nav/chrome-transition';
8
+ import {
9
+ isPanelNavCollapsed,
10
+ panelWidthPxFromTokens,
11
+ readPanelNavWidthTokens,
12
+ type PanelNavWidthTokens,
13
+ } from '../../nav/shell-chrome-metrics';
3
14
  import {
4
15
  SHELL_GRADIENT_IMAGE_VAR,
5
16
  SHELL_GRADIENT_OPACITY,
@@ -34,21 +45,32 @@ export class AppShell {
34
45
  @Element() el!: HTMLElement;
35
46
 
36
47
  private resizeObserver: ResizeObserver | null = null;
48
+ private chromeSyncRafId = 0;
49
+ private chromeTransitionDepth = 0;
50
+ private panelWidthTokens: PanelNavWidthTokens = { expandedPx: 0, collapsedPx: 0 };
51
+ private cachedShellWidth = 0;
52
+ private cachedShellHeight = 0;
37
53
 
38
54
  componentDidLoad() {
39
55
  this.syncSlottedNavStyle();
40
56
  this.connectMetricsObserver();
41
- this.scheduleChromeSync();
42
- }
43
-
44
- private scheduleChromeSync() {
45
- this.syncChrome();
46
- requestAnimationFrame(() => this.syncChrome());
57
+ this.el.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);
58
+ this.el.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);
59
+ requestAnimationFrame(() => {
60
+ this.cachePanelWidthTokens();
61
+ this.scheduleChromeSync();
62
+ });
47
63
  }
48
64
 
49
65
  disconnectedCallback() {
50
66
  this.resizeObserver?.disconnect();
51
67
  this.resizeObserver = null;
68
+ if (this.chromeSyncRafId) {
69
+ cancelAnimationFrame(this.chromeSyncRafId);
70
+ this.chromeSyncRafId = 0;
71
+ }
72
+ this.el.removeEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);
73
+ this.el.removeEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);
52
74
  }
53
75
 
54
76
  @Watch('navStyle')
@@ -59,6 +81,35 @@ export class AppShell {
59
81
  this.scheduleChromeSync();
60
82
  }
61
83
 
84
+ private onChromeTransitionStart = (event: Event) => {
85
+ const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
86
+ if (source === 'panel-nav') {
87
+ this.chromeTransitionDepth += 1;
88
+ this.cachedShellWidth = this.el.clientWidth;
89
+ this.cachedShellHeight = this.el.clientHeight;
90
+ this.syncChrome();
91
+ }
92
+ };
93
+
94
+ private onChromeTransitionEnd = (event: Event) => {
95
+ const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
96
+ if (source !== 'panel-nav') return;
97
+
98
+ this.chromeTransitionDepth = Math.max(0, this.chromeTransitionDepth - 1);
99
+ if (this.chromeTransitionDepth === 0) {
100
+ this.scheduleChromeSync();
101
+ }
102
+ };
103
+
104
+ /** Coalesce ResizeObserver bursts to one layout read per frame. */
105
+ private scheduleChromeSync() {
106
+ if (this.chromeSyncRafId) return;
107
+ this.chromeSyncRafId = requestAnimationFrame(() => {
108
+ this.chromeSyncRafId = 0;
109
+ this.syncChrome();
110
+ });
111
+ }
112
+
62
113
  private syncSlottedNavStyle() {
63
114
  const panel = this.el.querySelector('ds-panel-nav') as (HTMLElement & { navStyle: NavChromeStyle }) | null;
64
115
  const bar = this.el.querySelector('ds-bar-nav') as (HTMLElement & { navStyle: NavChromeStyle }) | null;
@@ -73,11 +124,19 @@ export class AppShell {
73
124
  }
74
125
 
75
126
  private connectMetricsObserver() {
76
- this.resizeObserver = new ResizeObserver(() => this.scheduleChromeSync());
127
+ this.resizeObserver = new ResizeObserver(() => {
128
+ if (this.chromeTransitionDepth > 0) return;
129
+ this.scheduleChromeSync();
130
+ });
77
131
  this.resizeObserver.observe(this.el);
78
132
  const panelWrap = this.el.querySelector('.app-shell__panel');
79
133
  if (panelWrap) this.resizeObserver.observe(panelWrap);
80
- this.scheduleChromeSync();
134
+ }
135
+
136
+ private cachePanelWidthTokens() {
137
+ const navRoot = this.el.querySelector('ds-panel-nav .panel-nav') as HTMLElement | null;
138
+ if (!navRoot) return;
139
+ this.panelWidthTokens = readPanelNavWidthTokens(navRoot);
81
140
  }
82
141
 
83
142
  private applyGradientVars(target: HTMLElement, vars: Record<string, string | null>) {
@@ -114,19 +173,50 @@ export class AppShell {
114
173
  }
115
174
  }
116
175
 
176
+ private resolvePanelWidthPx(panelNav: HTMLElement | null): number {
177
+ const navRoot = panelNav?.querySelector('.panel-nav') as HTMLElement | null;
178
+
179
+ if (this.chromeTransitionDepth > 0 && this.panelWidthTokens.expandedPx > 0) {
180
+ const collapsed = isPanelNavCollapsed(panelNav, navRoot);
181
+ return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);
182
+ }
183
+
184
+ const panelWrap = this.el.querySelector('.app-shell__panel') as HTMLElement | null;
185
+ const measured = panelWrap?.getBoundingClientRect().width ?? 0;
186
+ if (measured > 0) return measured;
187
+
188
+ if (this.panelWidthTokens.expandedPx > 0) {
189
+ const collapsed = isPanelNavCollapsed(panelNav, navRoot);
190
+ return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);
191
+ }
192
+
193
+ return 0;
194
+ }
195
+
196
+ private resolveShellDimensions(): { width: number; height: number } {
197
+ if (this.chromeTransitionDepth > 0 && this.cachedShellWidth > 0) {
198
+ return {
199
+ width: this.cachedShellWidth,
200
+ height: this.cachedShellHeight || this.el.clientHeight,
201
+ };
202
+ }
203
+
204
+ const rect = this.el.getBoundingClientRect();
205
+ return { width: rect.width, height: rect.height };
206
+ }
207
+
117
208
  private syncChrome() {
118
- const panel = this.el.querySelector('ds-panel-nav') as HTMLElement | null;
209
+ const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;
119
210
  const bar = this.el.querySelector('ds-bar-nav') as HTMLElement | null;
120
- const targets = [this.el, panel, bar].filter((el): el is HTMLElement => el !== null);
211
+ const targets = [this.el, panelNav, bar].filter((el): el is HTMLElement => el !== null);
121
212
 
122
- const shellRect = this.el.getBoundingClientRect();
123
- const panelWrap = this.el.querySelector('.app-shell__panel') as HTMLElement | null;
124
- const panelWidth = panelWrap?.getBoundingClientRect().width ?? 0;
213
+ const { width: shellWidth, height: shellHeight } = this.resolveShellDimensions();
214
+ const panelWidth = this.resolvePanelWidthPx(panelNav);
125
215
 
126
216
  const panelPosition = shellGradientPositionPanel();
127
217
  const barPosition = shellGradientPositionBar(panelWidth);
128
218
 
129
- this.syncChromeLayoutVars(panel, bar, panelPosition, barPosition);
219
+ this.syncChromeLayoutVars(panelNav, bar, panelPosition, barPosition);
130
220
 
131
221
  if (!this.gradient) {
132
222
  this.clearGradientPaintVars(targets);
@@ -138,8 +228,8 @@ export class AppShell {
138
228
  : buildShellRadialGradient();
139
229
 
140
230
  const size = shellGradientSize({
141
- width: shellRect.width,
142
- height: shellRect.height,
231
+ width: shellWidth,
232
+ height: shellHeight,
143
233
  panelWidth,
144
234
  });
145
235
  const opacity = SHELL_GRADIENT_OPACITY;
@@ -28,6 +28,12 @@ import {
28
28
  tabsOverflowContainer,
29
29
  tabsToMenuSections,
30
30
  } from './bar-nav-tabs-menu-utils';
31
+ import {
32
+ CHROME_TRANSITION_END,
33
+ CHROME_TRANSITION_START,
34
+ type ChromeTransitionDetail,
35
+ } from '../../nav/chrome-transition';
36
+ import { readCssVarWidthPx } from '../../nav/shell-chrome-metrics';
31
37
 
32
38
  @Component({
33
39
  tag: 'ds-bar-nav',
@@ -91,6 +97,7 @@ export class BarNav {
91
97
 
92
98
  private static readonly HOST_PROP_SYNC_BUDGET = 8;
93
99
  private static readonly INTRINSIC_WIDTH_RETRY_MAX = 3;
100
+ private static readonly OVERFLOW_HYSTERESIS_PX = 8;
94
101
 
95
102
  private headerEl: HTMLElement | null = null;
96
103
  private triggerEl: HTMLButtonElement | null = null;
@@ -102,6 +109,9 @@ export class BarNav {
102
109
  private resizeObserver: ResizeObserver | null = null;
103
110
  private overflowCheckScheduled = false;
104
111
  private intrinsicWidthRetryCount = 0;
112
+ private chromeTransitionDepth = 0;
113
+ private chromeTransitionShell: HTMLElement | null = null;
114
+ private toolsDrawerOpening = false;
105
115
  /** Matches `basePath` once tabs + URL are reconciled for the active section. */
106
116
  @State() private committedSection = '';
107
117
  /** Section waiting for URL + tabs to catch up after a cross-area navigation. */
@@ -174,10 +184,15 @@ export class BarNav {
174
184
  this.syncHostPropsIfNeeded();
175
185
  this.scheduleDeferredHostPropSync();
176
186
  this.setupOverflowObserver();
187
+ this.bindChromeTransitionListeners();
177
188
  this.scheduleOverflowCheck();
178
189
  }
179
190
 
180
191
  componentDidRender() {
192
+ if (this.chromeTransitionDepth > 0) {
193
+ this.syncMenuAnchor();
194
+ return;
195
+ }
181
196
  this.scheduleOverflowCheck();
182
197
  this.syncMenuAnchor();
183
198
  if (this.tabsCollapsed) {
@@ -189,8 +204,63 @@ export class BarNav {
189
204
  this.resizeObserver?.disconnect();
190
205
  this.resizeObserver = null;
191
206
  this.syncTriggerLabelObserver(true);
207
+ this.unbindChromeTransitionListeners();
192
208
  }
193
209
 
210
+ private bindChromeTransitionListeners() {
211
+ const shell = this.el.closest('ds-app-shell');
212
+ if (!shell) return;
213
+ this.chromeTransitionShell = shell;
214
+ shell.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);
215
+ shell.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);
216
+ }
217
+
218
+ private unbindChromeTransitionListeners() {
219
+ if (!this.chromeTransitionShell) return;
220
+ this.chromeTransitionShell.removeEventListener(
221
+ CHROME_TRANSITION_START,
222
+ this.onChromeTransitionStart,
223
+ );
224
+ this.chromeTransitionShell.removeEventListener(
225
+ CHROME_TRANSITION_END,
226
+ this.onChromeTransitionEnd,
227
+ );
228
+ this.chromeTransitionShell = null;
229
+ }
230
+
231
+ private onChromeTransitionStart = (event: Event) => {
232
+ const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
233
+ if (source === 'panel-nav') {
234
+ this.chromeTransitionDepth += 1;
235
+ return;
236
+ }
237
+ if (source === 'panel-tools') {
238
+ this.toolsDrawerOpening = true;
239
+ this.el.classList.add('bar-nav--tools-drawer-opening');
240
+ if (this.resolvedTabs.length > 0 && !this.hideTabsForDetailRoute) {
241
+ this.tabsCollapsed = true;
242
+ this.menuOpen = false;
243
+ }
244
+ this.scheduleOverflowCheck();
245
+ }
246
+ };
247
+
248
+ private onChromeTransitionEnd = (event: Event) => {
249
+ const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
250
+ if (source === 'panel-nav') {
251
+ this.chromeTransitionDepth = Math.max(0, this.chromeTransitionDepth - 1);
252
+ if (this.chromeTransitionDepth === 0) {
253
+ this.scheduleOverflowCheck();
254
+ }
255
+ return;
256
+ }
257
+ if (source === 'panel-tools') {
258
+ this.toolsDrawerOpening = false;
259
+ this.el.classList.remove('bar-nav--tools-drawer-opening');
260
+ this.scheduleOverflowCheck();
261
+ }
262
+ };
263
+
194
264
  /** Re-resolve props assigned by the host after componentWillLoad (Angular ngAfterViewInit). */
195
265
  private syncHostPropsIfNeeded() {
196
266
  if (
@@ -224,15 +294,20 @@ export class BarNav {
224
294
  private setupOverflowObserver() {
225
295
  if (typeof ResizeObserver === 'undefined' || !this.headerEl) return;
226
296
  this.resizeObserver?.disconnect();
227
- this.resizeObserver = new ResizeObserver(() => this.scheduleOverflowCheck());
297
+ this.resizeObserver = new ResizeObserver(() => {
298
+ if (this.chromeTransitionDepth > 0) return;
299
+ this.scheduleOverflowCheck();
300
+ });
228
301
  this.resizeObserver.observe(this.headerEl);
229
302
  }
230
303
 
231
304
  private scheduleOverflowCheck() {
305
+ if (this.chromeTransitionDepth > 0) return;
232
306
  if (this.overflowCheckScheduled) return;
233
307
  this.overflowCheckScheduled = true;
234
308
  requestAnimationFrame(() => {
235
309
  this.overflowCheckScheduled = false;
310
+ if (this.chromeTransitionDepth > 0) return;
236
311
  this.updateTabsCollapsed();
237
312
  this.updateTriggerLabelTruncation();
238
313
  });
@@ -292,7 +367,20 @@ export class BarNav {
292
367
  const available =
293
368
  this.headerEl.clientWidth - horizontalPadding - actionsWidth - spacing;
294
369
 
295
- return Math.max(0, available);
370
+ return Math.max(0, available - this.toolsDrawerOpeningReservePx());
371
+ }
372
+
373
+ /** While the tools drawer is opening, reserve width not yet claimed by flex shrink. */
374
+ private toolsDrawerOpeningReservePx(): number {
375
+ if (!this.toolsDrawerOpening) return 0;
376
+
377
+ const tools = this.el.closest('ds-app-shell')?.querySelector('ds-panel-tools') as HTMLElement | null;
378
+ if (!tools) return 0;
379
+
380
+ const drawerTarget = readCssVarWidthPx(tools, '--_panel-tools-drawer-width');
381
+ const drawerNow =
382
+ tools.querySelector('.panel-tools__drawer')?.getBoundingClientRect().width ?? 0;
383
+ return Math.max(0, drawerTarget - drawerNow);
296
384
  }
297
385
 
298
386
  private getTabsIntrinsicWidth(): number {
@@ -327,6 +415,7 @@ export class BarNav {
327
415
  intrinsicWidth,
328
416
  this.getLeftZoneAvailableWidth(),
329
417
  this.tabsCollapsed,
418
+ BarNav.OVERFLOW_HYSTERESIS_PX,
330
419
  );
331
420
 
332
421
  if (shouldCollapse !== this.tabsCollapsed) {
@@ -1,4 +1,5 @@
1
1
  import { Component, Prop, Event, EventEmitter, Watch, State, Element, h, Host } from '@stencil/core';
2
+ import type { ChromeTransitionDetail } from '../../nav/chrome-transition';
2
3
  import type { NavChromeStyle } from '../../nav/nav-chrome';
3
4
  import {
4
5
  deriveActiveIdFromUrl,
@@ -69,6 +70,13 @@ export class PanelNav {
69
70
  /** Emitted when the collapse toggle is clicked. Detail = new collapsed state. */
70
71
  @Event() dsNavToggle!: EventEmitter<boolean>;
71
72
 
73
+ /** Bubbling lifecycle — `ds-app-shell` pauses chrome metrics during width motion. */
74
+ @Event({ bubbles: true, composed: true })
75
+ dsChromeTransitionStart!: EventEmitter<ChromeTransitionDetail>;
76
+
77
+ @Event({ bubbles: true, composed: true })
78
+ dsChromeTransitionEnd!: EventEmitter<ChromeTransitionDetail>;
79
+
72
80
  /** Emitted when the footer left button (gear / dashboard) is clicked. */
73
81
  @Event() dsNavFooterAction!: EventEmitter<void>;
74
82
 
@@ -181,6 +189,7 @@ export class PanelNav {
181
189
 
182
190
  private startCollapseAnimation() {
183
191
  this.isAnimating = true;
192
+ this.dsChromeTransitionStart.emit({ source: 'panel-nav' });
184
193
  const panel = this.el.querySelector('.panel-nav') as HTMLElement | null;
185
194
  if (this.transitionEndHandler) {
186
195
  panel?.removeEventListener('transitionend', this.transitionEndHandler);
@@ -188,6 +197,7 @@ export class PanelNav {
188
197
  this.transitionEndHandler = (e: TransitionEvent) => {
189
198
  if (e.propertyName === 'width') {
190
199
  this.isAnimating = false;
200
+ this.dsChromeTransitionEnd.emit({ source: 'panel-nav' });
191
201
  panel?.removeEventListener('transitionend', this.transitionEndHandler!);
192
202
  }
193
203
  };
@@ -1,4 +1,5 @@
1
1
  import { Component, Prop, Event, EventEmitter, Element, State, Watch, h, Host } from '@stencil/core';
2
+ import type { ChromeTransitionDetail } from '../../nav/chrome-transition';
2
3
  import {
3
4
  PANEL_TOOLS_LABELS,
4
5
  PANEL_TOOLS_PRIMARY_TOOL_ID,
@@ -37,6 +38,13 @@ export class PanelTools {
37
38
  selected: boolean;
38
39
  }>;
39
40
 
41
+ /** Bubbling lifecycle — `ds-bar-nav` defers overflow checks during drawer motion. */
42
+ @Event({ bubbles: true, composed: true })
43
+ dsChromeTransitionStart!: EventEmitter<ChromeTransitionDetail>;
44
+
45
+ @Event({ bubbles: true, composed: true })
46
+ dsChromeTransitionEnd!: EventEmitter<ChromeTransitionDetail>;
47
+
40
48
  /** Arms open vs close easing for the in-flight width transition. */
41
49
  @State() private motion: 'opening' | 'closing' | 'idle' = 'idle';
42
50
 
@@ -74,6 +82,7 @@ export class PanelTools {
74
82
  openChanged(isOpen: boolean, wasOpen?: boolean) {
75
83
  if (this.readyForMotion && wasOpen !== undefined && wasOpen !== isOpen) {
76
84
  this.motion = isOpen ? 'opening' : 'closing';
85
+ this.dsChromeTransitionStart.emit({ source: 'panel-tools' });
77
86
  }
78
87
  this.deferMotionEnable();
79
88
  }
@@ -97,6 +106,7 @@ export class PanelTools {
97
106
  if (event.target !== this.el.querySelector('.panel-tools__drawer')) return;
98
107
  if (event.propertyName !== 'max-width') return;
99
108
  this.motion = 'idle';
109
+ this.dsChromeTransitionEnd.emit({ source: 'panel-tools' });
100
110
  };
101
111
 
102
112
  /** Rail selection follows `open` immediately — independent of the slide animation. */
@@ -26,6 +26,7 @@ import { MenuItemData, MenuSection } from "./components/Menu/menu-types";
26
26
  import { MenuAlign, MenuSide } from "./components/Menu/menu-position";
27
27
  import { ModalWidth } from "./components/Modal/Modal";
28
28
  import { PanelNavGroup, PanelNavRouterMode, PanelNavUserActionDetail } from "./components/PanelNav/panel-nav-types";
29
+ import { ChromeTransitionDetail } from "./nav/chrome-transition";
29
30
  import { PanelToolsItem, PanelToolsToolId } from "./components/PanelTools/panel-tools-types";
30
31
  import { RadioOption } from "./components/RadioGroup/RadioGroup";
31
32
  import { ScrollbarVariant } from "./components/Scrollbar/Scrollbar";
@@ -63,6 +64,7 @@ export { MenuItemData, MenuSection } from "./components/Menu/menu-types";
63
64
  export { MenuAlign, MenuSide } from "./components/Menu/menu-position";
64
65
  export { ModalWidth } from "./components/Modal/Modal";
65
66
  export { PanelNavGroup, PanelNavRouterMode, PanelNavUserActionDetail } from "./components/PanelNav/panel-nav-types";
67
+ export { ChromeTransitionDetail } from "./nav/chrome-transition";
66
68
  export { PanelToolsItem, PanelToolsToolId } from "./components/PanelTools/panel-tools-types";
67
69
  export { RadioOption } from "./components/RadioGroup/RadioGroup";
68
70
  export { ScrollbarVariant } from "./components/Scrollbar/Scrollbar";
@@ -1476,6 +1478,8 @@ declare global {
1476
1478
  interface HTMLDsPanelNavElementEventMap {
1477
1479
  "dsNavSelect": string;
1478
1480
  "dsNavToggle": boolean;
1481
+ "dsChromeTransitionStart": ChromeTransitionDetail;
1482
+ "dsChromeTransitionEnd": ChromeTransitionDetail;
1479
1483
  "dsNavFooterAction": void;
1480
1484
  "dsNavUserAction": PanelNavUserActionDetail;
1481
1485
  }
@@ -1498,6 +1502,8 @@ declare global {
1498
1502
  id: PanelToolsToolId;
1499
1503
  selected: boolean;
1500
1504
  };
1505
+ "dsChromeTransitionStart": ChromeTransitionDetail;
1506
+ "dsChromeTransitionEnd": ChromeTransitionDetail;
1501
1507
  }
1502
1508
  interface HTMLDsPanelToolsElement extends Components.DsPanelTools, HTMLStencilElement {
1503
1509
  addEventListener<K extends keyof HTMLDsPanelToolsElementEventMap>(type: K, listener: (this: HTMLDsPanelToolsElement, ev: DsPanelToolsCustomEvent<HTMLDsPanelToolsElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
@@ -2378,6 +2384,11 @@ declare namespace LocalJSX {
2378
2384
  * @default 'dashboard'
2379
2385
  */
2380
2386
  "navStyle"?: NavChromeStyle;
2387
+ "onDsChromeTransitionEnd"?: (event: DsPanelNavCustomEvent<ChromeTransitionDetail>) => void;
2388
+ /**
2389
+ * Bubbling lifecycle — `ds-app-shell` pauses chrome metrics during width motion.
2390
+ */
2391
+ "onDsChromeTransitionStart"?: (event: DsPanelNavCustomEvent<ChromeTransitionDetail>) => void;
2381
2392
  /**
2382
2393
  * Emitted when the footer left button (gear / dashboard) is clicked.
2383
2394
  */
@@ -2431,6 +2442,11 @@ declare namespace LocalJSX {
2431
2442
  * @default ''
2432
2443
  */
2433
2444
  "itemsJson"?: string;
2445
+ "onDsChromeTransitionEnd"?: (event: DsPanelToolsCustomEvent<ChromeTransitionDetail>) => void;
2446
+ /**
2447
+ * Bubbling lifecycle — `ds-bar-nav` defers overflow checks during drawer motion.
2448
+ */
2449
+ "onDsChromeTransitionStart"?: (event: DsPanelToolsCustomEvent<ChromeTransitionDetail>) => void;
2434
2450
  /**
2435
2451
  * Emitted when a rail button is toggled. Detail = { id, selected }.
2436
2452
  */
@@ -0,0 +1,10 @@
1
+ /** Bubbling/composed events — `ds-app-shell` and `ds-bar-nav` coordinate during width motion. */
2
+
3
+ export const CHROME_TRANSITION_START = 'dsChromeTransitionStart';
4
+ export const CHROME_TRANSITION_END = 'dsChromeTransitionEnd';
5
+
6
+ export type ChromeTransitionSource = 'panel-nav' | 'panel-tools';
7
+
8
+ export interface ChromeTransitionDetail {
9
+ source: ChromeTransitionSource;
10
+ }
@@ -35,6 +35,19 @@ export {
35
35
  shellChromeSurfacePosition,
36
36
  } from './shell-gradient';
37
37
  export type { ShellGradientLayout } from './shell-gradient';
38
+ export type { ChromeTransitionDetail, ChromeTransitionSource } from './chrome-transition';
39
+ export {
40
+ CHROME_TRANSITION_END,
41
+ CHROME_TRANSITION_START,
42
+ } from './chrome-transition';
43
+ export {
44
+ barGradientPositionFromPanelWidth,
45
+ isPanelNavCollapsed,
46
+ panelWidthPxFromTokens,
47
+ readCssVarWidthPx,
48
+ readPanelNavWidthTokens,
49
+ } from './shell-chrome-metrics';
50
+ export type { PanelNavWidthTokens } from './shell-chrome-metrics';
38
51
  export {
39
52
  BADGE_GRADIENT_POSITION_VAR,
40
53
  badgeGradientPosition,
@@ -0,0 +1,49 @@
1
+ import { shellGradientPositionBar } from './shell-gradient';
2
+
3
+ /** Read a `var(--token)` width in px using a hidden probe (works with calc() tokens). */
4
+ export function readCssVarWidthPx(context: HTMLElement, cssVarName: string): number {
5
+ if (typeof document === 'undefined') return 0;
6
+
7
+ const probe = document.createElement('div');
8
+ probe.style.cssText =
9
+ 'position:absolute;visibility:hidden;pointer-events:none;height:0;width:var(' +
10
+ cssVarName +
11
+ ');';
12
+ context.appendChild(probe);
13
+ const width = probe.getBoundingClientRect().width;
14
+ context.removeChild(probe);
15
+ return width;
16
+ }
17
+
18
+ export interface PanelNavWidthTokens {
19
+ expandedPx: number;
20
+ collapsedPx: number;
21
+ }
22
+
23
+ /** Resolve panel-nav expanded/collapsed widths from scoped CSS vars on `.panel-nav`. */
24
+ export function readPanelNavWidthTokens(navRoot: HTMLElement): PanelNavWidthTokens {
25
+ return {
26
+ expandedPx: readCssVarWidthPx(navRoot, '--_nav-width'),
27
+ collapsedPx: readCssVarWidthPx(navRoot, '--_nav-width-collapsed'),
28
+ };
29
+ }
30
+
31
+ export function isPanelNavCollapsed(
32
+ panelNavHost: HTMLElement | null,
33
+ navRoot: HTMLElement | null,
34
+ ): boolean {
35
+ if (navRoot?.classList.contains('panel-nav--collapsed')) return true;
36
+ const collapsedProp = (panelNavHost as (HTMLElement & { collapsed?: boolean }) | null)?.collapsed;
37
+ return collapsedProp === true;
38
+ }
39
+
40
+ export function panelWidthPxFromTokens(
41
+ tokens: PanelNavWidthTokens,
42
+ collapsed: boolean,
43
+ ): number {
44
+ return collapsed ? tokens.collapsedPx : tokens.expandedPx;
45
+ }
46
+
47
+ export function barGradientPositionFromPanelWidth(panelWidthPx: number): string {
48
+ return shellGradientPositionBar(panelWidthPx);
49
+ }