@ds-mo/ui 1.7.3 → 1.8.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 (72) 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-badge.js +1 -1
  5. package/dist/components/ds-bar-nav-action.js +1 -1
  6. package/dist/components/ds-bar-nav.js +1 -1
  7. package/dist/components/ds-bar-nav.js.map +1 -1
  8. package/dist/components/ds-checkbox.js +1 -1
  9. package/dist/components/ds-fade.js +1 -1
  10. package/dist/components/ds-fade.js.map +1 -1
  11. package/dist/components/ds-field.js +1 -1
  12. package/dist/components/ds-panel-nav.js +1 -1
  13. package/dist/components/ds-panel-nav.js.map +1 -1
  14. package/dist/components/ds-panel-tools.d.ts +11 -0
  15. package/dist/components/ds-panel-tools.js +2 -0
  16. package/dist/components/ds-panel-tools.js.map +1 -0
  17. package/dist/components/ds-slider.js +1 -1
  18. package/dist/components/ds-slider.js.map +1 -1
  19. package/dist/components/ds-tab-group-nav.js +1 -1
  20. package/dist/components/ds-tab-group.js +1 -1
  21. package/dist/components/ds-toggle.js +1 -1
  22. package/dist/components/p-B8VZmH43.js +2 -0
  23. package/dist/components/p-B8VZmH43.js.map +1 -0
  24. package/dist/components/p-BucV1U6A.js +2 -0
  25. package/dist/components/p-BucV1U6A.js.map +1 -0
  26. package/dist/components/p-CX7-_qOW.js +2 -0
  27. package/dist/components/p-CX7-_qOW.js.map +1 -0
  28. package/dist/components/{p-rzEgrLPJ.js → p-DZDXwEMg.js} +2 -2
  29. package/dist/components/{p-Cz8OKcrG.js → p-DrvpsLK6.js} +2 -2
  30. package/dist/components/p-TtnqbcnR.js +2 -0
  31. package/dist/components/p-TtnqbcnR.js.map +1 -0
  32. package/dist/types/components/Badge/Badge.d.ts +5 -2
  33. package/dist/types/components/BarNav/BarNav.d.ts +9 -2
  34. package/dist/types/components/Fade/Fade.d.ts +5 -0
  35. package/dist/types/components/PanelTools/PanelTools.d.ts +27 -0
  36. package/dist/types/components/PanelTools/index.d.ts +3 -0
  37. package/dist/types/components/PanelTools/panel-tools-types.d.ts +5 -0
  38. package/dist/types/components.d.ts +39 -0
  39. package/dist/types/nav/index.d.ts +1 -1
  40. package/dist/types/nav/shell-view-transition.d.ts +3 -0
  41. package/dist/types/utils/index.d.ts +2 -0
  42. package/dist/types/utils/scroll-edge-fade.d.ts +13 -0
  43. package/package.json +2 -2
  44. package/src/angular/index.ts +1 -0
  45. package/src/angular/proxies.ts +23 -0
  46. package/src/react/components.ts +1 -0
  47. package/src/react/ds-panel-tools.ts +26 -0
  48. package/src/wc/components/AppShell/AppShell.tsx +7 -2
  49. package/src/wc/components/Badge/Badge.tsx +39 -15
  50. package/src/wc/components/BarNav/BarNav.tsx +75 -19
  51. package/src/wc/components/Fade/Fade.tsx +22 -1
  52. package/src/wc/components/PanelNav/PanelNav.tsx +10 -9
  53. package/src/wc/components/PanelTools/PanelTools.tsx +163 -0
  54. package/src/wc/components/PanelTools/index.ts +2 -0
  55. package/src/wc/components/PanelTools/panel-tools-types.ts +18 -0
  56. package/src/wc/components.d.ts +39 -0
  57. package/src/wc/nav/index.ts +2 -0
  58. package/src/wc/nav/shell-gradient.ts +2 -2
  59. package/src/wc/nav/shell-view-transition.ts +24 -5
  60. package/src/wc/utils/index.ts +7 -0
  61. package/src/wc/utils/scroll-edge-fade.css +73 -0
  62. package/src/wc/utils/scroll-edge-fade.ts +38 -0
  63. package/dist/components/p-B6NnMfZA.js +0 -2
  64. package/dist/components/p-B6NnMfZA.js.map +0 -1
  65. package/dist/components/p-CXkDP-ct.js +0 -2
  66. package/dist/components/p-CXkDP-ct.js.map +0 -1
  67. package/dist/components/p-DfL2MOso.js +0 -2
  68. package/dist/components/p-DfL2MOso.js.map +0 -1
  69. package/dist/components/p-vW9EMP8s.js +0 -2
  70. package/dist/components/p-vW9EMP8s.js.map +0 -1
  71. /package/dist/components/{p-rzEgrLPJ.js.map → p-DZDXwEMg.js.map} +0 -0
  72. /package/dist/components/{p-Cz8OKcrG.js.map → p-DrvpsLK6.js.map} +0 -0
