mtrl 0.2.8 → 0.2.9

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 (42) hide show
  1. package/index.ts +2 -0
  2. package/package.json +1 -1
  3. package/src/components/navigation/api.ts +131 -96
  4. package/src/components/navigation/features/controller.ts +273 -0
  5. package/src/components/navigation/features/items.ts +133 -64
  6. package/src/components/navigation/navigation.ts +17 -2
  7. package/src/components/navigation/system-types.ts +124 -0
  8. package/src/components/navigation/system.ts +776 -0
  9. package/src/components/slider/config.ts +20 -2
  10. package/src/components/slider/features/controller.ts +761 -0
  11. package/src/components/slider/features/handlers.ts +18 -15
  12. package/src/components/slider/features/index.ts +3 -2
  13. package/src/components/slider/features/range.ts +104 -0
  14. package/src/components/slider/slider.ts +34 -14
  15. package/src/components/slider/structure.ts +152 -0
  16. package/src/components/textfield/api.ts +53 -0
  17. package/src/components/textfield/features.ts +322 -0
  18. package/src/components/textfield/textfield.ts +8 -0
  19. package/src/components/textfield/types.ts +12 -3
  20. package/src/components/timepicker/clockdial.ts +1 -4
  21. package/src/core/compose/features/textinput.ts +15 -2
  22. package/src/core/composition/features/dom.ts +33 -0
  23. package/src/core/composition/features/icon.ts +131 -0
  24. package/src/core/composition/features/index.ts +11 -0
  25. package/src/core/composition/features/label.ts +156 -0
  26. package/src/core/composition/features/structure.ts +22 -0
  27. package/src/core/composition/index.ts +26 -0
  28. package/src/core/index.ts +1 -1
  29. package/src/core/structure.ts +288 -0
  30. package/src/index.ts +1 -0
  31. package/src/styles/components/_navigation-mobile.scss +244 -0
  32. package/src/styles/components/_navigation-system.scss +151 -0
  33. package/src/styles/components/_textfield.scss +250 -11
  34. package/demo/build.ts +0 -349
  35. package/demo/index.html +0 -110
  36. package/demo/main.js +0 -448
  37. package/demo/styles.css +0 -239
  38. package/server.ts +0 -86
  39. package/src/components/slider/features/slider.ts +0 -318
  40. package/src/components/slider/features/structure.ts +0 -181
  41. package/src/components/slider/features/ui.ts +0 -388
  42. package/src/components/textfield/constants.ts +0 -100
@@ -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,7 +172,7 @@ 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
177
  triggerEvent(SLIDER_EVENTS.CHANGE, e);
175
178
  } catch (error) {
@@ -244,7 +247,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
244
247
  state.value = newValue;
245
248
  }
246
249
 
247
- updateUi();
250
+ render();
248
251
  triggerEvent(SLIDER_EVENTS.INPUT, e);
249
252
  } catch (error) {
250
253
  console.warn('Error during slider drag:', error);
@@ -274,7 +277,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
274
277
 
275
278
  // Reset active handle and update UI
276
279
  state.activeHandle = null;
277
- updateUi();
280
+ render();
278
281
 
279
282
  // Trigger events
280
283
  triggerEvent(SLIDER_EVENTS.CHANGE, e);
@@ -355,7 +358,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
355
358
  }
356
359
 
357
360
  // Update UI and trigger events
358
- updateUi();
361
+ render();
359
362
  triggerEvent(SLIDER_EVENTS.INPUT, e);
360
363
  triggerEvent(SLIDER_EVENTS.CHANGE, e);
361
364
  };
@@ -406,7 +409,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
406
409
  * Set up all event listeners
407
410
  */
