@ds-mo/ui 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +1 -1
  2. package/dist/.build-stamp +1 -1
  3. package/dist/components/ds-banner.js +1 -1
  4. package/dist/components/ds-banner.js.map +1 -1
  5. package/dist/components/ds-bar-nav.js +1 -1
  6. package/dist/components/ds-button-group.js +1 -1
  7. package/dist/components/ds-button.js +1 -1
  8. package/dist/components/ds-loader.js +1 -1
  9. package/dist/components/ds-menu.js +1 -1
  10. package/dist/components/ds-modal.js +1 -1
  11. package/dist/components/ds-modal.js.map +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-select.js +1 -1
  15. package/dist/components/ds-toast-provider.js +1 -1
  16. package/dist/components/ds-toast-provider.js.map +1 -1
  17. package/dist/components/ds-toggle-button-group.js +1 -1
  18. package/dist/components/ds-toggle-button.js +1 -1
  19. package/dist/components/ds-tooltip.js +1 -1
  20. package/dist/components/ds-tooltip.js.map +1 -1
  21. package/dist/components/p-BRju0sEC.js +2 -0
  22. package/dist/components/p-BRju0sEC.js.map +1 -0
  23. package/dist/components/p-BvmooLHV.js +2 -0
  24. package/dist/components/p-BvmooLHV.js.map +1 -0
  25. package/dist/components/p-C4WbCXBj.js +2 -0
  26. package/dist/components/p-C4WbCXBj.js.map +1 -0
  27. package/dist/components/p-CYJ-de0u.js +2 -0
  28. package/dist/components/p-CYJ-de0u.js.map +1 -0
  29. package/dist/components/{p--YDw8Q4o.js → p-CsO880L-.js} +2 -2
  30. package/dist/components/p-DTjpgYJV.js +2 -0
  31. package/dist/components/p-DTjpgYJV.js.map +1 -0
  32. package/dist/components/p-DfL2MOso.js +2 -0
  33. package/dist/components/p-DfL2MOso.js.map +1 -0
  34. package/dist/components/p-DiWQFROF.js +2 -0
  35. package/dist/components/p-DiWQFROF.js.map +1 -0
  36. package/dist/types/components/Banner/Banner.d.ts +2 -0
  37. package/dist/types/components/Loader/Loader.d.ts +3 -2
  38. package/dist/types/components/Menu/Menu.d.ts +11 -2
  39. package/dist/types/components/Modal/Modal.d.ts +1 -0
  40. package/dist/types/components/PanelNav/PanelNav.d.ts +3 -6
  41. package/dist/types/components/Toast/Toast.d.ts +1 -0
  42. package/dist/types/components/Tooltip/Tooltip.d.ts +14 -3
  43. package/dist/types/components.d.ts +42 -103
  44. package/dist/types/nav/shell-view-transition.d.ts +2 -2
  45. package/dist/types/utils/index.d.ts +5 -0
  46. package/dist/types/utils/resolve-css-length-px.d.ts +8 -0
  47. package/dist/types/utils/resolve-css-time-ms.d.ts +8 -0
  48. package/dist/types/utils/token-defaults.d.ts +27 -0
  49. package/package.json +8 -2
  50. package/src/angular/index.ts +0 -1
  51. package/src/angular/proxies.ts +0 -33
  52. package/src/react/components.ts +0 -1
  53. package/src/wc/components/Banner/Banner.tsx +11 -5
  54. package/src/wc/components/Loader/Loader.tsx +8 -3
  55. package/src/wc/components/Menu/Menu.tsx +61 -26
  56. package/src/wc/components/Modal/Modal.tsx +10 -7
  57. package/src/wc/components/PanelNav/PanelNav.tsx +5 -8
  58. package/src/wc/components/Toast/Toast.tsx +7 -2
  59. package/src/wc/components/Toast/toast-service.ts +8 -1
  60. package/src/wc/components/ToggleButton/ToggleButton.tsx +7 -3
  61. package/src/wc/components/Tooltip/Tooltip.tsx +70 -31
  62. package/src/wc/components.d.ts +42 -103
  63. package/src/wc/nav/shell-view-transition.ts +4 -10
  64. package/src/wc/utils/index.ts +4 -0
  65. package/src/wc/utils/resolve-css-length-px.ts +64 -0
  66. package/src/wc/utils/resolve-css-time-ms.ts +46 -0
  67. package/src/wc/utils/token-defaults.ts +48 -0
  68. package/dist/components/ds-sidebar.d.ts +0 -11
  69. package/dist/components/ds-sidebar.js +0 -2
  70. package/dist/components/ds-sidebar.js.map +0 -1
  71. package/dist/components/p-BcuUR5Ao.js +0 -2
  72. package/dist/components/p-BcuUR5Ao.js.map +0 -1
  73. package/dist/components/p-CpbnmEu1.js +0 -2
  74. package/dist/components/p-CpbnmEu1.js.map +0 -1
  75. package/dist/components/p-DznbYg1m.js +0 -2
  76. package/dist/components/p-DznbYg1m.js.map +0 -1
  77. package/dist/components/p-uMN1Ppw2.js +0 -2
  78. package/dist/components/p-uMN1Ppw2.js.map +0 -1
  79. package/dist/types/components/Sidebar/Sidebar.d.ts +0 -29
  80. package/src/react/ds-sidebar.ts +0 -33
  81. package/src/wc/components/Sidebar/Sidebar.tsx +0 -245
  82. /package/dist/components/{p--YDw8Q4o.js.map → p-CsO880L-.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ds-mo/ui",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "CompoMo — composable web components styled with TokoMo design tokens",
5
5
  "keywords": [
6
6
  "web-components",
@@ -45,6 +45,10 @@
45
45
  "types": "./src/wc/nav/index.ts",
46
46
  "import": "./src/wc/nav/index.ts"
47
47
  },
48
+ "./utils": {
49
+ "types": "./src/wc/utils/index.ts",
50
+ "import": "./src/wc/utils/index.ts"
51
+ },
48
52
  "./dist/components/*": "./dist/components/*"
49
53
  },
