mtrl 0.2.5 → 0.2.7

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 (196) hide show
  1. package/index.ts +18 -0
  2. package/package.json +1 -1
  3. package/src/components/badge/_styles.scss +123 -115
  4. package/src/components/badge/api.ts +57 -59
  5. package/src/components/badge/badge.ts +16 -2
  6. package/src/components/badge/config.ts +65 -11
  7. package/src/components/badge/constants.ts +22 -12
  8. package/src/components/badge/features.ts +44 -40
  9. package/src/components/badge/types.ts +42 -30
  10. package/src/components/bottom-app-bar/_styles.scss +103 -0
  11. package/src/components/bottom-app-bar/bottom-app-bar.ts +196 -0
  12. package/src/components/bottom-app-bar/config.ts +73 -0
  13. package/src/components/bottom-app-bar/index.ts +11 -0
  14. package/src/components/bottom-app-bar/types.ts +108 -0
  15. package/src/components/button/_styles.scss +0 -66
  16. package/src/components/button/api.ts +5 -0
  17. package/src/components/button/button.ts +0 -2
  18. package/src/components/button/config.ts +5 -0
  19. package/src/components/button/constants.ts +0 -6
  20. package/src/components/button/index.ts +2 -2
  21. package/src/components/button/types.ts +7 -7
  22. package/src/components/card/_styles.scss +67 -25
  23. package/src/components/card/api.ts +54 -3
  24. package/src/components/card/card.ts +25 -6
  25. package/src/components/card/config.ts +189 -22
  26. package/src/components/card/constants.ts +20 -19
  27. package/src/components/card/content.ts +299 -2
  28. package/src/components/card/features.ts +158 -4
  29. package/src/components/card/index.ts +31 -9
  30. package/src/components/card/types.ts +166 -15
  31. package/src/components/checkbox/_styles.scss +0 -2
  32. package/src/components/chip/chip.ts +1 -9
  33. package/src/components/chip/constants.ts +0 -10
  34. package/src/components/chip/index.ts +1 -1
  35. package/src/components/chip/types.ts +1 -4
  36. package/src/components/datepicker/_styles.scss +358 -0
  37. package/src/components/datepicker/api.ts +272 -0
  38. package/src/components/datepicker/config.ts +144 -0
  39. package/src/components/datepicker/constants.ts +98 -0
  40. package/src/components/datepicker/datepicker.ts +346 -0
  41. package/src/components/datepicker/index.ts +9 -0
  42. package/src/components/datepicker/render.ts +452 -0
  43. package/src/components/datepicker/types.ts +268 -0
  44. package/src/components/datepicker/utils.ts +290 -0
  45. package/src/components/dialog/_styles.scss +174 -128
  46. package/src/components/dialog/api.ts +48 -13
  47. package/src/components/dialog/config.ts +9 -5
  48. package/src/components/dialog/dialog.ts +6 -3
  49. package/src/components/dialog/features.ts +290 -130
  50. package/src/components/dialog/types.ts +7 -4
  51. package/src/components/divider/_styles.scss +57 -0
  52. package/src/components/divider/config.ts +81 -0
  53. package/src/components/divider/divider.ts +37 -0
  54. package/src/components/divider/features.ts +207 -0
  55. package/src/components/divider/index.ts +5 -0
  56. package/src/components/divider/types.ts +55 -0
  57. package/src/components/extended-fab/_styles.scss +267 -0
  58. package/src/components/extended-fab/api.ts +141 -0
  59. package/src/components/extended-fab/config.ts +108 -0
  60. package/src/components/extended-fab/constants.ts +36 -0
  61. package/src/components/extended-fab/extended-fab.ts +125 -0
  62. package/src/components/extended-fab/index.ts +4 -0
  63. package/src/components/extended-fab/types.ts +287 -0
  64. package/src/components/fab/_styles.scss +225 -0
  65. package/src/components/fab/api.ts +97 -0
  66. package/src/components/fab/config.ts +94 -0
  67. package/src/components/fab/constants.ts +41 -0
  68. package/src/components/fab/fab.ts +67 -0
  69. package/src/components/fab/index.ts +4 -0
  70. package/src/components/fab/types.ts +234 -0
  71. package/src/components/navigation/_styles.scss +1 -0
  72. package/src/components/navigation/api.ts +78 -50
  73. package/src/components/navigation/features/items.ts +280 -0
  74. package/src/components/navigation/nav-item.ts +72 -23
  75. package/src/components/navigation/navigation.ts +54 -2
  76. package/src/components/navigation/types.ts +210 -188
  77. package/src/components/progress/_styles.scss +0 -65
  78. package/src/components/progress/config.ts +1 -2
  79. package/src/components/progress/constants.ts +0 -14
  80. package/src/components/progress/index.ts +1 -1
  81. package/src/components/progress/progress.ts +1 -4
  82. package/src/components/progress/types.ts +1 -4
  83. package/src/components/radios/_styles.scss +0 -45
  84. package/src/components/radios/api.ts +85 -60
  85. package/src/components/radios/config.ts +1 -2
  86. package/src/components/radios/constants.ts +0 -9
  87. package/src/components/radios/index.ts +1 -1
  88. package/src/components/radios/radio.ts +34 -11
  89. package/src/components/radios/radios.ts +2 -1
  90. package/src/components/radios/types.ts +1 -7
  91. package/src/components/search/_styles.scss +306 -0
  92. package/src/components/search/api.ts +203 -0
  93. package/src/components/search/config.ts +87 -0
  94. package/src/components/search/constants.ts +21 -0
  95. package/src/components/search/features/index.ts +4 -0
  96. package/src/components/search/features/search.ts +718 -0
  97. package/src/components/search/features/states.ts +165 -0
  98. package/src/components/search/features/structure.ts +198 -0
  99. package/src/components/search/index.ts +10 -0
  100. package/src/components/search/search.ts +52 -0
  101. package/src/components/search/types.ts +163 -0
  102. package/src/components/segmented-button/_styles.scss +117 -0
  103. package/src/components/segmented-button/config.ts +67 -0
  104. package/src/components/segmented-button/constants.ts +42 -0
  105. package/src/components/segmented-button/index.ts +4 -0
  106. package/src/components/segmented-button/segment.ts +155 -0
  107. package/src/components/segmented-button/segmented-button.ts +250 -0
  108. package/src/components/segmented-button/types.ts +219 -0
  109. package/src/components/slider/_styles.scss +221 -168
  110. package/src/components/slider/accessibility.md +59 -0
  111. package/src/components/slider/api.ts +41 -120
  112. package/src/components/slider/config.ts +51 -49
  113. package/src/components/slider/features/handlers.ts +495 -0
  114. package/src/components/slider/features/index.ts +1 -2
  115. package/src/components/slider/features/slider.ts +66 -84
  116. package/src/components/slider/features/states.ts +195 -0
  117. package/src/components/slider/features/structure.ts +141 -184
  118. package/src/components/slider/features/ui.ts +150 -201
  119. package/src/components/slider/index.ts +2 -11
  120. package/src/components/slider/slider.ts +9 -12
  121. package/src/components/slider/types.ts +39 -24
  122. package/src/components/switch/_styles.scss +0 -2
  123. package/src/components/tabs/_styles.scss +346 -154
  124. package/src/components/tabs/api.ts +178 -400
  125. package/src/components/tabs/config.ts +46 -52
  126. package/src/components/tabs/constants.ts +85 -8
  127. package/src/components/tabs/features.ts +403 -0
  128. package/src/components/tabs/index.ts +60 -3
  129. package/src/components/tabs/indicator.ts +285 -0
  130. package/src/components/tabs/responsive.ts +144 -0
  131. package/src/components/tabs/scroll-indicators.ts +149 -0
  132. package/src/components/tabs/state.ts +186 -0
  133. package/src/components/tabs/tab-api.ts +258 -0
  134. package/src/components/tabs/tab.ts +255 -0
  135. package/src/components/tabs/tabs.ts +50 -31
  136. package/src/components/tabs/types.ts +332 -128
  137. package/src/components/tabs/utils.ts +107 -0
  138. package/src/components/textfield/_styles.scss +0 -98
  139. package/src/components/textfield/config.ts +2 -3
  140. package/src/components/textfield/constants.ts +0 -14
  141. package/src/components/textfield/index.ts +2 -2
  142. package/src/components/textfield/textfield.ts +0 -2
  143. package/src/components/textfield/types.ts +1 -4
  144. package/src/components/timepicker/README.md +277 -0
  145. package/src/components/timepicker/_styles.scss +451 -0
  146. package/src/components/timepicker/api.ts +632 -0
  147. package/src/components/timepicker/clockdial.ts +482 -0
  148. package/src/components/timepicker/config.ts +130 -0
  149. package/src/components/timepicker/constants.ts +138 -0
  150. package/src/components/timepicker/index.ts +8 -0
  151. package/src/components/timepicker/render.ts +613 -0
  152. package/src/components/timepicker/timepicker.ts +117 -0
  153. package/src/components/timepicker/types.ts +336 -0
  154. package/src/components/timepicker/utils.ts +241 -0
  155. package/src/components/top-app-bar/_styles.scss +225 -0
  156. package/src/components/top-app-bar/config.ts +83 -0
  157. package/src/components/top-app-bar/index.ts +11 -0
  158. package/src/components/top-app-bar/top-app-bar.ts +316 -0
  159. package/src/components/top-app-bar/types.ts +140 -0
  160. package/src/core/build/_ripple.scss +6 -6
  161. package/src/core/build/ripple.ts +72 -95
  162. package/src/core/compose/component.ts +1 -1
  163. package/src/core/compose/features/badge.ts +79 -0
  164. package/src/core/compose/features/icon.ts +3 -1
  165. package/src/core/compose/features/index.ts +3 -1
  166. package/src/core/compose/features/ripple.ts +4 -1
  167. package/src/core/compose/features/textlabel.ts +26 -2
  168. package/src/core/dom/create.ts +5 -0
  169. package/src/index.ts +9 -0
  170. package/src/styles/abstract/_theme.scss +115 -3
  171. package/src/styles/themes/_autumn.scss +21 -0
  172. package/src/styles/themes/_base-theme.scss +61 -0
  173. package/src/styles/themes/_baseline.scss +58 -0
  174. package/src/styles/themes/_bluekhaki.scss +125 -0
  175. package/src/styles/themes/_brownbeige.scss +125 -0
  176. package/src/styles/themes/_browngreen.scss +125 -0
  177. package/src/styles/themes/_forest.scss +6 -0
  178. package/src/styles/themes/_greenbeige.scss +125 -0
  179. package/src/styles/themes/_material.scss +125 -0
  180. package/src/styles/themes/_ocean.scss +6 -0
  181. package/src/styles/themes/_sageivory.scss +125 -0
  182. package/src/styles/themes/_spring.scss +6 -0
  183. package/src/styles/themes/_summer.scss +5 -0
  184. package/src/styles/themes/_sunset.scss +5 -0
  185. package/src/styles/themes/_tealcaramel.scss +125 -0
  186. package/src/styles/themes/_winter.scss +6 -0
  187. package/src/components/card/actions.ts +0 -48
  188. package/src/components/card/header.ts +0 -88
  189. package/src/components/card/media.ts +0 -52
  190. package/src/components/navigation/features/items.js +0 -192
  191. package/src/components/slider/features/appearance.ts +0 -94
  192. package/src/components/slider/features/disabled.ts +0 -43
  193. package/src/components/slider/features/events.ts +0 -164
  194. package/src/components/slider/features/interactions.ts +0 -261
  195. package/src/components/slider/features/keyboard.ts +0 -112
  196. package/src/core/collection/adapters/mongodb.js +0 -232