@@ -688,6 +688,29 @@ export declare interface DsPanelNav extends Components.DsPanelNav {
688
688
  }
689
689
 
690
690
 
691
+ @ProxyCmp({
692
+ inputs: ['activeTool', 'open']
693
+ })
694
+ @Component({
695
+ selector: 'ds-panel-tools',
696
+ changeDetection: ChangeDetectionStrategy.OnPush,
697
+ template: '<ng-content></ng-content>',
698
+ // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
699
+ inputs: ['activeTool', 'open'],
700
+ standalone: false
701
+ })
702
+ export class DsPanelTools {
703
+ protected el: HTMLDsPanelToolsElement;
704
+ constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
705
+ c.detach();
706
+ this.el = r.nativeElement;
707
+ }
708
+ }
709
+
710
+
711
+ export declare interface DsPanelTools extends Components.DsPanelTools {}
712
+
713
+
691
714
  @ProxyCmp({
692
715
  inputs: ['ariaLabel', 'ariaLabelledby', 'direction', 'inactive', 'options', 'value']
693
716
  })
@@ -28,6 +28,7 @@ export { DsMenu } from "./ds-menu.js";
28
28
  export { DsModal } from "./ds-modal.js";
29
29
  export { DsPagination } from "./ds-pagination.js";
30
30
  export { DsPanelNav } from "./ds-panel-nav.js";
31
+ export { DsPanelTools } from "./ds-panel-tools.js";
31
32
  export { DsRadioGroup } from "./ds-radio-group.js";
32
33
  export { DsScrollbar } from "./ds-scrollbar.js";