50
54
  "files": [
@@ -55,6 +59,7 @@
55
59
  "!src/wc/components/**/*.stories.ts",
56
60
  "!src/wc/components/**/*.stories.tsx",
57
61
  "src/wc/nav",
62
+ "src/wc/utils",
58
63
  "src/angular",
59
64
  "src/react"
60
65
  ],
@@ -63,7 +68,8 @@
63
68
  "pretypecheck": "node scripts/generate-icon-catalog.mjs",
64
69
  "pretest": "node scripts/generate-icon-catalog.mjs",
65
70
  "build": "stencil build && node scripts/patch-index-types.mjs && node scripts/verify-icons-externalized.mjs && node scripts/write-build-stamp.mjs",
66
- "test": "node --import tsx/esm --test tests/panel-nav-utils.test.ts tests/shell-view-transition.test.ts tests/shell-gradient.test.ts tests/bar-nav-utils.test.ts tests/bar-nav-tabs-menu-utils.test.ts tests/bar-nav-dom-utils.test.ts tests/icon-catalog.test.ts",
71
+ "verify:pack": "node scripts/verify-npm-pack.mjs && node scripts/verify-nav-import.mjs",
72
+ "test": "node --import tsx/esm --test tests/panel-nav-utils.test.ts tests/shell-view-transition.test.ts tests/shell-gradient.test.ts tests/resolve-css-length-px.test.ts tests/resolve-css-time-ms.test.ts tests/overlay-positioning.test.ts tests/bar-nav-utils.test.ts tests/bar-nav-tabs-menu-utils.test.ts tests/bar-nav-dom-utils.test.ts tests/icon-catalog.test.ts",
67
73
  "test:e2e": "npm run build && playwright test",
68
74
  "test:e2e:install": "playwright install chromium",
69
75
  "build:docs": "stencil build --docs",
@@ -29,7 +29,6 @@ export const DIRECTIVES = [
29
29
  d.DsRadioGroup,
30
30
  d.DsScrollbar,
31
31
  d.DsSelect,
32
- d.DsSidebar,
33
32
  d.DsSkeleton,
34
33
  d.DsSlider,
35
34
  d.DsSurface,
@@ -767,39 +767,6 @@ export declare interface DsSelect extends Components.DsSelect {
767
767
  }
768
768
 
769
769
 
770
- @ProxyCmp({
771
- inputs: ['collapsed', 'mobile', 'resizable', 'width']
772
- })
773
- @Component({
774
- selector: 'ds-sidebar',
775
- changeDetection: ChangeDetectionStrategy.OnPush,
776
- template: '<ng-content></ng-content>',
777
- // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
778
- inputs: ['collapsed', 'mobile', 'resizable', 'width'],
779
- outputs: ['dsToggle', 'dsWidthChange'],
780
- standalone: false
781
- })
782
- export class DsSidebar {
783
- protected el: HTMLDsSidebarElement;
784
- @Output() dsToggle = new EventEmitter<CustomEvent<void>>();
785
- @Output() dsWidthChange = new EventEmitter<CustomEvent<IDsSidebarSidebarWidth>>();
786
- constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
787
- c.detach();
788
- this.el = r.nativeElement;
789
- }
790
- }
791
-
792
-
793
- import type { SidebarWidth as IDsSidebarSidebarWidth } from '@ds-mo/ui';
794
-
795
- export declare interface DsSidebar extends Components.DsSidebar {
796
-
797
- dsToggle: EventEmitter<CustomEvent<void>>;
798
-
799
- dsWidthChange: EventEmitter<CustomEvent<IDsSidebarSidebarWidth>>;
800
- }
801
-
802
-
803
770
  @ProxyCmp({
804
771
  inputs: ['height', 'lines', 'shimmer', 'variant', 'width']
805
772
  })