408
411
  const setupEventListeners = () => {
409
- if (!state.component || !state.component.structure) {
412
+ if (!state.component || !state.component.components) {
410
413
  console.warn('Cannot set up event listeners: missing component structure');
411
414
  return;
412
415
  }
@@ -436,7 +439,7 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
436
439
  * Clean up all event listeners
437
440
  */
438
441
  const cleanupEventListeners = () => {
439
- if (!state.component || !state.component.structure) return;
442
+ if (!state.component || !state.component.components) return;
440
443
 
441
444
  // Clean up container listeners
442
445
  if (container) {
@@ -492,4 +495,4 @@ export const createHandlers = (config: SliderConfig, state, uiHelpers, eventHelp
492
495
  hideAllBubbles,
493
496
  clearKeyboardFocus
494
497
  };
495
- };
498
+ }
@@ -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.structureDefinition) {
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 structureDefinition = JSON.parse(JSON.stringify(component.structureDefinition));
29
+
30
+ // Add range class to root element
31
+ const rootClasses = structureDefinition.element.options.className || [];
32
+ if (Array.isArray(rootClasses)) {
33
+ rootClasses.push(getClass('slider--range'));
34
+ } else {
35
+ structureDefinition.element.options.className = `${rootClasses} ${getClass('slider--range')}`.trim();
36
+ }
37
+
38
+ // Add start track segment to track children
39
+ const trackChildren = structureDefinition.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 = structureDefinition.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
+ structureDefinition
99
+ };
100
+ } catch (error) {
101
+ console.warn('Error enhancing structure with range functionality:', error);
102
+ return component;
103
+ }
104
+ };
@@ -1,42 +1,62 @@
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 { withStructure, 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';
10
-
13
+ import { createBaseConfig, getApiConfig } from './config';
11
14
  /**
12
15
  * Creates a new Slider component
16
+ *
17
+ * Slider follows a clear architectural pattern:
18
+ * 1. Structure definition - Describes the DOM structure declaratively
19
+ * 2. Feature enhancement - Adds specific capabilities (range, icons, labels)
20
+ * 3. DOM creation - Turns the structure into actual DOM elements
21
+ * 4. State management - Handles visual states and appearance
22
+ * 5. Controller - Manages behavior, events, and UI rendering
23
+ * 6. Lifecycle - Handles component lifecycle events
24
+ * 7. Public API - Exposes a clean, consistent API
25
+ *
13
26
  * @param {SliderConfig} config - Slider configuration object
14
27
  * @returns {SliderComponent} Slider component instance
15
28
  */
16
29
  const createSlider = (config: SliderConfig = {}): SliderComponent => {
30
+ // Process configuration with defaults
17
31
  const baseConfig = createBaseConfig(config);
18
32
 
19
33
  try {
20
- // Create the component with all required features
34
+ // Create the component by composing features in a specific order
21
35
  const component = pipe(
36
+ // Base component with event system
22
37
  createBase,
23
38
  withEvents(),
24
- withElement(getElementConfig(baseConfig)),
25
- withIcon(baseConfig),
26
- withTextLabel(baseConfig),
27
39
  withStructure(baseConfig),
40
+ withIcon(baseConfig),
41
+ withLabel(baseConfig),
42
+ withRange(baseConfig),
43
+
44
+ // Now create the actual DOM elements from the complete structure
45
+ withDom(),
46
+
47
+ // Add state management and behavior
28
48
  withStates(baseConfig),
29
- withSlider(baseConfig),
49
+ withController(baseConfig),
30
50
  withLifecycle()
31
51
  )(baseConfig);
32
52
 
33
- // Generate the API configuration
53
+ // Generate the API configuration based on the enhanced component
34
54
  const apiOptions = getApiConfig(component);
35
55
 
36
- // Apply the API layer
56
+ // Apply the public API layer
37
57
  const slider = withAPI(apiOptions)(component);
38
58
 
39
- // Register event handlers from config
59
+ // Register event handlers from config for convenience
40
60
  if (baseConfig.on && typeof slider.on === 'function') {
41
61
  Object.entries(baseConfig.on).forEach(([event, handler]) => {
42
62
  if (typeof handler === 'function') {
@@ -0,0 +1,152 @@
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 definition object
10
+ */
11
+ export function createSliderDefinition(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
+ tag: 'div',
30
+ className: [getClass('slider'), config.class].filter(Boolean),
31
+ attrs: {
32
+ tabindex: '-1',
33
+ role: 'none',
34
+ 'aria-disabled': isDisabled ? 'true' : 'false'
35
+ }
36
+ },
37
+ children: {
38
+ // Container with all slider elements
39
+ container: {
40
+ options: {
41
+ tag: 'div',
42
+ className: getClass('slider-container')
43
+ },
44
+ children: {
45
+ // Track with segments
46
+ track: {
47
+ options: {
48
+ tag: 'div',
49
+ className: getClass('slider-track')
50
+ },
51
+ children: {
52
+ activeTrack: {
53
+ options: {
54
+ tag: 'div',
55
+ className: getClass('slider-active-track'),
56
+ style: {
57
+ width: `${valuePercent}%`
58
+ }
59
+ }
60
+ },
61
+ startTrack: {
62
+ options: {
63
+ tag: 'div',
64
+ className: getClass('slider-start-track'),
65
+ style: {
66
+ display: 'none', // Initially hidden for single slider
67
+ width: '0%'
68
+ }
69
+ }
70
+ },
71
+ remainingTrack: {
72
+ options: {
73
+ tag: 'div',
74
+ className: getClass('slider-remaining-track'),
75
+ style: {
76
+ width: `${100 - valuePercent}%`
77
+ }
78
+ }
79
+ }
80
+ }
81
+ },
82
+
83
+ // Ticks container
84
+ ticksContainer: {
85
+ options: {
86
+ tag: 'div',
87
+ className: getClass('slider-ticks-container')
88
+ }
89
+ },
90
+
91
+ // Dots for ends
92
+ startDot: {
93
+ options: {
94
+ tag: 'div',
95
+ className: [
96
+ getClass('slider-dot'),
97
+ getClass('slider-dot--start')
98
+ ]
99
+ }
100
+ },
101
+ endDot: {
102
+ options: {
103
+ tag: 'div',
104
+ className: [
105
+ getClass('slider-dot'),
106
+ getClass('slider-dot--end')
107
+ ]
108
+ }
109
+ },
110
+
111
+ // Main handle
112
+ handle: {
113
+ options: {
114
+ tag: 'div',
115
+ className: getClass('slider-handle'),
116
+ attrs: {
117
+ role: 'slider',
118
+ 'aria-valuemin': String(min),
119
+ 'aria-valuemax': String(max),
120
+ 'aria-valuenow': String(value),
121
+ 'aria-orientation': 'horizontal',
122
+ tabindex: isDisabled ? '-1' : '0',
123
+ 'aria-disabled': isDisabled ? 'true' : 'false',
124
+ 'data-value': String(value),
125
+ 'data-handle-index': '0'
126
+ },
127
+ style: {
128
+ left: `${valuePercent}%`
129
+ }
130
+ }
131
+ },
132
+ // Main value bubble
133
+ valueBubble: {
134
+ options: {
135
+ tag: 'div',
136
+ className: getClass('slider-value'),
137
+ attrs: {
138
+ 'aria-hidden': 'true',
139
+ 'data-handle-index': '0'
140
+ },
141
+ text: formatter(value),
142
+ style: {
143
+ left: `${valuePercent}%`
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ };
152
+ }
@@ -1,6 +1,11 @@
1
1
  // src/components/textfield/api.ts
2
2
  import { BaseComponent, TextfieldComponent, ApiOptions } from './types';
3
3
 
4
+ /**
5
+ * Enhances textfield component with API methods
6
+ * @param {ApiOptions} options - API configuration
7
+ * @returns {Function} Higher-order function that adds API methods to component
8
+ */
4
9
  /**
5
10
  * Enhances textfield component with API methods
6
11
  * @param {ApiOptions} options - API configuration
@@ -43,6 +48,54 @@ export const withAPI = ({ disabled, lifecycle }: ApiOptions) =>
43
48
  getLabel(): string {
44
49
  return component.label?.getText() || '';
45
50
  },
51
+
52
+ // Leading icon management (if present)
53
+ leadingIcon: component.leadingIcon || null,
54
+ setLeadingIcon(html: string): TextfieldComponent {
55
+ if (component.setLeadingIcon) {
56
+ component.setLeadingIcon(html);
57
+ }
58
+ return this;
59
+ },
60
+
61
+ removeLeadingIcon(): TextfieldComponent {
62
+ if (component.removeLeadingIcon) {
63
+ component.removeLeadingIcon();
64
+ }
65
+ return this;
66
+ },
67
+
68
+ // Trailing icon management (if present)
69
+ trailingIcon: component.trailingIcon || null,
70
+ setTrailingIcon(html: string): TextfieldComponent {
71
+ if (component.setTrailingIcon) {
72
+ component.setTrailingIcon(html);
73
+ }
74
+ return this;
75
+ },
76
+
77
+ removeTrailingIcon(): TextfieldComponent {
78
+ if (component.removeTrailingIcon) {
79
+ component.removeTrailingIcon();
80
+ }
81
+ return this;
82
+ },
83
+
84
+ // Supporting text management (if present)
85
+ supportingTextElement: component.supportingTextElement || null,
86
+ setSupportingText(text: string, isError?: boolean): TextfieldComponent {
87
+ if (component.setSupportingText) {
88
+ component.setSupportingText(text, isError);
89
+ }
90
+ return this;
91
+ },
92
+
93
+ removeSupportingText(): TextfieldComponent {
94
+ if (component.removeSupportingText) {
95
+ component.removeSupportingText();
96
+ }
97
+ return this;
98
+ },
46
99
 
47
100
  // Event handling
48
101
  on(event: string, handler: Function): TextfieldComponent {