@ds-mo/ui 1.0.2 → 1.2.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/README.md +0 -1
  2. package/dist/components/ds-bar-nav-action.js +1 -1
  3. package/dist/components/ds-bar-nav.js +1 -1
  4. package/dist/components/ds-bar-nav.js.map +1 -1
  5. package/dist/components/ds-breadcrumb.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-card.js +1 -1
  9. package/dist/components/ds-card.js.map +1 -1
  10. package/dist/components/ds-checkbox.js +1 -1
  11. package/dist/components/ds-divider.js +1 -1
  12. package/dist/components/ds-divider.js.map +1 -1
  13. package/dist/components/ds-empty-state.js +1 -1
  14. package/dist/components/ds-empty-state.js.map +1 -1
  15. package/dist/components/ds-fade.js +1 -1
  16. package/dist/components/ds-field.js +1 -1
  17. package/dist/components/ds-header.js +1 -1
  18. package/dist/components/ds-header.js.map +1 -1
  19. package/dist/components/ds-icon.js +1 -1
  20. package/dist/components/ds-input.js +1 -1
  21. package/dist/components/ds-menu.js +1 -1
  22. package/dist/components/ds-panel-nav.js +1 -1
  23. package/dist/components/ds-select.js +1 -1
  24. package/dist/components/ds-slider.js +1 -1
  25. package/dist/components/{ds-label-wrap.d.ts → ds-tab-group-nav.d.ts} +4 -4
  26. package/dist/components/ds-tab-group-nav.js +2 -0
  27. package/dist/components/ds-tab-group-nav.js.map +1 -0
  28. package/dist/components/ds-tab-group.js +1 -1
  29. package/dist/components/ds-tab-group.js.map +1 -1
  30. package/dist/components/ds-table.js +1 -1
  31. package/dist/components/ds-table.js.map +1 -1
  32. package/dist/components/ds-tag.js +1 -1
  33. package/dist/components/ds-text.js +1 -1
  34. package/dist/components/ds-toast-provider.js +1 -1
  35. package/dist/components/ds-toggle-button-group.js +1 -1
  36. package/dist/components/ds-toggle-button.js +1 -1
  37. package/dist/components/ds-toggle.js +1 -1
  38. package/dist/components/ds-tooltip.js +1 -1
  39. package/dist/components/p-BQq26pt9.js +2 -0
  40. package/dist/components/p-BQq26pt9.js.map +1 -0
  41. package/dist/components/{p-DouEVPmi.js → p-CE5zeV1w.js} +2 -2
  42. package/dist/components/{p-D3CwGoIu.js → p-COsKOwZR.js} +2 -2
  43. package/dist/components/p-CQQe6VNo.js +2 -0
  44. package/dist/components/p-CQQe6VNo.js.map +1 -0
  45. package/dist/components/p-Cj00JkJb.js +2 -0
  46. package/dist/components/p-Cj00JkJb.js.map +1 -0
  47. package/dist/components/p-CpbnmEu1.js +2 -0
  48. package/dist/components/p-CpbnmEu1.js.map +1 -0
  49. package/dist/components/{p-BvxKLqNx.js → p-H89PzqGU.js} +2 -2
  50. package/dist/components/{p-BYe8Rfdg.js → p-Vwlmhl4E.js} +2 -2
  51. package/dist/types/.stencil/BarNav/BarNav.d.ts +7 -2
  52. package/dist/types/.stencil/BarNav/bar-nav-dom-utils.d.ts +1 -2
  53. package/dist/types/.stencil/Divider/Divider.d.ts +14 -0
  54. package/dist/types/.stencil/TabGroupNav/TabGroupNav.d.ts +27 -0
  55. package/dist/types/.stencil/Text/Text.d.ts +2 -0
  56. package/dist/types/components.d.ts +125 -63
  57. package/package.json +37 -7
  58. package/src/angular/index.ts +1 -1
  59. package/src/angular/proxies.ts +32 -27
  60. package/src/react/components.ts +45 -0
  61. package/src/react/ds-accordion.ts +27 -0
  62. package/src/react/ds-badge.ts +26 -0
  63. package/src/react/ds-banner.ts +27 -0
  64. package/src/react/ds-bar-nav-action.ts +27 -0
  65. package/src/react/ds-bar-nav.ts +33 -0
  66. package/src/react/ds-breadcrumb.ts +27 -0
  67. package/src/react/ds-button-group.ts +27 -0
  68. package/src/react/ds-button.ts +35 -0
  69. package/src/react/ds-card.ts +26 -0
  70. package/src/react/ds-checkbox.ts +27 -0
  71. package/src/react/ds-divider.ts +26 -0
  72. package/src/react/ds-empty-state.ts +26 -0
  73. package/src/react/ds-fade.ts +26 -0
  74. package/src/react/ds-field.ts +26 -0
  75. package/src/react/ds-header.ts +26 -0
  76. package/src/react/ds-icon.ts +26 -0
  77. package/src/react/ds-input.ts +33 -0
  78. package/src/react/ds-loader.ts +26 -0
  79. package/src/react/ds-menu.ts +33 -0
  80. package/src/react/ds-modal.ts +27 -0
  81. package/src/react/ds-pagination.ts +27 -0
  82. package/src/react/ds-panel-nav.ts +37 -0
  83. package/src/react/ds-radio-group.ts +27 -0
  84. package/src/react/ds-scrollbar.ts +26 -0
  85. package/src/react/ds-select.ts +27 -0
  86. package/src/react/ds-sidebar.ts +33 -0
  87. package/src/react/ds-skeleton.ts +26 -0
  88. package/src/react/ds-slider.ts +27 -0
  89. package/src/react/ds-surface.ts +26 -0
  90. package/src/react/ds-tab-group-nav.ts +27 -0
  91. package/src/react/ds-tab-group.ts +27 -0
  92. package/src/react/ds-table.ts +35 -0
  93. package/src/react/ds-tag.ts +35 -0
  94. package/src/react/ds-text.ts +26 -0
  95. package/src/react/ds-toast-provider.ts +26 -0
  96. package/src/react/ds-toggle-button-group.ts +27 -0
  97. package/src/react/ds-toggle-button.ts +27 -0
  98. package/src/react/ds-toggle.ts +27 -0
  99. package/src/react/ds-tooltip.ts +26 -0
  100. package/src/wc/components/Accordion/Accordion.tsx +144 -0
  101. package/src/wc/components/Badge/Badge.tsx +34 -0
  102. package/src/wc/components/Badge/index.ts +1 -0
  103. package/src/wc/components/Banner/Banner.tsx +148 -0
  104. package/src/wc/components/BarNav/BarNav.tsx +605 -0
  105. package/src/wc/components/BarNav/bar-nav-dom-utils.ts +23 -0
  106. package/src/wc/components/BarNav/bar-nav-tabs-menu-utils.ts +59 -0
  107. package/src/wc/components/BarNav/bar-nav-utils.ts +76 -0
  108. package/src/wc/components/BarNav/index.ts +1 -0
  109. package/src/wc/components/BarNavAction/BarNavAction.tsx +67 -0
  110. package/src/wc/components/BarNavAction/index.ts +1 -0
  111. package/src/wc/components/Breadcrumb/Breadcrumb.tsx +61 -0
  112. package/src/wc/components/Button/Button.tsx +158 -0
  113. package/src/wc/components/Button/index.ts +1 -0
  114. package/src/wc/components/ButtonGroup/ButtonGroup.tsx +110 -0
  115. package/src/wc/components/Card/Card.tsx +26 -0
  116. package/src/wc/components/Checkbox/Checkbox.tsx +60 -0
  117. package/src/wc/components/Checkbox/index.ts +1 -0
  118. package/src/wc/components/Divider/Divider.tsx +107 -0
  119. package/src/wc/components/Divider/index.ts +2 -0
  120. package/src/wc/components/EmptyState/EmptyState.tsx +32 -0
  121. package/src/wc/components/Fade/Fade.tsx +32 -0
  122. package/src/wc/components/Fade/index.ts +1 -0
  123. package/src/wc/components/Field/Field.tsx +28 -0
  124. package/src/wc/components/Header/Header.tsx +33 -0
  125. package/src/wc/components/Icon/Icon.tsx +112 -0
  126. package/src/wc/components/Icon/flag-icon-catalog.ts +68 -0
  127. package/src/wc/components/Icon/index.ts +1 -0
  128. package/src/wc/components/Icon/system-icon-catalog.ts +798 -0
  129. package/src/wc/components/Input/Input.tsx +100 -0
  130. package/src/wc/components/Input/index.ts +1 -0
  131. package/src/wc/components/Loader/Loader.tsx +47 -0
  132. package/src/wc/components/Loader/index.ts +1 -0
  133. package/src/wc/components/Menu/Menu.tsx +340 -0
  134. package/src/wc/components/Modal/Modal.tsx +197 -0
  135. package/src/wc/components/Pagination/Pagination.tsx +97 -0
  136. package/src/wc/components/PanelNav/PanelNav.tsx +666 -0
  137. package/src/wc/components/PanelNav/index.ts +7 -0
  138. package/src/wc/components/PanelNav/panel-nav-utils.ts +124 -0
  139. package/src/wc/components/RadioGroup/RadioGroup.tsx +128 -0
  140. package/src/wc/components/RadioGroup/index.ts +1 -0
  141. package/src/wc/components/Scrollbar/Scrollbar.tsx +189 -0
  142. package/src/wc/components/Select/Select.tsx +195 -0
  143. package/src/wc/components/Sidebar/Sidebar.tsx +245 -0
  144. package/src/wc/components/Skeleton/Skeleton.tsx +64 -0
  145. package/src/wc/components/Skeleton/index.ts +1 -0
  146. package/src/wc/components/Slider/Slider.tsx +68 -0
  147. package/src/wc/components/Slider/index.ts +1 -0
  148. package/src/wc/components/Surface/Surface.tsx +81 -0
  149. package/src/wc/components/TabGroup/TabGroup.tsx +188 -0
  150. package/src/wc/components/TabGroup/tab-item-utils.ts +24 -0
  151. package/src/wc/components/TabGroupNav/TabGroupNav.tsx +188 -0
  152. package/src/wc/components/Table/Table.tsx +268 -0
  153. package/src/wc/components/Tag/Tag.tsx +135 -0
  154. package/src/wc/components/Tag/index.ts +1 -0
  155. package/src/wc/components/Text/Text.tsx +103 -0
  156. package/src/wc/components/Text/index.ts +1 -0
  157. package/src/wc/components/Toast/Toast.tsx +174 -0
  158. package/src/wc/components/Toast/toast-service.ts +60 -0
  159. package/src/wc/components/Toggle/Toggle.tsx +43 -0
  160. package/src/wc/components/Toggle/index.ts +1 -0
  161. package/src/wc/components/ToggleButton/ToggleButton.tsx +120 -0
  162. package/src/wc/components/ToggleButtonGroup/ToggleButtonGroup.tsx +118 -0
  163. package/src/wc/components/Tooltip/Tooltip.tsx +196 -0
  164. package/src/wc/components.d.ts +3022 -0
  165. package/dist/components/ds-label-wrap.js +0 -2
  166. package/dist/components/ds-label-wrap.js.map +0 -1
  167. package/dist/components/p-C4p1fG67.js +0 -2
  168. package/dist/components/p-C4p1fG67.js.map +0 -1
  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/types/.stencil/LabelWrap/LabelWrap.d.ts +0 -15
  174. /package/dist/components/{p-DouEVPmi.js.map → p-CE5zeV1w.js.map} +0 -0
  175. /package/dist/components/{p-D3CwGoIu.js.map → p-COsKOwZR.js.map} +0 -0
  176. /package/dist/components/{p-BvxKLqNx.js.map → p-H89PzqGU.js.map} +0 -0
  177. /package/dist/components/{p-BYe8Rfdg.js.map → p-Vwlmhl4E.js.map} +0 -0
