mtrl 0.2.8 → 0.3.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 (64) hide show
  1. package/index.ts +4 -0
  2. package/package.json +1 -1
  3. package/src/components/button/button.ts +34 -5
  4. package/src/components/navigation/api.ts +131 -96
  5. package/src/components/navigation/features/controller.ts +273 -0
  6. package/src/components/navigation/features/items.ts +133 -64
  7. package/src/components/navigation/navigation.ts +17 -2
  8. package/src/components/navigation/system/core.ts +302 -0
  9. package/src/components/navigation/system/events.ts +240 -0
  10. package/src/components/navigation/system/index.ts +184 -0
  11. package/src/components/navigation/system/mobile.ts +278 -0
  12. package/src/components/navigation/system/state.ts +77 -0
  13. package/src/components/navigation/system/types.ts +364 -0
  14. package/src/components/slider/config.ts +20 -2
  15. package/src/components/slider/features/controller.ts +737 -0
  16. package/src/components/slider/features/handlers.ts +18 -16
  17. package/src/components/slider/features/index.ts +3 -2
  18. package/src/components/slider/features/range.ts +104 -0
  19. package/src/components/slider/schema.ts +141 -0
  20. package/src/components/slider/slider.ts +34 -13
  21. package/src/components/switch/api.ts +16 -0
  22. package/src/components/switch/config.ts +1 -18
  23. package/src/components/switch/features.ts +198 -0
  24. package/src/components/switch/index.ts +1 -0
  25. package/src/components/switch/switch.ts +3 -3
  26. package/src/components/switch/types.ts +14 -2
  27. package/src/components/textfield/api.ts +53 -0
  28. package/src/components/textfield/features.ts +322 -0
  29. package/src/components/textfield/textfield.ts +8 -0
  30. package/src/components/textfield/types.ts +12 -3
  31. package/src/components/timepicker/clockdial.ts +1 -4
  32. package/src/core/compose/features/textinput.ts +15 -2
  33. package/src/core/composition/features/dom.ts +45 -0
  34. package/src/core/composition/features/icon.ts +131 -0
  35. package/src/core/composition/features/index.ts +12 -0
  36. package/src/core/composition/features/label.ts +155 -0
  37. package/src/core/composition/features/layout.ts +47 -0
  38. package/src/core/composition/index.ts +26 -0
  39. package/src/core/index.ts +1 -1
  40. package/src/core/layout/README.md +350 -0
  41. package/src/core/layout/array.ts +181 -0
  42. package/src/core/layout/create.ts +55 -0
  43. package/src/core/layout/index.ts +26 -0
  44. package/src/core/layout/object.ts +124 -0
  45. package/src/core/layout/processor.ts +58 -0
  46. package/src/core/layout/result.ts +85 -0
  47. package/src/core/layout/types.ts +125 -0
  48. package/src/core/layout/utils.ts +136 -0
  49. package/src/index.ts +1 -0
  50. package/src/styles/abstract/_variables.scss +28 -0
  51. package/src/styles/components/_navigation-mobile.scss +244 -0
  52. package/src/styles/components/_navigation-system.scss +151 -0
  53. package/src/styles/components/_switch.scss +133 -69
  54. package/src/styles/components/_textfield.scss +259 -27
  55. package/demo/build.ts +0 -349
  56. package/demo/index.html +0 -110
  57. package/demo/main.js +0 -448
  58. package/demo/styles.css +0 -239
  59. package/server.ts +0 -86
  60. package/src/components/slider/features/slider.ts +0 -318
  61. package/src/components/slider/features/structure.ts +0 -181
  62. package/src/components/slider/features/ui.ts +0 -388
  63. package/src/components/textfield/constants.ts +0 -100
  64. package/src/core/layout/index.js +0 -95
@@ -7,20 +7,23 @@ import { SliderConfig, SliderEvent } from '../types';
7
7
  *
8
8
  * @param config Slider configuration
9
9
  * @param state Slider state object
10
- * @param uiHelpers UI helper methods
10
+ * @param uiRenderer UI renderer interface from controller
11
11
  * @param eventHelpers Event helper methods
