mtrl 0.0.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 (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +251 -0
  3. package/index.js +10 -0
  4. package/package.json +17 -0
  5. package/src/components/button/api.js +54 -0
  6. package/src/components/button/button.js +81 -0
  7. package/src/components/button/config.js +8 -0
  8. package/src/components/button/constants.js +63 -0
  9. package/src/components/button/index.js +2 -0
  10. package/src/components/button/styles.scss +231 -0
  11. package/src/components/checkbox/api.js +45 -0
  12. package/src/components/checkbox/checkbox.js +95 -0
  13. package/src/components/checkbox/constants.js +88 -0
  14. package/src/components/checkbox/index.js +2 -0
  15. package/src/components/checkbox/styles.scss +183 -0
  16. package/src/components/container/api.js +42 -0
  17. package/src/components/container/container.js +45 -0
  18. package/src/components/container/index.js +2 -0
  19. package/src/components/container/styles.scss +59 -0
  20. package/src/components/list/constants.js +89 -0
  21. package/src/components/list/index.js +2 -0
  22. package/src/components/list/list-item.js +147 -0
  23. package/src/components/list/list.js +267 -0
  24. package/src/components/list/styles/_list-item.scss +142 -0
  25. package/src/components/list/styles/_list.scss +89 -0
  26. package/src/components/list/styles/_variables.scss +13 -0
  27. package/src/components/list/styles.scss +19 -0
  28. package/src/components/navigation/api.js +43 -0
  29. package/src/components/navigation/constants.js +235 -0
  30. package/src/components/navigation/features/items.js +192 -0
  31. package/src/components/navigation/index.js +2 -0
  32. package/src/components/navigation/nav-item.js +137 -0
  33. package/src/components/navigation/navigation.js +55 -0
  34. package/src/components/navigation/styles/_bar.scss +51 -0
  35. package/src/components/navigation/styles/_base.scss +129 -0
  36. package/src/components/navigation/styles/_drawer.scss +169 -0
  37. package/src/components/navigation/styles/_rail.scss +65 -0
  38. package/src/components/navigation/styles.scss +6 -0
  39. package/src/components/snackbar/api.js +125 -0
  40. package/src/components/snackbar/constants.js +41 -0
  41. package/src/components/snackbar/features.js +69 -0
  42. package/src/components/snackbar/index.js +2 -0
  43. package/src/components/snackbar/position.js +63 -0
  44. package/src/components/snackbar/queue.js +74 -0
  45. package/src/components/snackbar/snackbar.js +70 -0
  46. package/src/components/snackbar/styles.scss +182 -0
  47. package/src/components/switch/api.js +44 -0
  48. package/src/components/switch/constants.js +80 -0
  49. package/src/components/switch/index.js +2 -0
  50. package/src/components/switch/styles.scss +172 -0
  51. package/src/components/switch/switch.js +71 -0
  52. package/src/components/textfield/api.js +49 -0
  53. package/src/components/textfield/constants.js +81 -0
  54. package/src/components/textfield/index.js +2 -0
  55. package/src/components/textfield/styles/base.scss +107 -0
  56. package/src/components/textfield/styles/filled.scss +58 -0
  57. package/src/components/textfield/styles/outlined.scss +66 -0
  58. package/src/components/textfield/styles.scss +6 -0
  59. package/src/components/textfield/textfield.js +68 -0
  60. package/src/core/build/constants.js +51 -0
  61. package/src/core/build/icon.js +78 -0
  62. package/src/core/build/ripple.js +92 -0
  63. package/src/core/build/text.js +54 -0
  64. package/src/core/collection/adapters/base.js +26 -0
  65. package/src/core/collection/adapters/mongodb.js +232 -0
  66. package/src/core/collection/adapters/route.js +201 -0
  67. package/src/core/collection/collection.js +259 -0
  68. package/src/core/collection/list-manager.js +157 -0
  69. package/src/core/compose/base.js +8 -0
  70. package/src/core/compose/component.js +225 -0
  71. package/src/core/compose/features/checkable.js +114 -0
  72. package/src/core/compose/features/disabled.js +25 -0
  73. package/src/core/compose/features/events.js +48 -0
  74. package/src/core/compose/features/icon.js +33 -0
  75. package/src/core/compose/features/index.js +20 -0
  76. package/src/core/compose/features/input.js +92 -0
  77. package/src/core/compose/features/lifecycle.js +69 -0
  78. package/src/core/compose/features/position.js +60 -0
  79. package/src/core/compose/features/ripple.js +32 -0
  80. package/src/core/compose/features/size.js +9 -0
  81. package/src/core/compose/features/style.js +12 -0
  82. package/src/core/compose/features/text.js +17 -0
  83. package/src/core/compose/features/textinput.js +118 -0
  84. package/src/core/compose/features/textlabel.js +28 -0
  85. package/src/core/compose/features/track.js +49 -0
  86. package/src/core/compose/features/variant.js +9 -0
  87. package/src/core/compose/features/withEvents.js +67 -0
  88. package/src/core/compose/index.js +16 -0
  89. package/src/core/compose/pipe.js +69 -0
  90. package/src/core/config.js +140 -0
  91. package/src/core/dom/attributes.js +33 -0
  92. package/src/core/dom/classes.js +70 -0
  93. package/src/core/dom/create.js +133 -0
  94. package/src/core/dom/events.js +175 -0
  95. package/src/core/dom/index.js +5 -0
  96. package/src/core/dom/utils.js +22 -0
  97. package/src/core/index.js +23 -0
  98. package/src/core/layout/index.js +93 -0
  99. package/src/core/state/disabled.js +14 -0
  100. package/src/core/state/emitter.js +63 -0
  101. package/src/core/state/events.js +29 -0
  102. package/src/core/state/index.js +6 -0
  103. package/src/core/state/lifecycle.js +64 -0
  104. package/src/core/state/store.js +112 -0
  105. package/src/core/utils/index.js +39 -0
  106. package/src/core/utils/mobile.js +74 -0
  107. package/src/core/utils/object.js +22 -0
  108. package/src/core/utils/validate.js +37 -0
  109. package/src/index.js +11 -0
  110. package/src/styles/abstract/_base.scss +2 -0
  111. package/src/styles/abstract/_config.scss +28 -0
  112. package/src/styles/abstract/_functions.scss +124 -0
  113. package/src/styles/abstract/_mixins.scss +261 -0
  114. package/src/styles/abstract/_variables.scss +158 -0
  115. package/src/styles/main.scss +78 -0
  116. package/src/styles/themes/_base-theme.scss +49 -0
  117. package/src/styles/themes/_baseline.scss +90 -0
  118. package/src/styles/themes/_forest.scss +71 -0
  119. package/src/styles/themes/_index.scss +6 -0
  120. package/src/styles/themes/_ocean.scss +71 -0
  121. package/src/styles/themes/_sunset.scss +55 -0
@@ -0,0 +1,92 @@
1
+ // src/core/compose/features/input.js
2
+
3
+ /**
4
+ * Creates an input element and adds it to a component
5
+ * Handles both input creation and event emission for state changes
6
+ *
7
+ * @param {Object} config - Input configuration
8
+ * @param {string} [config.name] - Input name attribute
9
+ * @param {boolean} [config.checked] - Initial checked state
10
+ * @param {boolean} [config.required] - Whether input is required
11
+ * @param {boolean} [config.disabled] - Whether input is disabled
12
+ * @param {string} [config.value='on'] - Input value attribute
13
+ * @param {string} [config.label] - Accessibility label text
14
+ * @param {string} [config.ariaLabel] - Alternative accessibility label
15
+ *
16
+ * @returns {Function} Component transformer that adds input functionality
17
+ */
18
+ export const withInput = (config = {}) => (component) => {
19
+ const input = document.createElement('input')
20
+ const name = component.componentName
21
+ input.type = 'checkbox'
22
+ input.className = `${component.getClass(name)}-input`
23
+
24
+ // Ensure input can receive focus
25
+ input.style.position = 'absolute'
26
+ input.style.opacity = '0'
27
+ input.style.cursor = 'pointer'
28
+ // Don't use display: none or visibility: hidden as they prevent focus
29
+
30
+ // The input itself should be focusable, not the wrapper
31
+ component.element.setAttribute('role', 'presentation')
32
+ input.setAttribute('role', name)
33
+
34
+ const attributes = {
35
+ name: config.name,
36
+ checked: config.checked,
37
+ required: config.required,
38
+ disabled: config.disabled,
39
+ value: config.value || 'on',
40
+ 'aria-label': config.label || config.ariaLabel
41
+ }
42
+
43
+ Object.entries(attributes).forEach(([key, value]) => {
44
+ if (value !== null && value !== undefined) {
45
+ input.setAttribute(key, value)
46
+ }
47
+ })
48
+
49
+ // Bridge native checkbox events to our event system
50
+ input.addEventListener('change', (event) => {
51
+ component.emit('change', {
52
+ checked: input.checked,
53
+ value: input.value,
54
+ nativeEvent: event
55
+ })
56
+ })
57
+
58
+ // Add keyboard handling
59
+ input.addEventListener('keydown', (event) => {
60
+ if (event.key === ' ' || event.key === 'Enter') {
61
+ event.preventDefault()
62
+ if (!input.disabled) {
63
+ input.checked = !input.checked
64
+ input.dispatchEvent(new Event('change', { bubbles: true }))
65
+ }
66
+ }
67
+ })
68
+
69
+ component.element.appendChild(input)
70
+
71
+ return {
72
+ ...component,
73
+ input,
74
+
75
+ /**
76
+ * Gets the current input value
77
+ * @returns {string} Current value
78
+ */
79
+ getValue: () => input.value,
80
+
81
+ /**
82
+ * Sets the input value and emits a value event
83
+ * @param {string} value - New value to set
84
+ * @returns {Object} Component instance
85
+ */
86
+ setValue (value) {
87
+ input.value = value
88
+ component.emit('value', { value })
89
+ return this
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,69 @@
1
+ // src/core/compose/features/withLifecycle.js
2
+ import { createEmitter } from '../../state/emitter'
3
+
4
+ export const withLifecycle = () => (component) => {
5
+ if (!component.element) return component
6
+
7
+ let mounted = false
8
+ const emitter = createEmitter()
9
+
10
+ return {
11
+ ...component,
12
+ lifecycle: {
13
+ // Mount/Unmount state management
14
+ onMount: (handler) => emitter.on('mount', handler),
15
+ onUnmount: (handler) => emitter.on('unmount', handler),
16
+
17
+ mount: () => {
18
+ if (!mounted) {
19
+ mounted = true
20
+ emitter.emit('mount')
21
+ }
22
+ },
23
+
24
+ unmount: () => {
25
+ if (mounted) {
26
+ mounted = false
27
+ emitter.emit('unmount')
28
+ emitter.clear()
29
+ }
30
+ },
31
+
32
+ isMounted: () => mounted,
33
+
34
+ // Cleanup and destruction
35
+ destroy () {
36
+ // First trigger unmount
37
+ if (mounted) {
38
+ this.unmount()
39
+ }
40
+
41
+ // Clean up all event listeners
42
+ if (component.events) {
43
+ component.events.destroy()
44
+ }
45
+
46
+ // Clean up text element
47
+ if (component.text) {
48
+ const textElement = component.text.getElement()
49
+ if (textElement) {
50
+ textElement.remove()
51
+ }
52
+ }
53
+
54
+ // Clean up icon element
55
+ if (component.icon) {
56
+ const iconElement = component.icon.getElement()
57
+ if (iconElement) {
58
+ iconElement.remove()
59
+ }
60
+ }
61
+
62
+ // Remove the main element
63
+ if (component.element) {
64
+ component.element.remove()
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,60 @@
1
+ // src/core/compose/features/position.js
2
+
3
+ /**
4
+ * Available position values
5
+ */
6
+ export const POSITIONS = {
7
+ LEFT: 'left',
8
+ RIGHT: 'right',
9
+ TOP: 'top',
10
+ BOTTOM: 'bottom',
11
+ START: 'start',
12
+ END: 'end',
13
+ CENTER: 'center'
14
+ }
15
+
16
+ /**
17
+ * Adds positioning functionality to a component
18
+ * @param {Object} config - Component configuration
19
+ * @param {string} config.position - Position value
20
+ * @param {string} [config.prefix='mtrl'] - CSS class prefix
21
+ * @param {string} [config.componentName] - Component name for class generation
22
+ * @returns {Function} Component transformer
23
+ */
24
+ export const withPosition = (config) => (component) => {
25
+ if (!config.position || !component.element) return component
26
+
27
+ const position = POSITIONS[config.position.toUpperCase()] || config.position
28
+ const className = `${config.prefix}-${config.componentName}--${position}`
29
+
30
+ component.element.classList.add(className)
31
+
32
+ return {
33
+ ...component,
34
+ position: {
35
+ /**
36
+ * Sets the component's position
37
+ * @param {string} newPosition - New position value
38
+ * @returns {Object} Component instance
39
+ */
40
+ setPosition (newPosition) {
41
+ const oldPosition = position
42
+ const oldClassName = `${config.prefix}-${config.componentName}--${oldPosition}`
43
+ const newClassName = `${config.prefix}-${config.componentName}--${newPosition}`
44
+
45
+ component.element.classList.remove(oldClassName)
46
+ component.element.classList.add(newClassName)
47
+
48
+ return this
49
+ },
50
+
51
+ /**
52
+ * Gets the current position
53
+ * @returns {string} Current position
54
+ */
55
+ getPosition () {
56
+ return position
57
+ }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,32 @@
1
+ // src/core/compose/features/with-ripple.js
2
+
3
+ import { createRipple } from '../../build/ripple'
4
+
5
+ /**
6
+ * Higher-order function that adds ripple effect functionality
7
+ * @param {Object} config - Feature configuration
8
+ * @param {boolean} [config.ripple] - Enable/disable ripple effect
9
+ * @param {Object} [config.rippleConfig] - Ripple animation configuration
10
+ * @returns {Function} Component enhancer
11
+ */
12
+ export const withRipple = (config = {}) => (component) => {
13
+ if (!config.ripple) return component
14
+
15
+ const rippleInstance = createRipple(config.rippleConfig)
16
+
17
+ return {
18
+ ...component,
19
+ ripple: rippleInstance,
20
+ lifecycle: {
21
+ ...component.lifecycle,
22
+ mount: () => {
23
+ component.lifecycle.mount()
24
+ rippleInstance.mount(component.element)
25
+ },
26
+ destroy: () => {
27
+ rippleInstance.unmount(component.element)
28
+ component.lifecycle.destroy()
29
+ }
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,9 @@
1
+ // src/core/compose/features/variant.js
2
+ export const withSize = config => component => {
3
+ if (config.size && component.element) {
4
+ // Use config.componentName since we know it's there
5
+ const className = `${config.prefix}-${config.componentName}--${config.size}`
6
+ component.element.classList.add(className)
7
+ }
8
+ return component
9
+ }
@@ -0,0 +1,12 @@
1
+ // src/core/compose/features/style.js
2
+ export const withStyle = (config = {}) => (layout) => {
3
+ if (config.variant) {
4
+ layout.element.classList.add(`${layout.getClass('button')}--${config.variant}`)
5
+ }
6
+
7
+ if (config.size) {
8
+ layout.element.classList.add(`${layout.getClass('button')}--${config.size}`)
9
+ }
10
+
11
+ return layout
12
+ }
@@ -0,0 +1,17 @@
1
+ import { createText } from '../../../core/build/text'
2
+
3
+ export const withText = (config = {}) => (component) => {
4
+ const text = createText(component.element, {
5
+ prefix: config.prefix,
6
+ type: 'button'
7
+ })
8
+
9
+ if (config.text) {
10
+ text.setText(config.text)
11
+ }
12
+
13
+ return {
14
+ ...component,
15
+ text
16
+ }
17
+ }
@@ -0,0 +1,118 @@
1
+ // src/core/compose/features/textinput.js
2
+ /**
3
+ * Enhances a component with text input functionality
4
+ * @param {Object} config - Text input configuration
5
+ * @param {string} [config.type] - Input type (text, password, etc.)
6
+ * @param {boolean} [config.multiline] - Whether to use textarea
7
+ * @returns {Function} Component enhancer
8
+ */
9
+ export const withTextInput = (config = {}) => (component) => {
10
+ const input = document.createElement(config.type === 'multiline' ? 'textarea' : 'input')
11
+ input.className = `${component.getClass('textfield')}-input`
12
+
13
+ // Set input attributes
14
+ const attributes = {
15
+ type: config.type === 'multiline' ? null : (config.type || 'text'),
16
+ name: config.name,
17
+ required: config.required,
18
+ disabled: config.disabled,
19
+ maxLength: config.maxLength,
20
+ pattern: config.pattern,
21
+ autocomplete: config.autocomplete,
22
+ value: config.value || ''
23
+ }
24
+
25
+ Object.entries(attributes).forEach(([key, value]) => {
26
+ if (value !== null && value !== undefined) {
27
+ input.setAttribute(key, value)
28
+ }
29
+ })
30
+
31
+ // Handle input state changes
32
+ const updateInputState = () => {
33
+ const isEmpty = !input.value
34
+ component.element.classList.toggle(`${component.getClass('textfield')}--empty`, isEmpty)
35
+ return isEmpty
36
+ }
37
+
38
+ // Detect autocomplete
39
+ const handleAutocomplete = (event) => {
40
+ // Chrome/Safari trigger animationstart
41
+ if (event.animationName === 'onAutoFillStart') {
42
+ component.element.classList.remove(`${component.getClass('textfield')}--empty`)
43
+ component.emit('input', { value: input.value, isEmpty: false, isAutofilled: true })
44
+ }
45
+ }
46
+
47
+ // Add required animation for autocomplete detection
48
+ const style = document.createElement('style')
49
+ style.textContent = `
50
+ @keyframes onAutoFillStart { from {} to {} }
51
+ .${component.getClass('textfield')}-input:-webkit-autofill {
52
+ animation-name: onAutoFillStart;
53
+ animation-duration: 1ms;
54
+ }
55
+ `
56
+ document.head.appendChild(style)
57
+
58
+ // Event listeners
59
+ input.addEventListener('focus', () => {
60
+ component.element.classList.add(`${component.getClass('textfield')}--focused`)
61
+ component.emit('focus', { isEmpty: updateInputState() })
62
+ })
63
+
64
+ input.addEventListener('blur', () => {
65
+ component.element.classList.remove(`${component.getClass('textfield')}--focused`)
66
+ component.emit('blur', { isEmpty: updateInputState() })
67
+ })
68
+
69
+ input.addEventListener('input', () => {
70
+ component.emit('input', {
71
+ value: input.value,
72
+ isEmpty: updateInputState(),
73
+ isAutofilled: false
74
+ })
75
+ })
76
+
77
+ input.addEventListener('animationstart', handleAutocomplete)
78
+
79
+ // Initial state
80
+ updateInputState()
81
+
82
+ component.element.appendChild(input)
83
+
84
+ // Cleanup
85
+ if (component.lifecycle) {
86
+ const originalDestroy = component.lifecycle.destroy
87
+ component.lifecycle.destroy = () => {
88
+ input.removeEventListener('animationstart', handleAutocomplete)
89
+ style.remove()
90
+ input.remove()
91
+ originalDestroy.call(component.lifecycle)
92
+ }
93
+ }
94
+
95
+ return {
96
+ ...component,
97
+ input,
98
+ setValue (value) {
99
+ input.value = value || ''
100
+ updateInputState()
101
+ return this
102
+ },
103
+ getValue () {
104
+ return input.value
105
+ },
106
+ setAttribute (name, value) {
107
+ input.setAttribute(name, value)
108
+ return this
109
+ },
110
+ getAttribute (name) {
111
+ return input.getAttribute(name)
112
+ },
113
+ removeAttribute (name) {
114
+ input.removeAttribute(name)
115
+ return this
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,28 @@
1
+ // src/core/compose/features/textlabel.js
2
+
3
+ export const withTextLabel = (config = {}) => (component) => {
4
+ if (!config.label) return component
5
+
6
+ const labelElement = document.createElement('label')
7
+ labelElement.className = `${config.prefix}-${config.componentName}-label`
8
+ labelElement.textContent = config.label
9
+
10
+ // Insert label after input for proper z-index stacking
11
+ component.element.appendChild(labelElement)
12
+
13
+ return {
14
+ ...component,
15
+ label: {
16
+ setText (text) {
17
+ labelElement.textContent = text
18
+ return this
19
+ },
20
+ getText () {
21
+ return labelElement.textContent
22
+ },
23
+ getElement () {
24
+ return labelElement
25
+ }
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,49 @@
1
+ // src/core/compose/features/track.js
2
+ /**
3
+ * @module core/compose/features
4
+ */
5
+
6
+ /**
7
+ * Default checkmark icon SVG
8
+ * @memberof module:core/compose/features
9
+ * @private
10
+ */
11
+ const DEFAULT_ICON = `
12
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
13
+ <polyline points="20 6 9 17 4 12"></polyline>
14
+ </svg>`
15
+
16
+ /**
17
+ * Adds track and thumb elements to a component
18
+ * @memberof module:core/compose/features
19
+ * @function withTrack
20
+ * @param {Object} config - Track configuration
21
+ * @param {string} config.prefix - Class prefix
22
+ * @param {string} config.componentName - Component name
23
+ * @param {string} [config.icon] - Custom icon HTML or 'none'
24
+ * @returns {Function} Component transformer
25
+ */
26
+ export const withTrack = (config) => (component) => {
27
+ const track = document.createElement('span')
28
+ track.className = `${config.prefix}-${config.componentName}-track`
29
+
30
+ const thumb = document.createElement('span')
31
+ thumb.className = `${config.prefix}-${config.componentName}-thumb`
32
+ track.appendChild(thumb)
33
+
34
+ // Add icon inside thumb if provided or use default
35
+ if (config.icon !== 'none') {
36
+ const icon = document.createElement('span')
37
+ icon.className = `${config.prefix}-${config.componentName}-thumb-icon`
38
+ icon.innerHTML = config.icon || DEFAULT_ICON
39
+ thumb.appendChild(icon)
40
+ }
41
+
42
+ component.element.appendChild(track)
43
+
44
+ return {
45
+ ...component,
46
+ track,
47
+ thumb
48
+ }
49
+ }
@@ -0,0 +1,9 @@
1
+ // src/core/compose/features/variant.js
2
+ export const withVariant = config => component => {
3
+ if (config.variant && component.element) {
4
+ // Use config.componentName since we know it's there
5
+ const className = `${config.prefix}-${config.componentName}--${config.variant}`
6
+ component.element.classList.add(className)
7
+ }
8
+ return component
9
+ }
@@ -0,0 +1,67 @@
1
+ // src/core/compose/features/withEvents.js
2
+ import { createEventManager } from '../../state/events'
3
+
4
+ /**
5
+ * Adds event handling capabilities to a component
6
+ * @param {HTMLElement} [target] - Optional custom event target
7
+ * @returns {Function} Component enhancer
8
+ */
9
+ export const withEvents = (target) => (component) => {
10
+ const events = createEventManager(target || component.element)
11
+
12
+ // Enhanced event methods
13
+ const enhancedEvents = {
14
+ /**
15
+ * Add multiple event listeners at once
16
+ * @param {Object} listeners - Map of event types to handlers
17
+ */
18
+ addListeners (listeners) {
19
+ Object.entries(listeners).forEach(([event, handler]) => {
20
+ events.on(event, handler)
21
+ })
22
+ return this
23
+ },
24
+
25
+ /**
26
+ * Remove multiple event listeners at once
27
+ * @param {Object} listeners - Map of event types to handlers
28
+ */
29
+ removeListeners (listeners) {
30
+ Object.entries(listeners).forEach(([event, handler]) => {
31
+ events.off(event, handler)
32
+ })
33
+ return this
34
+ },
35
+
36
+ /**
37
+ * One-time event handler
38
+ * @param {string} event - Event name
39
+ * @param {Function} handler - Event handler
40
+ */
41
+ once (event, handler) {
42
+ const wrappedHandler = (e) => {
43
+ handler(e)
44
+ events.off(event, wrappedHandler)
45
+ }
46
+ events.on(event, wrappedHandler)
47
+ return this
48
+ }
49
+ }
50
+
51
+ // Add lifecycle integration
52
+ if (component.lifecycle) {
53
+ const originalDestroy = component.lifecycle.destroy
54
+ component.lifecycle.destroy = () => {
55
+ events.destroy()
56
+ originalDestroy?.call(component.lifecycle)
57
+ }
58
+ }
59
+
60
+ return {
61
+ ...component,
62
+ events,
63
+ on: events.on.bind(events),
64
+ off: events.off.bind(events),
65
+ ...enhancedEvents
66
+ }
67
+ }
@@ -0,0 +1,16 @@
1
+ // src/core/compose/index.js
2
+ /**
3
+ * @module core/compose
4
+ * @description Core composition utilities for creating and combining components
5
+ */
6
+ export { pipe, compose, transform } from './pipe'
7
+ export {
8
+ withEvents,
9
+ withIcon,
10
+ withSize,
11
+ withPosition,
12
+ withText,
13
+ withVariant,
14
+ withTextInput
15
+ } from './features'
16
+ export { createBase, withElement } from './component' // Add this line
@@ -0,0 +1,69 @@
1
+ // @file src/core/compose/pipe.js
2
+ // @module core/compose
3
+
4
+ /**
5
+ * @namespace compose
6
+ * @description Core composition utilities for creating and combining components
7
+ */
8
+
9
+ /**
10
+ * Performs left-to-right function composition.
11
+ * Each function takes the return value of the previous function as its input.
12
+ *
13
+ * @memberof compose
14
+ * @function pipe
15
+ * @param {...Function} fns - Functions to compose
16
+ * @returns {Function} Composed function that passes its argument through the pipeline
17
+ *
18
+ * @example
19
+ * const addOne = x => x + 1;
20
+ * const double = x => x * 2;
21
+ * const addOneThenDouble = pipe(addOne, double);
22
+ * console.log(addOneThenDouble(3)); // Output: 8
23
+ */
24
+ export const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x)
25
+
26
+ /**
27
+ * Performs right-to-left function composition.
28
+ * This is the mathematical composition order: (f ∘ g)(x) = f(g(x))
29
+ *
30
+ * @memberof compose
31
+ * @function compose
32
+ * @param {...Function} fns - Functions to compose
33
+ * @returns {Function} Composed function following mathematical composition order
34
+ *
35
+ * @example
36
+ * const addOne = x => x + 1;
37
+ * const double = x => x * 2;
38
+ * const doubleTheAddOne = compose(addOne, double);
39
+ * console.log(doubleTheAddOne(3)); // Output: 7
40
+ */
41
+ export const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x)
42
+
43
+ /**
44
+ * Creates a function that applies transformations to an object with shared context.
45
+ * Useful for applying multiple transformations while maintaining a shared state.
46
+ *
47
+ * @memberof compose
48
+ * @function transform
49
+ * @param {...Function} transformers - Functions that transform the object
50
+ * @returns {Function} Function that applies all transformations with shared context
51
+ *
52
+ * @example
53
+ * const withName = (obj, context) => ({ ...obj, name: context.name });
54
+ * const withAge = (obj, context) => ({ ...obj, age: context.age });
55
+ * const createPerson = transform(withName, withAge);
56
+ *
57
+ * const person = createPerson({}, { name: 'John', age: 30 });
58
+ * // Result: { name: 'John', age: 30 }
59
+ */
60
+ export const transform = (...transformers) => (obj, context = {}) =>
61
+ transformers.reduce((acc, transformer) => ({
62
+ ...acc,
63
+ ...transformer(acc, context)
64
+ }), obj)
65
+
66
+ /**
67
+ * @typedef {Object} TransformContext
68
+ * @property {any} [key] - Any contextual data needed by transformers
69
+ */