@ds-mo/ui 1.0.2 → 1.1.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.
Files changed (177) hide show
  1. package/dist/components/ds-bar-nav-action.js +1 -1
  2. package/dist/components/ds-bar-nav.js +1 -1
  3. package/dist/components/ds-bar-nav.js.map +1 -1
  4. package/dist/components/ds-breadcrumb.js +1 -1
  5. package/dist/components/ds-button-group.js +1 -1
  6. package/dist/components/ds-button.js +1 -1
  7. package/dist/components/ds-card.js +1 -1
  8. package/dist/components/ds-card.js.map +1 -1
  9. package/dist/components/ds-checkbox.js +1 -1
  10. package/dist/components/ds-divider.js +1 -1
  11. package/dist/components/ds-empty-state.js +1 -1
  12. package/dist/components/ds-empty-state.js.map +1 -1
  13. package/dist/components/ds-fade.js +1 -1
  14. package/dist/components/ds-field.js +1 -1
  15. package/dist/components/ds-header.js +1 -1
  16. package/dist/components/ds-header.js.map +1 -1
  17. package/dist/components/ds-icon.js +1 -1
  18. package/dist/components/ds-input.js +1 -1
  19. package/dist/components/ds-label-wrap.js +1 -1
  20. package/dist/components/ds-menu.js +1 -1
  21. package/dist/components/ds-modal.js +1 -1
  22. package/dist/components/ds-panel-nav.js +1 -1
  23. package/dist/components/ds-radio-group.js +1 -1
  24. package/dist/components/ds-radio-group.js.map +1 -1
  25. package/dist/components/ds-scrollbar.js +1 -1
  26. package/dist/components/ds-select.js +1 -1
  27. package/dist/components/ds-slider.js +1 -1
  28. package/dist/components/ds-slider.js.map +1 -1
  29. package/dist/components/ds-surface.js +1 -1
  30. package/dist/components/ds-tab-group-nav.d.ts +11 -0
  31. package/dist/components/ds-tab-group-nav.js +2 -0
  32. package/dist/components/ds-tab-group-nav.js.map +1 -0
  33. package/dist/components/ds-tab-group.js +1 -1
  34. package/dist/components/ds-tab-group.js.map +1 -1
  35. package/dist/components/ds-table.js +1 -1
  36. package/dist/components/ds-table.js.map +1 -1
  37. package/dist/components/ds-tag.js +1 -1
  38. package/dist/components/ds-text.js +1 -1
  39. package/dist/components/ds-toast-provider.js +1 -1
  40. package/dist/components/ds-toggle-button-group.js +1 -1
  41. package/dist/components/ds-toggle-button.js +1 -1
  42. package/dist/components/ds-toggle.js +1 -1
  43. package/dist/components/ds-tooltip.js +1 -1
  44. package/dist/components/p-BQq26pt9.js +2 -0
  45. package/dist/components/p-BQq26pt9.js.map +1 -0
  46. package/dist/components/p-CMyQsk0K.js +2 -0
  47. package/dist/components/p-CMyQsk0K.js.map +1 -0
  48. package/dist/components/{p-D3CwGoIu.js → p-COsKOwZR.js} +2 -2
  49. package/dist/components/p-CpbnmEu1.js +2 -0
  50. package/dist/components/p-CpbnmEu1.js.map +1 -0
  51. package/dist/components/{p-DouEVPmi.js → p-Dd0GrKaF.js} +2 -2
  52. package/dist/components/{p-BvxKLqNx.js → p-H89PzqGU.js} +2 -2
  53. package/dist/components/{p-BYe8Rfdg.js → p-Vwlmhl4E.js} +2 -2
  54. package/dist/components/{p-C4p1fG67.js → p-woooYulR.js} +2 -2
  55. package/dist/types/.stencil/BarNav/BarNav.d.ts +7 -2
  56. package/dist/types/.stencil/BarNav/bar-nav-dom-utils.d.ts +1 -2
  57. package/dist/types/.stencil/TabGroupNav/TabGroupNav.d.ts +27 -0
  58. package/dist/types/components.d.ts +68 -0
  59. package/package.json +37 -7
  60. package/src/angular/index.ts +1 -0
  61. package/src/angular/proxies.ts +28 -0
  62. package/src/react/components.ts +46 -0
  63. package/src/react/ds-accordion.ts +27 -0
  64. package/src/react/ds-badge.ts +26 -0
  65. package/src/react/ds-banner.ts +27 -0
  66. package/src/react/ds-bar-nav-action.ts +27 -0
  67. package/src/react/ds-bar-nav.ts +33 -0
  68. package/src/react/ds-breadcrumb.ts +27 -0
  69. package/src/react/ds-button-group.ts +27 -0
  70. package/src/react/ds-button.ts +35 -0
  71. package/src/react/ds-card.ts +26 -0
  72. package/src/react/ds-checkbox.ts +27 -0
  73. package/src/react/ds-divider.ts +26 -0
  74. package/src/react/ds-empty-state.ts +26 -0
  75. package/src/react/ds-fade.ts +26 -0
  76. package/src/react/ds-field.ts +26 -0
  77. package/src/react/ds-header.ts +26 -0
  78. package/src/react/ds-icon.ts +26 -0
  79. package/src/react/ds-input.ts +33 -0
  80. package/src/react/ds-label-wrap.ts +26 -0
  81. package/src/react/ds-loader.ts +26 -0
  82. package/src/react/ds-menu.ts +33 -0
  83. package/src/react/ds-modal.ts +27 -0
  84. package/src/react/ds-pagination.ts +27 -0
  85. package/src/react/ds-panel-nav.ts +37 -0
  86. package/src/react/ds-radio-group.ts +27 -0
  87. package/src/react/ds-scrollbar.ts +26 -0
  88. package/src/react/ds-select.ts +27 -0
  89. package/src/react/ds-sidebar.ts +33 -0
  90. package/src/react/ds-skeleton.ts +26 -0
  91. package/src/react/ds-slider.ts +27 -0
  92. package/src/react/ds-surface.ts +26 -0
  93. package/src/react/ds-tab-group-nav.ts +27 -0
  94. package/src/react/ds-tab-group.ts +27 -0
  95. package/src/react/ds-table.ts +35 -0
  96. package/src/react/ds-tag.ts +35 -0
  97. package/src/react/ds-text.ts +26 -0
  98. package/src/react/ds-toast-provider.ts +26 -0
  99. package/src/react/ds-toggle-button-group.ts +27 -0
  100. package/src/react/ds-toggle-button.ts +27 -0
  101. package/src/react/ds-toggle.ts +27 -0
  102. package/src/react/ds-tooltip.ts +26 -0
  103. package/src/wc/components/Accordion/Accordion.tsx +144 -0
  104. package/src/wc/components/Badge/Badge.tsx +34 -0
  105. package/src/wc/components/Badge/index.ts +1 -0
  106. package/src/wc/components/Banner/Banner.tsx +148 -0
  107. package/src/wc/components/BarNav/BarNav.tsx +605 -0
  108. package/src/wc/components/BarNav/bar-nav-dom-utils.ts +23 -0
  109. package/src/wc/components/BarNav/bar-nav-tabs-menu-utils.ts +59 -0
  110. package/src/wc/components/BarNav/bar-nav-utils.ts +76 -0
  111. package/src/wc/components/BarNav/index.ts +1 -0
  112. package/src/wc/components/BarNavAction/BarNavAction.tsx +67 -0
  113. package/src/wc/components/BarNavAction/index.ts +1 -0
  114. package/src/wc/components/Breadcrumb/Breadcrumb.tsx +61 -0
  115. package/src/wc/components/Button/Button.tsx +158 -0
  116. package/src/wc/components/Button/index.ts +1 -0
  117. package/src/wc/components/ButtonGroup/ButtonGroup.tsx +110 -0
  118. package/src/wc/components/Card/Card.tsx +26 -0
  119. package/src/wc/components/Checkbox/Checkbox.tsx +60 -0
  120. package/src/wc/components/Checkbox/index.ts +1 -0
  121. package/src/wc/components/Divider/Divider.tsx +23 -0
  122. package/src/wc/components/Divider/index.ts +2 -0
  123. package/src/wc/components/EmptyState/EmptyState.tsx +32 -0
  124. package/src/wc/components/Fade/Fade.tsx +32 -0
  125. package/src/wc/components/Fade/index.ts +1 -0
  126. package/src/wc/components/Field/Field.tsx +28 -0
  127. package/src/wc/components/Header/Header.tsx +33 -0
  128. package/src/wc/components/Icon/Icon.tsx +112 -0
  129. package/src/wc/components/Icon/flag-icon-catalog.ts +68 -0
  130. package/src/wc/components/Icon/index.ts +1 -0
  131. package/src/wc/components/Icon/system-icon-catalog.ts +798 -0
  132. package/src/wc/components/Input/Input.tsx +100 -0
  133. package/src/wc/components/Input/index.ts +1 -0
  134. package/src/wc/components/LabelWrap/LabelWrap.tsx +38 -0
  135. package/src/wc/components/Loader/Loader.tsx +47 -0
  136. package/src/wc/components/Loader/index.ts +1 -0
  137. package/src/wc/components/Menu/Menu.tsx +340 -0
  138. package/src/wc/components/Modal/Modal.tsx +197 -0
  139. package/src/wc/components/Pagination/Pagination.tsx +97 -0
  140. package/src/wc/components/PanelNav/PanelNav.tsx +666 -0
  141. package/src/wc/components/PanelNav/index.ts +7 -0
  142. package/src/wc/components/PanelNav/panel-nav-utils.ts +124 -0
  143. package/src/wc/components/RadioGroup/RadioGroup.tsx +128 -0
  144. package/src/wc/components/RadioGroup/index.ts +1 -0
  145. package/src/wc/components/Scrollbar/Scrollbar.tsx +189 -0
  146. package/src/wc/components/Select/Select.tsx +195 -0
  147. package/src/wc/components/Sidebar/Sidebar.tsx +245 -0
  148. package/src/wc/components/Skeleton/Skeleton.tsx +64 -0
  149. package/src/wc/components/Skeleton/index.ts +1 -0
  150. package/src/wc/components/Slider/Slider.tsx +68 -0
  151. package/src/wc/components/Slider/index.ts +1 -0
  152. package/src/wc/components/Surface/Surface.tsx +81 -0
  153. package/src/wc/components/TabGroup/TabGroup.tsx +188 -0
  154. package/src/wc/components/TabGroup/tab-item-utils.ts +24 -0
  155. package/src/wc/components/TabGroupNav/TabGroupNav.tsx +188 -0
  156. package/src/wc/components/Table/Table.tsx +268 -0
  157. package/src/wc/components/Tag/Tag.tsx +135 -0
  158. package/src/wc/components/Tag/index.ts +1 -0
  159. package/src/wc/components/Text/Text.tsx +100 -0
  160. package/src/wc/components/Text/index.ts +1 -0
  161. package/src/wc/components/Toast/Toast.tsx +174 -0
  162. package/src/wc/components/Toast/toast-service.ts +60 -0
  163. package/src/wc/components/Toggle/Toggle.tsx +43 -0
  164. package/src/wc/components/Toggle/index.ts +1 -0
  165. package/src/wc/components/ToggleButton/ToggleButton.tsx +120 -0
  166. package/src/wc/components/ToggleButtonGroup/ToggleButtonGroup.tsx +118 -0
  167. package/src/wc/components/Tooltip/Tooltip.tsx +196 -0
  168. package/src/wc/components.d.ts +3028 -0
  169. package/dist/components/p-DV8pmt1m.js +0 -2
  170. package/dist/components/p-DV8pmt1m.js.map +0 -1
  171. package/dist/components/p-STnE_bmQ.js +0 -2
  172. package/dist/components/p-STnE_bmQ.js.map +0 -1
  173. /package/dist/components/{p-D3CwGoIu.js.map → p-COsKOwZR.js.map} +0 -0
  174. /package/dist/components/{p-DouEVPmi.js.map → p-Dd0GrKaF.js.map} +0 -0
  175. /package/dist/components/{p-BvxKLqNx.js.map → p-H89PzqGU.js.map} +0 -0
  176. /package/dist/components/{p-BYe8Rfdg.js.map → p-Vwlmhl4E.js.map} +0 -0
  177. /package/dist/components/{p-C4p1fG67.js.map → p-woooYulR.js.map} +0 -0
