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
@@ -1,388 +0,0 @@
1
- // src/components/slider/features/ui.ts
2
- import { SliderConfig } from '../types';
3
-
4
- /**
5
- * Create optimized UI update helpers for slider component
6
- *
7
- * @param config Slider configuration
8
- * @param state Slider state object
9
- * @returns UI update helper methods
10
- */
11
- export const createUiHelpers = (config: SliderConfig, state) => {
12
- // Return empty implementations if component structure is missing
13
- if (!state.component?.structure) {
14
- console.error('Cannot create UI helpers: component structure is missing');
15
- return {
16
- getPercentage: () => 0,
17
- getValueFromPosition: () => 0,
18
- roundToStep: v => v,
19
- clamp: (v, min, max) => Math.min(Math.max(v, min), max),
20
- updateUi: () => {},
21
- showValueBubble: () => {},
22
- generateTicks: () => {},
23
- updateTicks: () => {}
24
- };
25
- }
26
-
27
- const {
28
- container = null,
29
- track = null,
30
- activeTrack = null,
31
- startTrack = null,
32
- remainingTrack = null,
33
- handle = null,
34
- valueBubble = null,
35
- secondHandle = null,
36
- secondValueBubble = null,
37
- ticksContainer = null
38
- } = state.component.structure;
39
-
40
- /**
41
- * Calculates percentage position for a value
42
- */
43
- const getPercentage = (value) => {
44
- const range = state.max - state.min;
45
- return range === 0 ? 0 : ((value - state.min) / range) * 100;
46
- };
47
-
48
- /**
49
- * Gets track dimensions and constraints for positioning calculations
50
- */
51
- const getTrackDimensions = () => {
52
- if (!track || !handle || !container) return null;
53
-
54
- const handleRect = handle.getBoundingClientRect();
55
- const trackRect = container.getBoundingClientRect();
56
- const handleSize = handleRect.width || 20;
57
- const trackSize = trackRect.width;
58
-
59
- const edgeConstraint = (handleSize / 2) / trackSize * 100;
60
- const paddingPixels = state.activeHandle ? 6 : 8;
61
- const paddingPercent = (paddingPixels / trackSize) * 100;
62
-
63
- return { handleSize, trackSize, edgeConstraint, paddingPercent };
64
- };
65
-
66
- /**
67
- * Maps value percentage to visual position with edge constraints
68
- */
69
- const mapValueToVisualPercent = (valuePercent, edgeConstraint) => {
70
- const minEdge = edgeConstraint;
71
- const maxEdge = 100 - edgeConstraint;
72
- const visualRange = maxEdge - minEdge;
73
-
74
- if (valuePercent <= 0) return minEdge;
75
- if (valuePercent >= 100) return maxEdge;
76
- return minEdge + (valuePercent / 100) * visualRange;
77
- };
78
-
79
- /**
80
- * Gets slider value from a position on the track
81
- */
82
- const getValueFromPosition = (position) => {
83
- if (!track || !container) return state.min;
84
-
85
- try {
86
- const containerRect = container.getBoundingClientRect();
87
- const range = state.max - state.min;
88
- const handleWidth = handle.getBoundingClientRect().width || 20;
89
-
90
- const leftEdge = containerRect.left + (handleWidth / 2);
91
- const rightEdge = containerRect.right - (handleWidth / 2);
92
- const effectiveWidth = rightEdge - leftEdge;
93
-
94
- const adjustedPosition = Math.max(leftEdge, Math.min(rightEdge, position));
95
- const percentageFromLeft = (adjustedPosition - leftEdge) / effectiveWidth;
96
-
97
- return state.min + percentageFromLeft * range;
98
- } catch (error) {
99
- console.warn('Error calculating value from position:', error);
100
- return state.min;
101
- }
102
- };
103
-
104
- /**
105
- * Rounds a value to the nearest step
106
- */
107
- const roundToStep = (value) => {
108
- const step = state.step;
109
- if (step <= 0) return value;
110
-
111
- const steps = Math.round((value - state.min) / step);
112
- return state.min + (steps * step);
113
- };
114
-
115
- /**
116
- * Clamps a value between min and max
117
- */
118
- const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
119
-
120
- /**
121
- * Updates handle and bubble positions and transforms
122
- */
123
- const updateHandlePositions = () => {
124
- if (!handle || !container) return;
125
-
126
- const dims = getTrackDimensions();
127
- if (!dims) return;
128
-
129
- const { edgeConstraint } = dims;
130
-
131
- // Update main handle position
132
- const percent = getPercentage(state.value);
133
- const adjustedPercent = mapValueToVisualPercent(percent, edgeConstraint);
134
-
135
- handle.style.left = `${adjustedPercent}%`;
136
- handle.style.transform = 'translate(-50%, -50%)';
137
- if (valueBubble) {
138
- valueBubble.style.left = `${adjustedPercent}%`;
139
- valueBubble.style.transform = 'translateX(-50%)';
140
- }
141
-
142
- // Update second handle if range slider
143
- if (config.range && secondHandle && state.secondValue !== null) {
144
- const secondPercent = getPercentage(state.secondValue);
145
- const adjustedSecondPercent = mapValueToVisualPercent(secondPercent, edgeConstraint);
146
-
147
- secondHandle.style.left = `${adjustedSecondPercent}%`;
148
- secondHandle.style.transform = 'translate(-50%, -50%)';
149
- if (secondValueBubble) {
150
- secondValueBubble.style.left = `${adjustedSecondPercent}%`;
151
- secondValueBubble.style.transform = 'translateX(-50%)';
152
- }
153
- }
154
-
155
- // Update ARIA attributes
156
- handle.setAttribute('aria-valuenow', String(state.value));
157
- if (config.range && secondHandle && state.secondValue !== null) {
158
- secondHandle.setAttribute('aria-valuenow', String(state.secondValue));
159
- }
160
- };
161
-
162
- /**
163
- * Updates all track segments at once with optimized positioning
164
- */
165
- const updateTrackSegments = () => {
166
- if (!track || !container || !handle) return;
167
-
168
- const dims = getTrackDimensions();
169
- if (!dims) return;
170
-
171
- const { handleSize, trackSize, paddingPercent } = dims;
172
- const edgeConstraint = (handleSize / 2) / trackSize * 100;
173
-
174
- if (config.range && state.secondValue !== null) {
175
- // Range slider setup
176
- const lowerValue = Math.min(state.value, state.secondValue);
177
- const higherValue = Math.max(state.value, state.secondValue);
178
- const lowerPercent = getPercentage(lowerValue);
179
- const higherPercent = getPercentage(higherValue);
180
-
181
- const adjustedLower = mapValueToVisualPercent(lowerPercent, edgeConstraint);
182
- const adjustedHigher = mapValueToVisualPercent(higherPercent, edgeConstraint);
183
-
184
- // Start track (before first handle)
185
- if (lowerPercent > 1) {
186
- startTrack.style.display = 'block';
187
- startTrack.style.left = '0';
188
- startTrack.style.right = `${100 - (adjustedLower - paddingPercent)}%`;
189
- startTrack.style.width = 'auto';
190
- } else {
191
- startTrack.style.display = 'none';
192
- }
193
-
194
- // Active track (between handles)
195
- const valueDiffPercent = Math.abs(higherPercent - lowerPercent);
196
- const hideThreshold = (handleSize / trackSize) * 100;
197
-
198
- if (valueDiffPercent <= hideThreshold) {
199
- activeTrack.style.display = 'none';
200
- } else {
201
- activeTrack.style.display = 'block';
202
- activeTrack.style.left = `${adjustedLower + paddingPercent}%`;
203
- activeTrack.style.right = `${100 - (adjustedHigher - paddingPercent)}%`;
204
- activeTrack.style.width = 'auto';
205
- }
206
-
207
- // Remaining track (after second handle)
208
- remainingTrack.style.display = 'block';
209
- remainingTrack.style.left = `${adjustedHigher + paddingPercent}%`;
210
- remainingTrack.style.right = '0';
211
- remainingTrack.style.width = 'auto';
212
- } else {
213
- // Single handle slider
214
- const valuePercent = getPercentage(state.value);
215
- const adjustedPercent = mapValueToVisualPercent(valuePercent, edgeConstraint);
216
-
217
- // Hide start track for single slider
218
- startTrack.style.display = 'none';
219
-
220
- // Active track (filled part)
221
- activeTrack.style.display = 'block';
222
- activeTrack.style.left = '0';
223
- activeTrack.style.right = `${100 - (adjustedPercent - paddingPercent)}%`;
224
- activeTrack.style.width = 'auto';
225
-
226
- // Remaining track (unfilled part)
227
- remainingTrack.style.display = 'block';
228
- remainingTrack.style.left = `${adjustedPercent + paddingPercent}%`;
229
- remainingTrack.style.right = '0';
230
- remainingTrack.style.width = 'auto';
231
- }
232
- };
233
-
234
- /**
235
- * Updates value bubble content
236
- */
237
- const updateValueBubbles = () => {
238
- if (!valueBubble) return;
239
-
240
- const formatter = config.valueFormatter || (value => value.toString());
241
- valueBubble.textContent = formatter(state.value);
242
-
243
- if (config.range && secondValueBubble && state.secondValue !== null) {
244
- secondValueBubble.textContent = formatter(state.secondValue);
245
- }
246
- };
247
-
248
- /**
249
- * Shows or hides value bubble
250
- */
251
- const showValueBubble = (bubbleElement, show) => {
252
- if (!bubbleElement || !config.showValue) return;
253
-
254
- bubbleElement.classList[show ? 'add' : 'remove'](`${state.component.getClass('slider-value')}--visible`);
255
- };
256
-
257
- /**
258
- * Generates tick marks
259
- */
260
- const generateTicks = () => {
261
- if (!ticksContainer || !container) return;
262
-
263
- // Clear existing ticks
264
- while (ticksContainer.firstChild) {
265
- ticksContainer.removeChild(ticksContainer.firstChild);
266
- }
267
-
268
- state.ticks = [];
269
- if (!config.ticks) return;
270
-
271
- const numSteps = Math.floor((state.max - state.min) / state.step);
272
- const tickValues = [];
273
-
274
- // Generate tick values
275
- for (let i = 0; i <= numSteps; i++) {
276
- const value = state.min + (i * state.step);
277
- if (value <= state.max) tickValues.push(value);
278
- }
279
-
280
- // Ensure max value is included
281
- if (tickValues[tickValues.length - 1] !== state.max) {
282
- tickValues.push(state.max);
283
- }
284
-
285
- // CSS classes
286
- const tickClass = state.component.getClass('slider-tick');
287
- const activeClass = `${tickClass}--active`;
288
- const inactiveClass = `${tickClass}--inactive`;
289
- const hiddenClass = `${tickClass}--hidden`;
290
-
291
- // Create tick elements
292
- tickValues.forEach(value => {
293
- const percent = getPercentage(value);
294
- const tick = document.createElement('div');
295
- tick.classList.add(tickClass);
296
- tick.style.left = `${percent}%`;
297
-
298
- // Determine tick active state
299
- const isExactlySelected = (value === state.value ||
300
- (config.range && state.secondValue !== null && value === state.secondValue));
301
-
302
- if (isExactlySelected) {
303
- tick.classList.add(hiddenClass);
304
- } else if (config.range && state.secondValue !== null) {
305
- const lowerValue = Math.min(state.value, state.secondValue);
306
- const higherValue = Math.max(state.value, state.secondValue);
307
-
308
- tick.classList.add(value >= lowerValue && value <= higherValue ? activeClass : inactiveClass);
309
- } else {
310
- tick.classList.add(value <= state.value ? activeClass : inactiveClass);
311
- }
312
-
313
- ticksContainer.appendChild(tick);
314
- state.ticks.push(tick);
315
- });
316
- };
317
-
318
- /**
319
- * Updates active state of tick marks
320
- */
321
- const updateTicks = () => {
322
- if (!state.ticks || state.ticks.length === 0) return;
323
-
324
- const tickClass = state.component.getClass('slider-tick');
325
- const activeClass = `${tickClass}--active`;
326
- const inactiveClass = `${tickClass}--inactive`;
327
- const hiddenClass = `${tickClass}--hidden`;
328
-
329
- state.ticks.forEach((tick, index) => {
330
- // Calculate the value for this tick
331
- const tickValue = state.min + (index * state.step);
332
-
333
- // Reset visibility first
334
- tick.classList.remove(hiddenClass);
335
-
336
- // Check if this tick should be hidden (matches exactly a selected value)
337
- const isExactlySelected = (tickValue === state.value ||
338
- (config.range && state.secondValue !== null && tickValue === state.secondValue));
339
-
340
- if (isExactlySelected) {
341
- tick.classList.add(hiddenClass);
342
- } else if (config.range && state.secondValue !== null) {
343
- // Range slider - ticks between values should be active
344
- const lowerValue = Math.min(state.value, state.secondValue);
345
- const higherValue = Math.max(state.value, state.secondValue);
346
-
347
- if (tickValue >= lowerValue && tickValue <= higherValue) {
348
- tick.classList.add(activeClass);
349
- tick.classList.remove(inactiveClass);
350
- } else {
351
- tick.classList.remove(activeClass);
352
- tick.classList.add(inactiveClass);
353
- }
354
- } else {
355
- // Single slider - ticks below value should be active
356
- if (tickValue <= state.value) {
357
- tick.classList.add(activeClass);
358
- tick.classList.remove(inactiveClass);
359
- } else {
360
- tick.classList.remove(activeClass);
361
- tick.classList.add(inactiveClass);
362
- }
363
- }
364
- });
365
- };
366
-
367
- /**
368
- * Updates all UI elements
369
- */
370
- const updateUi = () => {
371
- updateHandlePositions();
372
- updateTrackSegments();
373
- updateValueBubbles();
374
- updateTicks();
375
- };
376
-
377
- // Return consolidated helper methods
378
- return {
379
- getPercentage,
380
- getValueFromPosition,
381
- roundToStep,
382
- clamp,
383
- showValueBubble,
384
- generateTicks,
385
- updateTicks,
386
- updateUi
387
- };
388
- }
@@ -1,100 +0,0 @@
1
- // src/components/textfield/constants.ts
2
-
3
- /**
4
- * Textfield visual variants
5
- */
6
- export const TEXTFIELD_VARIANTS = {
7
- FILLED: 'filled',
8
- OUTLINED: 'outlined'
9
- } as const;
10
-
11
- /**
12
- * Textfield input types
13
- */
14
- export const TEXTFIELD_TYPES = {
15
- TEXT: 'text',
16
- PASSWORD: 'password',
17
- EMAIL: 'email',
18
- NUMBER: 'number',
19
- TEL: 'tel',
20
- URL: 'url',
21
- SEARCH: 'search',
22
- MULTILINE: 'multiline'
23
- } as const;
24
-
25
- /**
26
- * Validation schema for textfield configuration
27
- */
28
- export const TEXTFIELD_SCHEMA = {
29
- type: {
30
- type: 'string',
31
- enum: Object.values(TEXTFIELD_TYPES),
32
- required: false
33
- },
34
- variant: {
35
- type: 'string',
36
- enum: Object.values(TEXTFIELD_VARIANTS),
37
- required: false
38
- },
39
- name: {
40
- type: 'string',
41
- required: false
42
- },
43
- label: {
44
- type: 'string',
45
- required: false
46
- },
47
- placeholder: {
48
- type: 'string',
49
- required: false
50
- },
51
- value: {
52
- type: 'string',
53
- required: false
54
- },
55
- required: {
56
- type: 'boolean',
57
- required: false
58
- },
59
- disabled: {
60
- type: 'boolean',
61
- required: false
62
- },
63
- maxLength: {
64
- type: 'number',
65
- required: false
66
- },
67
- pattern: {
68
- type: 'string',
69
- required: false
70
- },
71
- autocomplete: {
72
- type: 'string',
73
- required: false
74
- },
75
- class: {
76
- type: 'string',
77
- required: false
78
- }
79
- } as const;
80
-
81
- /**
82
- * Textfield state classes
83
- */
84
- export const TEXTFIELD_STATES = {
85
- FOCUSED: 'focused',
86
- FILLED: 'filled',
87
- DISABLED: 'disabled',
88
- INVALID: 'invalid'
89
- } as const;
90
-
91
- /**
92
- * Textfield element classes
93
- */
94
- export const TEXTFIELD_CLASSES = {
95
- ROOT: 'textfield',
96
- INPUT: 'textfield-input',
97
- LABEL: 'textfield-label',
98
- HELPER: 'textfield-helper',
99
- COUNTER: 'textfield-counter'
100
- } as const;
@@ -1,95 +0,0 @@
1
- import { isObject } from '../utils'
2
-
3
- const createComponent = (Component, options = {}) => {
4
- // Check if Component is a class (has prototype) or function
5
- const isClass = Component.prototype && Component.prototype.constructor === Component
6
-
7
- if (isClass) {
8
- return new Component(options)
9
- }
10
-
11
- return Component(options)
12
- }
13
-
14
- /**
15
- * Recursively creates components based on a provided schema.
16
- *
17
- * @param {Array} schema - An array of components or sub-schemas.
18
- * @param {HTMLElement} container - The container for the current level of components.
19
- * @param {Object} [structure={}] - An accumulator object for components, keyed by name.
20
- * @param {number} [level=0] - The current level of recursion.
21
- * @returns {Object} The structure containing all created components.
22
- */
23
- const createLayout = (schema, container, structure = {}, level = 0, components = []) => {
24
- level++
25
- let component
26
- const object = {}
27
- const fragment = document.createDocumentFragment()
28
-
29
- if (!Array.isArray(schema)) {
30
- console.error('Schema is not an array!', container, level, schema)
31
- }
32
-
33
- for (let i = 0; i < schema.length; i++) {
34
- let name
35
- let options = {}
36
-
37
- if (schema[i] instanceof Object && typeof schema[i] === 'function') {
38
- if (isObject(schema[i + 1])) {
39
- options = schema[i + 1]
40
- } else if (isObject(schema[i + 2])) {
41
- options = schema[i + 2]
42
- }
43
-
44
- if (options.id) {
45
- name = options.id
46
- } else if (typeof schema[i + 1] === 'string') {
47
- name = schema[i + 1]
48
- if (!schema[i].isElement && !schema[i].isComponent) {
49
- options.name = name
50
- }
51
- }
52
-
53
- component = createComponent(schema[i], options)
54
-
55
- const element = component.element || component
56
- if (level === 1) structure.element = element
57
-
58
- if (name) {
59
- structure[name] = component
60
- components.push([name, component])
61
- }
62
-
63
- if (component) {
64
- if (component.insert) component.insert(fragment)
65
- else fragment.appendChild(element)
66
-
67
- if (container) {
68
- // console.log('container', container)
69
- component._container = container
70
- if (component.onInserted) component.onInserted(container)
71
- }
72
- }
73
- } else if (Array.isArray(schema[i])) {
74
- if (!component) component = container
75
- createLayout(schema[i], component, structure, level, components)
76
- }
77
- }
78
-
79
- if (container && fragment.hasChildNodes()) {
80
- const wrapper = container.element || container
81
- wrapper.appendChild(fragment)
82
- }
83
-
84
- function get (name) {
85
- return structure[name] || null
86
- }
87
-
88
- object.component = structure
89
- object.components = components
90
- object.get = get
91
-
92
- return object
93
- }
94
-
95
- export default createLayout