12
12
  * @returns Event handlers for all slider interactions
13
13
  */
14
- export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelpers) => {
14
+ export const createHandlers = (config: SliderConfig, state, uiRenderer, eventHelpers) => {
15
15
  // Get required elements from structure (with fallbacks)
16
+ const components = state.component?.components || {};
17
+
18
+ // Extract needed components
16
19
  const {
17
20
  container = null,
18
- track = null,
19
- handle = null,
21
+ handle = null,
22
+ track = null,
20
23
  valueBubble = null,
21
24
  secondHandle = null,
22
25
  secondValueBubble = null
23
- } = state.component?.structure || {};
26
+ } = components;
24
27
 
25
28
  // Get required helper methods (with fallbacks)
26
29
  const {
@@ -28,8 +31,8 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
28
31
  roundToStep = value => value,
29
32
  clamp = (value, min, max) => value,
30
33
  showValueBubble = () => {},
31
- updateUi = () => {}
32
- } = uiHelpers;
34
+ render = () => {}
35
+ } = uiRenderer;
33
36
 
34
37
  const { triggerEvent = () => ({ defaultPrevented: false }) } = eventHelpers;
35
38
 
@@ -127,7 +130,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
127
130
  document.addEventListener('touchmove', handleMouseMove, { passive: false });
128
131
  document.addEventListener('touchend', handleMouseUp);
129
132
 
130
- updateUi();
133
+ render();
131
134
  triggerEvent(SLIDER_EVENTS.START, e);
132
135
  };
133
136
 
@@ -169,9 +172,8 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
169
172
  }
170
173
 
171
174
  // Update UI and trigger events
172
- updateUi();
175
+ render();
173
176
  triggerEvent(SLIDER_EVENTS.INPUT, e);
174
- triggerEvent(SLIDER_EVENTS.CHANGE, e);
175
177
  } catch (error) {
176
178
  console.warn('Error handling track click:', error);
177
179
  }
@@ -244,7 +246,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
244
246
  state.value = newValue;
245
247
  }
246
248
 
247
- updateUi();
249
+ render();
248
250
  triggerEvent(SLIDER_EVENTS.INPUT, e);
249
251
  } catch (error) {
250
252
  console.warn('Error during slider drag:', error);
@@ -274,7 +276,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
274
276
 
275
277
  // Reset active handle and update UI
276
278
  state.activeHandle = null;
277
- updateUi();
279
+ render();
278
280
 
279
281
  // Trigger events
280
282
  triggerEvent(SLIDER_EVENTS.CHANGE, e);
@@ -355,7 +357,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
355
357
  }
356
358
 
357
359
  // Update UI and trigger events
358
- updateUi();
360
+ render();
359
361
  triggerEvent(SLIDER_EVENTS.INPUT, e);
360
362
  triggerEvent(SLIDER_EVENTS.CHANGE, e);
361
363
  };
@@ -406,7 +408,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
406
408
  * Set up all event listeners
407
409
  */