@@ -0,0 +1,666 @@
1
+ import { Component, Prop, Event, EventEmitter, Watch, State, Element, h, Host } from '@stencil/core';
2
+ import {
3
+ deriveActiveIdFromUrl,
4
+ ensurePanelNavVtStyle,
5
+ parsePanelNavGroups,
6
+ resolvePanelNavDisableVt,
7
+ resolvePanelNavVariant,
8
+ shouldResyncPanelNavGroups,
9
+ shouldResyncPanelNavVariant,
10
+ } from './panel-nav-utils';
11
+
12
+ // Module-level WeakMaps for per-instance VT state. Using WeakMaps instead of
13
+ // class fields avoids Stencil generating getter-only property descriptors on
14
+ // the host element for private fields, which throws in strict environments.
15
+ // Minimal typings for the View Transitions API, which isn't in our TS lib target.
16
+ interface ViewTransition {
17
+ finished: Promise<void>;
18
+ skipTransition: () => void;
19
+ }
20
+ type DocumentWithViewTransition = Document & {
21
+ startViewTransition?: (callback: () => void | Promise<void>) => ViewTransition;
22
+ };
23
+
24
+ const vtTransitions = new WeakMap<object, ViewTransition>();
25
+ const vtResolvers = new WeakMap<object, () => void>();
26
+
27
+ /** Parse a CSS <time> value to milliseconds. Handles `s`, `ms`, and unitless
28
+ * (assumed ms). Critically: `parseFloat('.75s')` is 0.75 — WAAPI treats that as
29
+ * 0.75ms (instant/invisible), so the `s` unit must be scaled to ms. Design
30
+ * tokens (e.g. `--effect-animation-duration-long-1`) are authored in seconds. */
31
+ function parseCssTimeMs(value: string, fallback: number): number {
32
+ const v = value.trim();
33
+ const num = parseFloat(v);
34
+ if (Number.isNaN(num)) return fallback;
35
+ if (/ms\s*$/.test(v)) return num;
36
+ if (/s\s*$/.test(v)) return num * 1000;
37
+ return num;
38
+ }
39
+
40
+ export type PanelNavVariant = 'dashboard' | 'settings';
41
+ export type PanelNavRouterMode = 'anchor' | 'event';
42
+
43
+ export interface PanelNavItem {
44
+ id: string;
45
+ icon: string;
46
+ label: string;
47
+ /** Show a notification dot badge on the item */
48
+ dot?: boolean;
49
+ flag?: boolean;
50
+ /** Route path used for `currentUrl` matching. In `anchor` mode also sets `<a href>`.
51
+ * In `event` mode navigation is delegated to the host via `dsNavSelect`. */
52
+ href?: string;
53
+ }
54
+
55
+ export interface PanelNavGroup {
56
+ id?: string;
57
+ label?: string;
58
+ items: PanelNavItem[];
59
+ }
60
+
61
+ @Component({
62
+ tag: 'ds-panel-nav',
63
+ styleUrl: 'PanelNav.css',
64
+ scoped: true,
65
+ })
66
+ export class PanelNav {
67
+ /** Visual variant: `dashboard` = always-dark surface, `settings` = light surface */
68
+ @Prop() variant: PanelNavVariant = 'dashboard';
69
+
70
+ /** When `true`, the component does not run its own View Transition on variant
71
+ * change — it just updates the rendered surface synchronously. Use this when
72
+ * the host app orchestrates the page transition itself (e.g. Angular Router's
73
+ * `withViewTransitions`), so the two don't fight or nest. */
74
+ @Prop() disableViewTransition: boolean = false;
75
+
76
+ /** Nav groups — set via JS property (`el.groups = [...]`) or JSON string attribute. */
77
+ @Prop() groups: string | PanelNavGroup[] = '[]';
78
+
79
+ /** How items with `href` render:
80
+ * - `anchor` (default): native `<a href>` — works with routers that intercept anchors.
81
+ * - `event`: always `<button>`; host handles navigation via `dsNavSelect`. */
82
+ @Prop() routerMode: PanelNavRouterMode = 'anchor';
83
+
84
+ /** ID of the currently active/selected nav item. Overridden by `currentUrl` matching when set. */
85
+ @Prop() activeId: string = '';
86
+
87
+ /** Whether the nav is in collapsed (icon-only) state.
88
+ * When `storageKey` is set the component manages this internally —
89
+ * do not also bind it from outside in that mode. */
90
+ @Prop({ mutable: true }) collapsed: boolean = false;
91
+
92
+ /** Viewport width (px) below which the nav auto-collapses to icon-only mode. 0 = disabled. */
93
+ @Prop() breakpoint: number = 0;
94
+
95
+ /** `localStorage` key used to persist the collapsed state across page loads.
96
+ * When set the component is self-managing for collapsed state; `dsNavToggle`
97
+ * still fires for consumers that want to observe the change. */
98
+ @Prop() storageKey: string = '';
99
+
100
+ /** Current route URL (e.g. `window.location.pathname` or the router's active URL).
101
+ * When set the component derives the active item by matching item `href` values
102
+ * against this string (longest segment-boundary prefix wins), overriding `activeId`. */
103
+ @Prop() currentUrl: string = '';
104
+
105
+ /** Display name for the footer user section */
106
+ @Prop() userName: string = '';
107
+
108
+ /** Single character shown in the collapsed avatar */
109
+ @Prop() userInitial: string = '';
110
+
111
+ /** Emitted when a nav item is clicked. Detail = the item's `id`. */
112
+ @Event() dsNavSelect!: EventEmitter<string>;
113
+
114
+ /** Emitted when the collapse toggle is clicked. Detail = new collapsed state. */
115
+ @Event() dsNavToggle!: EventEmitter<boolean>;
116
+
117
+ /** Emitted when the footer left button (gear / dashboard) is clicked. */
118
+ @Event() dsNavFooterAction!: EventEmitter<void>;
119
+
120
+ /** Emitted when the footer user button is clicked. */
121
+ @Event() dsNavUserAction!: EventEmitter<void>;
122
+
123
+ @Element() el!: HTMLElement;
124
+
125
+ /** Mirrors `variant` but is only updated inside the VT callback so the
126
+ * View Transition captures a clean before/after snapshot. */
127
+ @State() private renderedVariant: PanelNavVariant = 'dashboard';
128
+
129
+ @State() private parsedGroups: PanelNavGroup[] = [];
130
+ @State() private atBottom = false;
131
+ @State() private isAnimating = false;
132
+ @State() private rovingIndex: number = 0;
133
+ @State() private viewportNarrow: boolean = false;
134
+ @State() private urlDerivedActiveId: string = '';
135
+
136
+ private transitionEndHandler?: (e: TransitionEvent) => void;
137
+ private resizeObserver?: ResizeObserver;
138
+
139
+ // Drag-to-resize state (not @State — no re-render needed)
140
+ private isDragging = false;
141
+ private dragStartX = 0;
142
+ private didSnap = false;
143
+ private lastDeltaX = 0;
144
+ private edgeOverlayTimer: number | null = null;
145
+ private globalMouseMoveHandler?: (e: MouseEvent) => void;
146
+ private globalMouseUpHandler?: () => void;
147
+
148
+ @State() private showEdgeOverlay = false;
149
+
150
+ private static readonly HOST_PROP_SYNC_BUDGET = 8;
151
+
152
+ /** The ID that should be treated as active. `currentUrl` matching takes precedence
153
+ * over `activeId` when both are present. */
154
+ private get effectiveActiveId(): string {
155
+ return this.urlDerivedActiveId || this.activeId;
156
+ }
157
+
158
+ /** Host may set `disable-view-transition` as an attribute before the JS prop lands. */
159
+ private get effectiveDisableViewTransition(): boolean {
160
+ return resolvePanelNavDisableVt(
161
+ this.disableViewTransition,
162
+ this.el.getAttribute('disable-view-transition'),
163
+ );
164
+ }
165
+
166
+ @Watch('collapsed')
167
+ @Watch('viewportNarrow')
168
+ onCollapsedChange() {
169
+ this.startCollapseAnimation();
170
+ }
171
+
172
+ @Watch('breakpoint')
173
+ onBreakpointChange() {
174
+ this.disconnectResizeObserver();
175
+ if (this.breakpoint > 0) this.connectResizeObserver();
176
+ }
177
+
178
+ @Watch('groups')
179
+ onGroupsChange(val: string | PanelNavGroup[]) {
180
+ this.parsedGroups = parsePanelNavGroups(val);
181
+ this.syncRovingIndex();
182
+ this.syncActiveFromUrl();
183
+ }
184
+
185
+ @Watch('activeId')
186
+ @Watch('urlDerivedActiveId')
187
+ onActiveIdChange() {
188
+ this.syncRovingIndex();
189
+ }
190
+
191
+ @Watch('currentUrl')
192
+ onCurrentUrlChange() {
193
+ this.syncActiveFromUrl();
194
+ }
195
+
196
+ @Watch('variant')
197
+ async onVariantChange(newVal: PanelNavVariant) {
198
+ // Host app drives the transition (e.g. Angular Router withViewTransitions):
199
+ // just reflect the new surface so the app's transition captures it.
200
+ if (this.effectiveDisableViewTransition) {
201
+ this.renderedVariant = newVal;
202
+ return;
203
+ }
204
+
205
+ // Skip any in-progress transition immediately so rapid variant changes
206
+ // (e.g. quick dashboard → settings → dashboard) never leave the nav stuck.
207
+ vtTransitions.get(this)?.skipTransition();
208
+ vtTransitions.delete(this);
209
+
210
+ const doc = document as DocumentWithViewTransition;
211
+ if (typeof doc.startViewTransition !== 'function') {
212
+ this.renderedVariant = newVal;
213
+ return;
214
+ }
215
+
216
+ ensurePanelNavVtStyle();
217
+
218
+ const btn = this.el.querySelector('.panel-nav__footer-btn') as HTMLElement | null;
219
+ const rect = btn?.getBoundingClientRect();
220
+ const x = rect ? Math.round(rect.left + rect.width / 2) : Math.round(window.innerWidth / 2);
221
+ const y = rect ? Math.round(rect.top + rect.height / 2) : Math.round(window.innerHeight / 2);
222
+ const maxR = Math.ceil(Math.hypot(
223
+ Math.max(x, window.innerWidth - x),
224
+ Math.max(y, window.innerHeight - y),
225
+ ));
226
+
227
+ document.documentElement.style.setProperty('--vt-x', `${x}px`);
228
+ document.documentElement.style.setProperty('--vt-y', `${y}px`);
229
+
230
+ const transition = doc.startViewTransition(() =>
231
+ new Promise<void>(resolve => {
232
+ vtResolvers.set(this, resolve);
233
+ this.renderedVariant = newVal;
234
+ })
235
+ );
236
+
237
+ vtTransitions.set(this, transition);
238
+ transition.finished.finally(() => {
239
+ if (vtTransitions.get(this) === transition) vtTransitions.delete(this);
240
+ vtResolvers.delete(this);
241
+ });
242
+
243
+ transition.ready.then(() => {
244
+ const durToken = getComputedStyle(document.documentElement)
245
+ .getPropertyValue('--effect-animation-duration-long-1').trim();
246
+ const duration = parseCssTimeMs(durToken, 750);
247
+ document.documentElement.animate(
248
+ { clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${maxR}px at ${x}px ${y}px)`] },
249
+ { duration, easing: 'ease-in-out', fill: 'forwards', pseudoElement: '::view-transition-new(root)' },
250
+ );
251
+ }).catch(() => { /* transition was skipped or aborted */ });
252
+ }
253
+
254
+ componentWillLoad() {
255
+ this.renderedVariant = resolvePanelNavVariant(this.variant, this.el.getAttribute('variant'));
256
+ if (this.storageKey) {
257
+ try {
258
+ const stored = localStorage.getItem(this.storageKey);
259
+ if (stored !== null) this.collapsed = stored === 'true';
260
+ } catch { /* localStorage unavailable */ }
261
+ }
262
+ this.onGroupsChange(this.groups);
263
+ this.syncActiveFromUrl();
264
+ }
265
+
266
+ componentDidLoad() {
267
+ this.syncHostPropsIfNeeded();
268
+ this.scheduleDeferredHostPropSync();
269
+ this.checkScroll();
270
+ if (this.breakpoint > 0) this.connectResizeObserver();
271
+ }
272
+
273
+ componentDidRender() {
274
+ // If a view transition callback is waiting for the render to complete,
275
+ // resolve it now so the "after" snapshot is taken at exactly the right moment.
276
+ const resolve = vtResolvers.get(this);
277
+ if (resolve) {
278
+ vtResolvers.delete(this);
279
+ resolve();
280
+ }
281
+ }
282
+
283
+ disconnectedCallback() {
284
+ this.disconnectResizeObserver();
285
+ this.clearEdgeOverlayTimer();
286
+ if (this.globalMouseMoveHandler) {
287
+ window.removeEventListener('mousemove', this.globalMouseMoveHandler);
288
+ }
289
+ if (this.globalMouseUpHandler) {
290
+ window.removeEventListener('mouseup', this.globalMouseUpHandler);
291
+ }
292
+ }
293
+
294
+ private startCollapseAnimation() {
295
+ this.isAnimating = true;
296
+ const panel = this.el.querySelector('.panel-nav') as HTMLElement | null;
297
+ if (this.transitionEndHandler) {
298
+ panel?.removeEventListener('transitionend', this.transitionEndHandler);
299
+ }
300
+ this.transitionEndHandler = (e: TransitionEvent) => {
301
+ if (e.propertyName === 'width') {
302
+ this.isAnimating = false;
303
+ panel?.removeEventListener('transitionend', this.transitionEndHandler!);
304
+ }
305
+ };
306
+ panel?.addEventListener('transitionend', this.transitionEndHandler);
307
+ }
308
+
309
+ private connectResizeObserver() {
310
+ this.viewportNarrow = window.innerWidth < this.breakpoint;
311
+ this.resizeObserver = new ResizeObserver(() => {
312
+ this.viewportNarrow = window.innerWidth < this.breakpoint;
313
+ });
314
+ this.resizeObserver.observe(document.documentElement);
315
+ }
316
+
317
+ private disconnectResizeObserver() {
318
+ this.resizeObserver?.disconnect();
319
+ this.resizeObserver = undefined;
320
+ }
321
+
322
+ /** Derive active ID from the current URL by matching item `href` values.
323
+ * Uses longest segment-boundary prefix match. */
324
+ private syncActiveFromUrl() {
325
+ if (!this.currentUrl) {
326
+ this.urlDerivedActiveId = '';
327
+ return;
328
+ }
329
+ this.urlDerivedActiveId = deriveActiveIdFromUrl(this.currentUrl, this.getAllItems());
330
+ }
331
+
332
+ /** Re-parse props assigned by the host after componentWillLoad (Angular ngAfterViewInit). */
333
+ private syncHostPropsIfNeeded() {
334
+ this.syncRenderedVariantIfNeeded();
335
+
336
+ if (shouldResyncPanelNavGroups(this.parsedGroups, this.groups)) {
337
+ this.onGroupsChange(this.groups);
338
+ } else if (this.currentUrl) {
339
+ this.syncActiveFromUrl();
340
+ }
341
+ }
342
+
343
+ /** Align `renderedVariant` with `variant` when host bindings land after first paint. */
344
+ private syncRenderedVariantIfNeeded() {
345
+ if (!shouldResyncPanelNavVariant(this.renderedVariant, this.variant)) return;
346
+
347
+ if (this.effectiveDisableViewTransition) {
348
+ vtTransitions.get(this)?.skipTransition();
349
+ vtTransitions.delete(this);
350
+ this.renderedVariant = this.variant;
351
+ } else {
352
+ void this.onVariantChange(this.variant);
353
+ }
354
+ }
355
+
356
+ /** Poll across animation frames — host props may land without triggering @Watch. */
357
+ private scheduleDeferredHostPropSync() {
358
+ let remaining = PanelNav.HOST_PROP_SYNC_BUDGET;
359
+ const tick = () => {
360
+ this.syncHostPropsIfNeeded();
361
+ if (--remaining > 0) {
362
+ requestAnimationFrame(tick);
363
+ }
364
+ };
365
+ queueMicrotask(tick);
366
+ }
367
+
368
+ /** Centralised toggle: always emits `dsNavToggle`; when `storageKey` is set
369
+ * also mutates `collapsed` directly and persists to `localStorage`. */
370
+ private applyToggle(next: boolean) {
371
+ this.dsNavToggle.emit(next);
372
+ if (this.storageKey) {
373
+ this.collapsed = next;
374
+ try { localStorage.setItem(this.storageKey, String(next)); } catch { /* unavailable */ }
375
+ }
376
+ }
377
+
378
+ private getAllItems(): PanelNavItem[] {
379
+ return this.parsedGroups.flatMap(g => g.items);
380
+ }
381
+
382
+ private syncRovingIndex() {
383
+ const idx = this.getAllItems().findIndex(i => i.id === this.effectiveActiveId);
384
+ this.rovingIndex = idx >= 0 ? idx : 0;
385
+ }
386
+
387
+ private handleItemKeyDown(e: KeyboardEvent, index: number) {
388
+ const total = this.getAllItems().length;
389
+ let next: number;
390
+ switch (e.key) {
391
+ case 'ArrowDown': e.preventDefault(); next = (index + 1) % total; break;
392
+ case 'ArrowUp': e.preventDefault(); next = (index - 1 + total) % total; break;
393
+ case 'Home': e.preventDefault(); next = 0; break;
394
+ case 'End': e.preventDefault(); next = total - 1; break;
395
+ default: return;
396
+ }
397
+ this.rovingIndex = next;
398
+ const items = Array.from(
399
+ this.el.querySelectorAll<HTMLElement>('.panel-nav__body .panel-nav__item')
400
+ );
401
+ items[next]?.focus();
402
+ }
403
+
404
+ private checkScroll() {
405
+ const body = this.el.querySelector('.panel-nav__body') as HTMLElement | null;
406
+ if (!body) return;
407
+ const remaining = body.scrollHeight - body.scrollTop - body.clientHeight;
408
+ this.atBottom = remaining < 2;
409
+ }
410
+
411
+ private handleBodyScroll(e: Event) {
412
+ const body = e.target as HTMLElement;
413
+ const remaining = body.scrollHeight - body.scrollTop - body.clientHeight;
414
+ this.atBottom = remaining < 2;
415
+ }
416
+
417
+ private handleItemClick(id: string) {
418
+ this.dsNavSelect.emit(id);
419
+ }
420
+
421
+ private handleToggle() {
422
+ this.applyToggle(!this.collapsed);
423
+ }
424
+
425
+ private handleFooterAction() {
426
+ this.dsNavFooterAction.emit();
427
+ }
428
+
429
+ private handleUserAction() {
430
+ this.dsNavUserAction.emit();
431
+ }
432
+
433
+ private clearEdgeOverlayTimer() {
434
+ if (this.edgeOverlayTimer !== null) {
435
+ window.clearTimeout(this.edgeOverlayTimer);
436
+ this.edgeOverlayTimer = null;
437
+ }
438
+ }
439
+
440
+ private handleResizeHandleMouseEnter() {
441
+ this.clearEdgeOverlayTimer();
442
+ this.edgeOverlayTimer = window.setTimeout(() => {
443
+ this.showEdgeOverlay = true;
444
+ this.edgeOverlayTimer = null;
445
+ }, 500);
446
+ }
447
+
448
+ private handleResizeHandleMouseLeave() {
449
+ if (this.isDragging) return;
450
+ this.clearEdgeOverlayTimer();
451
+ this.showEdgeOverlay = false;
452
+ }
453
+
454
+ private handleResizeHandleMouseDown(e: MouseEvent) {
455
+ e.preventDefault();
456
+ e.stopPropagation();
457
+
458
+ const wasCollapsed = this.collapsed || this.viewportNarrow;
459
+ this.isDragging = true;
460
+ this.dragStartX = e.clientX;
461
+ this.didSnap = false;
462
+ this.lastDeltaX = 0;
463
+ this.clearEdgeOverlayTimer();
464
+ this.showEdgeOverlay = false;
465
+
466
+ document.body.style.cursor = 'ew-resize';
467
+ document.body.style.userSelect = 'none';
468
+
469
+ const onMove = (ev: MouseEvent) => {
470
+ const deltaX = ev.clientX - this.dragStartX;
471
+ this.lastDeltaX = Math.abs(deltaX);
472
+ if (this.didSnap) return;
473
+ if (!wasCollapsed && deltaX < -8) {
474
+ this.applyToggle(true);
475
+ this.didSnap = true;
476
+ } else if (wasCollapsed && deltaX > 8) {
477
+ this.applyToggle(false);
478
+ this.didSnap = true;
479
+ }
480
+ };
481
+
482
+ const onUp = () => {
483
+ if (!this.didSnap && this.lastDeltaX < 3) {
484
+ this.applyToggle(!wasCollapsed);
485
+ }
486
+ this.isDragging = false;
487
+ this.showEdgeOverlay = false;
488
+ document.body.style.cursor = '';
489
+ document.body.style.userSelect = '';
490
+ window.removeEventListener('mousemove', onMove);
491
+ window.removeEventListener('mouseup', onUp);
492
+ this.globalMouseMoveHandler = undefined;
493
+ this.globalMouseUpHandler = undefined;
494
+ };
495
+
496
+ this.globalMouseMoveHandler = onMove;
497
+ this.globalMouseUpHandler = onUp;
498
+ window.addEventListener('mousemove', onMove);
499
+ window.addEventListener('mouseup', onUp);
500
+ }
501
+
502
+ private renderNavItem(item: PanelNavItem, idx: number, collapsed: boolean) {
503
+ const isActive = item.id === this.effectiveActiveId;
504
+
505
+ const itemContent = [
506
+ <span class="panel-nav__item-icon">
507
+ <ds-icon name={item.icon} size="md" color="inherit" flag={item.flag} />
508
+ </span>,
509
+ <span class="panel-nav__item-label">
510
+ <span class="panel-nav__item-label-text">
511
+ <ds-text
512
+ as="span"
513
+ variant={isActive ? 'text-body-medium-emphasis' : 'text-body-medium'}
514
+ color="inherit"
515
+ >
516
+ {item.label}
517
+ </ds-text>
518
+ </span>
519
+ </span>,
520
+ item.dot && <span class="panel-nav__item-dot" aria-hidden="true" />,
521
+ ];
522
+
523
+ const sharedProps = {
524
+ class: { 'panel-nav__item': true, 'panel-nav__item--active': isActive },
525
+ 'aria-current': isActive ? ('page' as const) : undefined,
526
+ title: collapsed ? item.label : undefined,
527
+ tabIndex: idx === this.rovingIndex ? 0 : -1,
528
+ onClick: () => this.handleItemClick(item.id),
529
+ onKeyDown: (e: KeyboardEvent) => this.handleItemKeyDown(e, idx),
530
+ onFocus: () => { this.rovingIndex = idx; },
531
+ };
532
+
533
+ const useAnchor = this.routerMode === 'anchor' && item.href;
534
+
535
+ return useAnchor
536
+ ? <a {...sharedProps} href={item.href}>{itemContent}</a>
537
+ : <button {...sharedProps} type="button">{itemContent}</button>;
538
+ }
539
+
540
+ render() {
541
+ const isDashboard = this.renderedVariant === 'dashboard';
542
+ const collapsed = this.collapsed || this.viewportNarrow;
543
+ const { userName, userInitial } = this;
544
+
545
+ const navCls: Record<string, boolean> = {
546
+ 'panel-nav': true,
547
+ 'panel-nav--dashboard': isDashboard,
548
+ 'panel-nav--settings': !isDashboard,
549
+ 'panel-nav--collapsed': collapsed,
550
+ 'panel-nav--animating': this.isAnimating,
551
+ 'panel-nav--at-bottom': this.atBottom,
552
+ };
553
+
554
+ return (
555
+ <Host style={{ display: 'block', position: 'relative' }}>
556
+ <nav class={navCls} aria-label={isDashboard ? 'Main navigation' : 'Settings navigation'}>
557
+
558
+ {/* ── Header: Motive logo, reveals collapse toggle on hover ── */}
559
+ <div class="panel-nav__header">
560
+ <button
561
+ type="button"
562
+ class="panel-nav__header-btn"
563
+ onClick={() => this.handleToggle()}
564
+ aria-label={collapsed ? 'Expand navigation' : 'Collapse navigation'}
565
+ aria-expanded={collapsed ? 'false' : 'true'}
566
+ >
567
+ {/* Motive M mark — fades out on hover to reveal collapse toggle */}
568
+ <span class="panel-nav__header-logo" aria-hidden="true">
569
+ <svg class="panel-nav__m-mark" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false" aria-hidden="true">
570
+ <path d="M11.1159 4.31537H7.67021L2.53401 13.0703H0V15.6875H4.02716L8.24319 8.49978V15.6846H11.6342L15.8289 8.47829V15.6846H18.7122V4.3125H15.2559L11.1159 11.3648V4.31537Z" fill="currentColor"/>
571
+ </svg>
572
+ </span>
573
+ {/* Collapse / expand icon — revealed on hover via CSS */}
574
+ <span class="panel-nav__header-toggle" aria-hidden="true">
575
+ <ds-icon
576
+ name={collapsed ? 'LeftExpandB' : 'LeftCollapseB'}
577
+ size="md"
578
+ color="inherit"
579
+ />
580
+ </span>
581
+ </button>
582
+ </div>
583
+
584
+ {/* ── Scrollable body ── */}
585
+ <div class="panel-nav__body" onScroll={e => this.handleBodyScroll(e)}>
586
+ {(() => {
587
+ let flatIdx = 0;
588
+ return this.parsedGroups.map(group => (
589
+ <div class="panel-nav__group">
590
+ {group.label && (
591
+ <span class="panel-nav__group-label">
592
+ <span class="panel-nav__item-label-text">
593
+ <ds-text as="span" variant="text-caption-emphasis" color="inherit">
594
+ {group.label}
595
+ </ds-text>
596
+ </span>
597
+ </span>
598
+ )}
599
+ {group.items.map(item => this.renderNavItem(item, flatIdx++, collapsed))}
600
+ </div>
601
+ ));
602
+ })()}
603
+ </div>
604
+
605
+ {/* ── Footer ── */}
606
+ <div class="panel-nav__footer">
607
+ {/* Left icon button — Gear in dashboard, Dashboard in settings */}
608
+ <button
609
+ type="button"
610
+ class="panel-nav__footer-btn"
611
+ title={isDashboard ? 'Settings' : 'Dashboard'}
612
+ aria-label={isDashboard ? 'Open settings' : 'Go to dashboard'}
613
+ onClick={() => this.handleFooterAction()}
614
+ >
615
+ <ds-icon name={isDashboard ? 'Gear' : 'Dashboard'} size="md" color="inherit" />
616
+ </button>
617
+
618
+ {/* Right user — label fades like nav items; right icon cross-fades chevron ↔ circle+initial */}
619
+ <button
620
+ type="button"
621
+ class="panel-nav__item panel-nav__footer-user"
622
+ aria-label={collapsed ? `User: ${userName}` : `User menu for ${userName}`}
623
+ onClick={() => this.handleUserAction()}
624
+ >
625
+ <span class="panel-nav__item-label panel-nav__footer-user-label">
626
+ <span class="panel-nav__item-label-text">
627
+ <ds-text as="span" variant="text-body-medium-emphasis" color="inherit">
628
+ {userName}
629
+ </ds-text>
630
+ </span>
631
+ </span>
632
+ <span class="panel-nav__item-icon panel-nav__footer-user-icon" aria-hidden="true">
633
+ <span class="panel-nav__footer-icon-expanded">
634
+ <ds-icon name="ChevronUpDown" size="md" color="inherit" />
635
+ </span>
636
+ <span class="panel-nav__footer-icon-collapsed">
637
+ <ds-icon name="Circle" size="md" color="inherit" />
638
+ <span class="panel-nav__user-initial">
639
+ <ds-text as="span" variant="text-caption-emphasis" color="inherit">
640
+ {userInitial}
641
+ </ds-text>
642
+ </span>
643
+ </span>
644
+ </span>
645
+ </button>
646
+ </div>
647
+
648
+ </nav>
649
+
650
+ {/* Drag-to-resize handle — always rendered, hidden only when auto-collapsed by breakpoint */}
651
+ {!this.viewportNarrow && (
652
+ <div
653
+ class={{
654
+ 'panel-nav__resize-handle': true,
655
+ 'panel-nav__resize-handle--overlay': this.showEdgeOverlay,
656
+ }}
657
+ onMouseEnter={() => this.handleResizeHandleMouseEnter()}
658
+ onMouseLeave={() => this.handleResizeHandleMouseLeave()}
659
+ onMouseDown={(e: MouseEvent) => this.handleResizeHandleMouseDown(e)}
660
+ aria-hidden="true"
661
+ />
662
+ )}
663
+ </Host>
664
+ );
665
+ }
666
+ }
@@ -0,0 +1,7 @@
1
+ export type { PanelNavVariant, PanelNavRouterMode, PanelNavItem, PanelNavGroup } from './PanelNav';
2
+ export {
3
+ PANEL_NAV_VARIANT_HINT_ATTR,
4
+ clearPanelNavVariantHint,
5
+ ensurePanelNavVtStyle,
6
+ setPanelNavVariantHint,
7
+ } from './panel-nav-utils';