@@ -31,7 +31,6 @@ export { DsPanelNav } from "./ds-panel-nav.js";
31
31
  export { DsRadioGroup } from "./ds-radio-group.js";
32
32
  export { DsScrollbar } from "./ds-scrollbar.js";
33
33
  export { DsSelect } from "./ds-select.js";
34
- export { DsSidebar } from "./ds-sidebar.js";
35
34
  export { DsSkeleton } from "./ds-skeleton.js";
36
35
  export { DsSlider } from "./ds-slider.js";
37
36
  export { DsSurface } from "./ds-surface.js";
@@ -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;
@@ -1,13 +1,9 @@
1
1
  import { Component, Prop, State, Element, Watch, h, Host } from '@stencil/core';
2
+ import { resolveCssLengthPx, resolveCssTimeMs, TOKEN_DEFAULTS } from '../../utils';
2
3
 
3
4
  export type TooltipSide = 'top' | 'right' | 'bottom' | 'left';
4
5
  export type TooltipAlign = 'start' | 'center' | 'end';
5
6
 
6
- const HOVER_DELAY_MS = 600;
7
- const INSTANT_REOPEN_MS = 300;
8
- const FADE_OUT_MS = 200;
9
- const VP_PAD = 4;
10
-
11
7
  let lastDismissedAt = 0;
12
8
  let tooltipIdCounter = 0;
13
9
 
@@ -22,9 +18,12 @@ export class Tooltip {
22
18
  @Prop() label!: string;
23
19
  @Prop() side: TooltipSide = 'top';
24
20
  @Prop() align: TooltipAlign = 'center';
25
- @Prop() sideOffset: number = 4;
26
- @Prop() alignOffset: number = 0;
27
- @Prop() delay: number = HOVER_DELAY_MS;
21
+ /** Gap between anchor and tooltip — number (px) or TokoMo length. */
22
+ @Prop() sideOffset: number | string = TOKEN_DEFAULTS.space050;
23
+ /** Cross-axis offset — number (px) or TokoMo length. */
24
+ @Prop() alignOffset: number | string = 0;
25
+ /** Show delay — number (ms) or TokoMo time token / `var(--effect-animation-delay-*)`. */
26
+ @Prop() delay: number | string = TOKEN_DEFAULTS.animationDelayMedium1;
28
27
  @Prop() shortcutKey: string | undefined;
29
28
  @Prop() shortcutKeyPosition: 'start' | 'end' = 'end';
30
29
 
@@ -54,10 +53,47 @@ export class Tooltip {
54
53
  @Watch('label')
55
54
  @Watch('side')
56
55
  @Watch('align')
56
+ @Watch('sideOffset')
57
+ @Watch('alignOffset')
57
58
  onPositionPropsChange() {
58
59
  if (this.visible) this.calculatePosition();
59
60
  }
60
61
 
62
+ private get viewportPadPx(): number {
63
+ return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);
64
+ }
65
+
66
+ private get sideOffsetPx(): number {
67
+ return resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050);
68
+ }
69
+
70
+ private get alignOffsetPx(): number {
71
+ return resolveCssLengthPx(this.alignOffset, 0);
72
+ }
73
+
74
+ private get hoverDelayMs(): number {
75
+ return resolveCssTimeMs(this.delay, TOKEN_DEFAULTS.animationDelayMedium1);
76
+ }
77
+
78
+ private get instantReopenMs(): number {
79
+ return resolveCssTimeMs(
80
+ TOKEN_DEFAULTS.animationDurationMedium1,
81
+ TOKEN_DEFAULTS.animationDurationMedium1,
82
+ );
83
+ }
84
+
85
+ private get fadeOutMs(): number {
86
+ return resolveCssTimeMs(TOKEN_DEFAULTS.motionShort3, TOKEN_DEFAULTS.animationDurationShort3);
87
+ }
88
+
89
+ private get tooltipFallbackWidthPx(): number {
90
+ return resolveCssLengthPx(TOKEN_DEFAULTS.tooltipFallbackWidth, TOKEN_DEFAULTS.tooltipFallbackWidth);
91
+ }
92
+
93
+ private get tooltipFallbackHeightPx(): number {
94
+ return resolveCssLengthPx(TOKEN_DEFAULTS.size300, TOKEN_DEFAULTS.size300);
95
+ }
96
+
61
97
  private bindAnchor() {
62
98
  const slot = this.el.querySelector('slot') as HTMLSlotElement | null;
63
99
  const slotted = slot?.assignedElements?.() ?? [];
@@ -88,7 +124,7 @@ export class Tooltip {
88
124
 
89
125
  private show() {
90
126
  this.clearTimers();
91
- const instant = Date.now() - lastDismissedAt < INSTANT_REOPEN_MS;
127
+ const instant = Date.now() - lastDismissedAt < this.instantReopenMs;
92
128
  if (instant) {
93
129
  this.closing = false;
94
130
  this.visible = true;
@@ -100,7 +136,7 @@ export class Tooltip {
100
136
  this.closing = false;
101
137
  this.visible = true;
102
138
  requestAnimationFrame(() => this.calculatePosition());
103
- }, this.delay);
139
+ }, this.hoverDelayMs);
104
140
  }
