@ds-mo/ui 1.4.0 → 1.6.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 (92) hide show
  1. package/README.md +1 -1
  2. package/dist/.build-stamp +1 -0
  3. package/dist/components/{ds-sidebar.d.ts → ds-app-shell.d.ts} +4 -4
  4. package/dist/components/ds-app-shell.js +2 -0
  5. package/dist/components/ds-app-shell.js.map +1 -0
  6. package/dist/components/ds-banner.js +1 -1
  7. package/dist/components/ds-banner.js.map +1 -1
  8. package/dist/components/ds-bar-nav.js +1 -1
  9. package/dist/components/ds-bar-nav.js.map +1 -1
  10. package/dist/components/ds-button-group.js +1 -1
  11. package/dist/components/ds-button.js +1 -1
  12. package/dist/components/ds-checkbox.js +1 -1
  13. package/dist/components/ds-field.js +1 -1
  14. package/dist/components/ds-loader.js +1 -1
  15. package/dist/components/ds-menu.js +1 -1
  16. package/dist/components/ds-modal.js +1 -1
  17. package/dist/components/ds-modal.js.map +1 -1
  18. package/dist/components/ds-panel-nav.js +1 -1
  19. package/dist/components/ds-panel-nav.js.map +1 -1
  20. package/dist/components/ds-select.js +1 -1
  21. package/dist/components/ds-slider.js +1 -1
  22. package/dist/components/ds-toast-provider.js +1 -1
  23. package/dist/components/ds-toast-provider.js.map +1 -1
  24. package/dist/components/ds-toggle-button-group.js +1 -1
  25. package/dist/components/ds-toggle-button.js +1 -1
  26. package/dist/components/ds-toggle.js +1 -1
  27. package/dist/components/ds-tooltip.js +1 -1
  28. package/dist/components/ds-tooltip.js.map +1 -1
  29. package/dist/components/p-BRju0sEC.js +2 -0
  30. package/dist/components/p-BRju0sEC.js.map +1 -0
  31. package/dist/components/p-BvmooLHV.js +2 -0
  32. package/dist/components/p-BvmooLHV.js.map +1 -0
  33. package/dist/components/p-C4WbCXBj.js +2 -0
  34. package/dist/components/p-C4WbCXBj.js.map +1 -0
  35. package/dist/components/p-CYJ-de0u.js +2 -0
  36. package/dist/components/p-CYJ-de0u.js.map +1 -0
  37. package/dist/components/{p--YDw8Q4o.js → p-CsO880L-.js} +2 -2
  38. package/dist/components/p-DTjpgYJV.js +2 -0
  39. package/dist/components/p-DTjpgYJV.js.map +1 -0
  40. package/dist/components/p-DfL2MOso.js +2 -0
  41. package/dist/components/p-DfL2MOso.js.map +1 -0
  42. package/dist/components/p-DiWQFROF.js +2 -0
  43. package/dist/components/p-DiWQFROF.js.map +1 -0
  44. package/dist/types/components/AppShell/AppShell.d.ts +25 -0
  45. package/dist/types/components/Banner/Banner.d.ts +2 -0
  46. package/dist/types/components/Loader/Loader.d.ts +3 -2
  47. package/dist/types/components/Menu/Menu.d.ts +11 -2
  48. package/dist/types/components/Modal/Modal.d.ts +1 -0
  49. package/dist/types/components/PanelNav/PanelNav.d.ts +3 -6
  50. package/dist/types/components/Toast/Toast.d.ts +1 -0
  51. package/dist/types/components/Tooltip/Tooltip.d.ts +14 -3
  52. package/dist/types/components.d.ts +92 -105
  53. package/dist/types/nav/index.d.ts +2 -0
  54. package/dist/types/nav/shell-gradient.d.ts +25 -0
  55. package/dist/types/nav/shell-view-transition.d.ts +2 -2
  56. package/dist/types/utils/index.d.ts +5 -0
  57. package/dist/types/utils/resolve-css-length-px.d.ts +8 -0
  58. package/dist/types/utils/resolve-css-time-ms.d.ts +8 -0
  59. package/dist/types/utils/token-defaults.d.ts +27 -0
  60. package/package.json +4 -4
  61. package/src/angular/index.ts +1 -1
  62. package/src/angular/proxies.ts +23 -33
  63. package/src/react/components.ts +1 -1
  64. package/src/react/ds-app-shell.ts +26 -0
  65. package/src/wc/components/AppShell/AppShell.tsx +180 -0
  66. package/src/wc/components/Banner/Banner.tsx +11 -5
  67. package/src/wc/components/Loader/Loader.tsx +8 -3
  68. package/src/wc/components/Menu/Menu.tsx +61 -26
  69. package/src/wc/components/Modal/Modal.tsx +10 -7
  70. package/src/wc/components/PanelNav/PanelNav.tsx +5 -8
  71. package/src/wc/components/Toast/Toast.tsx +7 -2
  72. package/src/wc/components/Toast/toast-service.ts +8 -1
  73. package/src/wc/components/ToggleButton/ToggleButton.tsx +7 -3
  74. package/src/wc/components/Tooltip/Tooltip.tsx +70 -31
  75. package/src/wc/components.d.ts +92 -105
  76. package/src/wc/nav/index.ts +14 -0
  77. package/src/wc/nav/shell-gradient.ts +44 -0
  78. package/src/wc/nav/shell-view-transition.ts +4 -10
  79. package/dist/components/ds-sidebar.js +0 -2
  80. package/dist/components/ds-sidebar.js.map +0 -1
  81. package/dist/components/p-BcuUR5Ao.js +0 -2
  82. package/dist/components/p-BcuUR5Ao.js.map +0 -1
  83. package/dist/components/p-CpbnmEu1.js +0 -2
  84. package/dist/components/p-CpbnmEu1.js.map +0 -1
  85. package/dist/components/p-DznbYg1m.js +0 -2
  86. package/dist/components/p-DznbYg1m.js.map +0 -1
  87. package/dist/components/p-uMN1Ppw2.js +0 -2
  88. package/dist/components/p-uMN1Ppw2.js.map +0 -1
  89. package/dist/types/components/Sidebar/Sidebar.d.ts +0 -29
  90. package/src/react/ds-sidebar.ts +0 -33
  91. package/src/wc/components/Sidebar/Sidebar.tsx +0 -245
  92. /package/dist/components/{p--YDw8Q4o.js.map → p-CsO880L-.js.map} +0 -0
