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,157 @@
1
+ // src/core/collection/list-manager.js
2
+
3
+ import { createRouteAdapter } from './adapters/route'
4
+
5
+ /**
6
+ * Creates a list manager for a specific collection
7
+ * @param {string} collection - Collection name
8
+ * @param {Object} config - Configuration options
9
+ * @param {Function} config.transform - Transform function for items
10
+ * @param {string} config.baseUrl - Base API URL
11
+ * @returns {Object} List manager methods
12
+ */
13
+ export const createListManager = (collection, config = {}) => {
14
+ const {
15
+ transform = (item) => item,
16
+ baseUrl = 'http://localhost:4000/api'
17
+ } = config
18
+
19
+ // Initialize route adapter
20
+ const adapter = createRouteAdapter({
21
+ base: baseUrl,
22
+ endpoints: {
23
+ list: `/${collection}`
24
+ },
25
+ headers: {
26
+ 'Content-Type': 'application/json'
27
+ }
28
+ })
29
+
30
+ // Load items with cursor pagination
31
+ const loadItems = async (params = {}) => {
32
+ try {
33
+ const response = await adapter.read(params)
34
+
35
+ return {
36
+ items: response.items.map(transform),
37
+ meta: response.meta
38
+ }
39
+ } catch (error) {
40
+ console.error(`Error loading ${collection}:`, error)
41
+ return {
42
+ items: [],
43
+ meta: {
44
+ cursor: null,
45
+ hasNext: false
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ // Utility to create a cursor-based page loader
52
+ const createPageLoader = (list, { onLoad, pageSize = 20 } = {}) => {
53
+ let currentCursor = null
54
+ let loading = false
55
+ const pageHistory = []
56
+
57
+ const load = async (cursor = null, addToHistory = true) => {
58
+ if (loading) return
59
+
60
+ loading = true
61
+ onLoad?.({ loading: true })
62
+
63
+ const { items, meta } = await loadItems({
64
+ limit: pageSize,
65
+ cursor
66
+ })
67
+
68
+ if (addToHistory && cursor) {
69
+ pageHistory.push(currentCursor)
70
+ }
71
+ currentCursor = meta.cursor
72
+
73
+ list.setItems(items)
74
+ loading = false
75
+
76
+ onLoad?.({
77
+ loading: false,
78
+ hasNext: meta.hasNext,
79
+ hasPrev: pageHistory.length > 0,
80
+ items
81
+ })
82
+
83
+ return {
84
+ hasNext: meta.hasNext,
85
+ hasPrev: pageHistory.length > 0
86
+ }
87
+ }
88
+
89
+ const loadNext = () => load(currentCursor)
90
+
91
+ const loadPrev = () => {
92
+ const previousCursor = pageHistory.pop()
93
+ return load(previousCursor, false)
94
+ }
95
+
96
+ return {
97
+ load,
98
+ loadNext,
99
+ loadPrev,
100
+ get loading () { return loading },
101
+ get cursor () { return currentCursor }
102
+ }
103
+ }
104
+
105
+ return {
106
+ loadItems,
107
+ createPageLoader
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Transform functions for common collections
113
+ */
114
+ export const transforms = {
115
+ track: (track) => ({
116
+ id: track._id,
117
+ headline: track.title || 'Untitled',
118
+ supportingText: track.artist || 'Unknown Artist',
119
+ meta: track.year?.toString() || ''
120
+ }),
121
+
122
+ playlist: (playlist) => ({
123
+ id: playlist._id,
124
+ headline: playlist.name || 'Untitled Playlist',
125
+ supportingText: `${playlist.tracks?.length || 0} tracks`,
126
+ meta: playlist.creator || ''
127
+ }),
128
+
129
+ country: (country) => ({
130
+ id: country._id,
131
+ headline: country.name || country.code,
132
+ supportingText: country.continent || '',
133
+ meta: country.code || ''
134
+ })
135
+ }
136
+
137
+ /**
138
+ * Usage example:
139
+ *
140
+ * const trackManager = createListManager('track', {
141
+ * transform: transforms.track
142
+ * })
143
+ *
144
+ * const loader = trackManager.createPageLoader(list, {
145
+ * onLoad: ({ loading, hasNext, items }) => {
146
+ * updateNavigation({ loading, hasNext })
147
+ * logEvent(`Loaded ${items.length} tracks`)
148
+ * }
149
+ * })
150
+ *
151
+ * // Initial load
152
+ * await loader.load()
153
+ *
154
+ * // Navigation
155
+ * nextButton.onclick = () => loader.loadNext()
156
+ * prevButton.onclick = () => loader.loadPrev()
157
+ */
@@ -0,0 +1,8 @@
1
+ // src/core/compose/base.js
2
+ export const createComponent = (config = {}) => ({
3
+ element: null,
4
+ config,
5
+ setup () {
6
+ return this
7
+ }
8
+ })
@@ -0,0 +1,225 @@
1
+ // src/core/compose/component.js
2
+ /**
3
+ * @module core/compose/component
4
+ * @description Core utilities for component composition and creation with built-in mobile support
5
+ */
6
+
7
+ import { createElement } from '../dom/create'
8
+ import {
9
+ normalizeEvent,
10
+ hasTouchSupport,
11
+ TOUCH_CONFIG,
12
+ PASSIVE_EVENTS
13
+ } from '../utils/mobile'
14
+
15
+ /**
16
+ * Creates helper functions for managing CSS class names with a prefix
17
+ * @param {string} prefix - Prefix to apply to class names
18
+ * @returns {Object} Class name utilities
19
+ * @property {Function} getClass - Gets a class name with prefix
20
+ * @property {Function} getModifierClass - Gets a modifier class with prefix
21
+ * @property {Function} getElementClass - Gets an element class with prefix
22
+ * @example
23
+ * const { getClass } = withPrefix('mtrl');
24
+ * getClass('button'); // Returns 'mtrl-button'
25
+ */
26
+ const withPrefix = prefix => ({
27
+ /**
28
+ * Gets a prefixed class name
29
+ * @param {string} name - Base class name
30
+ * @returns {string} Prefixed class name
31
+ */
32
+ getClass: (name) => `${prefix}-${name}`,
33
+ /**
34
+ * Gets a prefixed modifier class name
35
+ * @param {string} base - Base class name
36
+ * @param {string} modifier - Modifier name
37
+ * @returns {string} Prefixed modifier class
38
+ */
39
+ getModifierClass: (base, modifier) => `${base}--${modifier}`,
40
+ /**
41
+ * Gets a prefixed element class name
42
+ * @param {string} base - Base class name
43
+ * @param {string} element - Element name
44
+ * @returns {string} Prefixed element class
45
+ */
46
+ getElementClass: (base, element) => `${base}-${element}`
47
+ })
48
+
49
+ /**
50
+ * Creates a base component with configuration and prefix utilities.
51
+ * This forms the foundation for all components in the system.
52
+ *
53
+ * @param {Object} config - Component configuration
54
+ * @param {string} [config.prefix='mtrl'] - CSS class prefix
55
+ * @param {string} [config.componentName] - Component name for class generation
56
+ * @returns {Object} Base component with prefix utilities
57
+ */
58
+ export const createBase = (config = {}) => ({
59
+ config,
60
+ componentName: config.componentName,
61
+ ...withPrefix(config.prefix || 'mtrl'),
62
+
63
+ /**
64
+ * Manages the touch interaction state for the component.
65
+ * This helps track touch gestures and interactions.
66
+ */
67
+ touchState: {
68
+ startTime: 0,
69
+ startPosition: { x: 0, y: 0 },
70
+ isTouching: false,
71
+ activeTarget: null
72
+ },
73
+
74
+ /**
75
+ * Updates the component's touch state based on user interactions.
76
+ * Tracks touch position and timing for gesture recognition.
77
+ */
78
+ updateTouchState (event, status) {
79
+ const normalized = normalizeEvent(event)
80
+
81
+ if (status === 'start') {
82
+ this.touchState = {
83
+ startTime: Date.now(),
84
+ startPosition: {
85
+ x: normalized.clientX,
86
+ y: normalized.clientY
87
+ },
88
+ isTouching: true,
89
+ activeTarget: normalized.target
90
+ }
91
+ } else if (status === 'end') {
92
+ this.touchState.isTouching = false
93
+ this.touchState.activeTarget = null
94
+ }
95
+ }
96
+ })
97
+
98
+ /**
99
+ * Higher-order function that adds a DOM element to a component
100
+ * @param {Object} options - Element creation options
101
+ * @param {string} [options.tag='div'] - HTML tag name
102
+ * @param {string} [options.componentName] - Component name for class generation
103
+ * @param {Object} [options.attrs] - HTML attributes
104
+ * @param {string|string[]} [options.className] - Additional CSS classes
105
+ * @param {Object} [options.forwardEvents] - Native events to forward to component events
106
+ * @returns {Function} Component enhancer
107
+ * @example
108
+ * pipe(
109
+ * createBase,
110
+ * withElement({
111
+ * tag: 'button',
112
+ * componentName: 'button',
113
+ * attrs: { type: 'button' },
114
+ * forwardEvents: {
115
+ * click: component => !component.element.disabled
116
+ * }
117
+ * })
118
+ * )({ prefix: 'app' })
119
+ */
120
+ export const withElement = (options = {}) => (base) => {
121
+ /**
122
+ * Handles the start of a touch interaction.
123
+ * Initializes touch tracking and provides visual feedback.
124
+ */
125
+ const handleTouchStart = (event) => {
126
+ base.updateTouchState(event, 'start')
127
+ element.classList.add(`${base.getClass('touch-active')}`)
128
+
129
+ if (options.forwardEvents?.touchstart) {
130
+ base.emit?.('touchstart', normalizeEvent(event))
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Handles the end of a touch interaction.
136
+ * Detects taps and cleans up touch state.
137
+ */
138
+ const handleTouchEnd = (event) => {
139
+ if (!base.touchState.isTouching) return
140
+
141
+ const touchDuration = Date.now() - base.touchState.startTime
142
+ element.classList.remove(`${base.getClass('touch-active')}`)
143
+ base.updateTouchState(event, 'end')
144
+
145
+ // Emit tap event for short touches
146
+ if (touchDuration < TOUCH_CONFIG.TAP_THRESHOLD) {
147
+ base.emit?.('tap', normalizeEvent(event))
148
+ }
149
+
150
+ if (options.forwardEvents?.touchend) {
151
+ base.emit?.('touchend', normalizeEvent(event))
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Handles touch movement.
157
+ * Detects swipes and other gesture-based interactions.
158
+ */
159
+ const handleTouchMove = (event) => {
160
+ if (!base.touchState.isTouching) return
161
+
162
+ const normalized = normalizeEvent(event)
163
+ const deltaX = normalized.clientX - base.touchState.startPosition.x
164
+ const deltaY = normalized.clientY - base.touchState.startPosition.y
165
+
166
+ // Detect and emit swipe gestures
167
+ if (Math.abs(deltaX) > TOUCH_CONFIG.SWIPE_THRESHOLD) {
168
+ base.emit?.('swipe', {
169
+ direction: deltaX > 0 ? 'right' : 'left',
170
+ deltaX,
171
+ deltaY
172
+ })
173
+ }
174
+
175
+ if (options.forwardEvents?.touchmove) {
176
+ base.emit?.('touchmove', { ...normalized, deltaX, deltaY })
177
+ }
178
+ }
179
+
180
+ // Create the element with appropriate classes
181
+ const element = createElement({
182
+ ...options,
183
+ className: [
184
+ base.getClass(options.componentName || base.componentName || 'component'),
185
+ hasTouchSupport() && options.interactive ? base.getClass('interactive') : null,
186
+ options.className
187
+ ].filter(Boolean),
188
+ context: base
189
+ })
190
+
191
+ // Add event listeners only if touch is supported and the component is interactive
192
+ if (hasTouchSupport() && options.interactive) {
193
+ element.addEventListener('touchstart', handleTouchStart, PASSIVE_EVENTS)
194
+ element.addEventListener('touchend', handleTouchEnd)
195
+ element.addEventListener('touchmove', handleTouchMove, PASSIVE_EVENTS)
196
+ }
197
+
198
+ return {
199
+ ...base,
200
+ element,
201
+
202
+ /**
203
+ * Adds CSS classes to the element
204
+ * @param {...string} classes - CSS classes to add
205
+ * @returns {Object} Component instance for chaining
206
+ */
207
+ addClass (...classes) {
208
+ element.classList.add(...classes.filter(Boolean))
209
+ return this
210
+ },
211
+
212
+ /**
213
+ * Removes the element and cleans up event listeners.
214
+ * Ensures proper resource cleanup when the component is destroyed.
215
+ */
216
+ destroy () {
217
+ if (hasTouchSupport() && options.interactive) {
218
+ element.removeEventListener('touchstart', handleTouchStart)
219
+ element.removeEventListener('touchend', handleTouchEnd)
220
+ element.removeEventListener('touchmove', handleTouchMove)
221
+ }
222
+ element.remove()
223
+ }
224
+ }
225
+ }
@@ -0,0 +1,114 @@
1
+ // src/core/compose/features/checkable.js
2
+
3
+ /**
4
+ * Adds checked state management to a component with an input
5
+ * Manages visual state and event emission for checked changes
6
+ *
7
+ * @param {Object} config - Checkable configuration
8
+ * @param {boolean} [config.checked] - Initial checked state
9
+ *
10
+ * @returns {Function} Component transformer that adds checkable functionality
11
+ *
12
+ * @example
13
+ * const component = pipe(
14
+ * createBase,
15
+ * withEvents(),
16
+ * withInput(config),
17
+ * withCheckable({ checked: true })
18
+ * )(config);
19
+ *
20
+ * // Use the checkable API
21
+ * component.checkable.toggle();
22
+ * component.checkable.check();
23
+ * component.checkable.uncheck();
24
+ *
25
+ * // Listen for changes
26
+ * component.on('change', ({ checked }) => {
27
+ * console.log('State changed:', checked);
28
+ * });
29
+ */
30
+ export const withCheckable = (config = {}) => (component) => {
31
+ if (!component.input) return component
32
+
33
+ /**
34
+ * Updates component classes to reflect checked state
35
+ * @private
36
+ */
37
+ const updateStateClasses = () => {
38
+ component.element.classList.toggle(
39
+ `${component.getClass('switch')}--checked`,
40
+ component.input.checked
41
+ )
42
+ }
43
+
44
+ // Set initial state
45
+ if (config.checked) {
46
+ component.input.checked = true
47
+ updateStateClasses()
48
+ }
49
+
50
+ // Update classes whenever checked state changes
51
+ component.on('change', updateStateClasses)
52
+
53
+ return {
54
+ ...component,
55
+ checkable: {
56
+ /**
57
+ * Sets the checked state to true
58
+ * Emits change event if state changes
59
+ * @returns {Object} Checkable interface
60
+ */
61
+ check () {
62
+ if (!component.input.checked) {
63
+ component.input.checked = true
64
+ updateStateClasses()
65
+ component.emit('change', {
66
+ checked: true,
67
+ value: component.input.value
68
+ })
69
+ }
70
+ return this
71
+ },
72
+
73
+ /**
74
+ * Sets the checked state to false
75
+ * Emits change event if state changes
76
+ * @returns {Object} Checkable interface
77
+ */
78
+ uncheck () {
79
+ if (component.input.checked) {
80
+ component.input.checked = false
81
+ updateStateClasses()
82
+ component.emit('change', {
83
+ checked: false,
84
+ value: component.input.value
85
+ })
86
+ }
87
+ return this
88
+ },
89
+
90
+ /**
91
+ * Toggles the current checked state
92
+ * Always emits change event
93
+ * @returns {Object} Checkable interface
94
+ */
95
+ toggle () {
96
+ component.input.checked = !component.input.checked
97
+ updateStateClasses()
98
+ component.emit('change', {
99
+ checked: component.input.checked,
100
+ value: component.input.value
101
+ })
102
+ return this
103
+ },
104
+
105
+ /**
106
+ * Gets the current checked state
107
+ * @returns {boolean} Whether component is checked
108
+ */
109
+ isChecked () {
110
+ return component.input.checked
111
+ }
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,25 @@
1
+ // src/core/compose/features/disabled.js
2
+
3
+ export const withDisabled = (config) => (component) => {
4
+ if (!component.element) return component
5
+
6
+ return {
7
+ ...component,
8
+ disabled: {
9
+ enable () {
10
+ console.debug('disabled')
11
+ component.element.disabled = false
12
+ const className = `${config.prefix}-${config.componentName}--disable`
13
+ component.element.classList.remove(className)
14
+ return this
15
+ },
16
+ disable () {
17
+ console.debug('disabled')
18
+ component.element.disabled = true
19
+ const className = `${config.prefix}-${config.componentName}--disable`
20
+ component.element.classList.add(className)
21
+ return this
22
+ }
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,48 @@
1
+ // src/core/compose/features/withEvents.js
2
+ /**
3
+ * @module core/compose/features
4
+ */
5
+
6
+ import { createEmitter } from '../../state/emitter'
7
+
8
+ /**
9
+ * Adds event handling capabilities to a component
10
+ * @memberof module:core/compose/features
11
+ * @function withEvents
12
+ * @param {HTMLElement} [target] - Event target element
13
+ * @returns {Function} Component transformer
14
+ * @example
15
+ * const button = pipe(
16
+ * createBase({ componentName: 'button' }),
17
+ * withElement(),
18
+ * withEvents()
19
+ * )({})
20
+ *
21
+ * button.on('click', () => console.log('clicked'))
22
+ */
23
+
24
+ /**
25
+ * Adds event handling capabilities to a component
26
+ * Returns event system ready to use immediately
27
+ */
28
+ export const withEvents = () => (component) => {
29
+ const emitter = createEmitter()
30
+
31
+ return {
32
+ ...component,
33
+ on (event, handler) {
34
+ emitter.on(event, handler)
35
+ return this
36
+ },
37
+
38
+ off (event, handler) {
39
+ emitter.off(event, handler)
40
+ return this
41
+ },
42
+
43
+ emit (event, data) {
44
+ emitter.emit(event, data)
45
+ return this
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,33 @@
1
+ // src/core/compose/features/icon.js
2
+ import { createIcon } from '../../../core/build/icon'
3
+
4
+ const updateCircularStyle = (component, config) => {
5
+ const hasText = config.text
6
+ const hasIcon = config.icon
7
+
8
+ const circularClass = `${component.getClass('button')}--circular`
9
+ if (!hasText && hasIcon) {
10
+ component.element.classList.add(circularClass)
11
+ } else {
12
+ component.element.classList.remove(circularClass)
13
+ }
14
+ }
15
+
16
+ export const withIcon = (config = {}) => (component) => {
17
+ const icon = createIcon(component.element, {
18
+ prefix: config.prefix,
19
+ type: 'button',
20
+ position: config.iconPosition
21
+ })
22
+
23
+ if (config.icon) {
24
+ icon.setIcon(config.icon)
25
+ }
26
+
27
+ updateCircularStyle(component, config)
28
+
29
+ return {
30
+ ...component,
31
+ icon
32
+ }
33
+ }
@@ -0,0 +1,20 @@
1
+ // src/core/compose/features/index.js
2
+
3
+ // Core features
4
+ export { withEvents } from './events'
5
+ export { withText } from './text'
6
+ export { withIcon } from './icon'
7
+ export { withVariant } from './variant'
8
+ export { withSize } from './size'
9
+ export { withPosition } from './position'
10
+ export { withInput } from './input'
11
+ export { withTrack } from './track'
12
+ export { withTextInput } from './textinput'
13
+ export { withTextLabel } from './textlabel'
14
+ export { withRipple } from './ripple'
15
+
16
+ // State management features
17
+ export { withDisabled } from './disabled'
18
+ export { withCheckable } from './checkable'
19
+
20
+ export { withLifecycle } from './lifecycle'