mtrl 0.1.0 → 0.1.3

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/index.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  createLayout,
4
4
  createElement,
5
- createButton, createCheckbox, createTextfield, createSwitch, createContainer, createList, createSnackbar, createNavigation, createMenu
5
+ createButton, createCard, createCheckbox, createTextfield, createSwitch, createContainer, createList, createSnackbar, createNavigation, createMenu
6
6
  } from './src/index.js'
7
7
 
8
8
  export {
9
9
  createLayout,
10
- createElement, createButton, createCheckbox, createTextfield, createSwitch, createContainer, createList, createSnackbar, createNavigation, createMenu
10
+ createElement, createCard, createButton, createCheckbox, createTextfield, createSwitch, createContainer, createList, createSnackbar, createNavigation, createMenu
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtrl",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "A functional JavaScript component library with composable architecture based on Material Design 3",
5
5
  "keywords": ["component", "library", "ui", "user interface", "functional", "composable"],
6
6
  "main": "index.js",
@@ -0,0 +1,51 @@
1
+ // src/components/card/actions.js
2
+ import { PREFIX } from '../../core/config'
3
+ import { pipe } from '../../core/compose'
4
+ import { createBase, withElement } from '../../core/compose/component'
5
+
6
+ /**
7
+ * Creates a card actions component
8
+ * @param {Object} config - Actions configuration
9
+ * @param {Array<HTMLElement>} [config.actions] - Action elements to include
10
+ * @param {boolean} [config.fullBleed=false] - Whether actions extend full width
11
+ * @param {boolean} [config.vertical=false] - Whether to stack actions vertically
12
+ * @param {string} [config.align='start'] - Horizontal alignment ('start', 'center', 'end', 'space-between')
13
+ * @returns {HTMLElement} Card actions element
14
+ */
15
+ export const createCardActions = (config = {}) => {
16
+ const baseConfig = {
17
+ ...config,
18
+ componentName: 'card-actions',
19
+ prefix: PREFIX
20
+ }
21
+
22
+ try {
23
+ const actions = pipe(
24
+ createBase,
25
+ withElement({
26
+ tag: 'div',
27
+ componentName: 'card-actions',
28
+ className: [
29
+ config.class,
30
+ config.fullBleed ? `${PREFIX}-card-actions--full-bleed` : null,
31
+ config.vertical ? `${PREFIX}-card-actions--vertical` : null,
32
+ config.align ? `${PREFIX}-card-actions--${config.align}` : null
33
+ ]
34
+ })
35
+ )(baseConfig)
36
+
37
+ // Add action elements if provided
38
+ if (Array.isArray(config.actions)) {
39
+ config.actions.forEach(action => {
40
+ if (action instanceof HTMLElement) {
41
+ actions.element.appendChild(action)
42
+ }
43
+ })
44
+ }
45
+
46
+ return actions.element
47
+ } catch (error) {
48
+ console.error('Card actions creation error:', error)
49
+ throw new Error(`Failed to create card actions: ${error.message}`)
50
+ }
51
+ }
@@ -0,0 +1,102 @@
1
+ // src/components/card/api.js
2
+
3
+ /**
4
+ * Enhances a card component with API methods
5
+ * @param {Object} options - API configuration options
6
+ * @param {Object} options.lifecycle - Object containing lifecycle methods
7
+ * @returns {Function} Higher-order function that adds API methods to component
8
+ * @internal This is an internal utility for the Card component
9
+ */
10
+ export const withAPI = ({ lifecycle }) => (component) => ({
11
+ ...component,
12
+ element: component.element,
13
+
14
+ /**
15
+ * Adds content to the card
16
+ * @param {HTMLElement} contentElement - The content element to add
17
+ * @returns {Object} The card instance for chaining
18
+ */
19
+ addContent (contentElement) {
20
+ if (contentElement && contentElement.classList.contains(`${component.getClass('card')}-content`)) {
21
+ component.element.appendChild(contentElement)
22
+ }
23
+ return this
24
+ },
25
+
26
+ /**
27
+ * Sets the card header
28
+ * @param {HTMLElement} headerElement - The header element to add
29
+ * @returns {Object} The card instance for chaining
30
+ */
31
+ setHeader (headerElement) {
32
+ if (headerElement && headerElement.classList.contains(`${component.getClass('card')}-header`)) {
33
+ // Remove existing header if present
34
+ const existingHeader = component.element.querySelector(`.${component.getClass('card')}-header`)
35
+ if (existingHeader) {
36
+ existingHeader.remove()
37
+ }
38
+
39
+ // Insert at the beginning of the card
40
+ component.element.insertBefore(headerElement, component.element.firstChild)
41
+ }
42
+ return this
43
+ },
44
+
45
+ /**
46
+ * Adds media to the card
47
+ * @param {HTMLElement} mediaElement - The media element to add
48
+ * @param {string} [position='top'] - Position to place media ('top', 'bottom')
49
+ * @returns {Object} The card instance for chaining
50
+ */
51
+ addMedia (mediaElement, position = 'top') {
52
+ if (mediaElement && mediaElement.classList.contains(`${component.getClass('card')}-media`)) {
53
+ if (position === 'top') {
54
+ component.element.insertBefore(mediaElement, component.element.firstChild)
55
+ } else {
56
+ component.element.appendChild(mediaElement)
57
+ }
58
+ }
59
+ return this
60
+ },
61
+
62
+ /**
63
+ * Sets the card actions section
64
+ * @param {HTMLElement} actionsElement - The actions element to add
65
+ * @returns {Object} The card instance for chaining
66
+ */
67
+ setActions (actionsElement) {
68
+ if (actionsElement && actionsElement.classList.contains(`${component.getClass('card')}-actions`)) {
69
+ // Remove existing actions if present
70
+ const existingActions = component.element.querySelector(`.${component.getClass('card')}-actions`)
71
+ if (existingActions) {
72
+ existingActions.remove()
73
+ }
74
+
75
+ // Add actions at the end
76
+ component.element.appendChild(actionsElement)
77
+ }
78
+ return this
79
+ },
80
+
81
+ /**
82
+ * Makes the card draggable
83
+ * @param {Function} [dragStartCallback] - Callback for drag start event
84
+ * @returns {Object} The card instance for chaining
85
+ */
86
+ makeDraggable (dragStartCallback) {
87
+ component.element.setAttribute('draggable', 'true')
88
+
89
+ if (typeof dragStartCallback === 'function') {
90
+ component.element.addEventListener('dragstart', dragStartCallback)
91
+ }
92
+
93
+ return this
94
+ },
95
+
96
+ /**
97
+ * Destroys the card component and removes event listeners
98
+ */
99
+ destroy () {
100
+ lifecycle.destroy()
101
+ }
102
+ })
@@ -0,0 +1,102 @@
1
+ // src/components/card/card.js
2
+ import { PREFIX } from '../../core/config'
3
+ import { pipe } from '../../core/compose'
4
+ import { createBase, withElement } from '../../core/compose/component'
5
+ import {
6
+ withEvents,
7
+ withVariant,
8
+ withRipple,
9
+ withLifecycle
10
+ } from '../../core/compose/features'
11
+ import { withAPI } from './api'
12
+ import { CARD_VARIANTS, CARD_ELEVATIONS } from './constants'
13
+ import defaultConfig from './config'
14
+
15
+ /**
16
+ * Creates a new Card component following Material Design 3 principles
17
+ * @param {Object} config - Card configuration object
18
+ * @param {string} [config.variant='elevated'] - Card variant (elevated, filled, outlined)
19
+ * @param {boolean} [config.interactive=false] - Whether the card has hover/focus states
20
+ * @param {boolean} [config.fullWidth=false] - Whether the card spans full width of container
21
+ * @param {boolean} [config.clickable=false] - Whether the card is clickable with ripple effect
22
+ * @param {boolean} [config.draggable=false] - Whether the card is draggable
23
+ * @param {string} [config.class] - Additional CSS classes
24
+ * @returns {Object} Card component instance
25
+ */
26
+ const createCard = (config = {}) => {
27
+ const baseConfig = {
28
+ ...defaultConfig,
29
+ ...config,
30
+ componentName: 'card',
31
+ prefix: PREFIX
32
+ }
33
+
34
+ try {
35
+ const card = pipe(
36
+ createBase,
37
+ withEvents(),
38
+ withElement({
39
+ tag: 'div',
40
+ componentName: 'card',
41
+ className: [
42
+ config.class,
43
+ config.fullWidth ? `${PREFIX}-card--full-width` : null,
44
+ config.interactive ? `${PREFIX}-card--interactive` : null
45
+ ],
46
+ forwardEvents: {
47
+ click: (component) => config.clickable,
48
+ mouseenter: (component) => config.interactive,
49
+ mouseleave: (component) => config.interactive
50
+ },
51
+ interactive: config.interactive || config.clickable
52
+ }),
53
+ withVariant(baseConfig),
54
+ config.clickable ? withRipple(baseConfig) : (c) => c,
55
+ withLifecycle(),
56
+ comp => {
57
+ // Implement hover state elevation changes for interactive cards
58
+ if (comp.config.interactive) {
59
+ comp.element.addEventListener('mouseenter', () => {
60
+ if (comp.config.variant === CARD_VARIANTS.ELEVATED) {
61
+ comp.element.style.setProperty('--card-elevation', CARD_ELEVATIONS.HOVERED)
62
+ }
63
+ })
64
+
65
+ comp.element.addEventListener('mouseleave', () => {
66
+ if (comp.config.variant === CARD_VARIANTS.ELEVATED) {
67
+ comp.element.style.setProperty('--card-elevation', CARD_ELEVATIONS.RESTING)
68
+ }
69
+ })
70
+ }
71
+
72
+ // Set up draggable
73
+ if (comp.config.draggable) {
74
+ comp.element.setAttribute('draggable', 'true')
75
+ comp.element.addEventListener('dragstart', (e) => {
76
+ comp.element.style.setProperty('--card-elevation', CARD_ELEVATIONS.DRAGGED)
77
+ comp.emit('dragstart', { event: e })
78
+ })
79
+
80
+ comp.element.addEventListener('dragend', (e) => {
81
+ comp.element.style.setProperty('--card-elevation', CARD_ELEVATIONS.RESTING)
82
+ comp.emit('dragend', { event: e })
83
+ })
84
+ }
85
+
86
+ return comp
87
+ },
88
+ comp => withAPI({
89
+ lifecycle: {
90
+ destroy: () => comp.lifecycle.destroy()
91
+ }
92
+ })(comp)
93
+ )(baseConfig)
94
+
95
+ return card
96
+ } catch (error) {
97
+ console.error('Card creation error:', error)
98
+ throw new Error(`Failed to create card: ${error.message}`)
99
+ }
100
+ }
101
+
102
+ export default createCard
@@ -0,0 +1,16 @@
1
+ // src/components/card/config.js
2
+
3
+ import { PREFIX } from '../../core/config'
4
+ import { CARD_VARIANTS } from './constants'
5
+
6
+ const defaultConfig = {
7
+ componentName: 'card',
8
+ prefix: PREFIX,
9
+ variant: CARD_VARIANTS.ELEVATED,
10
+ interactive: false,
11
+ fullWidth: false,
12
+ clickable: false,
13
+ draggable: false
14
+ }
15
+
16
+ export default defaultConfig
@@ -0,0 +1,68 @@
1
+ // src/components/card/constants.js
2
+
3
+ /**
4
+ * Card variant types following Material Design 3
5
+ * @enum {string}
6
+ */
7
+ export const CARD_VARIANTS = {
8
+ ELEVATED: 'elevated',
9
+ FILLED: 'filled',
10
+ OUTLINED: 'outlined'
11
+ }
12
+
13
+ /**
14
+ * Card elevation levels
15
+ * @enum {number}
16
+ */
17
+ export const CARD_ELEVATIONS = {
18
+ RESTING: 1,
19
+ HOVERED: 2,
20
+ DRAGGED: 4
21
+ }
22
+
23
+ /**
24
+ * Validation schema for card configuration
25
+ */
26
+ export const CARD_SCHEMA = {
27
+ variant: {
28
+ type: 'string',
29
+ enum: Object.values(CARD_VARIANTS),
30
+ default: CARD_VARIANTS.ELEVATED
31
+ },
32
+ interactive: {
33
+ type: 'boolean',
34
+ default: false
35
+ },
36
+ fullWidth: {
37
+ type: 'boolean',
38
+ default: false
39
+ },
40
+ clickable: {
41
+ type: 'boolean',
42
+ default: false
43
+ },
44
+ draggable: {
45
+ type: 'boolean',
46
+ default: false
47
+ },
48
+ class: {
49
+ type: 'string',
50
+ required: false
51
+ },
52
+ headerConfig: {
53
+ type: 'object',
54
+ required: false
55
+ },
56
+ contentConfig: {
57
+ type: 'object',
58
+ required: false
59
+ },
60
+ actionsConfig: {
61
+ type: 'object',
62
+ required: false
63
+ },
64
+ mediaConfig: {
65
+ type: 'object',
66
+ required: false
67
+ }
68
+ }
@@ -0,0 +1,51 @@
1
+ // src/components/card/content.js
2
+ import { PREFIX } from '../../core/config'
3
+ import { pipe } from '../../core/compose'
4
+ import { createBase, withElement } from '../../core/compose/component'
5
+
6
+ /**
7
+ * Creates a card content component
8
+ * @param {Object} config - Content configuration
9
+ * @param {string} [config.text] - Text content
10
+ * @param {string} [config.html] - HTML content
11
+ * @param {Array<HTMLElement>} [config.children] - Child elements to append
12
+ * @param {boolean} [config.padding=true] - Whether to apply default padding
13
+ * @returns {HTMLElement} Card content element
14
+ */
15
+ export const createCardContent = (config = {}) => {
16
+ const baseConfig = {
17
+ ...config,
18
+ componentName: 'card-content',
19
+ prefix: PREFIX
20
+ }
21
+
22
+ try {
23
+ const content = pipe(
24
+ createBase,
25
+ withElement({
26
+ tag: 'div',
27
+ componentName: 'card-content',
28
+ className: [
29
+ config.class,
30
+ config.padding === false ? `${PREFIX}-card-content--no-padding` : null
31
+ ],
32
+ html: config.html,
33
+ text: config.text
34
+ })
35
+ )(baseConfig)
36
+
37
+ // Add children if provided
38
+ if (Array.isArray(config.children)) {
39
+ config.children.forEach(child => {
40
+ if (child instanceof HTMLElement) {
41
+ content.element.appendChild(child)
42
+ }
43
+ })
44
+ }
45
+
46
+ return content.element
47
+ } catch (error) {
48
+ console.error('Card content creation error:', error)
49
+ throw new Error(`Failed to create card content: ${error.message}`)
50
+ }
51
+ }
@@ -0,0 +1,218 @@
1
+ // src/components/card/features.js
2
+ import { PREFIX } from '../../core/config'
3
+ import { createElement } from '../../core/dom/create'
4
+
5
+ /**
6
+ * Higher-order function to add loading state to a card
7
+ * @param {Object} config - Loading state configuration
8
+ * @param {boolean} [config.initialState=false] - Initial loading state
9
+ * @returns {Function} Card component enhancer
10
+ */
11
+ export const withLoading = (config = {}) => (component) => {
12
+ const initialState = config.initialState || false
13
+ let loadingElement = null
14
+ let isLoading = initialState
15
+
16
+ if (initialState) {
17
+ setLoading(true)
18
+ }
19
+
20
+ function setLoading (loading) {
21
+ isLoading = loading
22
+
23
+ if (loading && !loadingElement) {
24
+ // Create and add loading overlay
25
+ loadingElement = createElement({
26
+ tag: 'div',
27
+ className: `${PREFIX}-card-loading-overlay`,
28
+ container: component.element
29
+ })
30
+
31
+ // Add spinner
32
+ createElement({
33
+ tag: 'div',
34
+ className: `${PREFIX}-card-loading-spinner`,
35
+ container: loadingElement
36
+ })
37
+
38
+ component.element.classList.add(`${PREFIX}-card--state-loading`)
39
+ } else if (!loading && loadingElement) {
40
+ // Remove loading overlay
41
+ loadingElement.remove()
42
+ loadingElement = null
43
+ component.element.classList.remove(`${PREFIX}-card--state-loading`)
44
+ }
45
+ }
46
+
47
+ return {
48
+ ...component,
49
+ loading: {
50
+ isLoading: () => isLoading,
51
+ setLoading
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Higher-order function to add expandable behavior to a card
58
+ * @param {Object} config - Expandable configuration
59
+ * @param {boolean} [config.initialExpanded=false] - Whether card is initially expanded
60
+ * @param {HTMLElement} [config.expandableContent] - Content to show when expanded
61
+ * @returns {Function} Card component enhancer
62
+ */
63
+ export const withExpandable = (config = {}) => (component) => {
64
+ const initialExpanded = config.initialExpanded || false
65
+ let isExpanded = initialExpanded
66
+ const expandableContent = config.expandableContent
67
+ let expandButton = null
68
+
69
+ // Create expand/collapse button
70
+ expandButton = createElement({
71
+ tag: 'button',
72
+ className: `${PREFIX}-card-expand-button`,
73
+ attrs: {
74
+ 'aria-expanded': isExpanded ? 'true' : 'false',
75
+ 'aria-label': isExpanded ? 'Collapse' : 'Expand'
76
+ }
77
+ })
78
+
79
+ // Add to card as action if not already present
80
+ const actionsContainer = component.element.querySelector(`.${PREFIX}-card-actions`)
81
+ if (actionsContainer) {
82
+ actionsContainer.appendChild(expandButton)
83
+ } else {
84
+ // Create actions container if not present
85
+ const newActionsContainer = createElement({
86
+ tag: 'div',
87
+ className: `${PREFIX}-card-actions`,
88
+ container: component.element
89
+ })
90
+ newActionsContainer.appendChild(expandButton)
91
+ }
92
+
93
+ // Set initial state
94
+ if (expandableContent) {
95
+ expandableContent.classList.add(`${PREFIX}-card-expandable-content`)
96
+ if (!initialExpanded) {
97
+ expandableContent.style.display = 'none'
98
+ }
99
+ component.element.appendChild(expandableContent)
100
+ }
101
+
102
+ // Toggle expanded state
103
+ function toggleExpanded () {
104
+ setExpanded(!isExpanded)
105
+ }
106
+
107
+ // Set expanded state
108
+ function setExpanded (expanded) {
109
+ isExpanded = expanded
110
+
111
+ if (expandableContent) {
112
+ expandableContent.style.display = expanded ? 'block' : 'none'
113
+ }
114
+
115
+ expandButton.setAttribute('aria-expanded', expanded ? 'true' : 'false')
116
+ expandButton.setAttribute('aria-label', expanded ? 'Collapse' : 'Expand')
117
+
118
+ if (expanded) {
119
+ component.element.classList.add(`${PREFIX}-card--expanded`)
120
+ } else {
121
+ component.element.classList.remove(`${PREFIX}-card--expanded`)
122
+ }
123
+
124
+ component.emit('expandedChanged', { expanded })
125
+ }
126
+
127
+ // Add click handler to toggle button
128
+ expandButton.addEventListener('click', (e) => {
129
+ e.stopPropagation()
130
+ toggleExpanded()
131
+ })
132
+
133
+ return {
134
+ ...component,
135
+ expandable: {
136
+ isExpanded: () => isExpanded,
137
+ setExpanded,
138
+ toggleExpanded
139
+ }
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Higher-order function to add swipeable behavior to a card
145
+ * @param {Object} config - Swipeable configuration
146
+ * @param {Function} [config.onSwipeLeft] - Callback when card is swiped left
147
+ * @param {Function} [config.onSwipeRight] - Callback when card is swiped right
148
+ * @param {number} [config.threshold=100] - Swipe distance threshold to trigger action
149
+ * @returns {Function} Card component enhancer
150
+ */
151
+ export const withSwipeable = (config = {}) => (component) => {
152
+ const threshold = config.threshold || 100
153
+ let startX = 0
154
+ let currentX = 0
155
+
156
+ function handleTouchStart (e) {
157
+ startX = e.touches[0].clientX
158
+ component.element.style.transition = 'none'
159
+ }
160
+
161
+ function handleTouchMove (e) {
162
+ if (!startX) return
163
+
164
+ currentX = e.touches[0].clientX
165
+ const diffX = currentX - startX
166
+
167
+ // Apply transform to move card
168
+ component.element.style.transform = `translateX(${diffX}px)`
169
+ }
170
+
171
+ function handleTouchEnd () {
172
+ if (!startX) return
173
+
174
+ component.element.style.transition = 'transform 0.3s ease'
175
+ const diffX = currentX - startX
176
+
177
+ if (Math.abs(diffX) >= threshold) {
178
+ // Swipe threshold reached
179
+ if (diffX > 0 && config.onSwipeRight) {
180
+ // Swipe right
181
+ component.element.style.transform = 'translateX(100%)'
182
+ config.onSwipeRight(component)
183
+ } else if (diffX < 0 && config.onSwipeLeft) {
184
+ // Swipe left
185
+ component.element.style.transform = 'translateX(-100%)'
186
+ config.onSwipeLeft(component)
187
+ } else {
188
+ // Reset if no handler
189
+ component.element.style.transform = 'translateX(0)'
190
+ }
191
+ } else {
192
+ // Reset if below threshold
193
+ component.element.style.transform = 'translateX(0)'
194
+ }
195
+
196
+ startX = 0
197
+ currentX = 0
198
+ }
199
+
200
+ // Add event listeners
201
+ component.element.addEventListener('touchstart', handleTouchStart)
202
+ component.element.addEventListener('touchmove', handleTouchMove)
203
+ component.element.addEventListener('touchend', handleTouchEnd)
204
+
205
+ // Add swipeable class
206
+ component.element.classList.add(`${PREFIX}-card--swipeable`)
207
+
208
+ // Return enhanced component
209
+ return {
210
+ ...component,
211
+ swipeable: {
212
+ reset: () => {
213
+ component.element.style.transition = 'transform 0.3s ease'
214
+ component.element.style.transform = 'translateX(0)'
215
+ }
216
+ }
217
+ }
218
+ }