@@ -35,6 +35,29 @@ export declare interface DsAccordion extends Components.DsAccordion {
35
35
  }
36
36
 
37
37
 
38
+ @ProxyCmp({
39
+ inputs: ['gradient', 'gradientSrc', 'navStyle']
40
+ })
41
+ @Component({
42
+ selector: 'ds-app-shell',
43
+ changeDetection: ChangeDetectionStrategy.OnPush,
44
+ template: '<ng-content></ng-content>',
45
+ // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
46
+ inputs: ['gradient', 'gradientSrc', 'navStyle'],
47
+ standalone: false
48
+ })
49
+ export class DsAppShell {
50
+ protected el: HTMLDsAppShellElement;
51
+ constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
52
+ c.detach();
53
+ this.el = r.nativeElement;
54
+ }
55
+ }
56
+
57
+
58
+ export declare interface DsAppShell extends Components.DsAppShell {}
59
+
60
+
38
61
  @ProxyCmp({
39
62
  inputs: ['background', 'count', 'isSelected', 'label', 'max', 'surface', 'variant']
40
63
  })
@@ -744,39 +767,6 @@ export declare interface DsSelect extends Components.DsSelect {
744
767
  }
745
768
 
746
769
 
747
- @ProxyCmp({
748
- inputs: ['collapsed', 'mobile', 'resizable', 'width']
749
- })
750
- @Component({
751
- selector: 'ds-sidebar',
752
- changeDetection: ChangeDetectionStrategy.OnPush,
753
- template: '<ng-content></ng-content>',
754
- // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
755
- inputs: ['collapsed', 'mobile', 'resizable', 'width'],
756
- outputs: ['dsToggle', 'dsWidthChange'],
757
- standalone: false
758
- })
759
- export class DsSidebar {
760
- protected el: HTMLDsSidebarElement;
761
- @Output() dsToggle = new EventEmitter<CustomEvent<void>>();
762
- @Output() dsWidthChange = new EventEmitter<CustomEvent<IDsSidebarSidebarWidth>>();
763
- constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
764
- c.detach();
765
- this.el = r.nativeElement;
766
- }
767
- }
768
-
769
-
770
- import type { SidebarWidth as IDsSidebarSidebarWidth } from '@ds-mo/ui';
771
-
772
- export declare interface DsSidebar extends Components.DsSidebar {
773
-
774
- dsToggle: EventEmitter<CustomEvent<void>>;
775
-
776
- dsWidthChange: EventEmitter<CustomEvent<IDsSidebarSidebarWidth>>;
777
- }
778
-
779
-
780
770
  @ProxyCmp({
781
771
  inputs: ['height', 'lines', 'shimmer', 'variant', 'width']
782
772
  })
@@ -5,6 +5,7 @@
5
5
 
6
6
  /* eslint-disable */
7
7
  export { DsAccordion } from "./ds-accordion.js";
8
+ export { DsAppShell } from "./ds-app-shell.js";
8
9
  export { DsBadge } from "./ds-badge.js";
9
10
  export { DsBanner } from "./ds-banner.js";
10
11
  export { DsBarNavAction } from "./ds-bar-nav-action.js";
@@ -30,7 +31,6 @@ export { DsPanelNav } from "./ds-panel-nav.js";
30
31
  export { DsRadioGroup } from "./ds-radio-group.js";