@@ -0,0 +1,285 @@
1
+ // src/components/tabs/indicator.ts
2
+ import { TabComponent } from './types';
3
+
4
+ /**
5
+ * Configuration for tab indicator
6
+ */
7
+ export interface TabIndicatorConfig {
8
+ /** Height of the indicator in pixels */
9
+ height?: number;
10
+ /** Width strategy - fixed size or dynamic based on tab width */
11
+ widthStrategy?: 'fixed' | 'dynamic' | 'content' | 'auto';
12
+ /** Fixed width in pixels when using fixed strategy */
13
+ fixedWidth?: number;
14
+ /** Animation duration in milliseconds */
15
+ animationDuration?: number;
16
+ /** Animation timing function */
17
+ animationTiming?: string;
18
+ /** Whether to show the indicator */
19
+ visible?: boolean;
20
+ /** CSS class prefix */
21
+ prefix?: string;
22
+ /** Custom color for the indicator */
23
+ color?: string;
24
+ /** Tabs variant (primary or secondary) */
25
+ variant?: string;
26
+ }
27
+
28
+ /**
29
+ * Tab indicator API
30
+ */
31
+ export interface TabIndicator {
32
+ /** The indicator DOM element */
33
+ element: HTMLElement;
34
+ /** Move the indicator to a specific tab */
35
+ moveToTab: (tab: TabComponent, immediate?: boolean) => void;
36
+ /** Show the indicator */
37
+ show: () => void;
38
+ /** Hide the indicator */
39
+ hide: () => void;
40
+ /** Set indicator color */
41
+ setColor: (color: string) => void;
42
+ /** Update indicator position (e.g. after resize) */
43
+ update: () => void;
44
+ /** Destroy the indicator and clean up */
45
+ destroy: () => void;
46
+ }
47
+
48
+ /**
49
+ * Default configuration for tab indicator
50
+ */
51
+ const DEFAULT_CONFIG: TabIndicatorConfig = {
52
+ widthStrategy: 'auto', // Changed to 'auto' to match variant behavior
53
+ fixedWidth: 40,
54
+ animationDuration: 250,
55
+ animationTiming: 'cubic-bezier(0.4, 0, 0.2, 1)',
56
+ visible: true,
57
+ prefix: 'mtrl',
58
+ variant: 'primary'
59
+ };
60
+
61
+ /**
62
+ * Creates a tab indicator component
63
+ * @param config - Indicator configuration
64
+ * @returns Tab indicator instance
65
+ */
66
+ export const createTabIndicator = (config: TabIndicatorConfig = {}): TabIndicator => {
67
+ // Merge with default config
68
+ const mergedConfig = { ...DEFAULT_CONFIG, ...config };
69
+ const prefix = mergedConfig.prefix || 'mtrl';
70
+
71
+ // Create indicator element
72
+ const element = document.createElement('div');
73
+ element.className = `${prefix}-tabs-indicator`;
74
+ element.style.transition = `transform ${mergedConfig.animationDuration}ms ${mergedConfig.animationTiming},
75
+ width ${mergedConfig.animationDuration}ms ${mergedConfig.animationTiming}`;
76
+ element.style.width = `${mergedConfig.fixedWidth}px`; // Set initial width
77
+
78
+ // Set initial visibility
79
+ if (!mergedConfig.visible) {
80
+ element.style.opacity = '0';
81
+ }
82
+
83
+ // Track current tab to be able to update on resize
84
+ let currentTab: TabComponent | null = null;
85
+
86
+ /**
87
+ * Calculates indicator width based on strategy and variant
88
+ * @param tab - Target tab
89
+ * @returns Width in pixels
90
+ */
91
+ const calculateWidth = (tab: TabComponent): number => {
92
+ // Use auto strategy to determine based on variant
93
+ if (mergedConfig.widthStrategy === 'auto') {
94
+ // For secondary tabs, use full tab width
95
+ if (mergedConfig.variant === 'secondary') {
96
+ return tab.element.offsetWidth;
97
+ }
98
+
99
+ // For primary tabs (default), use text label width
100
+ const textElement = tab.element.querySelector(`.${prefix}-tab-text`) ||
101
+ tab.element.querySelector(`.${prefix}-button-text`);
102
+
103
+ if (textElement) {
104
+ return textElement.clientWidth;
105
+ }
106
+
107
+ // Fallback to dynamic if text element not found
108
+ return Math.max(tab.element.offsetWidth / 2, 30);
109
+ }
110
+
111
+ // Handle other strategies when explicitly set
112
+ switch (mergedConfig.widthStrategy) {
113
+ case 'dynamic':
114
+ return Math.max(tab.element.offsetWidth / 2, 30);
115
+ case 'content':
116
+ // Try to match content width
117
+ const text = tab.element.querySelector(`.${prefix}-button-text`) ||
118
+ tab.element.querySelector(`.${prefix}-tab-text`);
119
+ if (text) {
120
+ return Math.max(text.clientWidth, 30);
121
+ }
122
+ return mergedConfig.fixedWidth || 40;
123
+ case 'fixed':
124
+ default:
125
+ return mergedConfig.fixedWidth || 40;
126
+ }
127
+ };
128
+
129
+ /**
130
+ * Gets the direct DOM position for a tab element
131
+ * @param tabElement - The tab element
132
+ * @returns {Object} Position information
133
+ */
134
+ const getTabPosition = (tabElement: HTMLElement): { left: number, width: number } => {
135
+ // Find the scroll container (should be the parent of the tab)
136
+ const scrollContainer = tabElement.parentElement;
137
+ if (!scrollContainer) {
138
+ console.error('Tab has no parent element, cannot position indicator');
139
+ return { left: 0, width: tabElement.offsetWidth };
140
+ }
141
+
142
+ // Get positions using getBoundingClientRect for most accurate values
143
+ const tabRect = tabElement.getBoundingClientRect();
144
+ const containerRect = scrollContainer.getBoundingClientRect();
145
+
146
+ // Calculate position relative to scroll container
147
+ return {
148
+ left: tabRect.left - containerRect.left,
149
+ width: tabRect.width
150
+ };
151
+ };
152
+
153
+ /**
154
+ * Calculates indicator position based on variant and width
155
+ * @param tab - Target tab
156
+ * @param indicatorWidth - Width of the indicator
157
+ * @returns {Object} Position information
158
+ */
159
+ const calculatePosition = (tab: TabComponent, indicatorWidth: number): { left: number } => {
160
+ const { left, width: tabWidth } = getTabPosition(tab.element);
161
+
162
+ // For primary tabs with text label width, center under the text
163
+ if (mergedConfig.variant !== 'secondary' &&
164
+ (mergedConfig.widthStrategy === 'auto' || mergedConfig.widthStrategy === 'content')) {
165
+ const textElement = tab.element.querySelector(`.${prefix}-tab-text`) ||
166
+ tab.element.querySelector(`.${prefix}-button-text`);
167
+
168
+ if (textElement) {
169
+ // Get text element position relative to tab
170
+ const textRect = textElement.getBoundingClientRect();
171
+ const tabRect = tab.element.getBoundingClientRect();
172
+ const textLeft = textRect.left - tabRect.left;
173
+
174
+ // Center indicator under text
175
+ return {
176
+ left: left + textLeft
177
+ };
178
+ }
179
+ }
180
+
181
+ // For secondary tabs or when no text element found
182
+ // For centered indicators, center in the tab
183
+ if (indicatorWidth < tabWidth) {
184
+ return {
185
+ left: left + ((tabWidth - indicatorWidth) / 2)
186
+ };
187
+ }
188
+
189
+ // For full-width indicators
190
+ return { left };
191
+ };
192
+
193
+ /**
194
+ * Moves indicator to specified tab
195
+ * @param tab - Target tab
196
+ * @param immediate - Whether to skip animation
197
+ */
198
+ const moveToTab = (tab: TabComponent, immediate: boolean = false): void => {
199
+ if (!tab || !tab.element) {
200
+ console.error('Invalid tab or tab has no element');
201
+ return;
202
+ }
203
+
204
+ // Store current tab for later updates
205
+ currentTab = tab;
206
+
207
+ // Calculate indicator width based on strategy and variant
208
+ const width = calculateWidth(tab);
209
+
210
+ // Calculate position based on width and variant
211
+ const { left } = calculatePosition(tab, width);
212
+
213
+ // Apply position immediately if requested
214
+ if (immediate) {
215
+ element.style.transition = 'none';
216
+
217
+ // Force reflow to ensure transition is skipped
218
+ element.offsetHeight; // eslint-disable-line no-unused-expressions
219
+ }
220
+
221
+ // Update position and width
222
+ element.style.width = `${width}px`;
223
+ element.style.transform = `translateX(${left}px)`;
224
+
225
+ // Restore transition after immediate update
226
+ if (immediate) {
227
+ // Need to use timeout to ensure browser processes the style change
228
+ setTimeout(() => {
229
+ element.style.transition = `transform ${mergedConfig.animationDuration}ms ${mergedConfig.animationTiming},
230
+ width ${mergedConfig.animationDuration}ms ${mergedConfig.animationTiming}`;
231
+ }, 10);
232
+ }
233
+ };
234
+
235
+ /**
236
+ * Updates indicator position (e.g. after resize)
237
+ */
238
+ const update = (): void => {
239
+ if (currentTab) {
240
+ moveToTab(currentTab, true);
241
+ }
242
+ };
243
+
244
+ /**
245
+ * Shows the indicator
246
+ */
247
+ const show = (): void => {
248
+ element.style.opacity = '1';
249
+ };
250
+
251
+ /**
252
+ * Hides the indicator
253
+ */
254
+ const hide = (): void => {
255
+ element.style.opacity = '0';
256
+ };
257
+
258
+ /**
259
+ * Sets indicator color
260
+ * @param color - CSS color value
261
+ */
262
+ const setColor = (color: string): void => {
263
+ element.style.backgroundColor = color;
264
+ };
265
+
266
+ /**
267
+ * Cleans up and destroys the indicator
268
+ */
269
+ const destroy = (): void => {
270
+ if (element.parentNode) {
271
+ element.parentNode.removeChild(element);
272
+ }
273
+ currentTab = null;
274
+ };
275
+
276
+ return {
277
+ element,
278
+ moveToTab,
279
+ show,
280
+ hide,
281
+ setColor,
282
+ update,
283
+ destroy
284
+ };
285
+ };
@@ -0,0 +1,144 @@
1
+ // src/components/tabs/responsive.ts
2
+ import { TabsComponent, TabComponent } from './types';
3
+
4
+ /**
5
+ * Breakpoints for responsive behavior
6
+ */
7
+ export const RESPONSIVE_BREAKPOINTS = {
8
+ /** Small screens (mobile) */
9
+ SMALL: 600,
10
+ /** Medium screens (tablet) */
11
+ MEDIUM: 904,
12
+ /** Large screens (desktop) */
13
+ LARGE: 1240
14
+ };
15
+
16
+ /**
17
+ * Configuration for responsive behavior
18
+ */
19
+ export interface ResponsiveConfig {
20
+ /** Whether to enable responsive behavior */
21
+ responsive?: boolean;
22
+ /** Options for small screens */
23
+ smallScreen?: {
24
+ /** Layout to use on small screens */
25
+ layout?: 'icon-only' | 'text-only' | 'icon-and-text';
26
+ /** Maximum tabs to show before scrolling */
27
+ maxVisibleTabs?: number;
28
+ };
29
+ /** Custom breakpoint values */
30
+ breakpoints?: {
31
+ small?: number;
32
+ medium?: number;
33
+ large?: number;
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Enhances tabs with responsive behavior
39
+ * @param tabs - The tabs component to enhance
40
+ * @param config - Responsive configuration
41
+ */
42
+ export const setupResponsiveBehavior = (
43
+ tabs: TabsComponent,
44
+ config: ResponsiveConfig = {}
45
+ ): void => {
46
+ if (config.responsive === false) return;
47
+
48
+ // Merge custom breakpoints with defaults
49
+ const breakpoints = {
50
+ small: config.breakpoints?.small || RESPONSIVE_BREAKPOINTS.SMALL,
51
+ medium: config.breakpoints?.medium || RESPONSIVE_BREAKPOINTS.MEDIUM,
52
+ large: config.breakpoints?.large || RESPONSIVE_BREAKPOINTS.LARGE
53
+ };
54
+
55
+ // Default small screen configuration
56
+ const smallScreen = {
57
+ layout: config.smallScreen?.layout || 'icon-only',
58
+ maxVisibleTabs: config.smallScreen?.maxVisibleTabs || 4
59
+ };
60
+
61
+ // Store original tab layouts to restore later
62
+ const originalLayouts = new Map<TabComponent, string>();
63
+
64
+ // Get all tabs
65
+ const allTabs = tabs.getTabs();
66
+
67
+ // Save original layouts
68
+ allTabs.forEach(tab => {
69
+ // Determine current layout
70
+ let layout = 'text-only';
71
+ if (tab.getIcon() && tab.getText()) {
72
+ layout = 'icon-and-text';
73
+ } else if (tab.getIcon()) {
74
+ layout = 'icon-only';
75
+ }
76
+
77
+ originalLayouts.set(tab, layout);
78
+ });
79
+
80
+ /**
81
+ * Update tabs layout based on screen size
82
+ */
83
+ const updateLayout = (): void => {
84
+ const width = window.innerWidth;
85
+
86
+ if (width < breakpoints.small) {
87
+ // Small screen behavior
88
+ allTabs.forEach(tab => {
89
+ // Skip if tab has no icon but we want icon-only
90
+ if (smallScreen.layout === 'icon-only' && !tab.getIcon()) {
91
+ return;
92
+ }
93
+
94
+ // Apply layout according to small screen config
95
+ if (smallScreen.layout === 'icon-only' && tab.getIcon()) {
96
+ // Keep text for accessibility but visually show only icon
97
+ tab.element.classList.remove(`${tab.getClass('tab')}--icon-and-text`);
98
+ tab.element.classList.remove(`${tab.getClass('tab')}--text-only`);
99
+ tab.element.classList.add(`${tab.getClass('tab')}--icon-only`);
100
+ } else if (smallScreen.layout === 'text-only') {
101
+ tab.element.classList.remove(`${tab.getClass('tab')}--icon-and-text`);
102
+ tab.element.classList.remove(`${tab.getClass('tab')}--icon-only`);
103
+ tab.element.classList.add(`${tab.getClass('tab')}--text-only`);
104
+ }
105
+ });
106
+
107
+ // Add responsive class
108
+ tabs.element.classList.add(`${tabs.getClass('tabs')}--responsive-small`);
109
+ } else {
110
+ // Restore original layouts for medium and large screens
111
+ allTabs.forEach(tab => {
112
+ const originalLayout = originalLayouts.get(tab) || 'text-only';
113
+
114
+ tab.element.classList.remove(`${tab.getClass('tab')}--icon-only`);
115
+ tab.element.classList.remove(`${tab.getClass('tab')}--text-only`);
116
+ tab.element.classList.remove(`${tab.getClass('tab')}--icon-and-text`);
117
+
118
+ tab.element.classList.add(`${tab.getClass('tab')}--${originalLayout}`);
119
+ });
120
+
121
+ // Remove responsive class
122
+ tabs.element.classList.remove(`${tabs.getClass('tabs')}--responsive-small`);
123
+ }
124
+ };
125
+
126
+ // Initial layout update
127
+ updateLayout();
128
+
129
+ // Set up resize listener
130
+ const resizeObserver = new ResizeObserver(updateLayout);
131
+ resizeObserver.observe(document.body);
132
+
133
+ // Store the observer on the component for cleanup
134
+ (tabs as any)._resizeObserver = resizeObserver;
135
+
136
+ // Enhance destroy method to clean up observer
137
+ const originalDestroy = tabs.destroy;
138
+ tabs.destroy = function() {
139
+ if ((this as any)._resizeObserver) {
140
+ (this as any)._resizeObserver.disconnect();
141
+ }
142
+ originalDestroy.call(this);
143
+ };
144
+ };
@@ -0,0 +1,149 @@
1
+ // src/components/tabs/scroll-indicators.ts
2
+ import { TabsComponent } from './types';
3
+
4
+ /**
5
+ * Configuration for scroll indicators
6
+ */
7
+ export interface ScrollIndicatorConfig {
8
+ /** Whether to show scroll indicators */
9
+ enabled?: boolean;
10
+ /** Whether to add scroll buttons */
11
+ showButtons?: boolean;
12
+ /** Scroll indicator appearance */
13
+ appearance?: 'fade' | 'shadow';
14
+ }
15
+
16
+ /**
17
+ * Adds scroll indicators to a tabs component
18
+ * @param tabs - Tabs component to enhance
19
+ * @param config - Scroll indicator configuration
20
+ */
21
+ export const addScrollIndicators = (
22
+ tabs: TabsComponent,
23
+ config: ScrollIndicatorConfig = {}
24
+ ): void => {
25
+ const {
26
+ enabled = true,
27
+ showButtons = false,
28
+ appearance = 'fade'
29
+ } = config;
30
+
31
+ if (!enabled) return;
32
+
33
+ // Find scroll container
34
+ const scrollContainer = tabs.element.querySelector(`.${tabs.getClass('tabs')}-scroll`);
35
+ if (!scrollContainer) return;
36
+
37
+ // Add indicator elements
38
+ const leftIndicator = document.createElement('div');
39
+ leftIndicator.className = `${tabs.getClass('tabs')}-scroll-indicator ${tabs.getClass('tabs')}-scroll-indicator--left`;
40
+ leftIndicator.classList.add(`${tabs.getClass('tabs')}-scroll-indicator--${appearance}`);
41
+
42
+ const rightIndicator = document.createElement('div');
43
+ rightIndicator.className = `${tabs.getClass('tabs')}-scroll-indicator ${tabs.getClass('tabs')}-scroll-indicator--right`;
44
+ rightIndicator.classList.add(`${tabs.getClass('tabs')}-scroll-indicator--${appearance}`);
45
+
46
+ tabs.element.appendChild(leftIndicator);
47
+ tabs.element.appendChild(rightIndicator);
48
+
49
+ // Add buttons if requested
50
+ if (showButtons) {
51
+ const leftButton = document.createElement('button');
52
+ leftButton.className = `${tabs.getClass('tabs')}-scroll-button ${tabs.getClass('tabs')}-scroll-button--left`;
53
+ leftButton.setAttribute('aria-label', 'Scroll tabs left');
54
+ leftButton.innerHTML = '<svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>';
55
+
56
+ const rightButton = document.createElement('button');
57
+ rightButton.className = `${tabs.getClass('tabs')}-scroll-button ${tabs.getClass('tabs')}-scroll-button--right`;
58
+ rightButton.setAttribute('aria-label', 'Scroll tabs right');
59
+ rightButton.innerHTML = '<svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>';
60
+
61
+ tabs.element.appendChild(leftButton);
62
+ tabs.element.appendChild(rightButton);
63
+
64
+ // Add button click handlers
65
+ leftButton.addEventListener('click', () => {
66
+ scrollContainer.scrollBy({
67
+ left: -100,
68
+ behavior: 'smooth'
69
+ });
70
+ });
71
+
72
+ rightButton.addEventListener('click', () => {
73
+ scrollContainer.scrollBy({
74
+ left: 100,
75
+ behavior: 'smooth'
76
+ });
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Updates indicator visibility based on scroll position
82
+ */
83
+ const updateIndicators = (): void => {
84
+ const { scrollLeft, scrollWidth, clientWidth } = scrollContainer as HTMLElement;
85
+
86
+ // Show left indicator only when scrolled right
87
+ leftIndicator.classList.toggle('visible', scrollLeft > 0);
88
+
89
+ // Show right indicator only when more content is available to scroll
90
+ rightIndicator.classList.toggle('visible', scrollLeft + clientWidth < scrollWidth - 1);
91
+
92
+ // Update button states if present
93
+ if (showButtons) {
94
+ const leftButton = tabs.element.querySelector(`.${tabs.getClass('tabs')}-scroll-button--left`) as HTMLButtonElement;
95
+ const rightButton = tabs.element.querySelector(`.${tabs.getClass('tabs')}-scroll-button--right`) as HTMLButtonElement;
96
+
97
+ if (leftButton) {
98
+ leftButton.disabled = scrollLeft <= 0;
99
+ }
100
+
101
+ if (rightButton) {
102
+ rightButton.disabled = scrollLeft + clientWidth >= scrollWidth - 1;
103
+ }
104
+ }
105
+ };
106
+
107
+ // Initial update
108
+ updateIndicators();
109
+
110
+ // Add scroll listener
111
+ scrollContainer.addEventListener('scroll', updateIndicators);
112
+
113
+ // Add resize observer to update on container size changes
114
+ const resizeObserver = new ResizeObserver(updateIndicators);
115
+ resizeObserver.observe(scrollContainer as Element);
116
+
117
+ // Add cleanup to component destroy method
118
+ const originalDestroy = tabs.destroy;
119
+ tabs.destroy = function() {
120
+ if (resizeObserver) {
121
+ resizeObserver.disconnect();
122
+ }
123
+
124
+ scrollContainer.removeEventListener('scroll', updateIndicators);
125
+
126
+ if (leftIndicator.parentNode) {
127
+ leftIndicator.parentNode.removeChild(leftIndicator);
128
+ }
129
+
130
+ if (rightIndicator.parentNode) {
131
+ rightIndicator.parentNode.removeChild(rightIndicator);
132
+ }
133
+
134
+ if (showButtons) {
135
+ const leftButton = tabs.element.querySelector(`.${tabs.getClass('tabs')}-scroll-button--left`);
136
+ const rightButton = tabs.element.querySelector(`.${tabs.getClass('tabs')}-scroll-button--right`);
137
+
138
+ if (leftButton && leftButton.parentNode) {
139
+ leftButton.parentNode.removeChild(leftButton);
140
+ }
141
+
142
+ if (rightButton && rightButton.parentNode) {
143
+ rightButton.parentNode.removeChild(rightButton);
144
+ }
145
+ }
146
+
147
+ originalDestroy.call(this);
148
+ };
149
+ };