@ds-mo/ui 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/dist/components/ds-bar-nav-action.js +1 -1
  2. package/dist/components/ds-bar-nav.js +1 -1
  3. package/dist/components/ds-bar-nav.js.map +1 -1
  4. package/dist/components/ds-breadcrumb.js +1 -1
  5. package/dist/components/ds-button-group.js +1 -1
  6. package/dist/components/ds-button.js +1 -1
  7. package/dist/components/ds-card.js +1 -1
  8. package/dist/components/ds-card.js.map +1 -1
  9. package/dist/components/ds-checkbox.js +1 -1
  10. package/dist/components/ds-divider.js +1 -1
  11. package/dist/components/ds-empty-state.js +1 -1
  12. package/dist/components/ds-empty-state.js.map +1 -1
  13. package/dist/components/ds-fade.js +1 -1
  14. package/dist/components/ds-field.js +1 -1
  15. package/dist/components/ds-header.js +1 -1
  16. package/dist/components/ds-header.js.map +1 -1
  17. package/dist/components/ds-icon.js +1 -1
  18. package/dist/components/ds-input.js +1 -1
  19. package/dist/components/ds-label-wrap.js +1 -1
  20. package/dist/components/ds-menu.js +1 -1
  21. package/dist/components/ds-modal.js +1 -1
  22. package/dist/components/ds-panel-nav.js +1 -1
  23. package/dist/components/ds-radio-group.js +1 -1
  24. package/dist/components/ds-radio-group.js.map +1 -1
  25. package/dist/components/ds-scrollbar.js +1 -1
  26. package/dist/components/ds-select.js +1 -1
  27. package/dist/components/ds-slider.js +1 -1
  28. package/dist/components/ds-slider.js.map +1 -1
  29. package/dist/components/ds-surface.js +1 -1
  30. package/dist/components/ds-tab-group-nav.d.ts +11 -0
  31. package/dist/components/ds-tab-group-nav.js +2 -0
  32. package/dist/components/ds-tab-group-nav.js.map +1 -0
  33. package/dist/components/ds-tab-group.js +1 -1
  34. package/dist/components/ds-tab-group.js.map +1 -1
  35. package/dist/components/ds-table.js +1 -1
  36. package/dist/components/ds-table.js.map +1 -1
  37. package/dist/components/ds-tag.js +1 -1
  38. package/dist/components/ds-text.js +1 -1
  39. package/dist/components/ds-toast-provider.js +1 -1
  40. package/dist/components/ds-toggle-button-group.js +1 -1
  41. package/dist/components/ds-toggle-button.js +1 -1
  42. package/dist/components/ds-toggle.js +1 -1
  43. package/dist/components/ds-tooltip.js +1 -1
  44. package/dist/components/p-BQq26pt9.js +2 -0
  45. package/dist/components/p-BQq26pt9.js.map +1 -0
  46. package/dist/components/p-CMyQsk0K.js +2 -0
  47. package/dist/components/p-CMyQsk0K.js.map +1 -0
  48. package/dist/components/{p-D3CwGoIu.js → p-COsKOwZR.js} +2 -2
  49. package/dist/components/p-CpbnmEu1.js +2 -0
  50. package/dist/components/p-CpbnmEu1.js.map +1 -0
  51. package/dist/components/{p-DouEVPmi.js → p-Dd0GrKaF.js} +2 -2
  52. package/dist/components/{p-BvxKLqNx.js → p-H89PzqGU.js} +2 -2
  53. package/dist/components/{p-BYe8Rfdg.js → p-Vwlmhl4E.js} +2 -2
  54. package/dist/components/{p-C4p1fG67.js → p-woooYulR.js} +2 -2
  55. package/dist/types/.stencil/BarNav/BarNav.d.ts +7 -2
  56. package/dist/types/.stencil/BarNav/bar-nav-dom-utils.d.ts +1 -2
  57. package/dist/types/.stencil/TabGroupNav/TabGroupNav.d.ts +27 -0
  58. package/dist/types/components.d.ts +68 -0
  59. package/package.json +37 -7
  60. package/src/angular/index.ts +1 -0
  61. package/src/angular/proxies.ts +28 -0
  62. package/src/react/components.ts +46 -0
  63. package/src/react/ds-accordion.ts +27 -0
  64. package/src/react/ds-badge.ts +26 -0
  65. package/src/react/ds-banner.ts +27 -0
  66. package/src/react/ds-bar-nav-action.ts +27 -0
  67. package/src/react/ds-bar-nav.ts +33 -0
  68. package/src/react/ds-breadcrumb.ts +27 -0
  69. package/src/react/ds-button-group.ts +27 -0
  70. package/src/react/ds-button.ts +35 -0
  71. package/src/react/ds-card.ts +26 -0
  72. package/src/react/ds-checkbox.ts +27 -0
  73. package/src/react/ds-divider.ts +26 -0
  74. package/src/react/ds-empty-state.ts +26 -0
  75. package/src/react/ds-fade.ts +26 -0
  76. package/src/react/ds-field.ts +26 -0
  77. package/src/react/ds-header.ts +26 -0
  78. package/src/react/ds-icon.ts +26 -0
  79. package/src/react/ds-input.ts +33 -0
  80. package/src/react/ds-label-wrap.ts +26 -0
  81. package/src/react/ds-loader.ts +26 -0
  82. package/src/react/ds-menu.ts +33 -0
  83. package/src/react/ds-modal.ts +27 -0
  84. package/src/react/ds-pagination.ts +27 -0
  85. package/src/react/ds-panel-nav.ts +37 -0
  86. package/src/react/ds-radio-group.ts +27 -0
  87. package/src/react/ds-scrollbar.ts +26 -0
  88. package/src/react/ds-select.ts +27 -0
  89. package/src/react/ds-sidebar.ts +33 -0
  90. package/src/react/ds-skeleton.ts +26 -0
  91. package/src/react/ds-slider.ts +27 -0
  92. package/src/react/ds-surface.ts +26 -0
  93. package/src/react/ds-tab-group-nav.ts +27 -0
  94. package/src/react/ds-tab-group.ts +27 -0
  95. package/src/react/ds-table.ts +35 -0
  96. package/src/react/ds-tag.ts +35 -0
  97. package/src/react/ds-text.ts +26 -0
  98. package/src/react/ds-toast-provider.ts +26 -0
  99. package/src/react/ds-toggle-button-group.ts +27 -0
  100. package/src/react/ds-toggle-button.ts +27 -0
  101. package/src/react/ds-toggle.ts +27 -0
  102. package/src/react/ds-tooltip.ts +26 -0
  103. package/src/wc/components/Accordion/Accordion.tsx +144 -0
  104. package/src/wc/components/Badge/Badge.tsx +34 -0
  105. package/src/wc/components/Badge/index.ts +1 -0
  106. package/src/wc/components/Banner/Banner.tsx +148 -0
  107. package/src/wc/components/BarNav/BarNav.tsx +605 -0
  108. package/src/wc/components/BarNav/bar-nav-dom-utils.ts +23 -0
  109. package/src/wc/components/BarNav/bar-nav-tabs-menu-utils.ts +59 -0
  110. package/src/wc/components/BarNav/bar-nav-utils.ts +76 -0
  111. package/src/wc/components/BarNav/index.ts +1 -0
  112. package/src/wc/components/BarNavAction/BarNavAction.tsx +67 -0
  113. package/src/wc/components/BarNavAction/index.ts +1 -0
  114. package/src/wc/components/Breadcrumb/Breadcrumb.tsx +61 -0
  115. package/src/wc/components/Button/Button.tsx +158 -0
  116. package/src/wc/components/Button/index.ts +1 -0
  117. package/src/wc/components/ButtonGroup/ButtonGroup.tsx +110 -0
  118. package/src/wc/components/Card/Card.tsx +26 -0
  119. package/src/wc/components/Checkbox/Checkbox.tsx +60 -0
  120. package/src/wc/components/Checkbox/index.ts +1 -0
  121. package/src/wc/components/Divider/Divider.tsx +23 -0
  122. package/src/wc/components/Divider/index.ts +2 -0
  123. package/src/wc/components/EmptyState/EmptyState.tsx +32 -0
  124. package/src/wc/components/Fade/Fade.tsx +32 -0
  125. package/src/wc/components/Fade/index.ts +1 -0
  126. package/src/wc/components/Field/Field.tsx +28 -0
  127. package/src/wc/components/Header/Header.tsx +33 -0
  128. package/src/wc/components/Icon/Icon.tsx +112 -0
  129. package/src/wc/components/Icon/flag-icon-catalog.ts +68 -0
  130. package/src/wc/components/Icon/index.ts +1 -0
  131. package/src/wc/components/Icon/system-icon-catalog.ts +798 -0
  132. package/src/wc/components/Input/Input.tsx +100 -0
  133. package/src/wc/components/Input/index.ts +1 -0
  134. package/src/wc/components/LabelWrap/LabelWrap.tsx +38 -0
  135. package/src/wc/components/Loader/Loader.tsx +47 -0
  136. package/src/wc/components/Loader/index.ts +1 -0
  137. package/src/wc/components/Menu/Menu.tsx +340 -0
  138. package/src/wc/components/Modal/Modal.tsx +197 -0
  139. package/src/wc/components/Pagination/Pagination.tsx +97 -0
  140. package/src/wc/components/PanelNav/PanelNav.tsx +666 -0
  141. package/src/wc/components/PanelNav/index.ts +7 -0
  142. package/src/wc/components/PanelNav/panel-nav-utils.ts +124 -0
  143. package/src/wc/components/RadioGroup/RadioGroup.tsx +128 -0
  144. package/src/wc/components/RadioGroup/index.ts +1 -0
  145. package/src/wc/components/Scrollbar/Scrollbar.tsx +189 -0
  146. package/src/wc/components/Select/Select.tsx +195 -0
  147. package/src/wc/components/Sidebar/Sidebar.tsx +245 -0
  148. package/src/wc/components/Skeleton/Skeleton.tsx +64 -0
  149. package/src/wc/components/Skeleton/index.ts +1 -0
  150. package/src/wc/components/Slider/Slider.tsx +68 -0
  151. package/src/wc/components/Slider/index.ts +1 -0
  152. package/src/wc/components/Surface/Surface.tsx +81 -0
  153. package/src/wc/components/TabGroup/TabGroup.tsx +188 -0
  154. package/src/wc/components/TabGroup/tab-item-utils.ts +24 -0
  155. package/src/wc/components/TabGroupNav/TabGroupNav.tsx +188 -0
  156. package/src/wc/components/Table/Table.tsx +268 -0
  157. package/src/wc/components/Tag/Tag.tsx +135 -0
  158. package/src/wc/components/Tag/index.ts +1 -0
  159. package/src/wc/components/Text/Text.tsx +100 -0
  160. package/src/wc/components/Text/index.ts +1 -0
  161. package/src/wc/components/Toast/Toast.tsx +174 -0
  162. package/src/wc/components/Toast/toast-service.ts +60 -0
  163. package/src/wc/components/Toggle/Toggle.tsx +43 -0
  164. package/src/wc/components/Toggle/index.ts +1 -0
  165. package/src/wc/components/ToggleButton/ToggleButton.tsx +120 -0
  166. package/src/wc/components/ToggleButtonGroup/ToggleButtonGroup.tsx +118 -0
  167. package/src/wc/components/Tooltip/Tooltip.tsx +196 -0
  168. package/src/wc/components.d.ts +3028 -0
  169. package/dist/components/p-DV8pmt1m.js +0 -2
  170. package/dist/components/p-DV8pmt1m.js.map +0 -1
  171. package/dist/components/p-STnE_bmQ.js +0 -2
  172. package/dist/components/p-STnE_bmQ.js.map +0 -1
  173. /package/dist/components/{p-D3CwGoIu.js.map → p-COsKOwZR.js.map} +0 -0
  174. /package/dist/components/{p-DouEVPmi.js.map → p-Dd0GrKaF.js.map} +0 -0
  175. /package/dist/components/{p-BvxKLqNx.js.map → p-H89PzqGU.js.map} +0 -0
  176. /package/dist/components/{p-BYe8Rfdg.js.map → p-Vwlmhl4E.js.map} +0 -0
  177. /package/dist/components/{p-C4p1fG67.js.map → p-woooYulR.js.map} +0 -0