31
32
  export { DsScrollbar } from "./ds-scrollbar.js";
32
33
  export { DsSelect } from "./ds-select.js";
33
- export { DsSidebar } from "./ds-sidebar.js";
34
34
  export { DsSkeleton } from "./ds-skeleton.js";
35
35
  export { DsSlider } from "./ds-slider.js";
36
36
  export { DsSurface } from "./ds-surface.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 { DsAppShell as DsAppShellElement, defineCustomElement as defineDsAppShell } from "@ds-mo/ui/dist/components/ds-app-shell.js";
16
+
17
+ export type DsAppShellEvents = NonNullable<unknown>;
18
+
19
+ export const DsAppShell: StencilReactComponent<DsAppShellElement, DsAppShellEvents, Components.DsAppShell> = /*@__PURE__*/ createComponent<DsAppShellElement, DsAppShellEvents, Components.DsAppShell>({
20
+ tagName: 'ds-app-shell',
21
+ elementClass: DsAppShellElement,
22
+ // @ts-ignore - ignore potential React type mismatches between the Stencil Output Target and your project.
23
+ react: React,
24
+ events: {} as DsAppShellEvents,
25
+ defineCustomElement: defineDsAppShell
26
+ });
@@ -0,0 +1,180 @@
1
+ import { Component, Prop, Element, Watch, h, Host } from '@stencil/core';
2
+ import type { NavChromeStyle } from '../../nav/nav-chrome';
3
+ import {
4
+ SHELL_GRADIENT_IMAGE_VAR,
5
+ SHELL_GRADIENT_OPACITY,
6
+ SHELL_GRADIENT_OPACITY_VAR,
7
+ SHELL_GRADIENT_POSITION_BAR_VAR,
8
+ SHELL_GRADIENT_POSITION_PANEL_VAR,
9
+ SHELL_GRADIENT_SIZE_VAR,
10
+ shellGradientImageForStyle,
11
+ shellGradientPositionBar,
12
+ shellGradientPositionPanel,
13
+ shellGradientSize,
14
+ } from '../../nav/shell-gradient';
15
+
16
+ @Component({
17
+ tag: 'ds-app-shell',
18
+ styleUrl: 'AppShell.css',
19
+ scoped: true,
20
+ })
21
+ export class AppShell {
22
+ /** Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`. */
23
+ @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'navigation';
24
+
25
+ /** When `true`, paints the shared L-shaped radial wash on panel + bar backgrounds. */
26
+ @Prop({ reflect: true }) gradient: boolean = false;
27
+
28
+ /**
29
+ * Optional custom gradient for `background-image` (e.g. SVG URL).
30
+ * When set, overrides the built-in radial wash.
31
+ */
32
+ @Prop() gradientSrc: string = '';
33
+
34
+ @Element() el!: HTMLElement;
35
+
36
+ private resizeObserver: ResizeObserver | null = null;
37
+
38
+ componentDidLoad() {
39
+ this.syncSlottedNavStyle();
40
+ this.connectMetricsObserver();
41
+ this.scheduleGradientVars();
42
+ }
43
+
44
+ private scheduleGradientVars() {
45
+ this.updateGradientVars();
46
+ requestAnimationFrame(() => this.updateGradientVars());
47
+ }
48
+
49
+ disconnectedCallback() {
50
+ this.resizeObserver?.disconnect();
51
+ this.resizeObserver = null;
52
+ }
53
+
54
+ @Watch('navStyle')
55
+ @Watch('gradient')
56
+ @Watch('gradientSrc')
57
+ onShellPropsChange() {
58
+ this.syncSlottedNavStyle();
59
+ this.scheduleGradientVars();
60
+ }
61
+
62
+ private connectMetricsObserver() {
63
+ this.resizeObserver = new ResizeObserver(() => this.scheduleGradientVars());
64
+ this.resizeObserver.observe(this.el);
65
+ const panelWrap = this.el.querySelector('.app-shell__panel');
66
+ const barWrap = this.el.querySelector('.app-shell__bar');
67
+ if (panelWrap) this.resizeObserver.observe(panelWrap);
68
+ if (barWrap) this.resizeObserver.observe(barWrap);
69
+ this.scheduleGradientVars();
70
+ }
71
+
72
+ private applyGradientVars(target: HTMLElement, vars: Record<string, string | null>) {
73
+ const style = target.style;
74
+ for (const [name, value] of Object.entries(vars)) {
75
+ if (value === null) style.removeProperty(name);
76
+ else style.setProperty(name, value);
77
+ }
78
+ }
79
+
80
+ private clearGradientVars(targets: HTMLElement[]) {
81
+ const keys = [
82
+ SHELL_GRADIENT_IMAGE_VAR,
83
+ SHELL_GRADIENT_SIZE_VAR,
84
+ SHELL_GRADIENT_POSITION_PANEL_VAR,
85
+ SHELL_GRADIENT_POSITION_BAR_VAR,
86
+ SHELL_GRADIENT_OPACITY_VAR,
87
+ ];
88
+ for (const target of targets) {
89
+ for (const key of keys) target.style.removeProperty(key);
90
+ }
91
+ }
92
+
93
+ private syncSlottedNavStyle() {
94
+ const panel = this.el.querySelector('ds-panel-nav') as (HTMLElement & { navStyle: NavChromeStyle }) | null;
95
+ const bar = this.el.querySelector('ds-bar-nav') as (HTMLElement & { navStyle: NavChromeStyle }) | null;
96
+ if (panel) {
97
+ panel.setAttribute('nav-style', this.navStyle);
98
+ panel.navStyle = this.navStyle;
99
+ }
100
+ if (bar) {
101
+ bar.setAttribute('nav-style', this.navStyle);
102
+ bar.navStyle = this.navStyle;
103
+ }
104
+ }
105
+
106
+ private updateGradientVars() {
107
+ const panel = this.el.querySelector('ds-panel-nav') as HTMLElement | null;
108
+ const bar = this.el.querySelector('ds-bar-nav') as HTMLElement | null;
109
+ const targets = [this.el, panel, bar].filter((el): el is HTMLElement => el !== null);
110
+
111
+ if (!this.gradient) {
112
+ this.clearGradientVars(targets);
113
+ return;
114
+ }
115
+
116
+ const image = this.gradientSrc.trim()
117
+ ? `url(${this.gradientSrc.trim()})`
118
+ : shellGradientImageForStyle(this.navStyle);
119
+
120
+ const shellRect = this.el.getBoundingClientRect();
121
+ const panelWrap = this.el.querySelector('.app-shell__panel') as HTMLElement | null;
122
+ const panelWidth = panelWrap?.getBoundingClientRect().width ?? 0;
123
+
124
+ const size = shellGradientSize({
125
+ width: shellRect.width,
126
+ height: shellRect.height,
127
+ panelWidth,
128
+ });
129
+ const panelPosition = shellGradientPositionPanel();
130
+ const barPosition = shellGradientPositionBar(panelWidth);
131
+ const opacity = SHELL_GRADIENT_OPACITY;
132
+
133
+ for (const target of targets) {
134
+ if (target === this.el) {
135
+ this.applyGradientVars(target, {
136
+ [SHELL_GRADIENT_IMAGE_VAR]: image,
137
+ [SHELL_GRADIENT_SIZE_VAR]: size,
138
+ [SHELL_GRADIENT_POSITION_PANEL_VAR]: panelPosition,
139
+ [SHELL_GRADIENT_POSITION_BAR_VAR]: barPosition,
140
+ [SHELL_GRADIENT_OPACITY_VAR]: opacity,
141
+ });
142
+ continue;
143
+ }
144
+
145
+ const isBar = target.tagName.toLowerCase() === 'ds-bar-nav';
146
+ this.applyGradientVars(target, {
147
+ [SHELL_GRADIENT_IMAGE_VAR]: image,
148
+ [SHELL_GRADIENT_SIZE_VAR]: size,
149
+ [SHELL_GRADIENT_OPACITY_VAR]: opacity,
150
+ [SHELL_GRADIENT_POSITION_PANEL_VAR]: isBar ? null : panelPosition,
151
+ [SHELL_GRADIENT_POSITION_BAR_VAR]: isBar ? barPosition : null,
152
+ });
153
+ }
154
+ }
155
+
156
+ render() {
157
+ const shellCls: Record<string, boolean> = {
158
+ 'app-shell': true,
159
+ 'app-shell--gradient': this.gradient,
160
+ };
161
+
162
+ return (
163
+ <Host class={shellCls}>
164
+ <div class="app-shell__row">
165
+ <div class="app-shell__panel">
166
+ <slot name="panel" />
167
+ </div>
168
+ <div class="app-shell__main">
169
+ <div class="app-shell__bar">
170
+ <slot name="bar" />
171
+ </div>
172
+ <div class="app-shell__content">
173
+ <slot />
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </Host>
178
+ );
179
+ }
180
+ }
@@ -1,4 +1,5 @@
1
1
  import { Component, Prop, State, Event, EventEmitter, Watch, h, Host } from '@stencil/core';
