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.
- package/LICENSE +21 -0
- package/README.md +251 -0
- package/index.js +10 -0
- package/package.json +17 -0
- package/src/components/button/api.js +54 -0
- package/src/components/button/button.js +81 -0
- package/src/components/button/config.js +8 -0
- package/src/components/button/constants.js +63 -0
- package/src/components/button/index.js +2 -0
- package/src/components/button/styles.scss +231 -0
- package/src/components/checkbox/api.js +45 -0
- package/src/components/checkbox/checkbox.js +95 -0
- package/src/components/checkbox/constants.js +88 -0
- package/src/components/checkbox/index.js +2 -0
- package/src/components/checkbox/styles.scss +183 -0
- package/src/components/container/api.js +42 -0
- package/src/components/container/container.js +45 -0
- package/src/components/container/index.js +2 -0
- package/src/components/container/styles.scss +59 -0
- package/src/components/list/constants.js +89 -0
- package/src/components/list/index.js +2 -0
- package/src/components/list/list-item.js +147 -0
- package/src/components/list/list.js +267 -0
- package/src/components/list/styles/_list-item.scss +142 -0
- package/src/components/list/styles/_list.scss +89 -0
- package/src/components/list/styles/_variables.scss +13 -0
- package/src/components/list/styles.scss +19 -0
- package/src/components/navigation/api.js +43 -0
- package/src/components/navigation/constants.js +235 -0
- package/src/components/navigation/features/items.js +192 -0
- package/src/components/navigation/index.js +2 -0
- package/src/components/navigation/nav-item.js +137 -0
- package/src/components/navigation/navigation.js +55 -0
- package/src/components/navigation/styles/_bar.scss +51 -0
- package/src/components/navigation/styles/_base.scss +129 -0
- package/src/components/navigation/styles/_drawer.scss +169 -0
- package/src/components/navigation/styles/_rail.scss +65 -0
- package/src/components/navigation/styles.scss +6 -0
- package/src/components/snackbar/api.js +125 -0
- package/src/components/snackbar/constants.js +41 -0
- package/src/components/snackbar/features.js +69 -0
- package/src/components/snackbar/index.js +2 -0
- package/src/components/snackbar/position.js +63 -0
- package/src/components/snackbar/queue.js +74 -0
- package/src/components/snackbar/snackbar.js +70 -0
- package/src/components/snackbar/styles.scss +182 -0
- package/src/components/switch/api.js +44 -0
- package/src/components/switch/constants.js +80 -0
- package/src/components/switch/index.js +2 -0
- package/src/components/switch/styles.scss +172 -0
- package/src/components/switch/switch.js +71 -0
- package/src/components/textfield/api.js +49 -0
- package/src/components/textfield/constants.js +81 -0
- package/src/components/textfield/index.js +2 -0
- package/src/components/textfield/styles/base.scss +107 -0
- package/src/components/textfield/styles/filled.scss +58 -0
- package/src/components/textfield/styles/outlined.scss +66 -0
- package/src/components/textfield/styles.scss +6 -0
- package/src/components/textfield/textfield.js +68 -0
- package/src/core/build/constants.js +51 -0
- package/src/core/build/icon.js +78 -0
- package/src/core/build/ripple.js +92 -0
- package/src/core/build/text.js +54 -0
- package/src/core/collection/adapters/base.js +26 -0
- package/src/core/collection/adapters/mongodb.js +232 -0
- package/src/core/collection/adapters/route.js +201 -0
- package/src/core/collection/collection.js +259 -0
- package/src/core/collection/list-manager.js +157 -0
- package/src/core/compose/base.js +8 -0
- package/src/core/compose/component.js +225 -0
- package/src/core/compose/features/checkable.js +114 -0
- package/src/core/compose/features/disabled.js +25 -0
- package/src/core/compose/features/events.js +48 -0
- package/src/core/compose/features/icon.js +33 -0
- package/src/core/compose/features/index.js +20 -0
- package/src/core/compose/features/input.js +92 -0
- package/src/core/compose/features/lifecycle.js +69 -0
- package/src/core/compose/features/position.js +60 -0
- package/src/core/compose/features/ripple.js +32 -0
- package/src/core/compose/features/size.js +9 -0
- package/src/core/compose/features/style.js +12 -0
- package/src/core/compose/features/text.js +17 -0
- package/src/core/compose/features/textinput.js +118 -0
- package/src/core/compose/features/textlabel.js +28 -0
- package/src/core/compose/features/track.js +49 -0
- package/src/core/compose/features/variant.js +9 -0
- package/src/core/compose/features/withEvents.js +67 -0
- package/src/core/compose/index.js +16 -0
- package/src/core/compose/pipe.js +69 -0
- package/src/core/config.js +140 -0
- package/src/core/dom/attributes.js +33 -0
- package/src/core/dom/classes.js +70 -0
- package/src/core/dom/create.js +133 -0
- package/src/core/dom/events.js +175 -0
- package/src/core/dom/index.js +5 -0
- package/src/core/dom/utils.js +22 -0
- package/src/core/index.js +23 -0
- package/src/core/layout/index.js +93 -0
- package/src/core/state/disabled.js +14 -0
- package/src/core/state/emitter.js +63 -0
- package/src/core/state/events.js +29 -0
- package/src/core/state/index.js +6 -0
- package/src/core/state/lifecycle.js +64 -0
- package/src/core/state/store.js +112 -0
- package/src/core/utils/index.js +39 -0
- package/src/core/utils/mobile.js +74 -0
- package/src/core/utils/object.js +22 -0
- package/src/core/utils/validate.js +37 -0
- package/src/index.js +11 -0
- package/src/styles/abstract/_base.scss +2 -0
- package/src/styles/abstract/_config.scss +28 -0
- package/src/styles/abstract/_functions.scss +124 -0
- package/src/styles/abstract/_mixins.scss +261 -0
- package/src/styles/abstract/_variables.scss +158 -0
- package/src/styles/main.scss +78 -0
- package/src/styles/themes/_base-theme.scss +49 -0
- package/src/styles/themes/_baseline.scss +90 -0
- package/src/styles/themes/_forest.scss +71 -0
- package/src/styles/themes/_index.scss +6 -0
- package/src/styles/themes/_ocean.scss +71 -0
- 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
|
+
*/
|