mtrl 0.1.2 → 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 +2 -2
- package/package.json +1 -1
- package/src/components/card/actions.js +51 -0
- package/src/components/card/api.js +102 -0
- package/src/components/card/card.js +102 -0
- package/src/components/card/config.js +16 -0
- package/src/components/card/constants.js +68 -0
- package/src/components/card/content.js +51 -0
- package/src/components/card/features.js +218 -0
- package/src/components/card/header.js +92 -0
- package/src/components/card/index.js +7 -0
- package/src/components/card/media.js +56 -0
- package/src/components/card/styles.scss +287 -0
- package/src/index.js +1 -0
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.
|
|
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
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/components/card/header.js
|
|
2
|
+
import { PREFIX } from '../../core/config'
|
|
3
|
+
import { pipe } from '../../core/compose'
|
|
4
|
+
import { createBase, withElement } from '../../core/compose/component'
|
|
5
|
+
import { withText } from '../../core/compose/features'
|
|
6
|
+
import { createElement } from '../../core/dom/create'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a card header component
|
|
10
|
+
* @param {Object} config - Header configuration
|
|
11
|
+
* @param {string} [config.title] - Title text
|
|
12
|
+
* @param {string} [config.subtitle] - Subtitle text
|
|
13
|
+
* @param {HTMLElement|string} [config.avatar] - Avatar element or HTML string
|
|
14
|
+
* @param {HTMLElement|string} [config.action] - Action element or HTML string
|
|
15
|
+
* @returns {HTMLElement} Card header element
|
|
16
|
+
*/
|
|
17
|
+
export const createCardHeader = (config = {}) => {
|
|
18
|
+
const baseConfig = {
|
|
19
|
+
...config,
|
|
20
|
+
componentName: 'card-header',
|
|
21
|
+
prefix: PREFIX
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const header = pipe(
|
|
26
|
+
createBase,
|
|
27
|
+
withElement({
|
|
28
|
+
tag: 'div',
|
|
29
|
+
componentName: 'card-header',
|
|
30
|
+
className: config.class
|
|
31
|
+
})
|
|
32
|
+
)(baseConfig)
|
|
33
|
+
|
|
34
|
+
// Create text container
|
|
35
|
+
const textContainer = createElement({
|
|
36
|
+
tag: 'div',
|
|
37
|
+
className: `${PREFIX}-card-header-text`,
|
|
38
|
+
container: header.element
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Add title if provided
|
|
42
|
+
if (config.title) {
|
|
43
|
+
createElement({
|
|
44
|
+
tag: 'h3',
|
|
45
|
+
className: `${PREFIX}-card-header-title`,
|
|
46
|
+
text: config.title,
|
|
47
|
+
container: textContainer
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Add subtitle if provided
|
|
52
|
+
if (config.subtitle) {
|
|
53
|
+
createElement({
|
|
54
|
+
tag: 'h4',
|
|
55
|
+
className: `${PREFIX}-card-header-subtitle`,
|
|
56
|
+
text: config.subtitle,
|
|
57
|
+
container: textContainer
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Add avatar if provided
|
|
62
|
+
if (config.avatar) {
|
|
63
|
+
const avatarElement = typeof config.avatar === 'string'
|
|
64
|
+
? createElement({
|
|
65
|
+
tag: 'div',
|
|
66
|
+
className: `${PREFIX}-card-header-avatar`,
|
|
67
|
+
html: config.avatar
|
|
68
|
+
})
|
|
69
|
+
: config.avatar
|
|
70
|
+
|
|
71
|
+
header.element.insertBefore(avatarElement, header.element.firstChild)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Add action if provided
|
|
75
|
+
if (config.action) {
|
|
76
|
+
const actionElement = typeof config.action === 'string'
|
|
77
|
+
? createElement({
|
|
78
|
+
tag: 'div',
|
|
79
|
+
className: `${PREFIX}-card-header-action`,
|
|
80
|
+
html: config.action
|
|
81
|
+
})
|
|
82
|
+
: config.action
|
|
83
|
+
|
|
84
|
+
header.element.appendChild(actionElement)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return header.element
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Card header creation error:', error)
|
|
90
|
+
throw new Error(`Failed to create card header: ${error.message}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// src/components/card/index.js
|
|
2
|
+
export { default } from './card.js'
|
|
3
|
+
export { createCardContent } from './content.js'
|
|
4
|
+
export { createCardHeader } from './header.js'
|
|
5
|
+
export { createCardActions } from './actions.js'
|
|
6
|
+
export { createCardMedia } from './media.js'
|
|
7
|
+
export { CARD_VARIANTS, CARD_ELEVATIONS } from './constants.js'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/components/card/media.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 media component
|
|
8
|
+
* @param {Object} config - Media configuration
|
|
9
|
+
* @param {string} [config.src] - Image source URL
|
|
10
|
+
* @param {string} [config.alt] - Image alt text
|
|
11
|
+
* @param {HTMLElement} [config.element] - Custom media element
|
|
12
|
+
* @param {string} [config.aspectRatio='16:9'] - Media aspect ratio
|
|
13
|
+
* @param {boolean} [config.contain=false] - Whether to use object-fit: contain
|
|
14
|
+
* @returns {HTMLElement} Card media element
|
|
15
|
+
*/
|
|
16
|
+
export const createCardMedia = (config = {}) => {
|
|
17
|
+
const baseConfig = {
|
|
18
|
+
...config,
|
|
19
|
+
componentName: 'card-media',
|
|
20
|
+
prefix: PREFIX
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const media = pipe(
|
|
25
|
+
createBase,
|
|
26
|
+
withElement({
|
|
27
|
+
tag: 'div',
|
|
28
|
+
componentName: 'card-media',
|
|
29
|
+
className: [
|
|
30
|
+
config.class,
|
|
31
|
+
config.aspectRatio ? `${PREFIX}-card-media--${config.aspectRatio.replace(':', '-')}` : null,
|
|
32
|
+
config.contain ? `${PREFIX}-card-media--contain` : null
|
|
33
|
+
]
|
|
34
|
+
})
|
|
35
|
+
)(baseConfig)
|
|
36
|
+
|
|
37
|
+
// If custom element is provided, use it
|
|
38
|
+
if (config.element instanceof HTMLElement) {
|
|
39
|
+
media.element.appendChild(config.element)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Otherwise create an image if src is provided
|
|
43
|
+
else if (config.src) {
|
|
44
|
+
const img = document.createElement('img')
|
|
45
|
+
img.src = config.src
|
|
46
|
+
if (config.alt) img.alt = config.alt
|
|
47
|
+
img.className = `${PREFIX}-card-media-img`
|
|
48
|
+
media.element.appendChild(img)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return media.element
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Card media creation error:', error)
|
|
54
|
+
throw new Error(`Failed to create card media: ${error.message}`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// src/components/card/styles.scss
|
|
2
|
+
@use '../../styles/abstract/base' as base;
|
|
3
|
+
@use '../../styles/abstract/variables' as v;
|
|
4
|
+
@use '../../styles/abstract/functions' as f;
|
|
5
|
+
@use '../../styles/abstract/mixins' as m;
|
|
6
|
+
@use '../../styles/abstract/theme' as t;
|
|
7
|
+
|
|
8
|
+
$component: '#{base.$prefix}-card';
|
|
9
|
+
|
|
10
|
+
.#{$component} {
|
|
11
|
+
// Base styles
|
|
12
|
+
position: relative;
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
border-radius: v.shape('medium');
|
|
17
|
+
background-color: t.color('surface');
|
|
18
|
+
color: t.color('on-surface');
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
// width: v.card('width');
|
|
21
|
+
width: 340px;
|
|
22
|
+
--card-elevation: m.elevation(2);
|
|
23
|
+
|
|
24
|
+
// Typography
|
|
25
|
+
@include m.typography('body-medium');
|
|
26
|
+
|
|
27
|
+
// Transition for elevation and hover states
|
|
28
|
+
@include m.motion-transition(
|
|
29
|
+
box-shadow,
|
|
30
|
+
background-color,
|
|
31
|
+
border-color
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Ripple styles for clickable cards
|
|
35
|
+
.ripple {
|
|
36
|
+
position: absolute;
|
|
37
|
+
border-radius: 50%;
|
|
38
|
+
transform: scale(0);
|
|
39
|
+
pointer-events: none;
|
|
40
|
+
background-color: currentColor;
|
|
41
|
+
opacity: 0.08;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Ensure proper stacking for inner components
|
|
45
|
+
> :not(:last-child) {
|
|
46
|
+
margin-bottom: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// === Variants ===
|
|
50
|
+
|
|
51
|
+
// Elevated variant
|
|
52
|
+
&--elevated {
|
|
53
|
+
// @include m.elevation(m.elevation(2));
|
|
54
|
+
|
|
55
|
+
&:hover {
|
|
56
|
+
--card-elevation: 2;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Filled variant
|
|
61
|
+
&--filled {
|
|
62
|
+
background-color: t.color('surface-container-highest');
|
|
63
|
+
|
|
64
|
+
&:hover.#{$component}--interactive {
|
|
65
|
+
@include m.state-layer(t.color('on-surface'), 'hover');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Outlined variant
|
|
70
|
+
&--outlined {
|
|
71
|
+
border: 1px solid t.color('outline');
|
|
72
|
+
background-color: t.color('surface');
|
|
73
|
+
|
|
74
|
+
&:hover.#{$component}--interactive {
|
|
75
|
+
@include m.state-layer(t.color('on-surface'), 'hover');
|
|
76
|
+
border-color: t.color('outline-variant');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// === Modifiers ===
|
|
81
|
+
|
|
82
|
+
// Interactive cards
|
|
83
|
+
&--interactive {
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
user-select: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Full-width cards
|
|
89
|
+
&--full-width {
|
|
90
|
+
width: 100%;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// === Sub-components ===
|
|
94
|
+
|
|
95
|
+
// Card Header
|
|
96
|
+
&-header {
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
padding: 16px;
|
|
100
|
+
|
|
101
|
+
&-avatar {
|
|
102
|
+
margin-right: 16px;
|
|
103
|
+
flex-shrink: 0;
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
|
|
108
|
+
img {
|
|
109
|
+
width: 40px;
|
|
110
|
+
height: 40px;
|
|
111
|
+
border-radius: 50%;
|
|
112
|
+
object-fit: cover;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
&-text {
|
|
117
|
+
flex: 1;
|
|
118
|
+
overflow: hidden;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&-title {
|
|
122
|
+
margin: 0;
|
|
123
|
+
@include m.typography('title-medium');
|
|
124
|
+
@include m.truncate;
|
|
125
|
+
color: t.color('on-surface');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
&-subtitle {
|
|
129
|
+
margin: 0;
|
|
130
|
+
@include m.typography('body-medium');
|
|
131
|
+
@include m.truncate;
|
|
132
|
+
color: t.color('on-surface-variant');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
&-action {
|
|
136
|
+
margin-left: 8px;
|
|
137
|
+
flex-shrink: 0;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Card Media
|
|
142
|
+
&-media {
|
|
143
|
+
position: relative;
|
|
144
|
+
overflow: hidden;
|
|
145
|
+
|
|
146
|
+
&-img {
|
|
147
|
+
display: block;
|
|
148
|
+
width: 100%;
|
|
149
|
+
object-fit: cover;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Aspect ratios
|
|
153
|
+
&--16-9 {
|
|
154
|
+
aspect-ratio: 16 / 9;
|
|
155
|
+
|
|
156
|
+
img {
|
|
157
|
+
height: 100%;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
&--4-3 {
|
|
162
|
+
aspect-ratio: 4 / 3;
|
|
163
|
+
|
|
164
|
+
img {
|
|
165
|
+
height: 100%;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
&--1-1 {
|
|
170
|
+
aspect-ratio: 1 / 1;
|
|
171
|
+
|
|
172
|
+
img {
|
|
173
|
+
height: 100%;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
&--contain {
|
|
178
|
+
img {
|
|
179
|
+
object-fit: contain;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Card Content
|
|
185
|
+
&-content {
|
|
186
|
+
padding: 16px;
|
|
187
|
+
flex: 1 1 auto;
|
|
188
|
+
|
|
189
|
+
> *:first-child {
|
|
190
|
+
margin-top: 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
> *:last-child {
|
|
194
|
+
margin-bottom: 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// When content follows media without padding
|
|
198
|
+
.#{$component}-media + &:not(.#{$component}-content--no-padding) {
|
|
199
|
+
padding-top: 16px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// No padding modifier
|
|
203
|
+
&--no-padding {
|
|
204
|
+
padding: 0;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Card Actions
|
|
209
|
+
&-actions {
|
|
210
|
+
display: flex;
|
|
211
|
+
flex-wrap: wrap;
|
|
212
|
+
padding: 8px;
|
|
213
|
+
align-items: center;
|
|
214
|
+
|
|
215
|
+
> * {
|
|
216
|
+
margin: 0 4px;
|
|
217
|
+
|
|
218
|
+
&:first-child {
|
|
219
|
+
margin-left: 8px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
&:last-child {
|
|
223
|
+
margin-right: 8px;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Full-bleed actions
|
|
228
|
+
&--full-bleed {
|
|
229
|
+
padding: 0;
|
|
230
|
+
|
|
231
|
+
> * {
|
|
232
|
+
margin: 0;
|
|
233
|
+
border-radius: 0;
|
|
234
|
+
flex: 1 1 auto;
|
|
235
|
+
|
|
236
|
+
&:first-child {
|
|
237
|
+
margin-left: 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
&:last-child {
|
|
241
|
+
margin-right: 0;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Vertical actions
|
|
247
|
+
&--vertical {
|
|
248
|
+
flex-direction: column;
|
|
249
|
+
|
|
250
|
+
> * {
|
|
251
|
+
width: 100%;
|
|
252
|
+
margin: 4px 0;
|
|
253
|
+
|
|
254
|
+
&:first-child {
|
|
255
|
+
margin-top: 8px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
&:last-child {
|
|
259
|
+
margin-bottom: 8px;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Alignment variations
|
|
265
|
+
&--center {
|
|
266
|
+
justify-content: center;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
&--end {
|
|
270
|
+
justify-content: flex-end;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
&--space-between {
|
|
274
|
+
justify-content: space-between;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// State classes
|
|
279
|
+
&--state-disabled {
|
|
280
|
+
opacity: 0.38;
|
|
281
|
+
pointer-events: none;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
&--state-loading {
|
|
285
|
+
pointer-events: none;
|
|
286
|
+
}
|
|
287
|
+
}
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { createElement } from './core/dom/create'
|
|
3
3
|
export { default as createLayout } from './core/layout'
|
|
4
4
|
export { default as createButton } from './components/button'
|
|
5
|
+
export { default as createCard } from './components/card'
|
|
5
6
|
export { default as createCheckbox } from './components/checkbox'
|
|
6
7
|
export { default as createContainer } from './components/container'
|
|
7
8
|
export { default as createMenu } from './components/menu'
|