2
+ import { resolveCssTimeMs, TOKEN_DEFAULTS } from '../../utils';
2
3
 
3
4
  export type BannerIntent =
4
5
  | 'brand' | 'positive' | 'negative' | 'warning' | 'caution'
@@ -6,9 +7,6 @@ export type BannerIntent =
6
7
 
7
8
  export type BannerContrast = 'faint' | 'medium' | 'bold' | 'strong';
8
9
 
9
- const FADE_OUT_MS = 200;
10
- const AUTO_DISMISS_MS = 4000;
11
-
12
10
  @Component({
13
11
  tag: 'ds-banner',
14
12
  styleUrl: 'Banner.css',
@@ -32,6 +30,14 @@ export class Banner {
32
30
  private dismissTimer: ReturnType<typeof setTimeout> | null = null;
33
31
  private closeTimer: ReturnType<typeof setTimeout> | null = null;
34
32
 
33
+ private get fadeOutMs(): number {
34
+ return resolveCssTimeMs(TOKEN_DEFAULTS.motionShort3, TOKEN_DEFAULTS.animationDurationShort3);
35
+ }
36
+
37
+ private get autoDismissMs(): number {
38
+ return resolveCssTimeMs(TOKEN_DEFAULTS.animationDelayLong2, TOKEN_DEFAULTS.animationDelayLong2);
39
+ }
40
+
35
41
  componentDidLoad() {
36
42
  if (this.floating && this.message) this.scheduleDismiss();
37
43
  }
@@ -58,7 +64,7 @@ export class Banner {
58
64
  this.dismissTimer = setTimeout(() => {
59
65
  this.dismissTimer = null;
60
66
  this.beginClose();
61
- }, AUTO_DISMISS_MS);
67
+ }, this.autoDismissMs);
62
68
  }
63
69
 
64
70
  private beginClose() {
@@ -69,7 +75,7 @@ export class Banner {
69
75
  this.closing = false;
70
76
  this.closeTimer = null;
71
77
  this.dsDismiss.emit();
72
- }, FADE_OUT_MS);
78
+ }, this.fadeOutMs);
73
79
  } else {
74
80
  this.dsDismiss.emit();
75
81
  }