@@ -0,0 +1,103 @@
1
+ import { Component, Prop, h, Host } from '@stencil/core';
2
+
3
+ export type TextVariant =
4
+ | 'text-display-medium'
5
+ | 'text-display-small'
6
+ | 'text-title-large'
7
+ | 'text-title-medium'
8
+ | 'text-title-small'
9
+ | 'text-body-large'
10
+ | 'text-body-large-emphasis'
11
+ | 'text-body-medium'
12
+ | 'text-body-medium-emphasis'
13
+ | 'text-body-small'
14
+ | 'text-body-small-emphasis'
15
+ | 'text-caption'
16
+ | 'text-caption-emphasis';
17
+
18
+ export type TextColorToken =
19
+ | 'primary' | 'secondary' | 'tertiary'
20
+ | 'brand' | 'negative' | 'positive' | 'warning' | 'caution' | 'ai'
21
+ | 'on-strong' | 'on-bold' | 'inherit';
22
+
23
+ export type TextColor = TextColorToken | `var(--${string})`;
24
+ export type TextDecoration = 'none' | 'underline' | 'dotted-underline';
25
+ export type TextAlign = 'left' | 'center' | 'right';
26
+ export type LineTruncation = 1 | 2 | 3 | 4 | 5 | 'none';
27
+ export type TextWrap = 'wrap' | 'nowrap' | 'balance' | 'pretty';
28
+ export type TextElement = 'p' | 'span' | 'div' | 'label' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
29
+ export type TextFontFeature = 'normal' | 'tabular-nums';
30
+
31
+ @Component({
32
+ tag: 'ds-text',
33
+ styleUrl: 'Text.css',
34
+ scoped: true,
35
+ })
36
+ export class Text {
37
+ @Prop() variant: TextVariant = 'text-body-medium';
38
+ @Prop() color: TextColor | undefined;
39
+ @Prop() decoration: TextDecoration | undefined;
40
+ @Prop() italic: boolean = false;
41
+ @Prop() align: TextAlign | undefined;
42
+ @Prop() lineTruncation: LineTruncation = 'none';
43
+ @Prop() wrap: TextWrap | undefined;
44
+ @Prop() fontFeature: TextFontFeature = 'normal';
45
+ @Prop() as: TextElement = 'p';
46
+ /** Maps to `for` attribute on <label> elements. */
47
+ @Prop() for: string | undefined;
48
+
49
+ render() {
50
+ // Polymorphic element: TSX treats a variable tag as a component, so a
51
+ // dynamic intrinsic element name must be cast to `any` to render.
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ const Tag = this.as as any;
54
+ const isCustomColor = (c: TextColor) => typeof c === 'string' && c.startsWith('var(--');
55
+
56
+ const cls: Record<string, boolean> = {
57
+ [this.variant]: true,
58
+ 'text': true,
59
+ 'text--nowrap': this.wrap === 'nowrap',
60
+ 'text--balance': this.wrap === 'balance',
61
+ 'text--pretty': this.wrap === 'pretty',
62
+ 'text--truncate-1': this.lineTruncation === 1 && this.wrap !== 'nowrap',
63
+ 'text--truncate-2': this.lineTruncation === 2 && this.wrap !== 'nowrap',
64
+ 'text--truncate-3': this.lineTruncation === 3 && this.wrap !== 'nowrap',
65
+ 'text--truncate-4': this.lineTruncation === 4 && this.wrap !== 'nowrap',
66
+ 'text--truncate-5': this.lineTruncation === 5 && this.wrap !== 'nowrap',
67
+ 'text--color-primary': this.color === 'primary',
68
+ 'text--color-secondary': this.color === 'secondary',
69
+ 'text--color-tertiary': this.color === 'tertiary',
70
+ 'text--color-brand': this.color === 'brand',
71
+ 'text--color-negative': this.color === 'negative',
72
+ 'text--color-positive': this.color === 'positive',
73
+ 'text--color-warning': this.color === 'warning',
74
+ 'text--color-caution': this.color === 'caution',
75
+ 'text--color-ai': this.color === 'ai',
76
+ 'text--color-on-strong': this.color === 'on-strong',
77
+ 'text--color-on-bold': this.color === 'on-bold',
78
+ 'text--color-inherit': this.color === 'inherit',
79
+ 'text--decoration-underline': this.decoration === 'underline',
80
+ 'text--decoration-dotted-underline': this.decoration === 'dotted-underline',
81
+ 'text--italic': this.italic,
82
+ 'text--align-left': this.align === 'left',
83
+ 'text--align-center': this.align === 'center',
84
+ 'text--align-right': this.align === 'right',
85
+ 'text--font-feature-tabular-nums': this.fontFeature === 'tabular-nums',
86
+ };
87
+
88
+ const style = this.color && isCustomColor(this.color)
89
+ ? { color: this.color as string }
90
+ : undefined;
91
+
92
+ const extraProps: Record<string, string> = {};
93
+ if (this.as === 'label' && this.for) extraProps['for'] = this.for;
94
+
95
+ return (
96
+ <Host style={{ display: 'contents' }}>
97
+ <Tag class={cls} style={style} {...extraProps}>
98
+ <slot />
99
+ </Tag>
100
+ </Host>
101
+ );
102
+ }
103
+ }
@@ -0,0 +1 @@
1
+ export type { TextVariant, TextColor, TextColorToken, TextDecoration, TextAlign, LineTruncation, TextWrap, TextElement } from './Text';
@@ -0,0 +1,174 @@
1
+ import { Component, Prop, State, h, Host } from '@stencil/core';
2
+ import { subscribeToToasts, getToasts, toast as toastService, type ToastData, type ToastIntent } from './toast-service';
3
+
4
+ export type ToastPosition = 'top-center' | 'top-right' | 'bottom-center' | 'bottom-right';
5
+
6
+ const INTENT_BG: Record<ToastIntent, string> = {
7
+ neutral: 'var(--color-background-bold-neutral)',
8
+ brand: 'var(--color-background-bold-brand)',
9
+ positive: 'var(--color-background-bold-positive)',
10
+ negative: 'var(--color-background-bold-negative)',
11
+ warning: 'var(--color-background-bold-warning)',
12
+ caution: 'var(--color-background-bold-caution)',
13
+ };
14
+
15
+ interface ToastItemState {
16
+ data: ToastData;
17
+ exiting: boolean;
18
+ timer: ReturnType<typeof setTimeout> | null;
19
+ remaining: number;
20
+ startedAt: number;
21
+ paused: boolean;
22
+ }
23
+
24
+ @Component({
25
+ tag: 'ds-toast-provider',
26
+ styleUrl: 'Toast.css',
27
+ scoped: true,
28
+ })
29
+ export class ToastProvider {
30
+ @Prop() position: ToastPosition = 'top-center';
31
+
32
+ @State() private items: ToastItemState[] = [];
33
+
34
+ private unsubscribe: (() => void) | null = null;
35
+
36
+ connectedCallback() {
37
+ this.unsubscribe = subscribeToToasts(() => this.syncFromStore());
38
+ this.syncFromStore();
39
+ }
40
+
41
+ disconnectedCallback() {
42
+ this.items.forEach(item => { if (item.timer) clearTimeout(item.timer); });
43
+ this.unsubscribe?.();
44
+ }
45
+
46
+ private syncFromStore() {
47
+ const storeToasts = getToasts();
48
+ const existingIds = new Set(this.items.map(i => i.data.id));
49
+ const storeIds = new Set(storeToasts.map(t => t.id));
50
+
51
+ // Remove items no longer in store (instant, no animation since dismiss() already fires animation)
52
+ const kept = this.items.filter(i => storeIds.has(i.data.id));
53
+
54
+ // Add new items
55
+ const newItems: ToastItemState[] = storeToasts
56
+ .filter(t => !existingIds.has(t.id))
57
+ .map(data => {
58
+ const state: ToastItemState = {
59
+ data,
60
+ exiting: false,
61
+ timer: null,
62
+ remaining: data.duration,
63
+ startedAt: 0,
64
+ paused: false,
65
+ };
66
+ if (data.duration > 0) {
67
+ state.startedAt = Date.now();
68
+ state.timer = setTimeout(() => this.dismissItem(data.id), data.duration);
69
+ }
70
+ return state;
71
+ });
72
+
73
+ this.items = [...kept, ...newItems];
74
+ }
75
+
76
+ private dismissItem(id: string) {
77
+ this.items = this.items.map(i =>
78
+ i.data.id === id ? { ...i, exiting: true, timer: null } : i
79
+ );
80
+ setTimeout(() => {
81
+ this.items = this.items.filter(i => i.data.id !== id);
82
+ toastService.dismiss(id);
83
+ }, 200);
84
+ }
85
+
86
+ private pauseItem(id: string) {
87
+ this.items = this.items.map(i => {
88
+ if (i.data.id !== id || i.paused || !i.timer) return i;
89
+ clearTimeout(i.timer);
90
+ const elapsed = Date.now() - i.startedAt;
91
+ return { ...i, paused: true, timer: null, remaining: Math.max(0, i.remaining - elapsed) };
92
+ });
93
+ }
94
+
95
+ private resumeItem(id: string) {
96
+ this.items = this.items.map(i => {
97
+ if (i.data.id !== id || !i.paused || i.remaining <= 0) return i;
98
+ const timer = setTimeout(() => this.dismissItem(id), i.remaining);
99
+ return { ...i, paused: false, timer, startedAt: Date.now() };
100
+ });
101
+ }
102
+
103
+ private getPositionStyle(): Record<string, string> {
104
+ const base: Record<string, string> = {
105
+ position: 'fixed',
106
+ zIndex: '9999',
107
+ display: 'flex',
108
+ flexDirection: 'column',
109
+ gap: 'var(--dimension-space-075)',
110
+ pointerEvents: 'none',
111
+ padding: 'var(--dimension-space-100)',
112
+ maxWidth: '480px',
113
+ width: '100%',
114
+ boxSizing: 'border-box',
115
+ };
116
+ switch (this.position) {
117
+ case 'top-center':
118
+ return { ...base, top: '0', left: '50%', transform: 'translateX(-50%)', alignItems: 'center' };
119
+ case 'top-right':
120
+ return { ...base, top: '0', right: '0', alignItems: 'flex-end' };
121
+ case 'bottom-center':
122
+ return { ...base, bottom: '0', left: '50%', transform: 'translateX(-50%)', alignItems: 'center' };
123
+ case 'bottom-right':
124
+ return { ...base, bottom: '0', right: '0', alignItems: 'flex-end' };
125
+ }
126
+ }
127
+
128
+ render() {
129
+ return (
130
+ <Host style={{ display: 'contents' }}>
131
+ <div
132
+ class="toast-container"
133
+ role="region"
134
+ aria-label="Notifications"
135
+ aria-live="polite"
136
+ style={this.getPositionStyle()}
137
+ >
138
+ {this.items.map(item => (
139
+ <div
140
+ key={item.data.id}
141
+ class={{ toast: true, 'toast--exit': item.exiting }}
142
+ role={item.data.intent === 'negative' ? 'alert' : 'status'}
143
+ style={{
144
+ backgroundColor: INTENT_BG[item.data.intent],
145
+ borderRadius: 'var(--dimension-radius-125)',
146
+ boxShadow: 'var(--effect-elevation-elevated-floating)',
147
+ display: 'flex',
148
+ alignItems: 'center',
149
+ gap: 'var(--dimension-space-075)',
150
+ padding: 'var(--dimension-space-075) var(--dimension-space-100)',
151
+ minHeight: 'var(--dimension-size-400)',
152
+ pointerEvents: 'auto',
153
+ color: 'var(--color-foreground-on-bold-background-primary)',
154
+ boxSizing: 'border-box',
155
+ }}
156
+ onMouseEnter={() => this.pauseItem(item.data.id)}
157
+ onMouseLeave={() => this.resumeItem(item.data.id)}
158
+ >
159
+ <span class="text-body-medium" style={{ flex: '1' }}>{item.data.message}</span>
160
+ <button
161
+ type="button"
162
+ class="toast-dismiss"
163
+ onClick={() => this.dismissItem(item.data.id)}
164
+ aria-label="Dismiss"
165
+ >
166
+ &times;
167
+ </button>
168
+ </div>
169
+ ))}
170
+ </div>
171
+ </Host>
172
+ );
173
+ }
174
+ }
@@ -0,0 +1,60 @@
1
+ export type ToastIntent = 'neutral' | 'brand' | 'positive' | 'negative' | 'warning' | 'caution';
2
+
3
+ export interface ToastData {
4
+ id: string;
5
+ message: string;
6
+ intent: ToastIntent;
7
+ duration: number;
8
+ }
9
+
10
+ export interface ToastOptions {
11
+ message: string;
12
+ intent?: ToastIntent;
13
+ duration?: number;
14
+ }
15
+
16
+ let toasts: ToastData[] = [];
17
+ const listeners = new Set<() => void>();
18
+ let idCounter = 0;
19
+
20
+ function emit() {
21
+ listeners.forEach(fn => fn());
22
+ }
23
+
24
+ export function subscribeToToasts(fn: () => void): () => void {
25
+ listeners.add(fn);
26
+ return () => listeners.delete(fn);
27
+ }
28
+
29
+ export function getToasts(): ToastData[] {
30
+ return toasts;
31
+ }
32
+
33
+ export const toast = {
34
+ show(options: ToastOptions): string {
35
+ const id = `ds-toast-${++idCounter}`;
36
+ toasts = [...toasts, { id, intent: 'neutral', duration: 4000, ...options }];
37
+ emit();
38
+ return id;
39
+ },
40
+ success(message: string, options?: Partial<ToastOptions>): string {
41
+ return toast.show({ message, intent: 'positive', ...options });
42
+ },
43
+ error(message: string, options?: Partial<ToastOptions>): string {
44
+ return toast.show({ message, intent: 'negative', ...options });
45
+ },
46
+ warning(message: string, options?: Partial<ToastOptions>): string {
47
+ return toast.show({ message, intent: 'warning', ...options });
48
+ },
49
+ info(message: string, options?: Partial<ToastOptions>): string {
50
+ return toast.show({ message, intent: 'neutral', ...options });
51
+ },
52
+ dismiss(id: string) {
53
+ toasts = toasts.filter(t => t.id !== id);
54
+ emit();
55
+ },
56
+ dismissAll() {
57
+ toasts = [];
58
+ emit();
59
+ },
60
+ };
@@ -0,0 +1,43 @@
1
+ import { Component, Prop, Event, EventEmitter, h, Host } from '@stencil/core';
2
+
3
+ @Component({
4
+ tag: 'ds-toggle',
5
+ styleUrl: 'Toggle.css',
6
+ shadow: true,
7
+ })
8
+ export class Toggle {
9
+ @Prop({ mutable: true }) checked: boolean = false;
10
+ @Prop() inactive: boolean = false;
11
+
12
+ @Event() dsChange!: EventEmitter<boolean>;
13
+
14
+ private handleClick = () => {
15
+ if (this.inactive) return;
16
+ this.checked = !this.checked;
17
+ this.dsChange.emit(this.checked);
18
+ };
19
+
20
+ private handleKeyDown = (e: KeyboardEvent) => {
21
+ if (this.inactive) return;
22
+ if (e.key === ' ' || e.key === 'Enter') {
23
+ e.preventDefault();
24
+ this.handleClick();
25
+ }
26
+ };
27
+
28
+ render() {
29
+ return (
30
+ <Host
31
+ role="switch"
32
+ aria-checked={String(this.checked)}
33
+ aria-disabled={this.inactive || undefined}
34
+ tabIndex={this.inactive ? -1 : 0}
35
+ class={{ toggle: true, checked: this.checked, inactive: this.inactive }}
36
+ onClick={this.handleClick}
37
+ onKeyDown={this.handleKeyDown}
38
+ >
39
+ <span class="thumb" />
40
+ </Host>
41
+ );
42
+ }
43
+ }
@@ -0,0 +1 @@
1
+ export type { } from './Toggle';
@@ -0,0 +1,120 @@
1
+ import { Component, Prop, Event, EventEmitter, h, Host } from '@stencil/core';
2
+ import type { IconSize } from '../Icon/Icon';
3
+
4
+ export type ToggleButtonElevation = 'none' | 'flat' | 'elevated' | 'floating';
5
+ export type ToggleButtonSize = 'md' | 'sm' | 'xs';
6
+ export type ToggleButtonBackground = 'faint' | 'medium' | 'bold' | 'strong' | 'always-dark';
7
+
8
+ const ICON_SIZE: Record<ToggleButtonSize, IconSize> = { md: 'md', sm: 'sm', xs: 'xs' };
9
+
10
+ // Pixel size reference for internal layout (icon span font-size)
11
+ const ICON_PX: Record<ToggleButtonSize, number> = { md: 20, sm: 16, xs: 12 };
12
+
13
+ const TEXT_CLASS: Record<ToggleButtonSize, string> = {
14
+ md: 'text-body-medium-emphasis',
15
+ sm: 'text-body-small-emphasis',
16
+ xs: 'text-caption-emphasis',
17
+ };
18
+
19
+ @Component({
20
+ tag: 'ds-toggle-button',
21
+ styleUrl: 'ToggleButton.css',
22
+ scoped: true,
23
+ })
24
+ export class ToggleButton {
25
+ /**
26
+ * Chrome level.
27
+ * none — ghost (transparent, no border, no shadow)
28
+ * flat — border only, transparent bg
29
+ * elevated — bg-primary + shadow [default]
30
+ * floating — bg-primary + FAB-strength shadow
31
+ */
32
+ @Prop() elevation: ToggleButtonElevation = 'elevated';
33
+
34
+ /** Label text. */
35
+ @Prop() label: string | undefined;
36
+
37
+ /**
38
+ * Icon name for <ds-icon>. Set via JS property.
39
+ * @example el.icon = 'GridView';
40
+ */
41
+ @Prop() icon: string | undefined;
42
+
43
+ /** Size: md (32px), sm (24px), xs (16px). */
44
+ @Prop() size: ToggleButtonSize = 'md';
45
+
46
+ /** Pill shape. */
47
+ @Prop() rounded: boolean = false;
48
+
49
+ /** Parent surface context — adjusts hover tokens for colored backgrounds. */
50
+ @Prop() background: ToggleButtonBackground | undefined;
51
+
52
+ /** Controlled pressed state. */
53
+ @Prop({ mutable: true }) pressed: boolean = false;
54
+
55
+ /** Disables interaction. */
56
+ @Prop() inactive: boolean = false;
57
+
58
+ @Prop({ attribute: 'aria-label' }) ariaLabel: string | undefined;
59
+
60
+ /** Emits the new pressed value (!pressed) on click. */
61
+ @Event() dsChange!: EventEmitter<boolean>;
62
+
63
+ private handleClick = () => {
64
+ if (this.inactive) return;
65
+ this.dsChange.emit(!this.pressed);
66
+ };
67
+
68
+ render() {
69
+ const size = this.size;
70
+ const iconSize = ICON_SIZE[size];
71
+ const iconPx = ICON_PX[size];
72
+ const textClass = TEXT_CLASS[size];
73
+ const hasIcon = !!this.icon;
74
+ const hasLabel = !!this.label;
75
+ const isIconOnly = hasIcon && !hasLabel;
76
+ const isLabelOnly = hasLabel && !hasIcon;
77
+ const isIconAndLabel = hasIcon && hasLabel;
78
+ const bg = this.background;
79
+ const elevKey = this.elevation.charAt(0).toUpperCase() + this.elevation.slice(1);
80
+
81
+ const cls: Record<string, boolean> = {
82
+ 'toggle-btn': true,
83
+ [`elevation${elevKey}`]: true,
84
+ sizeSM: size === 'sm',
85
+ sizeXS: size === 'xs',
86
+ rounded: this.rounded,
87
+ inactive: this.inactive,
88
+ pressed: this.pressed,
89
+ iconOnly: isIconOnly,
90
+ labelOnly: isLabelOnly,
91
+ iconAndLabel: isIconAndLabel,
92
+ onMedium: bg === 'medium',
93
+ onBold: bg === 'bold',
94
+ onStrong: bg === 'strong',
95
+ onAlwaysDark: bg === 'always-dark',
96
+ };
97
+
98
+ return (
99
+ <Host>
100
+ <button
101
+ type="button"
102
+ class={cls}
103
+ disabled={this.inactive}
104
+ aria-label={this.ariaLabel ?? this.label ?? 'toggle'}
105
+ aria-pressed={this.pressed}
106
+ onClick={this.handleClick}
107
+ >
108
+ {hasIcon && (
109
+ <span class="toggle-btn__icon" style={{ fontSize: `${iconPx}px` }}>
110
+ <ds-icon name={this.icon} size={iconSize} />
111
+ </span>
112
+ )}
113
+ {hasLabel && (
114
+ <span class={`toggle-btn__label ${textClass}`}>{this.label}</span>
115
+ )}
116
+ </button>
117
+ </Host>
118
+ );
119
+ }
120
+ }
@@ -0,0 +1,118 @@
1
+ import { Component, Prop, Event, EventEmitter, h, Host } from '@stencil/core';
2
+
3
+ export type ToggleGroupElevation = 'none' | 'flat' | 'elevated' | 'floating';
4
+ export type ToggleGroupSize = 'md' | 'sm' | 'xs';
5
+ export type ToggleGroupBackground = 'faint' | 'medium' | 'bold' | 'strong' | 'always-dark';
6
+
7
+ export interface ToggleGroupItem {
8
+ id: string;
9
+ label?: string;
10
+ /** Icon name for <ds-icon>. */
11
+ icon?: string;
12
+ inactive?: boolean;
13
+ }
14
+
15
+ @Component({
16
+ tag: 'ds-toggle-button-group',
17
+ styleUrl: 'ToggleButtonGroup.css',
18
+ scoped: true,
19
+ })
20
+ export class ToggleButtonGroup {
21
+ /**
22
+ * Array of item descriptors. Set via JS property.
23
+ * @example el.items = [{ id: 'list', label: 'List' }, { id: 'grid', icon: 'GridView' }];
24
+ */
25
+ @Prop() items: ToggleGroupItem[] = [];
26
+
27
+ /** ID of the currently selected / pressed item. */
28
+ @Prop({ mutable: true }) value: string = '';
29
+
30
+ /** Chrome level for the group container. */
31
+ @Prop() elevation: ToggleGroupElevation = 'elevated';
32
+
33
+ /** Size passed to each ds-toggle-button. */
34
+ @Prop() size: ToggleGroupSize = 'md';
35
+
36
+ /** Pill shape for group + buttons. */
37
+ @Prop() rounded: boolean = false;
38
+
39
+ /** Parent surface context. */
40
+ @Prop() background: ToggleGroupBackground | undefined;
41
+
42
+ /** Emits the id of the newly selected item. */
43
+ @Event() dsChange!: EventEmitter<string>;
44
+
45
+ private handleItemChange(id: string) {
46
+ if (id === this.value) return;
47
+ this.value = id;
48
+ this.dsChange.emit(id);
49
+ }
50
+
51
+ render() {
52
+ const items = this.items;
53
+ const count = items.length;
54
+ const elev = this.elevation;
55
+ const elevKey = elev.charAt(0).toUpperCase() + elev.slice(1);
56
+ const bg = this.background;
57
+ const isGhost = elev === 'none';
58
+
59
+ const groupCls: Record<string, boolean> = {
60
+ group: true,
61
+ [`group${elevKey}`]: true,
62
+ groupRounded: this.rounded,
63
+ };
64
+
65
+ return (
66
+ <Host>
67
+ <div class={groupCls} role="group">
68
+ {items.map((item, i) => {
69
+ const isFirst = i === 0;
70
+ const isLast = i === count - 1;
71
+ const isMid = !isFirst && !isLast;
72
+ const isPressed = item.id === this.value;
73
+ const isIconOnly = !!item.icon && !item.label;
74
+ const needsRoundedCorrection = this.rounded && !isIconOnly && this.size !== 'xs';
75
+ const sizeKey = this.size.toUpperCase() as 'MD' | 'SM' | 'XS';
76
+
77
+ const itemCls: Record<string, boolean> = {
78
+ item: true,
79
+ first: isFirst,
80
+ last: isLast,
81
+ middle: isMid,
82
+ itemShort: isGhost,
83
+ itemPressed: isGhost && isPressed,
84
+ [`roundedFirst${sizeKey}`]: !!(needsRoundedCorrection && isFirst),
85
+ [`roundedLast${sizeKey}`]: !!(needsRoundedCorrection && isLast),
86
+ [`roundedMid${sizeKey}`]: !!(needsRoundedCorrection && isMid),
87
+ };
88
+
89
+ const dividerCls: Record<string, boolean> = {
90
+ divider: true,
91
+ dividerGhost: isGhost,
92
+ dividerGhostXS: isGhost && this.size === 'xs',
93
+ dividerGhostSM: isGhost && this.size === 'sm',
94
+ };
95
+
96
+ return [
97
+ i > 0 && (
98
+ <div class={dividerCls} aria-hidden="true" />
99
+ ),
100
+ <ds-toggle-button
101
+ class={itemCls}
102
+ elevation={elev}
103
+ size={this.size}
104
+ label={item.label}
105
+ icon={item.icon}
106
+ rounded={this.rounded}
107
+ background={bg}
108
+ pressed={isPressed}
109
+ inactive={item.inactive}
110
+ onDsChange={() => this.handleItemChange(item.id)}
111
+ />,
112
+ ];
113
+ })}
114
+ </div>
115
+ </Host>
116
+ );
117
+ }
118
+ }