408
410
  const setupEventListeners = () => {
409
- if (!state.component || !state.component.structure) {
411
+ if (!state.component || !state.component.components) {
410
412
  console.warn('Cannot set up event listeners: missing component structure');
411
413
  return;
412
414
  }
@@ -436,7 +438,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
436
438
  * Clean up all event listeners
437
439
  */
438
440
  const cleanupEventListeners = () => {
439
- if (!state.component || !state.component.structure) return;
441
+ if (!state.component || !state.component.components) return;
440
442
 
441
443
  // Clean up container listeners
442
444
  if (container) {
@@ -492,4 +494,4 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
492
494
  hideAllBubbles,
493
495
  clearKeyboardFocus
494
496
  };
495
- };
497
+ }
@@ -1,4 +1,5 @@
1
1
  // src/components/slider/features/index.ts
2
- export { withStructure } from './structure';
2
+ // Export slider-specific features
3
+ export { withRange } from './range';
3
4
  export { withStates } from './states';
4
- export { withSlider } from './slider';
5
+ export { withController } from './controller';
@@ -0,0 +1,104 @@
1
+ // src/components/slider/features/range.ts
2
+ import { SliderConfig } from '../types';
3
+ import { createElement } from '../../../core/dom/create';
4
+
5
+ /**
6
+ * Enhances structure definition with range slider elements
7
+ *
8
+ * @param config Slider configuration
9
+ * @returns Component enhancer that adds range slider to structure
10
+ */
11
+ export const withRange = (config: SliderConfig) => component => {
12
+ // If not a range slider or missing structure definition, return unmodified
13
+ if (!config.range || !config.secondValue || !component.schema) {
14
+ return component;
15
+ }
16
+
17
+ try {
18
+ // Calculate values for second handle
19
+ const min = config.min || 0;
20
+ const max = config.max || 100;
21
+ const secondValue = config.secondValue;
22
+ const secondValuePercent = ((secondValue - min) / (max - min)) * 100;
23
+ const formatter = config.valueFormatter || (val => val.toString());
24
+ const isDisabled = config.disabled === true;
25
+ const getClass = component.getClass;
26
+
27
+ // Clone the structure definition (deep copy)
28
+ const schema = JSON.parse(JSON.stringify(component.schema));
29
+
30
+ // Add range class to root element
31
+ const rootClasses = schema.element.options.className || [];
32
+ if (Array.isArray(rootClasses)) {
33
+ rootClasses.push(getClass('slider--range'));
34
+ } else {
35
+ schema.element.options.className = `${rootClasses} ${getClass('slider--range')}`.trim();
36
+ }
37
+
38
+ // Add start track segment to track children
39
+ const trackChildren = schema.element.children.container.children.track.children;
40
+ trackChildren.startTrack = {
41
+ name: 'startTrack',
42
+ creator: createElement,
43
+ options: {
44
+ tag: 'div',
45
+ className: getClass('slider-start-track'),
46
+ style: {
47
+ width: '0%'
48
+ }
49
+ }
50
+ };
51
+
52
+ // Add second handle to container children
53
+ const containerChildren = schema.element.children.container.children;
54
+ containerChildren.secondHandle = {
55
+ name: 'secondHandle',
56
+ creator: createElement,
57
+ options: {
58
+ tag: 'div',
59
+ className: getClass('slider-handle'),
60
+ attrs: {
61
+ role: 'slider',
62
+ 'aria-valuemin': String(min),
63
+ 'aria-valuemax': String(max),
64
+ 'aria-valuenow': String(secondValue),
65
+ 'aria-orientation': 'horizontal',
66
+ tabindex: isDisabled ? '-1' : '0',
67
+ 'aria-disabled': isDisabled ? 'true' : 'false',
68
+ 'data-value': String(secondValue),
69
+ 'data-handle-index': '1'
70
+ },
71
+ style: {
72
+ left: `${secondValuePercent}%`
73
+ }
74
+ }
75
+ };
76
+
77
+ // Add second value bubble to container children
78
+ containerChildren.secondValueBubble = {
79
+ name: 'secondValueBubble',
80
+ creator: createElement,
81
+ options: {
82
+ tag: 'div',
83
+ className: getClass('slider-value'),
84
+ attrs: {
85
+ 'aria-hidden': 'true',
86
+ 'data-handle-index': '1'
87
+ },
88
+ text: formatter(secondValue),
89
+ style: {
90
+ left: `${secondValuePercent}%`
91
+ }
92
+ }
93
+ };
94
+
95
+ // Return component with updated structure definition
96
+ return {
97
+ ...component,
98
+ schema
99
+ };
100
+ } catch (error) {
101
+ console.warn('Error enhancing structure with range functionality:', error);
102
+ return component;
103
+ }
104
+ };
@@ -0,0 +1,141 @@
1
+ // src/components/slider/structure.ts
2
+ import { SliderConfig } from './types';
3
+
4
+ /**
5
+ * Creates the base slider structure definition
6
+ *
7
+ * @param component Component for class name generation
8
+ * @param config Slider configuration
9
+ * @returns Structure schema object
10
+ */
11
+ export function createSliderSchema(component, config: SliderConfig) {
12
+ // Get prefixed class names
13
+ const getClass = (className) => component.getClass(className);
14
+
15
+ // Set default values
16
+ const min = config.min || 0;
17
+ const max = config.max || 100;
18
+ const value = config.value !== undefined ? config.value : min;
19
+ const isDisabled = config.disabled === true;
20
+ const formatter = config.valueFormatter || (val => val.toString());
21
+
22
+ // Calculate initial position
23
+ const valuePercent = ((value - min) / (max - min)) * 100;
24
+
25
+ // Return base structure definition formatted for createStructure
26
+ return {
27
+ element: {
28
+ options: {
29
+ className: [getClass('slider'), config.class].filter(Boolean),
30
+ attrs: {
31
+ tabindex: '-1',
32
+ role: 'none',
33
+ 'aria-disabled': isDisabled ? 'true' : 'false'
34
+ }
35
+ },
36
+ children: {
37
+ // Container with all slider elements
38
+ container: {
39
+ options: {
40
+ className: getClass('slider-container')
41
+ },
42
+ children: {
43
+ // Track with segments
44
+ track: {
45
+ options: {
46
+ className: getClass('slider-track')
47
+ },
48
+ children: {
49
+ activeTrack: {
50
+ options: {
51
+ className: getClass('slider-active-track'),
52
+ style: {
53
+ width: `${valuePercent}%`
54
+ }
55
+ }
56
+ },
57
+ startTrack: {
58
+ options: {
59
+ className: getClass('slider-start-track'),
60
+ style: {
61
+ display: 'none', // Initially hidden for single slider
62
+ width: '0%'
63
+ }
64
+ }
65
+ },
66
+ remainingTrack: {
67
+ options: {
68
+ className: getClass('slider-remaining-track'),
69
+ style: {
70
+ width: `${100 - valuePercent}%`
71
+ }
72
+ }
73
+ }
74
+ }
75
+ },
76
+
77
+ // Ticks container
78
+ ticksContainer: {
79
+ options: {
80
+ className: getClass('slider-ticks-container')
81
+ }
82
+ },
83
+
84
+ // Dots for ends
85
+ startDot: {
86
+ options: {
87
+ className: [
88
+ getClass('slider-dot'),
89
+ getClass('slider-dot--start')
90
+ ]
91
+ }
92
+ },
93
+ endDot: {
94
+ options: {
95
+ className: [
96
+ getClass('slider-dot'),
97
+ getClass('slider-dot--end')
98
+ ]
99
+ }
100
+ },
101
+
102
+ // Main handle
103
+ handle: {
104
+ options: {
105
+ className: getClass('slider-handle'),
106
+ attrs: {
107
+ role: 'slider',
108
+ 'aria-valuemin': String(min),
109
+ 'aria-valuemax': String(max),
110
+ 'aria-valuenow': String(value),
111
+ 'aria-orientation': 'horizontal',
112
+ tabindex: isDisabled ? '-1' : '0',
113
+ 'aria-disabled': isDisabled ? 'true' : 'false',
114
+ 'data-value': String(value),
115
+ 'data-handle-index': '0'
116
+ },
117
+ style: {
118
+ left: `${valuePercent}%`
119
+ }
120
+ }
121
+ },
122
+ // Main value bubble
123
+ valueBubble: {
124
+ options: {
125
+ className: getClass('slider-value'),
126
+ attrs: {
127
+ 'aria-hidden': 'true',
128
+ 'data-handle-index': '0'
129
+ },
130
+ text: formatter(value),
131
+ style: {
132
+ left: `${valuePercent}%`
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ };
141
+ }
@@ -1,42 +1,63 @@
1
1
  // src/components/slider/slider.ts
2
2
  import { pipe } from '../../core/compose/pipe';
3
- import { createBase, withElement } from '../../core/compose/component';
4
- import { withEvents, withLifecycle, withTextLabel, withIcon } from '../../core/compose/features';
5
- import { withStructure, withSlider } from './features';
6
- import { withStates } from './features/states';
3
+ import { createBase } from '../../core/compose/component';
4
+ import { withEvents, withLifecycle } from '../../core/compose/features';
5
+ import { withLayout, withIcon, withLabel, withDom } from '../../core/composition/features';
6
+ import {
7
+ withRange,
8
+ withStates,
9
+ withController
10
+ } from './features';
7
11
  import { withAPI } from './api';
8
12
  import { SliderConfig, SliderComponent } from './types';
9
- import { createBaseConfig, getElementConfig, getApiConfig } from './config';
13
+ import { createBaseConfig, getApiConfig } from './config';
10
14
 
11
15
  /**
12
16
  * Creates a new Slider component
17
+ *
18
+ * Slider follows a clear architectural pattern:
19
+ * 1. Structure definition - Describes the DOM structure declaratively
20
+ * 2. Feature enhancement - Adds specific capabilities (range, icons, labels)
21
+ * 3. DOM creation - Turns the structure into actual DOM elements
22
+ * 4. State management - Handles visual states and appearance
23
+ * 5. Controller - Manages behavior, events, and UI rendering
24
+ * 6. Lifecycle - Handles component lifecycle events
25
+ * 7. Public API - Exposes a clean, consistent API
26
+ *
13
27
  * @param {SliderConfig} config - Slider configuration object
14
28
  * @returns {SliderComponent} Slider component instance
15
29
  */
16
30
  const createSlider = (config: SliderConfig = {}): SliderComponent => {
31
+ // Process configuration with defaults
17
32
  const baseConfig = createBaseConfig(config);
18
33
 
19
34
  try {
20
- // Create the component with all required features
35
+ // Create the component by composing features in a specific order
21
36
  const component = pipe(
37
+ // Base component with event system
22
38
  createBase,
23
39
  withEvents(),
24
- withElement(getElementConfig(baseConfig)),
40
+ withLayout(baseConfig),
25
41
  withIcon(baseConfig),
26
- withTextLabel(baseConfig),
27
- withStructure(baseConfig),
42
+ withLabel(baseConfig),
43
+ withRange(baseConfig),
44
+
45
+ // Now create the actual DOM elements from the complete structure
46
+ withDom(),
47
+
48
+ // Add state management and behavior
28
49
  withStates(baseConfig),
29
- withSlider(baseConfig),
50
+ withController(baseConfig),
30
51
  withLifecycle()
31
52
  )(baseConfig);
32
53
 
33
- // Generate the API configuration
54
+ // Generate the API configuration based on the enhanced component
34
55
  const apiOptions = getApiConfig(component);
35
56
 
36
- // Apply the API layer
57
+ // Apply the public API layer
37
58
  const slider = withAPI(apiOptions)(component);
38
59
 
39
- // Register event handlers from config
60
+ // Register event handlers from config for convenience
40
61
  if (baseConfig.on && typeof slider.on === 'function') {
41
62
  Object.entries(baseConfig.on).forEach(([event, handler]) => {
42
63
  if (typeof handler === 'function') {
@@ -48,6 +48,22 @@ export const withAPI = ({ disabled, lifecycle, checkable }: ApiOptions) =>
48
48
  getLabel(): string {
49
49
  return component.text?.getText() || '';
50
50
  },
51
+
52
+ // Supporting text management (if present)
53
+ supportingTextElement: component.supportingTextElement || null,
54
+ setSupportingText(text: string, isError?: boolean): SwitchComponent {
55
+ if (component.setSupportingText) {
56
+ component.setSupportingText(text, isError);
57
+ }
58
+ return this;
59
+ },
60
+
61
+ removeSupportingText(): SwitchComponent {
62
+ if (component.removeSupportingText) {
63
+ component.removeSupportingText();
64
+ }
65
+ return this;
66
+ },
51
67
 
52
68
  // Event handling
53
69
  on(event: string, handler: Function): SwitchComponent {
@@ -9,9 +9,7 @@ import { SwitchConfig, BaseComponent, ApiOptions } from './types';
9
9
  /**
10
10
  * Default configuration for the Switch component
11
11
  */
12
- export const defaultConfig: SwitchConfig = {
13
- labelPosition: 'end'
14
- };
12
+ export const defaultConfig: SwitchConfig = {};
15
13
 
16
14
  /**
17
15
  * Creates the base configuration for Switch component
@@ -34,21 +32,6 @@ export const getElementConfig = (config: SwitchConfig) =>
34
32
  interactive: true
35
33
  });
36
34
 
37
- /**
38
- * Applies label position class to the component
39
- * @param {SwitchConfig} config - Component configuration
40
- */
41
- export const withLabelPosition = (config: SwitchConfig) => (component: BaseComponent): BaseComponent => {
42
- if (!config.label) return component;
43
-
44
- const position = config.labelPosition || 'end';
45
- const positionClass = `${config.prefix}-switch--label-${position}`;
46
-
47
- component.element.classList.add(positionClass);
48
-
49
- return component;
50
- };
51
-
52
35
  /**
53
36
  * Creates API configuration for the Switch component
54
37
  * @param {BaseComponent} comp - Component with disabled, lifecycle, and checkable features
@@ -0,0 +1,198 @@
1
+ // src/components/switch/features.ts
2
+ import { BaseComponent, ElementComponent } from '../../core/compose/component';
3
+
4
+ /**
5
+ * Configuration for supporting text feature
6
+ */
7
+ export interface SupportingTextConfig {
8
+ /**
9
+ * Supporting text content
10
+ */
11
+ supportingText?: string;
12
+
13
+ /**
14
+ * Whether supporting text indicates an error
15
+ */
16
+ error?: boolean;
17
+
18
+ /**
19
+ * CSS class prefix
20
+ */
21
+ prefix?: string;
22
+
23
+ /**
24
+ * Component name
25
+ */
26
+ componentName?: string;
27
+
28
+ [key: string]: any;
29
+ }
30
+
31
+ /**
32
+ * Component with supporting text capabilities
33
+ */
34
+ export interface SupportingTextComponent extends BaseComponent {
35
+ /**
36
+ * Supporting text element
37
+ */
38
+ supportingTextElement: HTMLElement | null;
39
+
40
+ /**
41
+ * Sets supporting text content
42
+ * @param text - Text content
43
+ * @param isError - Whether text represents an error
44
+ * @returns Component instance for chaining
45
+ */
46
+ setSupportingText: (text: string, isError?: boolean) => SupportingTextComponent;
47
+
48
+ /**
49
+ * Removes supporting text
50
+ * @returns Component instance for chaining
51
+ */
52
+ removeSupportingText: () => SupportingTextComponent;
53
+ }
54
+
55
+ /**
56
+ * Helper to ensure the switch has the proper container/content structure
57
+ * @param component - The component to enhance
58
+ * @param prefix - CSS class prefix
59
+ * @param componentName - Component name
60
+ * @returns The container and content elements
61
+ */
62
+ const ensureSwitchStructure = (
63
+ component: ElementComponent,
64
+ prefix: string,
65
+ componentName: string
66
+ ) => {
67
+ const PREFIX = prefix || 'mtrl';
68
+ const COMPONENT = componentName || 'switch';
69
+
70
+ // Create or find container
71
+ let container = component.element.querySelector(`.${PREFIX}-${COMPONENT}-container`);
72
+ if (!container) {
73
+ container = document.createElement('div');
74
+ container.className = `${PREFIX}-${COMPONENT}-container`;
75
+
76
+ // Find input and track to move them to container
77
+ const input = component.element.querySelector(`.${PREFIX}-${COMPONENT}-input`);
78
+ const track = component.element.querySelector(`.${PREFIX}-${COMPONENT}-track`);
79
+
80
+ // Gather all elements except container
81
+ const elementsToMove = [];
82
+ if (input) elementsToMove.push(input);
83
+ if (track) elementsToMove.push(track);
84
+
85
+ // Create content wrapper
86
+ const contentWrapper = document.createElement('div');
87
+ contentWrapper.className = `${PREFIX}-${COMPONENT}-content`;
88
+
89
+ // Find label and move to content
90
+ const label = component.element.querySelector(`.${PREFIX}-${COMPONENT}-label`);
91
+ if (label) {
92
+ contentWrapper.appendChild(label);
93
+ }
94
+
95
+ // Add content wrapper to container first
96
+ container.appendChild(contentWrapper);
97
+
98
+ // Add other elements to container
99
+ elementsToMove.forEach(el => container.appendChild(el));
100
+
101
+ // Add container to component
102
+ component.element.appendChild(container);
103
+
104
+ return { container, contentWrapper };
105
+ }
106
+
107
+ // Container exists, find or create content wrapper
108
+ let contentWrapper = component.element.querySelector(`.${PREFIX}-${COMPONENT}-content`);
109
+ if (!contentWrapper) {
110
+ contentWrapper = document.createElement('div');
111
+ contentWrapper.className = `${PREFIX}-${COMPONENT}-content`;
112
+
113
+ // Find label to move to content
114
+ const label = component.element.querySelector(`.${PREFIX}-${COMPONENT}-label`);
115
+ if (label) {
116
+ contentWrapper.appendChild(label);
117
+ }
118
+
119
+ // Insert content wrapper at beginning of container
120
+ container.insertBefore(contentWrapper, container.firstChild);
121
+ }
122
+
123
+ return { container, contentWrapper };
124
+ };
125
+
126
+ /**
127
+ * Creates and manages supporting text for a component
128
+ * @param config - Configuration object with supporting text settings
129
+ * @returns Function that enhances a component with supporting text functionality
130
+ */
131
+ export const withSupportingText = <T extends SupportingTextConfig>(config: T) =>
132
+ <C extends ElementComponent>(component: C): C & SupportingTextComponent => {
133
+ const PREFIX = config.prefix || 'mtrl';
134
+ const COMPONENT = config.componentName || 'switch';
135
+
136
+ // Ensure we have the proper container/content structure
137
+ const { contentWrapper } = ensureSwitchStructure(component, PREFIX, COMPONENT);
138
+
139
+ // Create supporting text element if needed
140
+ let supportingElement = null;
141
+ if (config.supportingText) {
142
+ supportingElement = document.createElement('div');
143
+ supportingElement.className = `${PREFIX}-${COMPONENT}-helper`;
144
+ supportingElement.textContent = config.supportingText;
145
+
146
+ if (config.error) {
147
+ supportingElement.classList.add(`${PREFIX}-${COMPONENT}-helper--error`);
148
+ component.element.classList.add(`${PREFIX}-${COMPONENT}--error`);
149
+ }
150
+
151
+ // Add supporting text to the content wrapper
152
+ contentWrapper.appendChild(supportingElement);
153
+ }
154
+
155
+ // Add lifecycle integration if available
156
+ if ('lifecycle' in component && component.lifecycle?.destroy && supportingElement) {
157
+ const originalDestroy = component.lifecycle.destroy;
158
+ component.lifecycle.destroy = () => {
159
+ if (supportingElement) supportingElement.remove();
160
+ originalDestroy.call(component.lifecycle);
161
+ };
162
+ }
163
+
164
+ return {
165
+ ...component,
166
+ supportingTextElement: supportingElement,
167
+
168
+ setSupportingText(text: string, isError = false) {
169
+ const { contentWrapper } = ensureSwitchStructure(component, PREFIX, COMPONENT);
170
+ let supportingElement = this.supportingTextElement;
171
+
172
+ if (!supportingElement) {
173
+ // Create if it doesn't exist
174
+ supportingElement = document.createElement('div');
175
+ supportingElement.className = `${PREFIX}-${COMPONENT}-helper`;
176
+ contentWrapper.appendChild(supportingElement);
177
+ this.supportingTextElement = supportingElement;
178
+ }
179
+
180
+ supportingElement.textContent = text;
181
+
182
+ // Handle error state
183
+ supportingElement.classList.toggle(`${PREFIX}-${COMPONENT}-helper--error`, isError);
184
+ component.element.classList.toggle(`${PREFIX}-${COMPONENT}--error`, isError);
185
+
186
+ return this;
187
+ },
188
+
189
+ removeSupportingText() {
190
+ if (this.supportingTextElement && this.supportingTextElement.parentNode) {
191
+ this.supportingTextElement.remove();
192
+ this.supportingTextElement = null;
193
+ component.element.classList.remove(`${PREFIX}-${COMPONENT}--error`);
194
+ }
195
+ return this;
196
+ }
197
+ };
198
+ };