@ds-mo/ui 1.10.0 → 1.11.0
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/dist/.build-stamp +1 -1
- package/dist/components/ds-app-shell.js +1 -1
- package/dist/components/ds-app-shell.js.map +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-nav.js.map +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-panel-nav.js.map +1 -1
- package/dist/components/ds-panel-tools.js +1 -1
- package/dist/components/ds-panel-tools.js.map +1 -1
- package/dist/components/p-DN65lYYc.js +2 -0
- package/dist/components/p-DN65lYYc.js.map +1 -0
- package/dist/types/components/AppShell/AppShell.d.ts +16 -2
- package/dist/types/components/BarNav/BarNav.d.ts +10 -0
- package/dist/types/components/PanelNav/PanelNav.d.ts +4 -0
- package/dist/types/components/PanelTools/PanelTools.d.ts +4 -0
- package/dist/types/components.d.ts +29 -2
- package/dist/types/nav/chrome-transition.d.ts +8 -0
- package/dist/types/nav/index.d.ts +5 -1
- package/dist/types/nav/shell-chrome-metrics.d.ts +12 -0
- package/dist/types/nav/shell-gradient.d.ts +2 -0
- package/package.json +2 -2
- package/src/angular/proxies.ts +22 -4
- package/src/react/ds-panel-nav.ts +5 -1
- package/src/react/ds-panel-tools.ts +11 -3
- package/src/wc/components/AppShell/AppShell.tsx +133 -21
- package/src/wc/components/BarNav/BarNav.tsx +91 -2
- package/src/wc/components/PanelNav/PanelNav.tsx +10 -0
- package/src/wc/components/PanelTools/PanelTools.tsx +10 -0
- package/src/wc/components.d.ts +29 -2
- package/src/wc/nav/chrome-transition.ts +10 -0
- package/src/wc/nav/index.ts +14 -0
- package/src/wc/nav/shell-chrome-metrics.ts +49 -0
- package/src/wc/nav/shell-gradient.ts +5 -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,
|
|
@@ -22,9 +33,12 @@ export class AppShell {
|
|
|
22
33
|
/** Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`. */
|
|
23
34
|
@Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';
|
|
24
35
|
|
|
25
|
-
/** When `true`, paints the
|
|
36
|
+
/** When `true`, paints the radial wash behind panel, bar, and tools (synced to shell layout). */
|
|
26
37
|
@Prop({ reflect: true }) gradient: boolean = false;
|
|
27
38
|
|
|
39
|
+
/** When `true`, paints the diagonal grid overlay on the shared chrome layer. Independent of `gradient`. */
|
|
40
|
+
@Prop({ reflect: true }) grid: boolean = false;
|
|
41
|
+
|
|
28
42
|
/**
|
|
29
43
|
* Optional custom gradient for `background-image` (e.g. SVG URL).
|
|
30
44
|
* When set, overrides the built-in radial wash.
|
|
@@ -34,31 +48,72 @@ export class AppShell {
|
|
|
34
48
|
@Element() el!: HTMLElement;
|
|
35
49
|
|
|
36
50
|
private resizeObserver: ResizeObserver | null = null;
|
|
51
|
+
private chromeSyncRafId = 0;
|
|
52
|
+
private chromeTransitionDepth = 0;
|
|
53
|
+
private panelWidthTokens: PanelNavWidthTokens = { expandedPx: 0, collapsedPx: 0 };
|
|
54
|
+
private cachedShellWidth = 0;
|
|
55
|
+
private cachedShellHeight = 0;
|
|
37
56
|
|
|
38
57
|
componentDidLoad() {
|
|
39
58
|
this.syncSlottedNavStyle();
|
|
40
59
|
this.connectMetricsObserver();
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
this.el.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);
|
|
61
|
+
this.el.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);
|
|
62
|
+
requestAnimationFrame(() => {
|
|
63
|
+
this.cachePanelWidthTokens();
|
|
64
|
+
this.scheduleChromeSync();
|
|
65
|
+
});
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
disconnectedCallback() {
|
|
50
69
|
this.resizeObserver?.disconnect();
|
|
51
70
|
this.resizeObserver = null;
|
|
71
|
+
if (this.chromeSyncRafId) {
|
|
72
|
+
cancelAnimationFrame(this.chromeSyncRafId);
|
|
73
|
+
this.chromeSyncRafId = 0;
|
|
74
|
+
}
|
|
75
|
+
this.el.removeEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);
|
|
76
|
+
this.el.removeEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);
|
|
52
77
|
}
|
|
53
78
|
|
|
54
79
|
@Watch('navStyle')
|
|
55
80
|
@Watch('gradient')
|
|
81
|
+
@Watch('grid')
|
|
56
82
|
@Watch('gradientSrc')
|
|
57
83
|
onShellPropsChange() {
|
|
58
84
|
this.syncSlottedNavStyle();
|
|
59
85
|
this.scheduleChromeSync();
|
|
60
86
|
}
|
|
61
87
|
|
|
88
|
+
private onChromeTransitionStart = (event: Event) => {
|
|
89
|
+
const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
|
|
90
|
+
if (source === 'panel-nav') {
|
|
91
|
+
this.chromeTransitionDepth += 1;
|
|
92
|
+
this.cachedShellWidth = this.el.clientWidth;
|
|
93
|
+
this.cachedShellHeight = this.el.clientHeight;
|
|
94
|
+
this.syncChrome();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
private onChromeTransitionEnd = (event: Event) => {
|
|
99
|
+
const source = (event as CustomEvent<ChromeTransitionDetail>).detail?.source;
|
|
100
|
+
if (source !== 'panel-nav') return;
|
|
101
|
+
|
|
102
|
+
this.chromeTransitionDepth = Math.max(0, this.chromeTransitionDepth - 1);
|
|
103
|
+
if (this.chromeTransitionDepth === 0) {
|
|
104
|
+
this.scheduleChromeSync();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Coalesce ResizeObserver bursts to one layout read per frame. */
|
|
109
|
+
private scheduleChromeSync() {
|
|
110
|
+
if (this.chromeSyncRafId) return;
|
|
111
|
+
this.chromeSyncRafId = requestAnimationFrame(() => {
|
|
112
|
+
this.chromeSyncRafId = 0;
|
|
113
|
+
this.syncChrome();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
62
117
|
private syncSlottedNavStyle() {
|
|
63
118
|
const panel = this.el.querySelector('ds-panel-nav') as (HTMLElement & { navStyle: NavChromeStyle }) | null;
|
|
64
119
|
const bar = this.el.querySelector('ds-bar-nav') as (HTMLElement & { navStyle: NavChromeStyle }) | null;
|
|
@@ -73,11 +128,19 @@ export class AppShell {
|
|
|
73
128
|
}
|
|
74
129
|
|
|
75
130
|
private connectMetricsObserver() {
|
|
76
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
131
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
132
|
+
if (this.chromeTransitionDepth > 0) return;
|
|
133
|
+
this.scheduleChromeSync();
|
|
134
|
+
});
|
|
77
135
|
this.resizeObserver.observe(this.el);
|
|
78
136
|
const panelWrap = this.el.querySelector('.app-shell__panel');
|
|
79
137
|
if (panelWrap) this.resizeObserver.observe(panelWrap);
|
|
80
|
-
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private cachePanelWidthTokens() {
|
|
141
|
+
const navRoot = this.el.querySelector('ds-panel-nav .panel-nav') as HTMLElement | null;
|
|
142
|
+
if (!navRoot) return;
|
|
143
|
+
this.panelWidthTokens = readPanelNavWidthTokens(navRoot);
|
|
81
144
|
}
|
|
82
145
|
|
|
83
146
|
private applyGradientVars(target: HTMLElement, vars: Record<string, string | null>) {
|
|
@@ -114,32 +177,79 @@ export class AppShell {
|
|
|
114
177
|
}
|
|
115
178
|
}
|
|
116
179
|
|
|
117
|
-
private
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
180
|
+
private resolvePanelWidthPx(panelNav: HTMLElement | null): number {
|
|
181
|
+
const navRoot = panelNav?.querySelector('.panel-nav') as HTMLElement | null;
|
|
182
|
+
|
|
183
|
+
if (this.chromeTransitionDepth > 0 && this.panelWidthTokens.expandedPx > 0) {
|
|
184
|
+
const collapsed = isPanelNavCollapsed(panelNav, navRoot);
|
|
185
|
+
return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);
|
|
186
|
+
}
|
|
121
187
|
|
|
122
|
-
const shellRect = this.el.getBoundingClientRect();
|
|
123
188
|
const panelWrap = this.el.querySelector('.app-shell__panel') as HTMLElement | null;
|
|
124
|
-
const
|
|
189
|
+
const measured = panelWrap?.getBoundingClientRect().width ?? 0;
|
|
190
|
+
if (measured > 0) return measured;
|
|
125
191
|
|
|
126
|
-
|
|
127
|
-
|
|
192
|
+
if (this.panelWidthTokens.expandedPx > 0) {
|
|
193
|
+
const collapsed = isPanelNavCollapsed(panelNav, navRoot);
|
|
194
|
+
return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);
|
|
195
|
+
}
|
|
128
196
|
|
|
129
|
-
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private resolveShellDimensions(): { width: number; height: number } {
|
|
201
|
+
if (this.chromeTransitionDepth > 0 && this.cachedShellWidth > 0) {
|
|
202
|
+
return {
|
|
203
|
+
width: this.cachedShellWidth,
|
|
204
|
+
height: this.cachedShellHeight || this.el.clientHeight,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const rect = this.el.getBoundingClientRect();
|
|
209
|
+
return { width: rect.width, height: rect.height };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private chromeLayerActive(): boolean {
|
|
213
|
+
return this.gradient || this.grid;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private syncChrome() {
|
|
217
|
+
const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;
|
|
218
|
+
const bar = this.el.querySelector('ds-bar-nav') as HTMLElement | null;
|
|
219
|
+
const targets = [this.el, panelNav, bar].filter((el): el is HTMLElement => el !== null);
|
|
220
|
+
|
|
221
|
+
const clearLayoutVars = () => {
|
|
222
|
+
if (panelNav) panelNav.style.removeProperty(SHELL_GRADIENT_POSITION_PANEL_VAR);
|
|
223
|
+
if (bar) bar.style.removeProperty(SHELL_GRADIENT_POSITION_BAR_VAR);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
if (!this.chromeLayerActive()) {
|
|
227
|
+
this.clearGradientPaintVars(targets);
|
|
228
|
+
clearLayoutVars();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
130
231
|
|
|
131
232
|
if (!this.gradient) {
|
|
132
233
|
this.clearGradientPaintVars(targets);
|
|
234
|
+
clearLayoutVars();
|
|
133
235
|
return;
|
|
134
236
|
}
|
|
135
237
|
|
|
238
|
+
const { width: shellWidth, height: shellHeight } = this.resolveShellDimensions();
|
|
239
|
+
const panelWidth = this.resolvePanelWidthPx(panelNav);
|
|
240
|
+
|
|
241
|
+
const panelPosition = shellGradientPositionPanel();
|
|
242
|
+
const barPosition = shellGradientPositionBar(panelWidth);
|
|
243
|
+
|
|
244
|
+
this.syncChromeLayoutVars(panelNav, bar, panelPosition, barPosition);
|
|
245
|
+
|
|
136
246
|
const image = this.gradientSrc.trim()
|
|
137
247
|
? `url(${this.gradientSrc.trim()})`
|
|
138
248
|
: buildShellRadialGradient();
|
|
139
249
|
|
|
140
250
|
const size = shellGradientSize({
|
|
141
|
-
width:
|
|
142
|
-
height:
|
|
251
|
+
width: shellWidth,
|
|
252
|
+
height: shellHeight,
|
|
143
253
|
panelWidth,
|
|
144
254
|
});
|
|
145
255
|
const opacity = SHELL_GRADIENT_OPACITY;
|
|
@@ -168,16 +278,18 @@ export class AppShell {
|
|
|
168
278
|
}
|
|
169
279
|
|
|
170
280
|
render() {
|
|
281
|
+
const chromeActive = this.chromeLayerActive();
|
|
171
282
|
const shellCls: Record<string, boolean> = {
|
|
172
283
|
'app-shell': true,
|
|
173
284
|
'app-shell--gradient': this.gradient,
|
|
285
|
+
'app-shell--grid': this.grid,
|
|
174
286
|
[`app-shell--${this.navStyle}`]: true,
|
|
175
287
|
};
|
|
176
288
|
|
|
177
289
|
return (
|
|
178
290
|
<Host class={shellCls}>
|
|
179
291
|
<div class="app-shell__row">
|
|
180
|
-
{
|
|
292
|
+
{chromeActive ? <div class="app-shell__chrome" aria-hidden="true" /> : null}
|
|
181
293
|
<div class="app-shell__panel">
|
|
182
294
|
<slot name="panel" />
|
|
183
295
|
</div>
|
|
@@ -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(() =>
|
|
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. */
|
package/src/wc/components.d.ts
CHANGED
|
@@ -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";
|
|
@@ -97,7 +99,7 @@ export namespace Components {
|
|
|
97
99
|
}
|
|
98
100
|
interface DsAppShell {
|
|
99
101
|
/**
|
|
100
|
-
* When `true`, paints the
|
|
102
|
+
* When `true`, paints the radial wash behind panel, bar, and tools (synced to shell layout).
|
|
101
103
|
* @default false
|
|
102
104
|
*/
|
|
103
105
|
"gradient": boolean;
|
|
@@ -106,6 +108,11 @@ export namespace Components {
|
|
|
106
108
|
* @default ''
|
|
107
109
|
*/
|
|
108
110
|
"gradientSrc": string;
|
|
111
|
+
/**
|
|
112
|
+
* When `true`, paints the diagonal grid overlay on the shared chrome layer. Independent of `gradient`.
|
|
113
|
+
* @default false
|
|
114
|
+
*/
|
|
115
|
+
"grid": boolean;
|
|
109
116
|
/**
|
|
110
117
|
* Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`.
|
|
111
118
|
* @default 'dashboard'
|
|
@@ -1476,6 +1483,8 @@ declare global {
|
|
|
1476
1483
|
interface HTMLDsPanelNavElementEventMap {
|
|
1477
1484
|
"dsNavSelect": string;
|
|
1478
1485
|
"dsNavToggle": boolean;
|
|
1486
|
+
"dsChromeTransitionStart": ChromeTransitionDetail;
|
|
1487
|
+
"dsChromeTransitionEnd": ChromeTransitionDetail;
|
|
1479
1488
|
"dsNavFooterAction": void;
|
|
1480
1489
|
"dsNavUserAction": PanelNavUserActionDetail;
|
|
1481
1490
|
}
|
|
@@ -1498,6 +1507,8 @@ declare global {
|
|
|
1498
1507
|
id: PanelToolsToolId;
|
|
1499
1508
|
selected: boolean;
|
|
1500
1509
|
};
|
|
1510
|
+
"dsChromeTransitionStart": ChromeTransitionDetail;
|
|
1511
|
+
"dsChromeTransitionEnd": ChromeTransitionDetail;
|
|
1501
1512
|
}
|
|
1502
1513
|
interface HTMLDsPanelToolsElement extends Components.DsPanelTools, HTMLStencilElement {
|
|
1503
1514
|
addEventListener<K extends keyof HTMLDsPanelToolsElementEventMap>(type: K, listener: (this: HTMLDsPanelToolsElement, ev: DsPanelToolsCustomEvent<HTMLDsPanelToolsElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
|
@@ -1775,7 +1786,7 @@ declare namespace LocalJSX {
|
|
|
1775
1786
|
}
|
|
1776
1787
|
interface DsAppShell {
|
|
1777
1788
|
/**
|
|
1778
|
-
* When `true`, paints the
|
|
1789
|
+
* When `true`, paints the radial wash behind panel, bar, and tools (synced to shell layout).
|
|
1779
1790
|
* @default false
|
|
1780
1791
|
*/
|
|
1781
1792
|
"gradient"?: boolean;
|
|
@@ -1784,6 +1795,11 @@ declare namespace LocalJSX {
|
|
|
1784
1795
|
* @default ''
|
|
1785
1796
|
*/
|
|
1786
1797
|
"gradientSrc"?: string;
|
|
1798
|
+
/**
|
|
1799
|
+
* When `true`, paints the diagonal grid overlay on the shared chrome layer. Independent of `gradient`.
|
|
1800
|
+
* @default false
|
|
1801
|
+
*/
|
|
1802
|
+
"grid"?: boolean;
|
|
1787
1803
|
/**
|
|
1788
1804
|
* Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`.
|
|
1789
1805
|
* @default 'dashboard'
|
|
@@ -2378,6 +2394,11 @@ declare namespace LocalJSX {
|
|
|
2378
2394
|
* @default 'dashboard'
|
|
2379
2395
|
*/
|
|
2380
2396
|
"navStyle"?: NavChromeStyle;
|
|
2397
|
+
"onDsChromeTransitionEnd"?: (event: DsPanelNavCustomEvent<ChromeTransitionDetail>) => void;
|
|
2398
|
+
/**
|
|
2399
|
+
* Bubbling lifecycle — `ds-app-shell` pauses chrome metrics during width motion.
|
|
2400
|
+
*/
|
|
2401
|
+
"onDsChromeTransitionStart"?: (event: DsPanelNavCustomEvent<ChromeTransitionDetail>) => void;
|
|
2381
2402
|
/**
|
|
2382
2403
|
* Emitted when the footer left button (gear / dashboard) is clicked.
|
|
2383
2404
|
*/
|
|
@@ -2431,6 +2452,11 @@ declare namespace LocalJSX {
|
|
|
2431
2452
|
* @default ''
|
|
2432
2453
|
*/
|
|
2433
2454
|
"itemsJson"?: string;
|
|
2455
|
+
"onDsChromeTransitionEnd"?: (event: DsPanelToolsCustomEvent<ChromeTransitionDetail>) => void;
|
|
2456
|
+
/**
|
|
2457
|
+
* Bubbling lifecycle — `ds-bar-nav` defers overflow checks during drawer motion.
|
|
2458
|
+
*/
|
|
2459
|
+
"onDsChromeTransitionStart"?: (event: DsPanelToolsCustomEvent<ChromeTransitionDetail>) => void;
|
|
2434
2460
|
/**
|
|
2435
2461
|
* Emitted when a rail button is toggled. Detail = { id, selected }.
|
|
2436
2462
|
*/
|
|
@@ -2855,6 +2881,7 @@ declare namespace LocalJSX {
|
|
|
2855
2881
|
interface DsAppShellAttributes {
|
|
2856
2882
|
"navStyle": NavChromeStyle;
|
|
2857
2883
|
"gradient": boolean;
|
|
2884
|
+
"grid": boolean;
|
|
2858
2885
|
"gradientSrc": string;
|
|
2859
2886
|
}
|
|
2860
2887
|
interface DsBadgeAttributes {
|
|
@@ -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
|
+
}
|
package/src/wc/nav/index.ts
CHANGED
|
@@ -33,8 +33,22 @@ export {
|
|
|
33
33
|
shellGradientPositionPanel,
|
|
34
34
|
shellGradientPositionBar,
|
|
35
35
|
shellChromeSurfacePosition,
|
|
36
|
+
shellChromeLayerActive,
|
|
36
37
|
} from './shell-gradient';
|
|
37
38
|
export type { ShellGradientLayout } from './shell-gradient';
|
|
39
|
+
export type { ChromeTransitionDetail, ChromeTransitionSource } from './chrome-transition';
|
|
40
|
+
export {
|
|
41
|
+
CHROME_TRANSITION_END,
|
|
42
|
+
CHROME_TRANSITION_START,
|
|
43
|
+
} from './chrome-transition';
|
|
44
|
+
export {
|
|
45
|
+
barGradientPositionFromPanelWidth,
|
|
46
|
+
isPanelNavCollapsed,
|
|
47
|
+
panelWidthPxFromTokens,
|
|
48
|
+
readCssVarWidthPx,
|
|
49
|
+
readPanelNavWidthTokens,
|
|
50
|
+
} from './shell-chrome-metrics';
|
|
51
|
+
export type { PanelNavWidthTokens } from './shell-chrome-metrics';
|
|
38
52
|
export {
|
|
39
53
|
BADGE_GRADIENT_POSITION_VAR,
|
|
40
54
|
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
|
+
}
|
|
@@ -10,6 +10,11 @@ export const SHELL_CHROME_SURFACE_POSITION_VAR = '--ds-shell-chrome-surface-posi
|
|
|
10
10
|
/** Layer opacity for the nav gradient wash. */
|
|
11
11
|
export const SHELL_GRADIENT_OPACITY = '0.1';
|
|
12
12
|
|
|
13
|
+
/** Whether the shared chrome layer (bg + optional wash/grid) should mount. */
|
|
14
|
+
export function shellChromeLayerActive(gradient: boolean, grid: boolean): boolean {
|
|
15
|
+
return gradient || grid;
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
const GRADIENT_GEOMETRY = '100% 100% at 0% 0%';
|
|
14
19
|
|
|
15
20
|
/**
|