105
141
 
106
142
  private hide() {
@@ -112,7 +148,7 @@ export class Tooltip {
112
148
  this.closeTimer = null;
113
149
  this.visible = false;
114
150
  this.closing = false;
115
- }, FADE_OUT_MS);
151
+ }, this.fadeOutMs);
116
152
  }
117
153
 
118
154
  private calculatePosition() {
@@ -120,40 +156,43 @@ export class Tooltip {
120
156
  const tip = this.el.querySelector('.tooltip-popup') as HTMLElement | null;
121
157
  if (!tip) return;
122
158
  const a = this.anchor.getBoundingClientRect();
123
- const tw = tip.offsetWidth || 80;
124
- const th = tip.offsetHeight || 24;
159
+ const tw = tip.offsetWidth || this.tooltipFallbackWidthPx;
160
+ const th = tip.offsetHeight || this.tooltipFallbackHeightPx;
161
+ const sideOffset = this.sideOffsetPx;
162
+ const alignOffset = this.alignOffsetPx;
163
+ const vpPad = this.viewportPadPx;
125
164
  let x = 0, y = 0;
126
165
 
127
166
  switch (this.side) {
128
167
  case 'top':
129
- y = a.top - th - this.sideOffset;
130
- x = this.align === 'start' ? a.left + this.alignOffset
131
- : this.align === 'end' ? a.right - tw + this.alignOffset
132
- : a.left + a.width / 2 - tw / 2 + this.alignOffset;
168
+ y = a.top - th - sideOffset;
169
+ x = this.align === 'start' ? a.left + alignOffset
170
+ : this.align === 'end' ? a.right - tw + alignOffset
171
+ : a.left + a.width / 2 - tw / 2 + alignOffset;
133
172
  break;
134
173
  case 'bottom':
135
- y = a.bottom + this.sideOffset;
136
- x = this.align === 'start' ? a.left + this.alignOffset
137
- : this.align === 'end' ? a.right - tw + this.alignOffset
138
- : a.left + a.width / 2 - tw / 2 + this.alignOffset;
174
+ y = a.bottom + sideOffset;
175
+ x = this.align === 'start' ? a.left + alignOffset
176
+ : this.align === 'end' ? a.right - tw + alignOffset
177
+ : a.left + a.width / 2 - tw / 2 + alignOffset;
139
178
  break;
140
179
  case 'left':
141
- x = a.left - tw - this.sideOffset;
142
- y = this.align === 'start' ? a.top + this.alignOffset
143
- : this.align === 'end' ? a.bottom - th + this.alignOffset
144
- : a.top + a.height / 2 - th / 2 + this.alignOffset;
180
+ x = a.left - tw - sideOffset;
181
+ y = this.align === 'start' ? a.top + alignOffset
182
+ : this.align === 'end' ? a.bottom - th + alignOffset
183
+ : a.top + a.height / 2 - th / 2 + alignOffset;
145
184
  break;
146
185
  case 'right':
147
- x = a.right + this.sideOffset;
148
- y = this.align === 'start' ? a.top + this.alignOffset
149
- : this.align === 'end' ? a.bottom - th + this.alignOffset
150
- : a.top + a.height / 2 - th / 2 + this.alignOffset;
186
+ x = a.right + sideOffset;
187
+ y = this.align === 'start' ? a.top + alignOffset
188
+ : this.align === 'end' ? a.bottom - th + alignOffset
189
+ : a.top + a.height / 2 - th / 2 + alignOffset;
151
190
  break;
152
191
  }
153
192
 
154
193
  this.pos = {
155
- x: Math.min(Math.max(x, VP_PAD), window.innerWidth - tw - VP_PAD),
156
- y: Math.min(Math.max(y, VP_PAD), window.innerHeight - th - VP_PAD),
194
+ x: Math.min(Math.max(x, vpPad), window.innerWidth - tw - vpPad),
195
+ y: Math.min(Math.max(y, vpPad), window.innerHeight - th - vpPad),
157
196
  };
158
197
  }
159
198