@@ -1,4 +1,5 @@
1
1
  import { Component, Prop, h } from '@stencil/core';
2
+ import { resolveCssLengthPx, TOKEN_DEFAULTS } from '../../utils';
2
3
 
3
4
  @Component({
4
5
  tag: 'ds-loader',
@@ -6,8 +7,8 @@ import { Component, Prop, h } from '@stencil/core';
6
7
  shadow: true,
7
8
  })
8
9
  export class Loader {
9
- /** Render size in px. Designed on a 16×16 viewBox; stroke scales proportionally. */
10
- @Prop() size: number = 20;
10
+ /** Render size number (px) or TokoMo length (`var(--dimension-iconography-md)`, etc.). */
11
+ @Prop() size: number | string = TOKEN_DEFAULTS.iconographyMd;
11
12
  /**
12
13
  * Accessible label for standalone usage. Wraps the spinner in a live region
13
14
  * and renders the label visually-hidden. Omit when the host element already
@@ -15,8 +16,12 @@ export class Loader {
15
16
  */
16
17
  @Prop() label: string | undefined;
17
18
 
19
+ private get sizePx(): number {
20
+ return resolveCssLengthPx(this.size, TOKEN_DEFAULTS.iconographyMd);
21
+ }
22
+
18
23
  render() {
19
- const px = `${this.size}px`;
24
+ const px = `${this.sizePx}px`;
20
25
  const svg = (
21
26
  <svg
22
27
  style={{ width: px, height: px }}
@@ -1,4 +1,5 @@
1
1
  import { Component, Prop, State, Event, EventEmitter, Element, Watch, Listen, h, Host } from '@stencil/core';
2
+ import { resolveCssLengthPx, resolveCssTimeMs, TOKEN_DEFAULTS } from '../../utils';
2
3
 
3
4
  export type MenuSide = 'top' | 'right' | 'bottom' | 'left';
4
5
  export type MenuAlign = 'start' | 'center' | 'end';
@@ -19,9 +20,6 @@ export interface MenuSection {
19
20
  items: MenuItemData[];
20
21
  }
21
22
 
22
- const CLOSE_ANIMATION_MS = 220;
23
- const VP_PAD = 4;
24
-
25
23
  @Component({
26
24
  tag: 'ds-menu',
27
25
  styleUrl: 'Menu.css',
@@ -35,8 +33,10 @@ export class Menu {
35
33
  @Prop() sections: MenuSection[] = [];
36
34
  @Prop() side: MenuSide = 'bottom';
37
35
  @Prop() align: MenuAlign = 'start';
38
- @Prop() sideOffset: number = 4;
39
- @Prop() alignOffset: number = 0;
36
+ /** Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.). */
37
+ @Prop() sideOffset: number | string = TOKEN_DEFAULTS.space050;
38
+ /** Cross-axis offset — number (px) or TokoMo length. */
39
+ @Prop() alignOffset: number | string = 0;
40
40
  @Prop() menuWidth: string | undefined;
41
41
  @Prop() minWidth: string | undefined;
42
42
  /** External trigger element to position against. Set via JS: menuEl.anchor = buttonEl */
@@ -85,10 +85,42 @@ export class Menu {
85
85
  this.shouldRender = false;
86
86
  this.closing = false;
87
87
  this.closeTimer = null;
88
- }, CLOSE_ANIMATION_MS);
88
+ }, this.closeAnimationMs);
89
89
  }
90
90
  }
91
91
 
92
+ @Watch('side')
93
+ @Watch('align')
94
+ @Watch('sideOffset')
95
+ @Watch('alignOffset')
96
+ onPositionPropsChange() {
97
+ if (this.open) this.calculatePosition();
98
+ }
99
+
100
+ private get viewportPadPx(): number {
101
+ return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);
102
+ }
103
+
104
+ private get sideOffsetPx(): number {
105
+ return resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050);
106
+ }
107
+
108
+ private get alignOffsetPx(): number {
109
+ return resolveCssLengthPx(this.alignOffset, 0);
110
+ }
111
+
112
+ private get popupFallbackWidthPx(): number {
113
+ return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthXs, TOKEN_DEFAULTS.menuWidthXs);
114
+ }
115
+
116
+ private get popupFallbackHeightPx(): number {
117
+ return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);
118
+ }
119
+
120
+ private get closeAnimationMs(): number {
121
+ return resolveCssTimeMs(TOKEN_DEFAULTS.motionShort3, TOKEN_DEFAULTS.animationDurationShort3);
122
+ }
123
+
92
124
  private get resolvedAnchor(): HTMLElement | null {
93
125
  if (this.anchor) return this.anchor;
94
126
  if (this.anchorId) return document.getElementById(this.anchorId);
@@ -112,40 +144,43 @@ export class Menu {
112
144
  if (!popup) return;
113
145
 
114
146
  const a = anchorEl.getBoundingClientRect();
115
- const pw = popup.offsetWidth || 200;
116
- const ph = popup.offsetHeight || 160;
147
+ const pw = popup.offsetWidth || this.popupFallbackWidthPx;
148
+ const ph = popup.offsetHeight || this.popupFallbackHeightPx;
149
+ const sideOffset = this.sideOffsetPx;
150
+ const alignOffset = this.alignOffsetPx;
151
+ const vpPad = this.viewportPadPx;
117
152
  let x = 0, y = 0;
118
153
 
119
154
  switch (this.side) {
120
155
  case 'top':
121
- y = a.top - ph - this.sideOffset;
122
- x = this.align === 'start' ? a.left + this.alignOffset
123
- : this.align === 'end' ? a.right - pw + this.alignOffset
124
- : a.left + a.width / 2 - pw / 2 + this.alignOffset;
156
+ y = a.top - ph - sideOffset;
157
+ x = this.align === 'start' ? a.left + alignOffset
158
+ : this.align === 'end' ? a.right - pw + alignOffset
159
+ : a.left + a.width / 2 - pw / 2 + alignOffset;
125
160
  break;
126
161
  case 'bottom':
127
- y = a.bottom + this.sideOffset;
128
- x = this.align === 'start' ? a.left + this.alignOffset
129
- : this.align === 'end' ? a.right - pw + this.alignOffset
130
- : a.left + a.width / 2 - pw / 2 + this.alignOffset;
162
+ y = a.bottom + sideOffset;
163
+ x = this.align === 'start' ? a.left + alignOffset
164
+ : this.align === 'end' ? a.right - pw + alignOffset
165
+ : a.left + a.width / 2 - pw / 2 + alignOffset;
131
166
  break;
132
167
  case 'left':
133
- x = a.left - pw - this.sideOffset;
134
- y = this.align === 'start' ? a.top + this.alignOffset
135
- : this.align === 'end' ? a.bottom - ph + this.alignOffset
136
- : a.top + a.height / 2 - ph / 2 + this.alignOffset;
168
+ x = a.left - pw - sideOffset;
169
+ y = this.align === 'start' ? a.top + alignOffset
170
+ : this.align === 'end' ? a.bottom - ph + alignOffset
171
+ : a.top + a.height / 2 - ph / 2 + alignOffset;
137
172
  break;
138
173
  case 'right':
139
- x = a.right + this.sideOffset;
140
- y = this.align === 'start' ? a.top + this.alignOffset
141
- : this.align === 'end' ? a.bottom - ph + this.alignOffset
142
- : a.top + a.height / 2 - ph / 2 + this.alignOffset;
174
+ x = a.right + sideOffset;
175
+ y = this.align === 'start' ? a.top + alignOffset
176
+ : this.align === 'end' ? a.bottom - ph + alignOffset
177
+ : a.top + a.height / 2 - ph / 2 + alignOffset;
143
178
  break;
144
179
  }
145
180
 
146
181
  this.pos = {
147
- x: Math.min(Math.max(x, VP_PAD), window.innerWidth - pw - VP_PAD),
148
- y: Math.min(Math.max(y, VP_PAD), window.innerHeight - ph - VP_PAD),
182
+ x: Math.min(Math.max(x, vpPad), window.innerWidth - pw - vpPad),
183
+ y: Math.min(Math.max(y, vpPad), window.innerHeight - ph - vpPad),
149
184
  };
150
185
  }
151
186
 
@@ -1,15 +1,14 @@
1
1
  import { Component, Prop, State, Event, EventEmitter, Element, Watch, h, Host } from '@stencil/core';
2
+ import { resolveCssTimeMs, TOKEN_DEFAULTS } from '../../utils';
2
3
 
3
4
  export type ModalWidth = 'sm' | 'md' | 'lg';
4
5
 
5
6
  const WIDTH_MAP: Record<ModalWidth, string> = {
6
- sm: 'var(--dimension-modal-width-sm, 400px)',
7
- md: 'var(--dimension-modal-width-md, 560px)',
8
- lg: 'var(--dimension-modal-width-lg, 720px)',
7
+ sm: 'var(--dimension-modal-width-sm)',
8
+ md: 'var(--dimension-modal-width-md)',
9
+ lg: 'var(--dimension-modal-width-lg)',
9
10
  };
10
11
 
11
- const CLOSE_ANIMATION_MS = 220;
12
-
13
12
  const FOCUSABLE_SEL = [
14
13
  'a[href]',
15
14
  'area[href]',
@@ -77,7 +76,7 @@ export class Modal {
77
76
  this.closeTimer = null;
78
77
  this.previousFocus?.focus?.();
79
78
  this.previousFocus = null;
80
- }, CLOSE_ANIMATION_MS);
79
+ }, this.closeAnimationMs);
81
80
  }