@@ -0,0 +1,245 @@
1
+ import { Component, Prop, State, Event, EventEmitter, Element, Watch, h, Host } from '@stencil/core';
2
+
3
+ export type SidebarWidth = 'mini' | 'default' | number;
4
+
5
+ const MINI_WIDTH = 56;
6
+ const DEFAULT_WIDTH = 240;
7
+ const MAX_WIDTH = 480;
8
+ const MIN_DRAG_WIDTH = 180;
9
+ const SNAP_THRESHOLD = 140;
10
+ const KEYBOARD_STEP = 16;
11
+
12
+ const FOCUSABLE_SEL = [
13
+ 'a[href]',
14
+ 'button:not([disabled])',
15
+ 'input:not([disabled])',
16
+ 'select:not([disabled])',
17
+ 'textarea:not([disabled])',
18
+ '[tabindex]:not([tabindex="-1"])',
19
+ ].join(',');
20
+
21
+ @Component({
22
+ tag: 'ds-sidebar',
23
+ styleUrl: 'Sidebar.css',
24
+ scoped: true,
25
+ })
26
+ export class Sidebar {
27
+ @Element() el!: HTMLElement;
28
+
29
+ @Prop({ mutable: true }) collapsed: boolean = false;
30
+ @Prop() width: SidebarWidth = 'default';
31
+ @Prop() resizable: boolean = true;
32
+ @Prop() mobile: boolean = false;
33
+
34
+ @State() private isResizing: boolean = false;
35
+
36
+ @Event() dsToggle!: EventEmitter<void>;
37
+ @Event() dsWidthChange!: EventEmitter<SidebarWidth>;
38
+
39
+ private triggerEl: HTMLElement | null = null;
40
+ private escapeHandler: ((e: KeyboardEvent) => void) | null = null;
41
+ private tabHandler: ((e: KeyboardEvent) => void) | null = null;
42
+ private mouseMoveHandler: ((e: MouseEvent) => void) | null = null;
43
+ private mouseUpHandler: (() => void) | null = null;
44
+
45
+ private get resolvedWidth(): number {
46
+ if (this.width === 'mini') return MINI_WIDTH;
47
+ if (this.width === 'default') return DEFAULT_WIDTH;
48
+ return this.width as number;
49
+ }
50
+
51
+ private get isMobileOpen(): boolean {
52
+ return this.mobile && !this.collapsed;
53
+ }
54
+
55
+ @Watch('collapsed')
56
+ onCollapsedChange() {
57
+ if (this.isMobileOpen) {
58
+ this.setupMobileFocusTrap();
59
+ } else {
60
+ this.teardownMobileFocusTrap();
61
+ }
62
+ }
63
+
64
+ componentDidLoad() {
65
+ if (this.isMobileOpen) {
66
+ this.setupMobileFocusTrap();
67
+ }
68
+ if (this.mobile) {
69
+ this.escapeHandler = (e: KeyboardEvent) => {
70
+ if (e.key === 'Escape' && !this.collapsed) {
71
+ this.dsToggle.emit();
72
+ }
73
+ };
74
+ document.addEventListener('keydown', this.escapeHandler);
75
+ }
76
+ }
77
+
78
+ disconnectedCallback() {
79
+ this.teardownMobileFocusTrap();
80
+ if (this.escapeHandler) {
81
+ document.removeEventListener('keydown', this.escapeHandler);
82
+ this.escapeHandler = null;
83
+ }
84
+ this.cleanupMouseHandlers();
85
+ }
86
+
87
+ private setupMobileFocusTrap() {
88
+ this.triggerEl = document.activeElement as HTMLElement | null;
89
+
90
+ const focusables = () =>
91
+ Array.from(this.el.querySelectorAll<HTMLElement>(FOCUSABLE_SEL)).filter(
92
+ el => !el.hasAttribute('aria-hidden') && el.offsetParent !== null,
93
+ );
94
+
95
+ const initial = focusables();
96
+ (initial[0] ?? this.el).focus();
97
+
98
+ this.tabHandler = (e: KeyboardEvent) => {
99
+ if (e.key !== 'Tab') return;
100
+ const items = focusables();
101
+ if (!items.length) { e.preventDefault(); this.el.focus(); return; }
102
+ const first = items[0];
103
+ const last = items[items.length - 1];
104
+ const active = document.activeElement as HTMLElement | null;
105
+ if (e.shiftKey && (active === first || !this.el.contains(active))) {
106
+ e.preventDefault(); last.focus();
107
+ } else if (!e.shiftKey && (active === last || !this.el.contains(active))) {
108
+ e.preventDefault(); first.focus();
109
+ }
110
+ };
111
+ document.addEventListener('keydown', this.tabHandler);
112
+ }
113
+
114
+ private teardownMobileFocusTrap() {
115
+ if (this.tabHandler) {
116
+ document.removeEventListener('keydown', this.tabHandler);
117
+ this.tabHandler = null;
118
+ }
119
+ this.triggerEl?.focus?.();
120
+ this.triggerEl = null;
121
+ }
122
+
123
+ private cleanupMouseHandlers() {
124
+ if (this.mouseMoveHandler) document.removeEventListener('mousemove', this.mouseMoveHandler);
125
+ if (this.mouseUpHandler) document.removeEventListener('mouseup', this.mouseUpHandler);
126
+ this.mouseMoveHandler = null;
127
+ this.mouseUpHandler = null;
128
+ document.body.style.cursor = '';
129
+ document.body.style.userSelect = '';
130
+ }
131
+
132
+ private handleResizeMouseDown(e: MouseEvent) {
133
+ if (!this.resizable) return;
134
+ e.preventDefault();
135
+ this.isResizing = true;
136
+
137
+ this.mouseMoveHandler = (ev: MouseEvent) => {
138
+ const newW = ev.clientX;
139
+ if (newW < SNAP_THRESHOLD) {
140
+ this.dsWidthChange.emit('mini');
141
+ } else if (newW < MIN_DRAG_WIDTH) {
142
+ this.dsWidthChange.emit('default');
143
+ } else {
144
+ this.dsWidthChange.emit(newW);
145
+ }
146
+ };
147
+
148
+ this.mouseUpHandler = () => {
149
+ this.isResizing = false;
150
+ this.cleanupMouseHandlers();
151
+ };
152
+
153
+ document.body.style.cursor = 'col-resize';
154
+ document.body.style.userSelect = 'none';
155
+ document.addEventListener('mousemove', this.mouseMoveHandler);
156
+ document.addEventListener('mouseup', this.mouseUpHandler);
157
+ }
158
+
159
+ private handleResizeKeyDown(e: KeyboardEvent) {
160
+ if (!this.resizable) return;
161
+ const w = this.width;
162
+ const resolved = this.resolvedWidth;
163
+
164
+ switch (e.key) {
165
+ case 'ArrowLeft': {
166
+ e.preventDefault();
167
+ if (w === 'mini') return;
168
+ const next = resolved - KEYBOARD_STEP;
169
+ this.dsWidthChange.emit(next < MIN_DRAG_WIDTH ? 'mini' : next);
170
+ break;
171
+ }
172
+ case 'ArrowRight': {
173
+ e.preventDefault();
174
+ if (w === 'mini') { this.dsWidthChange.emit('default'); return; }
175
+ this.dsWidthChange.emit(Math.min(resolved + KEYBOARD_STEP, MAX_WIDTH));
176
+ break;
177
+ }
178
+ case 'Home':
179
+ e.preventDefault();
180
+ this.dsWidthChange.emit('mini');
181
+ break;
182
+ case 'End':
183
+ e.preventDefault();
184
+ this.dsWidthChange.emit(MAX_WIDTH);
185
+ break;
186
+ }
187
+ }
188
+
189
+ render() {
190
+ if (this.collapsed && !this.mobile) return null;
191
+
192
+ const isMini = this.width === 'mini';
193
+ const isMobileOpen = this.isMobileOpen;
194
+ const sidebarWidth = this.collapsed ? 0 : this.resolvedWidth;
195
+
196
+ const dialogProps = isMobileOpen
197
+ ? { role: 'dialog', 'aria-modal': 'true', tabIndex: -1 }
198
+ : { role: 'navigation' };
199
+
200
+ return (
201
+ <Host>
202
+ {this.mobile && !this.collapsed && (
203
+ <div
204
+ class="backdrop"
205
+ onClick={() => this.dsToggle.emit()}
206
+ aria-hidden="true"
207
+ />
208
+ )}
209
+ <aside
210
+ class={{
211
+ sidebar: true,
212
+ 'sidebar--mini': isMini,
213
+ 'sidebar--mobile': this.mobile,
214
+ 'sidebar--collapsed': this.collapsed,
215
+ 'sidebar--resizing': this.isResizing,
216
+ }}
217
+ style={{ width: `${sidebarWidth}px` }}
218
+ aria-label="Sidebar"
219
+ {...dialogProps}
220
+ >
221
+ <div class="sidebar__content">
222
+ <slot />
223
+ </div>
224
+ <div class="sidebar__footer">
225
+ <slot name="footer" />
226
+ </div>
227
+ {this.resizable && !this.mobile && (
228
+ <div
229
+ class="sidebar__resize-handle"
230
+ onMouseDown={(e: MouseEvent) => this.handleResizeMouseDown(e)}
231
+ onKeyDown={(e: KeyboardEvent) => this.handleResizeKeyDown(e)}
232
+ role="separator"
233
+ aria-orientation="vertical"
234
+ aria-label="Resize sidebar"
235
+ aria-valuenow={this.resolvedWidth}
236
+ aria-valuemin={MINI_WIDTH}
237
+ aria-valuemax={MAX_WIDTH}
238
+ tabIndex={0}
239
+ />
240
+ )}
241
+ </aside>
242
+ </Host>
243
+ );
244
+ }
245
+ }
@@ -0,0 +1,64 @@
1
+ import { Component, Prop, h, Host } from '@stencil/core';
2
+
3
+ export type SkeletonVariant = 'text' | 'circular' | 'rectangular';
4
+
5
+ @Component({
6
+ tag: 'ds-skeleton',
7
+ styleUrl: 'Skeleton.css',
8
+ shadow: true,
9
+ })
10
+ export class Skeleton {
11
+ @Prop() variant: SkeletonVariant = 'text';
12
+ /** Width as a CSS value string (e.g. '200px', '100%') or number (px). */
13
+ @Prop() width: string | number | undefined;
14
+ /** Height as a CSS value string or number (px). */
15
+ @Prop() height: string | number | undefined;
16
+ /** Number of text lines. Only used when variant is 'text'. */
17
+ @Prop() lines: number = 1;
18
+ /** Whether to show the shimmer animation. */
19
+ @Prop() shimmer: boolean = true;
20
+
21
+ private get widthCss() {
22
+ const v = this.width;
23
+ if (v == null) return undefined;
24
+ return typeof v === 'number' ? `${v}px` : v;
25
+ }
26
+
27
+ private get heightCss() {
28
+ const v = this.height;
29
+ if (v == null) return undefined;
30
+ return typeof v === 'number' ? `${v}px` : v;
31
+ }
32
+
33
+ render() {
34
+ if (this.variant === 'text' && this.lines > 1) {
35
+ return (
36
+ <Host aria-hidden="true" style={{ width: this.widthCss }}>
37
+ <div class="text-group">
38
+ {Array.from({ length: this.lines }, (_, lineIdx) => (
39
+ <div
40
+ class={{ skeleton: true, text: true, animate: this.shimmer }}
41
+ style={{
42
+ width: lineIdx === this.lines - 1 ? '60%' : '100%',
43
+ height: this.heightCss,
44
+ }}
45
+ />
46
+ ))}
47
+ </div>
48
+ </Host>
49
+ );
50
+ }
51
+
52
+ return (
53
+ <Host
54
+ aria-hidden="true"
55
+ class={{
56
+ skeleton: true,
57
+ [this.variant]: true,
58
+ animate: this.shimmer,
59
+ }}
60
+ style={{ width: this.widthCss, height: this.heightCss }}
61
+ />
62
+ );
63
+ }
64
+ }
@@ -0,0 +1 @@
1
+ export type { SkeletonVariant } from './Skeleton';
@@ -0,0 +1,68 @@
1
+ import { Component, Prop, Event, EventEmitter, h, Host } from '@stencil/core';
2
+
3
+ let idCounter = 0;
4
+
5
+ @Component({
6
+ tag: 'ds-slider',
7
+ styleUrl: 'Slider.css',
8
+ scoped: true,
9
+ })
10
+ export class Slider {
11
+ private generatedId = `ds-slider-${++idCounter}`;
12
+
13
+ @Prop() value: number = 0;
14
+ @Prop() min: number = 0;
15
+ @Prop() max: number = 100;
16
+ @Prop() step: number = 1;
17
+ @Prop() label!: string;
18
+ @Prop() inactive: boolean = false;
19
+ @Prop() valueText: string | undefined;
20
+ /** Associates with an external <label>. Defaults to an auto-generated ID. */
21
+ @Prop() inputId: string | undefined;
22
+
23
+ @Event() dsChange!: EventEmitter<number>;
24
+
25
+ private handleInput = (e: Event) => {
26
+ this.dsChange.emit(Number((e.target as HTMLInputElement).value));
27
+ };
28
+
29
+ private get pct() {
30
+ const range = this.max - this.min;
31
+ return range === 0 ? 0 : Math.round(((this.value - this.min) / range) * 100);
32
+ }
33
+
34
+ render() {
35
+ const finalId = this.inputId ?? this.generatedId;
36
+ const pct = this.pct;
37
+
38
+ return (
39
+ <Host class="field">
40
+ <div class="label-row">
41
+ <label class="text-body-small-emphasis" htmlFor={finalId}>{this.label}</label>
42
+ <span class="text-body-small-emphasis">{this.value}</span>
43
+ </div>
44
+ <div
45
+ class={{ track: true, 'track--inactive': this.inactive, 'track--at-min': pct === 0 }}
46
+ style={{ '--slider-pct': String(pct) } as Record<string, string>}
47
+ >
48
+ <div class="track-inner">
49
+ <div class="fill-track"><div class="fill" /></div>
50
+ <div class="thumb" />
51
+ </div>
52
+ <input
53
+ type="range"
54
+ id={finalId}
55
+ min={this.min}
56
+ max={this.max}
57
+ step={this.step}
58
+ value={this.value}
59
+ disabled={this.inactive}
60
+ class="range-input"
61
+ aria-valuetext={this.valueText}
62
+ onInput={this.handleInput}
63
+ />
64
+ </div>
65
+ </Host>
66
+ );
67
+ }
68
+ }
@@ -0,0 +1 @@
1
+ export type { } from './Slider';
@@ -0,0 +1,81 @@
1
+ import { Component, Prop, h, Host } from '@stencil/core';
2
+
3
+ export type SurfaceIntent =
4
+ | 'brand' | 'positive' | 'negative' | 'warning' | 'caution'
5
+ | 'ai' | 'neutral' | 'walkthrough' | 'guide';
6
+ export type SurfaceBackground = 'primary' | 'secondary' | 'transparent' | 'translucent';
7
+ export type SurfaceContrast = 'faint' | 'medium' | 'bold' | 'strong';
8
+ export type SurfaceElevation =
9
+ | 'none' | 'depressed' | 'depressed-md' | 'flat' | 'elevated' | 'floating'
10
+ | 'overlayTop' | 'overlayRight' | 'overlayBottom' | 'overlayLeft';
11
+ export type SurfaceEdge = 'top' | 'right' | 'bottom' | 'left';
12
+ export type SurfaceRadius = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | (string & {});
13
+ export type SurfaceElement = 'div' | 'section' | 'aside' | 'article' | 'header' | 'footer' | 'main' | 'nav' | 'span';
14
+
15
+ const RADIUS_PRESETS = new Set(['none', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', 'full']);
16
+
17
+ @Component({
18
+ tag: 'ds-surface',
19
+ styleUrl: 'Surface.css',
20
+ scoped: true,
21
+ })
22
+ export class Surface {
23
+ @Prop() background: SurfaceBackground = 'transparent';
24
+ @Prop() intent: SurfaceIntent | undefined;
25
+ @Prop() contrast: SurfaceContrast = 'faint';
26
+ @Prop() elevation: SurfaceElevation = 'none';
27
+ /** Space-separated list of edges: "top bottom" etc. Only applies when elevation='none'. */
28
+ @Prop() edge: string | undefined;
29
+ @Prop() radius: SurfaceRadius | undefined;
30
+ @Prop() interactive: boolean = false;
31
+ @Prop() selected: boolean = false;
32
+ @Prop() inactive: boolean = false;
33
+ @Prop() as: SurfaceElement = 'div';
34
+
35
+ render() {
36
+ // Polymorphic element: TSX treats a variable tag as a component, so a
37
+ // dynamic intrinsic element name must be cast to `any` to render.
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ const Tag = this.as as any;
40
+
41
+ const intentClass = this.intent
42
+ ? `intent-${this.intent}-${this.contrast}`
43
+ : `bg-${this.background}`;
44
+
45
+ const elevClass = `elevation-${this.elevation}`;
46
+
47
+ const edgeClasses = this.edge && this.elevation === 'none'
48
+ ? this.edge.trim().split(/\s+/).map(e => `edge-${e}`)
49
+ : [];
50
+
51
+ const radiusClass = this.radius && RADIUS_PRESETS.has(this.radius)
52
+ ? `radius-${this.radius}`
53
+ : '';
54
+
55
+ const customRadius = this.radius && !RADIUS_PRESETS.has(this.radius)
56
+ ? this.radius
57
+ : undefined;
58
+
59
+ const cls = [
60
+ 'surface',
61
+ intentClass,
62
+ elevClass,
63
+ ...edgeClasses,
64
+ radiusClass,
65
+ this.interactive ? 'interactive' : '',
66
+ this.selected ? 'selected' : '',
67
+ this.inactive ? 'inactive' : '',
68
+ ].filter(Boolean).join(' ');
69
+
70
+ return (
71
+ <Host style={{ display: 'contents' }}>
72
+ <Tag
73
+ class={cls}
74
+ style={customRadius ? { borderRadius: customRadius } : undefined}
75
+ >
76
+ <slot />
77
+ </Tag>
78
+ </Host>
79
+ );
80
+ }
81
+ }
@@ -0,0 +1,188 @@
1
+ import { Component, Prop, Event, EventEmitter, Element, Listen, Watch, h, Host } from '@stencil/core';
2
+ import {
3
+ getSelectableTabs,
4
+ isTabDivider,
5
+ type TabItem,
6
+ type TabItemTab,
7
+ } from './tab-item-utils';
8
+
9
+ export type TabBackground = 'faint' | 'medium' | 'bold' | 'strong' | 'always-dark';
10
+
11
+ @Component({
12
+ tag: 'ds-tab-group',
13
+ styleUrl: 'TabGroup.css',
14
+ scoped: true,
15
+ })
16
+ export class TabGroup {
17
+ @Element() el!: HTMLElement;
18
+
19
+ @Prop({ mutable: true }) value: string = '';
20
+ @Prop() tabs: TabItem[] = [];
21
+ @Prop() background: TabBackground | undefined;
22
+ @Prop({ attribute: 'aria-label' }) ariaLabel: string | undefined;
23
+ @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;
24
+ @Prop() orientation: 'horizontal' | 'vertical' = 'horizontal';
25
+
26
+ @Event() dsChange!: EventEmitter<string>;
27
+
28
+ private get selectableTabs(): TabItemTab[] {
29
+ return getSelectableTabs(this.tabs);
30
+ }
31
+
32
+ private selectTab(id: string, options?: { focus?: boolean }) {
33
+ this.value = id;
34
+ this.dsChange.emit(id);
35
+ if (options?.focus !== false) {
36
+ this.focusTab(id);
37
+ }
38
+ }
39
+
40
+ private focusTab(id: string) {
41
+ const btn = this.el.querySelector(`[data-tab-id="${id}"]`) as HTMLElement | null;
42
+ btn?.focus({ preventScroll: true });
43
+ }
44
+
45
+ /** Keep focus on the selected tab when value changes externally (e.g. BarNav menu). */
46
+ @Watch('value')
47
+ onValueChange(next: string, prev: string | undefined) {
48
+ if (prev === undefined || next === prev) return;
49
+
50
+ requestAnimationFrame(() => {
51
+ const active = document.activeElement as HTMLElement | null;
52
+ if (!active || !this.el.contains(active)) return;
53
+
54
+ const activeId = active.getAttribute('data-tab-id');
55
+ if (activeId === next) return;
56
+
57
+ const nextTab = this.el.querySelector(`[data-tab-id="${next}"]`) as HTMLElement | null;
58
+ if (nextTab) {
59
+ nextTab.focus({ preventScroll: true });
60
+ } else {
61
+ active.blur();
62
+ }
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Find the next non-disabled tab index relative to `from`, stepping by `step`.
68
+ * Returns `from` unchanged if no other enabled tab exists.
69
+ */
70
+ private findEnabled(from: number, step: 1 | -1): number {
71
+ const tabs = this.selectableTabs;
72
+ const len = tabs.length;
73
+ for (let n = 0; n < len; n++) {
74
+ const i = (from + step * (n + 1) + len * (n + 1)) % len;
75
+ if (!tabs[i]?.disabled) return i;
76
+ }
77
+ return from;
78
+ }
79
+
80
+ @Listen('keydown')
81
+ handleKeyDown(e: KeyboardEvent) {
82
+ const tabs = this.selectableTabs;
83
+ if (!tabs.length) return;
84
+
85
+ const currentIdx = tabs.findIndex(t => t.id === this.value);
86
+ const isVertical = this.orientation === 'vertical';
87
+
88
+ let nextIdx: number | null = null;
89
+
90
+ if (!isVertical) {
91
+ if (e.key === 'ArrowRight') {
92
+ nextIdx = this.findEnabled(currentIdx, 1);
93
+ } else if (e.key === 'ArrowLeft') {
94
+ nextIdx = this.findEnabled(currentIdx, -1);
95
+ }
96
+ } else {
97
+ if (e.key === 'ArrowDown') {
98
+ nextIdx = this.findEnabled(currentIdx, 1);
99
+ } else if (e.key === 'ArrowUp') {
100
+ nextIdx = this.findEnabled(currentIdx, -1);
101
+ }
102
+ }
103
+
104
+ if (e.key === 'Home') {
105
+ const first = tabs.findIndex(t => !t.disabled);
106
+ nextIdx = first === -1 ? null : first;
107
+ } else if (e.key === 'End') {
108
+ const lastEnabled = [...tabs].reverse().findIndex(t => !t.disabled);
109
+ nextIdx = lastEnabled === -1 ? null : tabs.length - 1 - lastEnabled;
110
+ }
111
+
112
+ if (nextIdx !== null) {
113
+ e.preventDefault();
114
+ const next = tabs[nextIdx];
115
+ this.selectTab(next.id);
116
+ }
117
+ }
118
+
119
+ private getBgClass(): string {
120
+ if (!this.background || this.background === 'faint') return '';
121
+ if (this.background === 'always-dark') return 'on-always-dark';
122
+ return `on-${this.background}`;
123
+ }
124
+
125
+ render() {
126
+ const bgClass = this.getBgClass();
127
+ const isVertical = this.orientation === 'vertical';
128
+
129
+ return (
130
+ <Host class="tab-group-host">
131
+ <div
132
+ role="tablist"
133
+ class={{ 'tab-list': true, 'tab-list--vertical': isVertical }}
134
+ aria-label={this.ariaLabel}
135
+ aria-labelledby={this.ariaLabelledby}
136
+ aria-orientation={isVertical ? 'vertical' : undefined}
137
+ >
138
+ {this.tabs.map((tab, index) => {
139
+ if (isTabDivider(tab)) {
140
+ return (
141
+ <div
142
+ key={`divider-${index}`}
143
+ class={{
144
+ 'tab-divider': true,
145
+ 'tab-divider--vertical': isVertical,
146
+ }}
147
+ aria-hidden="true"
148
+ >
149
+ <div class="tab-divider__line" />
150
+ </div>
151
+ );
152
+ }
153
+
154
+ const isSelected = tab.id === this.value;
155
+ return (
156
+ <button
157
+ key={tab.id}
158
+ type="button"
159
+ role="tab"
160
+ data-tab-id={tab.id}
161
+ class={{
162
+ tab: true,
163
+ 'tab--selected': isSelected,
164
+ [bgClass]: !!bgClass,
165
+ }}
166
+ aria-selected={isSelected}
167
+ aria-disabled={tab.disabled || undefined}
168
+ aria-controls={tab.panelId ?? undefined}
169
+ disabled={tab.disabled}
170
+ tabIndex={isSelected ? 0 : -1}
171
+ onClick={() => !tab.disabled && this.selectTab(tab.id)}
172
+ >
173
+ <span class={{
174
+ tab__label: true,
175
+ 'tab__label--dot': !!tab.dot,
176
+ [isSelected ? 'text-body-small-emphasis' : 'text-body-small']: true,
177
+ }}>
178
+ {tab.label}
179
+ {tab.dot && <span class="tab__dot" aria-hidden="true" />}
180
+ </span>
181
+ </button>
182
+ );
183
+ })}
184
+ </div>
185
+ </Host>
186
+ );
187
+ }
188
+ }
@@ -0,0 +1,24 @@
1
+ export interface TabItemDivider {
2
+ type: 'divider';
3
+ }
4
+
5
+ export interface TabItemTab {
6
+ id: string;
7
+ label: string;
8
+ disabled?: boolean;
9
+ /** id of the tabpanel this tab controls */
10
+ panelId?: string;
11
+ /** Show a notification dot (brand) on the tab — no count, matching panel-nav. */
12
+ dot?: boolean;
13
+ }
14
+
15
+ export type TabItem = TabItemTab | TabItemDivider;
16
+
17
+ export function isTabDivider(item: TabItem): item is TabItemDivider {
18
+ return 'type' in item && item.type === 'divider';
19
+ }
20
+
21
+ /** Tab entries that participate in selection, keyboard nav, and URL matching. */
22
+ export function getSelectableTabs(tabs: TabItem[]): TabItemTab[] {
23
+ return tabs.filter((t): t is TabItemTab => !isTabDivider(t));
24
+ }