33
34
  export { DsSelect } from "./ds-select.js";
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * This file was automatically generated by the Stencil React Output Target.
5
+ * Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
6
+ */
7
+
8
+ /* eslint-disable */
9
+
10
+ import type { StencilReactComponent } from '@stencil/react-output-target/runtime';
11
+ import { createComponent } from '@stencil/react-output-target/runtime';
12
+ import React from 'react';
13
+
14
+ import type { Components } from "@ds-mo/ui/dist/components";
15
+ import { DsPanelTools as DsPanelToolsElement, defineCustomElement as defineDsPanelTools } from "@ds-mo/ui/dist/components/ds-panel-tools.js";
16
+
17
+ export type DsPanelToolsEvents = NonNullable<unknown>;
18
+
19
+ export const DsPanelTools: StencilReactComponent<DsPanelToolsElement, DsPanelToolsEvents, Components.DsPanelTools> = /*@__PURE__*/ createComponent<DsPanelToolsElement, DsPanelToolsEvents, Components.DsPanelTools>({
20
+ tagName: 'ds-panel-tools',
21
+ elementClass: DsPanelToolsElement,
22
+ // @ts-ignore - ignore potential React type mismatches between the Stencil Output Target and your project.
23
+ react: React,
24
+ events: {} as DsPanelToolsEvents,
25
+ defineCustomElement: defineDsPanelTools
26
+ });
@@ -169,8 +169,13 @@ export class AppShell {
169
169
  <div class="app-shell__bar">
170
170
  <slot name="bar" />
171
171
  </div>
172
- <div class="app-shell__content">
173
- <slot />
172
+ <div class="app-shell__workspace">
173
+ <div class="app-shell__content">
174
+ <slot />
175
+ </div>
176
+ <div class="app-shell__tools">
177
+ <slot name="tools" />
178
+ </div>
174
179
  </div>
175
180
  </div>
176
181
  </div>
@@ -1,8 +1,10 @@
1
- import { Component, Prop, Element, Watch, h, Host } from '@stencil/core';
1
+ import { Component, Prop, Element, State, Watch, h, Host } from '@stencil/core';
2
2
  import {
3
3
  BADGE_GRADIENT_POSITION_VAR,
4
+ badgeGradientPosition,
5
+ findGradientSurface,
4
6
  isShellGradientActive,
5
- syncBadgeGradientPosition,
7
+ readShellGradientPosition,
6
8
  } from '../../nav/badge-gradient-ring';
7
9
 
8
10
  export type BadgeVariant = 'counter' | 'dot';
@@ -63,6 +65,9 @@ export class Badge {
63
65
  /** Accessible label. Defaults to the count as a string. */
64
66
  @Prop() label: string | undefined;
65
67
 
68
+ /** Bumps on resize/layout so render recomputes gradient ring position. */
69
+ @State() private gradientLayoutVersion = 0;
70
+
66
71
  private gradientObserver: ResizeObserver | null = null;
67
72
  private gradientWindowListener: (() => void) | null = null;
68
73
 
@@ -70,19 +75,8 @@ export class Badge {
70
75
  this.enableShellGradientRingIfNeeded();
71
76
  }
72
77
 
73
- /** Imperative opt-in avoids Stencil aborting parent render for nested badges. */
74
- private enableShellGradientRingIfNeeded() {
75
- if (!this.gradientBackground && isShellGradientActive(this.el)) {
76
- this.gradientBackground = true;
77
- return;
78
- }
79
-
80
- this.bindGradientRingSync();
81
- }
82
-
83
78
  disconnectedCallback() {
84
79
  this.unbindGradientRingSync();
85
- this.el.style.removeProperty(BADGE_GRADIENT_POSITION_VAR);
86
80
  }
87
81
 
88
82
  @Watch('gradientBackground')
@@ -90,12 +84,25 @@ export class Badge {
90
84
  this.bindGradientRingSync();
91
85
  }
92
86
 
87
+ /** Imperative opt-in avoids Stencil aborting parent render for nested badges. */
88
+ private enableShellGradientRingIfNeeded() {
89
+ if (!this.gradientBackground && isShellGradientActive(this.el)) {
90
+ this.gradientBackground = true;
91
+ }
92
+
93
+ if (this.gradientBackground) {
94
+ this.bindGradientRingSync();
95
+ }
96
+ }
97
+
93
98
  private bindGradientRingSync() {
94
99
  this.unbindGradientRingSync();
95
100
 
96
101
  if (!this.gradientBackground) return;
97
102
 
98
- const update = () => syncBadgeGradientPosition(this.el);
103
+ const update = () => {
104
+ this.gradientLayoutVersion += 1;
105
+ };
99
106
  update();
100
107
 
101
108
  this.gradientWindowListener = update;
@@ -126,6 +133,23 @@ export class Badge {
126
133
  this.gradientObserver = null;
127
134
  }
128
135
 
136
+ private ringHostStyle(ring: string): Record<string, string> {
137
+ void this.gradientLayoutVersion;
138
+
139
+ const style: Record<string, string> = { '--_badge-ring': ring };
140
+ if (!this.gradientBackground) return style;
141
+
142
+ const surface = findGradientSurface(this.el);
143
+ if (!surface) return style;
144
+
145
+ style[BADGE_GRADIENT_POSITION_VAR] = badgeGradientPosition(
146
+ this.el,
147
+ surface,
148
+ readShellGradientPosition(surface),
149
+ );
150
+ return style;
151
+ }
152
+
129
153
  render() {
130
154
  const isDot = this.variant === 'dot';
131
155
 
@@ -145,7 +169,7 @@ export class Badge {
145
169
  'badge--on-gradient-background': this.gradientBackground,
146
170
  }}
147
171
  aria-label={ariaLabel}
148
- style={{ '--_badge-ring': ring }}
172
+ style={this.ringHostStyle(ring)}
149
173
  >
150
174
  <span
151
175
  class={{
@@ -105,6 +105,10 @@ export class BarNav {
105
105
  private resizeObserver: ResizeObserver | null = null;
106
106
  private overflowCheckScheduled = false;
107
107
  private intrinsicWidthRetryCount = 0;
108
+ /** Matches `basePath` once tabs + URL are reconciled for the active section. */
109
+ @State() private committedSection = '';
110
+ /** Section waiting for URL + tabs to catch up after a cross-area navigation. */
111
+ @State() private pendingSection = '';
108
112
 
109
113
  private get effectiveValue(): string {
110
114
  if (this.currentUrl && this.basePath) {
@@ -125,23 +129,10 @@ export class BarNav {
125
129
  @Watch('tabsJson')
126
130
  @Watch('actions')
127
131
  @Watch('actionsJson')
128
- onPropsChange() {
129
- this.resolvedTabs = this.tabsJson
130
- ? parseJsonArrayProp(this.tabsJson, [])
131
- : (this.tabs ?? []);
132
- this.resolvedActions = this.actionsJson
133
- ? parseJsonArrayProp(this.actionsJson, [])
134
- : (this.actions ?? []);
135
- this.syncValueFromUrl();
136
- this.intrinsicWidthRetryCount = 0;
137
- this.scheduleOverflowCheck();
138
- }
139
-
140
132
  @Watch('currentUrl')
141
133
  @Watch('basePath')
142
- onUrlChange() {
143
- this.syncValueFromUrl();
144
- this.scheduleOverflowCheck();
134
+ onHostPropsChange() {
135
+ this.applyHostProps();
145
136
  }
146
137
 
147
138
  @Watch('value')
@@ -179,7 +170,7 @@ export class BarNav {
179
170
  }
180
171
 
181
172
  componentWillLoad() {
182
- this.onPropsChange();
173
+ this.applyHostProps();
183
174
  }
184
175
 
185
176
  componentDidLoad() {
@@ -215,7 +206,7 @@ export class BarNav {
215
206
  this.actionsJson,
216
207
  )
217
208
  ) {
218
- this.onPropsChange();
209
+ this.applyHostProps();
219
210
  } else if (this.currentUrl && this.basePath) {
220
211
  this.syncValueFromUrl();
221
212
  }
@@ -367,22 +358,81 @@ export class BarNav {
367
358
  this.menuOpen = false;
368
359
  }
369
360
 
361
+ /** Batch tabs/basePath/currentUrl updates so URL derivation never runs with a mixed section. */
362
+ private incomingTabs(): BarNavTab[] {
363
+ return this.tabsJson
364
+ ? parseJsonArrayProp(this.tabsJson, [])
365
+ : (this.tabs ?? []);
366
+ }
367
+
368
+ private incomingActions(): BarNavActionItem[] {
369
+ return this.actionsJson
370
+ ? parseJsonArrayProp(this.actionsJson, [])
371
+ : (this.actions ?? []);
372
+ }
373
+
374
+ private applyHostProps() {
375
+ const nextBasePath = this.basePath;
376
+ const sectionChanged =
377
+ nextBasePath !== '' &&
378
+ this.committedSection !== '' &&
379
+ nextBasePath !== this.committedSection;
380
+
381
+ if (sectionChanged) {
382
+ this.urlDerivedValue = '';
383
+ this.committedSection = '';
384
+ this.pendingSection = nextBasePath;
385
+ this.resolvedTabs = [];
386
+ this.resolvedActions = this.incomingActions();
387
+ return;
388
+ }
389
+
390
+ if (this.pendingSection) {
391
+ if (!nextBasePath || nextBasePath !== this.pendingSection) {
392
+ return;
393
+ }
394
+ if (!this.currentUrl.startsWith(this.pendingSection)) {
395
+ return;
396
+ }
397
+ const tabs = this.incomingTabs();
398
+ if (tabs.length === 0) {
399
+ return;
400
+ }
401
+ this.pendingSection = '';
402
+ }
403
+
404
+ this.resolvedTabs = this.incomingTabs();
405
+ this.resolvedActions = this.incomingActions();
406
+ this.syncValueFromUrl();
407
+ this.committedSection = nextBasePath;
408
+ this.intrinsicWidthRetryCount = 0;
409
+ this.scheduleOverflowCheck();
410
+ }
411
+
370
412
  private syncMenuSections() {
371
413
  if (!this.menuEl) return;
372
414
  this.menuEl.sections = tabsToMenuSections(this.resolvedTabs, this.effectiveValue);
373
415
  }
374
416
 
375
417
  private syncValueFromUrl() {
418
+ const tabs = this.incomingTabs();
419
+
376
420
  if (!this.currentUrl || !this.basePath) {
377
421
  this.urlDerivedValue = '';
378
422
  this.hideTabsForDetailRoute = false;
379
423
  return;
380
424
  }
381
425
 
426
+ if (!this.currentUrl.startsWith(this.basePath)) {
427
+ this.urlDerivedValue = '';
428
+ this.hideTabsForDetailRoute = false;
429
+ return;
430
+ }
431
+
382
432
  const { value, hideTabs } = deriveBarNavValueFromUrl(
383
433
  this.currentUrl,
384
434
  this.basePath,
385
- this.resolvedTabs,
435
+ tabs,
386
436
  );
387
437
  this.urlDerivedValue = value;
388
438
  this.hideTabsForDetailRoute = hideTabs;
@@ -449,8 +499,12 @@ export class BarNav {
449
499
  }
450
500
 
451
501
  render() {
452
- const hasTabs = this.resolvedTabs.length > 0 && !this.hideTabsForDetailRoute;
502
+ const sectionReady =
503
+ this.basePath !== '' && this.committedSection === this.basePath;
504
+ const hasTabs =
505
+ sectionReady && this.resolvedTabs.length > 0 && !this.hideTabsForDetailRoute;
453
506
  const tabSurface = this.tabSurface;
507
+ const tabGroupKey = this.basePath || 'no-section';
454
508
 
455
509
  return (
456
510
  <Host>
@@ -472,6 +526,7 @@ export class BarNav {
472
526
  {hasTabs && (
473
527
  <div class="bar-nav__tabs-probe" aria-hidden="true" inert>
474
528
  <ds-tab-group-nav
529
+ key={`probe-${tabGroupKey}`}
475
530
  ref={el => {
476
531
  this.probeTabGroupEl = el ?? null;
477
532
  if (el) {
@@ -489,6 +544,7 @@ export class BarNav {
489
544
  {hasTabs && !this.tabsCollapsed && (
490
545
  <div class="bar-nav__left">
491
546
  <ds-tab-group-nav
547
+ key={`visible-${tabGroupKey}`}
492
548
  class="bar-nav__tabs-visible"
493
549
  ref={el => {
494
550
  this.visibleTabGroupEl = el ?? null;
@@ -1,4 +1,5 @@
1
- import { Component, Prop, h, Host } from '@stencil/core';
1
+ import { Component, Prop, Element, State, h, Host } from '@stencil/core';
2
+ import { isShellGradientActive } from '../../nav/badge-gradient-ring';
2
3
 
3
4
  export type FadeSide = 'top' | 'bottom' | 'left' | 'right';
4
5
  export type FadeSurface =
@@ -55,6 +56,11 @@ const SURFACE_BACKGROUND: Record<FadeSurface, string> = {
55
56
  scoped: true,
56
57
  })
57
58
  export class Fade {
59
+ @Element() el!: HTMLElement;
60
+
61
+ /** When under `ds-app-shell[gradient]`, composites the shell wash over the base fade. */
62
+ @State() private shellGradientChrome: 'panel' | 'bar' | null = null;
63
+
58
64
  /** Edge where the fade is anchored. */
59
65
  @Prop() side: FadeSide = 'bottom';
60
66
 
@@ -76,6 +82,18 @@ export class Fade {
76
82
  /** Controls visibility without removing the element from layout/positioning. */
77
83
  @Prop() visible: boolean = true;
78
84
 
85
+ componentDidLoad() {
86
+ this.syncShellGradient();
87
+ }
88
+
89
+ private syncShellGradient() {
90
+ if (!isShellGradientActive(this.el)) {
91
+ this.shellGradientChrome = null;
92
+ return;
93
+ }
94
+ this.shellGradientChrome = this.el.closest('ds-bar-nav') ? 'bar' : 'panel';
95
+ }
96
+
79
97
  private resolveSize(): string {
80
98
  if (this.height) return this.height;
81
99
 
@@ -100,6 +118,9 @@ export class Fade {
100
118
  'fade--hidden': !this.visible,
101
119
  'fade--vertical-edge': isVerticalEdge,
102
120
  'fade--horizontal-edge': !isVerticalEdge,
121
+ 'fade--shell-gradient': this.shellGradientChrome !== null,
122
+ 'fade--shell-gradient-panel': this.shellGradientChrome === 'panel',
123
+ 'fade--shell-gradient-bar': this.shellGradientChrome === 'bar',
103
124
  [`fade--${this.side}`]: true,
104
125
  }}
105
126
  style={{
@@ -37,6 +37,7 @@ const vtResolvers = new WeakMap<object, () => void>();
37
37
  @Component({
38
38
  tag: 'ds-panel-nav',
39
39
  styleUrl: 'PanelNav.css',
40
+ styleUrls: ['../../utils/scroll-edge-fade.css'],
40
41
  scoped: true,
41
42
  })
42
43
  export class PanelNav {
@@ -550,7 +551,15 @@ export class PanelNav {
550
551
  </div>
551
552
 
552
553
  {/* ── Scrollable body ── */}
553
- <div class="panel-nav__body" onScroll={e => this.handleBodyScroll(e)}>
554
+ <div
555
+ class={{
556
+ 'panel-nav__body': true,
557
+ 'scroll-edge-fade': true,
558
+ 'scroll-edge-fade--bottom': true,
559
+ 'scroll-edge-fade--at-end': this.atBottom,
560
+ }}
561
+ onScroll={e => this.handleBodyScroll(e)}
562
+ >
554
563
  {(() => {
555
564
  let flatIdx = 0;
556
565
  return this.parsedGroups.map(group => (
@@ -572,14 +581,6 @@ export class PanelNav {
572
581
 
573
582
  {/* ── Footer ── */}
574
583
  <div class="panel-nav__footer">
575
- <ds-fade
576
- class="panel-nav__footer-fade"
577
- side="bottom"
578
- size="size-600"
579
- background="var(--_nav-bg)"
580
- visible={!this.atBottom}
581
- />
582
-
583
584
  {/* Left icon button — Gear in navigation style, Dashboard in default style */}
584
585
  <button
585
586
  type="button"
@@ -0,0 +1,163 @@
1
+ import { Component, Prop, Element, State, Watch, h, Host } from '@stencil/core';
2
+ import {
3
+ PANEL_TOOLS_LABELS,
4
+ type PanelToolsToolId,
5
+ } from './panel-tools-types';
6
+
7
+ @Component({
8
+ tag: 'ds-panel-tools',
9
+ styleUrl: 'PanelTools.css',
10
+ scoped: true,
11
+ })
12
+ export class PanelTools {
13
+ @Element() el!: HTMLElement;
14
+
15
+ /** When false, width animates to 0. */
16
+ @Prop({ reflect: true }) open: boolean = false;
17
+
18
+ /** Active tool view — `search`, `messages`, `stacks`, `activity`, or `agents`. */
19
+ @Prop({ attribute: 'active-tool', reflect: true }) activeTool: PanelToolsToolId | '' = '';
20
+
21
+ /** Stays true until the close width transition finishes. */
22
+ @State() private expanded = false;
23
+
24
+ /** Suppresses width transition until the host has painted its initial open state. */
25
+ @State() private readyForMotion = false;
26
+
27
+ private motionEnableGeneration = 0;
28
+
29
+ componentWillLoad() {
30
+ if (this.open) this.expanded = true;
31
+ }
32
+
33
+ componentDidLoad() {
34
+ this.deferMotionEnable();
35
+ }
36
+
37
+ disconnectedCallback() {
38
+ this.el.removeEventListener('transitionend', this.handleTransitionEnd);
39
+ this.motionEnableGeneration += 1;
40
+ }
41
+
42
+ /** Two rAFs after the last pre-ready prop change — host bindings may land late on refresh. */
43
+ private deferMotionEnable() {
44
+ if (this.readyForMotion) return;
45
+ const generation = ++this.motionEnableGeneration;
46
+ requestAnimationFrame(() => {
47
+ if (generation !== this.motionEnableGeneration) return;
48
+ requestAnimationFrame(() => {
49
+ if (generation !== this.motionEnableGeneration) return;
50
+ this.readyForMotion = true;
51
+ });
52
+ });
53
+ }
54
+
55
+ connectedCallback() {
56
+ this.el.addEventListener('transitionend', this.handleTransitionEnd);
57
+ }
58
+
59
+ @Watch('open')
60
+ openChanged(isOpen: boolean) {
61
+ if (isOpen) this.expanded = true;
62
+ this.deferMotionEnable();
63
+ }
64
+
65
+ @Watch('activeTool')
66
+ activeToolChanged() {
67
+ this.deferMotionEnable();
68
+ }
69
+
70
+ private handleTransitionEnd = (event: TransitionEvent) => {
71
+ if (event.target !== this.el) return;
72
+ if (event.propertyName !== 'max-width') return;
73
+ if (!this.open) this.expanded = false;
74
+ };
75
+
76
+ private isShowingContent(): boolean {
77
+ return this.open || this.expanded;
78
+ }
79
+
80
+ private isActiveTool(id: PanelToolsToolId): boolean {
81
+ return this.isShowingContent() && this.activeTool === id;
82
+ }
83
+
84
+ private headerLabel(): string {
85
+ if (!this.activeTool) return '';
86
+ return PANEL_TOOLS_LABELS[this.activeTool as PanelToolsToolId] ?? '';
87
+ }
88
+
89
+ render() {
90
+ const showingContent = this.isShowingContent();
91
+
92
+ return (
93
+ <Host
94
+ class={{
95
+ 'panel-tools': true,
96
+ 'panel-tools--open': this.open,
97
+ 'panel-tools--ready': this.readyForMotion,
98
+ }}
99
+ role="complementary"
100
+ aria-label={this.headerLabel() || 'Tools'}
101
+ aria-hidden={showingContent ? null : 'true'}
102
+ >
103
+ <div class="panel-tools__inner">
104
+ <header class="panel-tools__header">
105
+ <span class="text-body-medium-emphasis">{this.headerLabel()}</span>
106
+ </header>
107
+ <div class="panel-tools__body">
108
+ <div
109
+ class={{
110
+ 'panel-tools__view': true,
111
+ 'panel-tools__view--active': this.isActiveTool('search'),
112
+ 'text-body-medium': true,
113
+ }}
114
+ hidden={!this.isActiveTool('search')}
115
+ >
116
+ <slot name="search" />
117
+ </div>
118
+ <div
119
+ class={{
120
+ 'panel-tools__view': true,
121
+ 'panel-tools__view--active': this.isActiveTool('messages'),
122
+ 'text-body-medium': true,
123
+ }}
124
+ hidden={!this.isActiveTool('messages')}
125
+ >
126
+ <slot name="messages" />
127
+ </div>
128
+ <div
129
+ class={{
130
+ 'panel-tools__view': true,
131
+ 'panel-tools__view--active': this.isActiveTool('stacks'),
132
+ 'text-body-medium': true,
133
+ }}
134
+ hidden={!this.isActiveTool('stacks')}
135
+ >
136
+ <slot name="stacks" />
137
+ </div>
138
+ <div
139
+ class={{
140
+ 'panel-tools__view': true,
141
+ 'panel-tools__view--active': this.isActiveTool('activity'),
142
+ 'text-body-medium': true,
143
+ }}
144
+ hidden={!this.isActiveTool('activity')}
145
+ >
146
+ <slot name="activity" />
147
+ </div>
148
+ <div
149
+ class={{
150
+ 'panel-tools__view': true,
151
+ 'panel-tools__view--active': this.isActiveTool('agents'),
152
+ 'text-body-medium': true,
153
+ }}
154
+ hidden={!this.isActiveTool('agents')}
155
+ >
156
+ <slot name="agents" />
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </Host>
161
+ );
162
+ }
163
+ }
@@ -0,0 +1,2 @@
1
+ export type { PanelToolsToolId } from './panel-tools-types';
2
+ export { PANEL_TOOLS_LABELS, PANEL_TOOLS_TOOL_IDS } from './panel-tools-types';
@@ -0,0 +1,18 @@
1
+ /** Bar-nav tool ids that map to `ds-panel-tools` views. */
2
+ export type PanelToolsToolId = 'search' | 'messages' | 'stacks' | 'activity' | 'agents';
3
+
4
+ export const PANEL_TOOLS_TOOL_IDS: PanelToolsToolId[] = [
5
+ 'search',
6
+ 'messages',
7
+ 'stacks',
8
+ 'activity',
9
+ 'agents',
10
+ ];
11
+
12
+ export const PANEL_TOOLS_LABELS: Record<PanelToolsToolId, string> = {
13
+ search: 'Search',
14
+ messages: 'Messages',
15
+ stacks: 'Stacks',
16
+ activity: 'Activity',
17
+ agents: 'Agents',
18
+ };