82
81
  }
83
82
 
@@ -154,6 +153,10 @@ export class Modal {
154
153
  return this.modalWidth as string;
155
154
  }
156
155
 
156
+ private get closeAnimationMs(): number {
157
+ return resolveCssTimeMs(TOKEN_DEFAULTS.motionShort3, TOKEN_DEFAULTS.animationDurationShort3);
158
+ }
159
+
157
160
  render() {
158
161
  // Always render slots so scoped-mode slot distribution doesn't leak content.
159
162
  // The backdrop is hidden via CSS when not active.
@@ -173,7 +176,7 @@ export class Modal {
173
176
  aria-labelledby={this.titleId}
174
177
  aria-describedby={this.subtitle ? this.subtitleId : undefined}
175
178
  tabIndex={-1}
176
- style={{ width: `min(${this.resolvedWidth}, calc(100vw - 32px))` }}
179
+ style={{ width: `min(${this.resolvedWidth}, calc(100vw - 2 * var(--dimension-space-200)))` }}
177
180
  >
178
181
  <div class="modal-header">
179
182
  <h2 id={this.titleId} class="text-title-small modal-title">{this.heading}</h2>
@@ -56,16 +56,14 @@ export class PanelNav {
56
56
  @Prop() activeId: string = '';
57
57
 
58
58
  /** Whether the nav is in collapsed (icon-only) state.
59
- * When `storageKey` is set the component manages this internally
60
- * do not also bind it from outside in that mode. */
59
+ * Set `storageKey` to persist across reloads. `dsNavToggle` still fires on change. */
61
60
  @Prop({ mutable: true }) collapsed: boolean = false;
62
61
 
63
62
  /** Viewport width (px) below which the nav auto-collapses to icon-only mode. 0 = disabled. */
64
63
  @Prop() breakpoint: number = 0;
65
64
 
66
65
  /** `localStorage` key used to persist the collapsed state across page loads.
67
- * When set the component is self-managing for collapsed state; `dsNavToggle`
68
- * still fires for consumers that want to observe the change. */
66
+ * When set, collapsed state is restored on mount and written on each toggle. */
69
67
  @Prop() storageKey: string = '';
70
68
 
71
69
  /** Current route URL (e.g. `window.location.pathname` or the router's active URL).
@@ -321,14 +319,13 @@ export class PanelNav {
321
319
  queueMicrotask(tick);
322
320
  }
323
321
 
324
- /** Centralised toggle: always emits `dsNavToggle`; when `storageKey` is set
325
- * also mutates `collapsed` directly and persists to `localStorage`. */
322
+ /** Centralised toggle: updates `collapsed`, emits `dsNavToggle`, optionally persists. */
326
323
  private applyToggle(next: boolean) {
327
- this.dsNavToggle.emit(next);
324
+ this.collapsed = next;
328
325
  if (this.storageKey) {
329
- this.collapsed = next;
330
326
  try { localStorage.setItem(this.storageKey, String(next)); } catch { /* unavailable */ }
331
327
  }
328
+ this.dsNavToggle.emit(next);
332
329
  }
333
330
 
334
331
  private getAllItems(): PanelNavItem[] {
@@ -1,4 +1,5 @@
1
1
  import { Component, Prop, State, h, Host } from '@stencil/core';
2
+ import { resolveCssTimeMs, TOKEN_DEFAULTS } from '../../utils';
2
3
  import { subscribeToToasts, getToasts, toast as toastService, type ToastData, type ToastIntent } from './toast-service';
3
4
 
4
5
  export type ToastPosition = 'top-center' | 'top-right' | 'bottom-center' | 'bottom-right';
@@ -33,6 +34,10 @@ export class ToastProvider {
33
34
 
34
35
  private unsubscribe: (() => void) | null = null;
35
36
 
37
+ private get fadeOutMs(): number {
38
+ return resolveCssTimeMs(TOKEN_DEFAULTS.motionShort3, TOKEN_DEFAULTS.animationDurationShort3);
39
+ }
40
+
36
41
  connectedCallback() {
37
42
  this.unsubscribe = subscribeToToasts(() => this.syncFromStore());
38
43
  this.syncFromStore();
@@ -80,7 +85,7 @@ export class ToastProvider {
80
85
  setTimeout(() => {
81
86
  this.items = this.items.filter(i => i.data.id !== id);
82
87
  toastService.dismiss(id);
83
- }, 200);
88
+ }, this.fadeOutMs);
84
89
  }
85
90
 
86
91
  private pauseItem(id: string) {
@@ -109,7 +114,7 @@ export class ToastProvider {
109
114
  gap: 'var(--dimension-space-075)',
110
115
  pointerEvents: 'none',
111
116
  padding: 'var(--dimension-space-100)',
112
- maxWidth: '480px',
117
+ maxWidth: `var(${TOKEN_DEFAULTS.panelWidthMd})`,
113
118
  width: '100%',
114
119
  boxSizing: 'border-box',
115
120
  };
@@ -1,3 +1,5 @@
1
+ import { resolveCssTimeMs, TOKEN_DEFAULTS } from '../../utils';
2
+
1
3
  export type ToastIntent = 'neutral' | 'brand' | 'positive' | 'negative' | 'warning' | 'caution';
2
4
 
3
5
  export interface ToastData {
@@ -30,10 +32,15 @@ export function getToasts(): ToastData[] {
30
32
  return toasts;
31
33
  }
32
34
 
35
+ function defaultToastDurationMs(): number {
36
+ return resolveCssTimeMs(TOKEN_DEFAULTS.animationDelayLong2, TOKEN_DEFAULTS.animationDelayLong2);
37
+ }
38
+
33
39
  export const toast = {
34
40
  show(options: ToastOptions): string {
35
41
  const id = `ds-toast-${++idCounter}`;
36
- toasts = [...toasts, { id, intent: 'neutral', duration: 4000, ...options }];
42
+ const duration = options.duration ?? defaultToastDurationMs();
43
+ toasts = [...toasts, { id, intent: 'neutral', ...options, duration }];
37
44
  emit();
38
45
  return id;
39
46
  },
@@ -1,5 +1,6 @@
1
1
  import { Component, Prop, Event, EventEmitter, h, Host } from '@stencil/core';
2
2
  import type { IconSize } from '../Icon/Icon';
3
+ import { resolveCssLengthPx, TOKEN_DEFAULTS } from '../../utils';
3
4
 
4
5
  export type ToggleButtonElevation = 'none' | 'flat' | 'elevated' | 'floating';
5
6
  export type ToggleButtonSize = 'md' | 'sm' | 'xs';
@@ -7,8 +8,11 @@ export type ToggleButtonBackground = 'faint' | 'medium' | 'bold' | 'strong' | 'a
7
8
 
8
9
  const ICON_SIZE: Record<ToggleButtonSize, IconSize> = { md: 'md', sm: 'sm', xs: 'xs' };
9
10
 
10
- // Pixel size reference for internal layout (icon span font-size)
11
- const ICON_PX: Record<ToggleButtonSize, number> = { md: 20, sm: 16, xs: 12 };
11
+ const ICON_TOKEN: Record<ToggleButtonSize, string> = {
12
+ md: TOKEN_DEFAULTS.iconographyMd,
13
+ sm: TOKEN_DEFAULTS.iconographySm,
14
+ xs: TOKEN_DEFAULTS.iconographyXs,
15
+ };
12
16
 
13
17
  const TEXT_CLASS: Record<ToggleButtonSize, string> = {
14
18
  md: 'text-body-medium-emphasis',
@@ -68,7 +72,7 @@ export class ToggleButton {
68
72
  render() {
69
73
  const size = this.size;
70
74
  const iconSize = ICON_SIZE[size];
71
- const iconPx = ICON_PX[size];
75
+ const iconPx = resolveCssLengthPx(ICON_TOKEN[size], ICON_TOKEN[size]);
72
76
  const textClass = TEXT_CLASS[size];
73
77
  const hasIcon = !!this.icon;
74
78
  const hasLabel = !!this.label;