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,169 @@
|
|
|
1
|
+
// src/components/navigation/styles/_drawer.scss
|
|
2
|
+
@use 'sass:map';
|
|
3
|
+
@use '../../../styles/abstract/config' as c;
|
|
4
|
+
@use 'base';
|
|
5
|
+
|
|
6
|
+
.#{c.$prefix}-nav {
|
|
7
|
+
&--drawer,
|
|
8
|
+
&--drawer-modal,
|
|
9
|
+
&--drawer-standard {
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
width: 256px;
|
|
12
|
+
height: 100%;
|
|
13
|
+
padding: 12px 0;
|
|
14
|
+
transition: transform 0.2s ease-in-out;
|
|
15
|
+
transform: translateX(0);
|
|
16
|
+
|
|
17
|
+
// Hidden state
|
|
18
|
+
&.#{c.$prefix}-nav--hidden {
|
|
19
|
+
transform: translateX(-100%);
|
|
20
|
+
|
|
21
|
+
@include c.rtl {
|
|
22
|
+
transform: translateX(100%);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Item container for nesting
|
|
27
|
+
.#{c.$prefix}-nav-item-container {
|
|
28
|
+
width: 100%;
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Base nav item styles
|
|
34
|
+
.#{c.$prefix}-nav-item {
|
|
35
|
+
padding: 12px 16px;
|
|
36
|
+
align-items: center;
|
|
37
|
+
justify-content: flex-start;
|
|
38
|
+
border-radius: 28px;
|
|
39
|
+
margin: 0 12px;
|
|
40
|
+
width: calc(100% - 24px);
|
|
41
|
+
|
|
42
|
+
&:hover {
|
|
43
|
+
@include c.state-layer(var(--mtrl-sys-color-on-surface), 'hover');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&--active {
|
|
47
|
+
background-color: var(--mtrl-sys-color-secondary-container);
|
|
48
|
+
color: var(--mtrl-sys-color-on-secondary-container);
|
|
49
|
+
|
|
50
|
+
&:hover {
|
|
51
|
+
background-color: var(--mtrl-sys-color-secondary-container);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
&-icon {
|
|
56
|
+
margin-right: 12px;
|
|
57
|
+
flex-shrink: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&-label {
|
|
61
|
+
@include c.typography('label-large');
|
|
62
|
+
flex-grow: 1;
|
|
63
|
+
text-align: left;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&-badge {
|
|
67
|
+
position: static;
|
|
68
|
+
margin-left: auto;
|
|
69
|
+
margin-right: 8px;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Nested navigation styles
|
|
74
|
+
.#{c.$prefix}-nav-nested-container {
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
width: 100%;
|
|
78
|
+
margin-left: 28px;
|
|
79
|
+
padding-right: 12px;
|
|
80
|
+
|
|
81
|
+
&[hidden] {
|
|
82
|
+
display: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Adjust nested items styles
|
|
86
|
+
.#{c.$prefix}-nav-item {
|
|
87
|
+
margin: 0;
|
|
88
|
+
padding: 8px 16px;
|
|
89
|
+
font-size: 14px;
|
|
90
|
+
|
|
91
|
+
&-icon {
|
|
92
|
+
width: 20px;
|
|
93
|
+
height: 20px;
|
|
94
|
+
padding: 6px;
|
|
95
|
+
|
|
96
|
+
svg {
|
|
97
|
+
width: 20px;
|
|
98
|
+
height: 20px;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Expand icon styles
|
|
105
|
+
.#{c.$prefix}-nav-expand-icon {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: center;
|
|
109
|
+
width: 20px;
|
|
110
|
+
height: 20px;
|
|
111
|
+
margin-left: auto;
|
|
112
|
+
color: inherit;
|
|
113
|
+
@include c.motion-transition(transform);
|
|
114
|
+
|
|
115
|
+
svg {
|
|
116
|
+
width: 20px;
|
|
117
|
+
height: 20px;
|
|
118
|
+
fill: none;
|
|
119
|
+
stroke: currentColor;
|
|
120
|
+
stroke-width: 2px;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// RTL support
|
|
125
|
+
@include c.rtl {
|
|
126
|
+
.#{c.$prefix}-nav-item {
|
|
127
|
+
&-icon {
|
|
128
|
+
margin-right: 0;
|
|
129
|
+
margin-left: 12px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
&-label {
|
|
133
|
+
text-align: right;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
&-badge {
|
|
137
|
+
margin-left: 8px;
|
|
138
|
+
margin-right: auto;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.#{c.$prefix}-nav-nested-container {
|
|
143
|
+
margin-left: 0;
|
|
144
|
+
margin-right: 28px;
|
|
145
|
+
padding-right: 0;
|
|
146
|
+
padding-left: 12px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.#{c.$prefix}-nav-expand-icon {
|
|
150
|
+
margin-left: 0;
|
|
151
|
+
margin-right: auto;
|
|
152
|
+
transform: scaleX(-1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
&--drawer-modal {
|
|
158
|
+
@include c.elevation(2);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
&--drawer-standard {
|
|
162
|
+
border-right: 1px solid var(--mtrl-sys-color-outline-variant);
|
|
163
|
+
|
|
164
|
+
@include c.rtl {
|
|
165
|
+
border-right: none;
|
|
166
|
+
border-left: 1px solid var(--mtrl-sys-color-outline-variant);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// src/components/navigation/styles/_rail.scss
|
|
2
|
+
@use 'sass:map';
|
|
3
|
+
@use '../../../styles/abstract/config' as c;
|
|
4
|
+
@use 'base';
|
|
5
|
+
|
|
6
|
+
.#{c.$prefix}-nav {
|
|
7
|
+
&--rail {
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
width: 80px;
|
|
10
|
+
height: 100%;
|
|
11
|
+
padding: 12px 0;
|
|
12
|
+
|
|
13
|
+
.#{c.$prefix}-nav-item {
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
width: 100%;
|
|
16
|
+
min-height: 56px;
|
|
17
|
+
padding: 2px;
|
|
18
|
+
margin: -2px auto 14px;
|
|
19
|
+
gap: 0;
|
|
20
|
+
|
|
21
|
+
&:hover {
|
|
22
|
+
.#{c.$prefix}-nav-item-icon {
|
|
23
|
+
@include c.state-layer(var(--mtrl-sys-color-on-surface), 'hover');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&--active {
|
|
28
|
+
.#{c.$prefix}-nav-item-icon {
|
|
29
|
+
background-color: var(--mtrl-sys-color-secondary-container);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
&-icon {
|
|
34
|
+
margin-bottom: 4px;
|
|
35
|
+
padding: 8px;
|
|
36
|
+
width: 56px;
|
|
37
|
+
height: 32px;
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
border-radius: 16px;
|
|
42
|
+
@include c.motion-transition(background-color);
|
|
43
|
+
|
|
44
|
+
svg {
|
|
45
|
+
width: 24px;
|
|
46
|
+
height: 24px;
|
|
47
|
+
fill: currentColor;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&-label {
|
|
52
|
+
@include c.typography('label-small');
|
|
53
|
+
text-align: center;
|
|
54
|
+
color: inherit;
|
|
55
|
+
font-size: 12px;
|
|
56
|
+
line-height: 16px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&-badge {
|
|
60
|
+
top: 4px;
|
|
61
|
+
right: 16px;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// src/components/snackbar/api.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Enhances snackbar component with API methods
|
|
5
|
+
* @param {Object} options - API configuration
|
|
6
|
+
* @param {Object} options.lifecycle - Lifecycle handlers
|
|
7
|
+
* @param {Object} options.queue - Snackbar queue manager
|
|
8
|
+
*/
|
|
9
|
+
export const withAPI = ({ lifecycle, queue }) => (component) => {
|
|
10
|
+
if (!queue) {
|
|
11
|
+
throw new Error('Snackbar queue is required')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let isVisible = false
|
|
15
|
+
|
|
16
|
+
const enhancedComponent = {
|
|
17
|
+
...component,
|
|
18
|
+
element: component.element,
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Shows the snackbar with animation
|
|
22
|
+
*/
|
|
23
|
+
show () {
|
|
24
|
+
if (isVisible) return this
|
|
25
|
+
isVisible = true
|
|
26
|
+
|
|
27
|
+
queue.add({
|
|
28
|
+
...this,
|
|
29
|
+
_show: () => {
|
|
30
|
+
document.body.appendChild(component.element)
|
|
31
|
+
|
|
32
|
+
// Force reflow for animation
|
|
33
|
+
const _ = component.element.offsetHeight
|
|
34
|
+
|
|
35
|
+
component.element.classList.add(`${component.getClass('snackbar')}--visible`)
|
|
36
|
+
|
|
37
|
+
if (component.timer) {
|
|
38
|
+
component.timer.start()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return this
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return this
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Hides the snackbar with animation and cleanup
|
|
50
|
+
*/
|
|
51
|
+
hide () {
|
|
52
|
+
if (!isVisible) return this
|
|
53
|
+
isVisible = false
|
|
54
|
+
|
|
55
|
+
if (component.timer) {
|
|
56
|
+
component.timer.stop()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const handleTransitionEnd = (event) => {
|
|
60
|
+
if (event.propertyName !== 'opacity') return
|
|
61
|
+
|
|
62
|
+
component.element.removeEventListener('transitionend', handleTransitionEnd)
|
|
63
|
+
if (component.element.parentNode) {
|
|
64
|
+
component.element.remove()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
component.element.addEventListener('transitionend', handleTransitionEnd)
|
|
69
|
+
component.element.classList.remove(`${component.getClass('snackbar')}--visible`)
|
|
70
|
+
|
|
71
|
+
return this
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
setMessage (text) {
|
|
75
|
+
component.text.setText(text)
|
|
76
|
+
return this
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
getMessage () {
|
|
80
|
+
return component.text.getText()
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
on: component.on,
|
|
84
|
+
off: component.off,
|
|
85
|
+
|
|
86
|
+
destroy () {
|
|
87
|
+
if (isVisible) {
|
|
88
|
+
component.element.remove()
|
|
89
|
+
}
|
|
90
|
+
if (component.timer) {
|
|
91
|
+
component.timer.stop()
|
|
92
|
+
}
|
|
93
|
+
lifecycle.destroy()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Set up action button handler
|
|
98
|
+
if (component.actionButton) {
|
|
99
|
+
component.actionButton.addEventListener('click', () => {
|
|
100
|
+
component.emit('action')
|
|
101
|
+
component.emit('dismiss') // Emit dismiss to handle queue cleanup
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Set up dismiss handler
|
|
106
|
+
if (component.on) {
|
|
107
|
+
// Store the handler reference so it can be properly removed
|
|
108
|
+
const dismissHandler = () => {
|
|
109
|
+
if (isVisible) {
|
|
110
|
+
enhancedComponent.hide()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
component.on('dismiss', dismissHandler)
|
|
115
|
+
|
|
116
|
+
// Add cleanup to lifecycle
|
|
117
|
+
const originalDestroy = lifecycle.destroy
|
|
118
|
+
lifecycle.destroy = () => {
|
|
119
|
+
component.off('dismiss', dismissHandler)
|
|
120
|
+
originalDestroy?.call(lifecycle)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return enhancedComponent
|
|
125
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/components/snackbar/constants.js
|
|
2
|
+
|
|
3
|
+
export const SNACKBAR_VARIANTS = {
|
|
4
|
+
BASIC: 'basic',
|
|
5
|
+
ACTION: 'action' // With action button
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const SNACKBAR_POSITIONS = {
|
|
9
|
+
CENTER: 'center',
|
|
10
|
+
START: 'start',
|
|
11
|
+
END: 'end'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const SNACKBAR_SCHEMA = {
|
|
15
|
+
variant: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
enum: Object.values(SNACKBAR_VARIANTS),
|
|
18
|
+
required: false
|
|
19
|
+
},
|
|
20
|
+
position: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
enum: Object.values(SNACKBAR_POSITIONS),
|
|
23
|
+
required: false
|
|
24
|
+
},
|
|
25
|
+
message: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
required: true
|
|
28
|
+
},
|
|
29
|
+
action: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
required: false
|
|
32
|
+
},
|
|
33
|
+
duration: {
|
|
34
|
+
type: 'number',
|
|
35
|
+
required: false
|
|
36
|
+
},
|
|
37
|
+
class: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
required: false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/components/snackbar/features.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Adds action button to snackbar
|
|
5
|
+
*/
|
|
6
|
+
export const withActionButton = (config) => (component) => {
|
|
7
|
+
if (!config.action) return component
|
|
8
|
+
|
|
9
|
+
const button = document.createElement('button')
|
|
10
|
+
button.className = `${config.prefix}-snackbar-action`
|
|
11
|
+
button.textContent = config.action
|
|
12
|
+
|
|
13
|
+
component.element.appendChild(button)
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
...component,
|
|
17
|
+
actionButton: button
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Adds auto-dismiss timer functionality
|
|
23
|
+
*/
|
|
24
|
+
export const withDismissTimer = (config) => (component) => {
|
|
25
|
+
let timeoutId = null
|
|
26
|
+
|
|
27
|
+
const startTimer = () => {
|
|
28
|
+
// Clear any existing timer
|
|
29
|
+
if (timeoutId) {
|
|
30
|
+
clearTimeout(timeoutId)
|
|
31
|
+
timeoutId = null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Only start timer if duration is positive and numeric
|
|
35
|
+
if (typeof config.duration === 'number' && config.duration > 0) {
|
|
36
|
+
timeoutId = setTimeout(() => {
|
|
37
|
+
if (component.element && component.emit) {
|
|
38
|
+
component.emit('dismiss')
|
|
39
|
+
}
|
|
40
|
+
}, config.duration)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const stopTimer = () => {
|
|
45
|
+
if (timeoutId) {
|
|
46
|
+
clearTimeout(timeoutId)
|
|
47
|
+
timeoutId = null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Clean up on destroy
|
|
52
|
+
const originalDestroy = component.lifecycle?.destroy
|
|
53
|
+
if (component.lifecycle) {
|
|
54
|
+
component.lifecycle.destroy = () => {
|
|
55
|
+
stopTimer()
|
|
56
|
+
if (originalDestroy) {
|
|
57
|
+
originalDestroy.call(component.lifecycle)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
...component,
|
|
64
|
+
timer: {
|
|
65
|
+
start: startTimer,
|
|
66
|
+
stop: stopTimer
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// src/components/snackbar/position.js
|
|
2
|
+
|
|
3
|
+
import { SNACKBAR_POSITIONS } from './constants'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adds position handling to snackbar
|
|
7
|
+
* @param {Object} config - Position configuration
|
|
8
|
+
* @param {string} config.prefix - Component prefix
|
|
9
|
+
* @param {string} config.position - Position variant (start, center, end)
|
|
10
|
+
*/
|
|
11
|
+
export const withPosition = (config) => (component) => {
|
|
12
|
+
const position = config.position || SNACKBAR_POSITIONS.CENTER
|
|
13
|
+
const positionClass = `${config.prefix}-snackbar--${position}`
|
|
14
|
+
|
|
15
|
+
// Add position class
|
|
16
|
+
component.element.classList.add(positionClass)
|
|
17
|
+
|
|
18
|
+
// Method to update position
|
|
19
|
+
const setPosition = (newPosition) => {
|
|
20
|
+
// Remove current position class
|
|
21
|
+
component.element.classList.remove(positionClass)
|
|
22
|
+
|
|
23
|
+
// Add new position class
|
|
24
|
+
const newPositionClass = `${config.prefix}-snackbar--${newPosition}`
|
|
25
|
+
component.element.classList.add(newPositionClass)
|
|
26
|
+
|
|
27
|
+
// Update visible state transform for center position
|
|
28
|
+
if (component.element.classList.contains(`${config.prefix}-snackbar--visible`)) {
|
|
29
|
+
if (newPosition === SNACKBAR_POSITIONS.CENTER) {
|
|
30
|
+
component.element.style.transform = 'translateX(-50%) scale(1)'
|
|
31
|
+
} else {
|
|
32
|
+
component.element.style.transform = 'scale(1)'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
...component,
|
|
39
|
+
position: {
|
|
40
|
+
/**
|
|
41
|
+
* Get current position
|
|
42
|
+
* @returns {string} Current position
|
|
43
|
+
*/
|
|
44
|
+
getPosition: () => position,
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Set new position
|
|
48
|
+
* @param {string} newPosition - New position to set
|
|
49
|
+
* @returns {Object} Component instance
|
|
50
|
+
*/
|
|
51
|
+
setPosition: (newPosition) => {
|
|
52
|
+
if (Object.values(SNACKBAR_POSITIONS).includes(newPosition)) {
|
|
53
|
+
setPosition(newPosition)
|
|
54
|
+
return component
|
|
55
|
+
} else {
|
|
56
|
+
console.warn(`Invalid position: ${newPosition}. Using default: ${SNACKBAR_POSITIONS.CENTER}`)
|
|
57
|
+
setPosition(SNACKBAR_POSITIONS.CENTER)
|
|
58
|
+
return component
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/components/snackbar/queue.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a queue manager for snackbars
|
|
5
|
+
* Ensures only one snackbar is visible at a time
|
|
6
|
+
*/
|
|
7
|
+
export const createSnackbarQueue = () => {
|
|
8
|
+
const queue = []
|
|
9
|
+
let isProcessing = false
|
|
10
|
+
|
|
11
|
+
const processQueue = () => {
|
|
12
|
+
if (queue.length === 0) {
|
|
13
|
+
isProcessing = false
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
isProcessing = true
|
|
18
|
+
const snackbar = queue[0]
|
|
19
|
+
|
|
20
|
+
const handleDismiss = () => {
|
|
21
|
+
// Remove from queue
|
|
22
|
+
queue.shift()
|
|
23
|
+
// Remove listener and cleanup
|
|
24
|
+
snackbar.off?.('dismiss', handleDismiss)
|
|
25
|
+
// Reset processing state if queue is empty
|
|
26
|
+
if (queue.length === 0) {
|
|
27
|
+
isProcessing = false
|
|
28
|
+
} else {
|
|
29
|
+
// Process next after a small delay
|
|
30
|
+
setTimeout(processQueue, 200)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle both normal dismiss and action button dismissal
|
|
35
|
+
snackbar.on?.('dismiss', handleDismiss)
|
|
36
|
+
snackbar._show()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
/**
|
|
41
|
+
* Adds a snackbar to the queue
|
|
42
|
+
* @param {Object} snackbar - Snackbar instance
|
|
43
|
+
*/
|
|
44
|
+
add (snackbar) {
|
|
45
|
+
if (!snackbar._show) {
|
|
46
|
+
throw new Error('Snackbar must implement _show method')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
queue.push(snackbar)
|
|
50
|
+
|
|
51
|
+
// Only start processing if not already processing
|
|
52
|
+
if (!isProcessing) {
|
|
53
|
+
processQueue()
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Clears all pending snackbars
|
|
59
|
+
*/
|
|
60
|
+
clear () {
|
|
61
|
+
// Remove all queued items
|
|
62
|
+
queue.length = 0
|
|
63
|
+
isProcessing = false
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gets current queue length
|
|
68
|
+
* @returns {number} Number of snackbars in queue
|
|
69
|
+
*/
|
|
70
|
+
getLength () {
|
|
71
|
+
return queue.length
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// src/components/snackbar/snackbar.js
|
|
2
|
+
import { PREFIX } from '../../core/config'
|
|
3
|
+
import { pipe } from '../../core/compose'
|
|
4
|
+
import { createBase, withElement } from '../../core/compose/component'
|
|
5
|
+
import { withActionButton, withDismissTimer } from './features'
|
|
6
|
+
import { withPosition } from './position'
|
|
7
|
+
import {
|
|
8
|
+
withEvents,
|
|
9
|
+
withText,
|
|
10
|
+
withVariant,
|
|
11
|
+
withLifecycle
|
|
12
|
+
} from '../../core/compose/features'
|
|
13
|
+
import { withAPI } from './api'
|
|
14
|
+
import { createSnackbarQueue } from './queue'
|
|
15
|
+
import { SNACKBAR_VARIANTS, SNACKBAR_POSITIONS } from './constants'
|
|
16
|
+
|
|
17
|
+
// Create a single queue instance to be shared across all snackbars
|
|
18
|
+
const queue = createSnackbarQueue()
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a new Snackbar component
|
|
22
|
+
* @param {Object} config - Snackbar configuration
|
|
23
|
+
* @param {string} config.message - Message to display
|
|
24
|
+
* @param {string} [config.action] - Action button text
|
|
25
|
+
* @param {string} [config.variant='basic'] - Snackbar variant
|
|
26
|
+
* @param {string} [config.position='center'] - Display position
|
|
27
|
+
* @param {number} [config.duration=4000] - Display duration in ms
|
|
28
|
+
* @param {string} [config.class] - Additional CSS classes
|
|
29
|
+
*/
|
|
30
|
+
const createSnackbar = (config = {}) => {
|
|
31
|
+
const baseConfig = {
|
|
32
|
+
...config,
|
|
33
|
+
componentName: 'snackbar',
|
|
34
|
+
prefix: PREFIX,
|
|
35
|
+
variant: config.variant || SNACKBAR_VARIANTS.BASIC,
|
|
36
|
+
position: config.position || SNACKBAR_POSITIONS.CENTER,
|
|
37
|
+
duration: config.duration ?? 4000 // Use nullish coalescing to allow 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
return pipe(
|
|
42
|
+
createBase,
|
|
43
|
+
withEvents(),
|
|
44
|
+
withElement({
|
|
45
|
+
tag: 'div',
|
|
46
|
+
componentName: 'snackbar',
|
|
47
|
+
className: config.class
|
|
48
|
+
}),
|
|
49
|
+
withVariant(baseConfig),
|
|
50
|
+
withPosition(baseConfig),
|
|
51
|
+
withText({
|
|
52
|
+
...baseConfig,
|
|
53
|
+
text: config.message
|
|
54
|
+
}),
|
|
55
|
+
withActionButton(baseConfig),
|
|
56
|
+
withLifecycle(),
|
|
57
|
+
// First apply timer
|
|
58
|
+
withDismissTimer(baseConfig),
|
|
59
|
+
// Then apply API which needs timer
|
|
60
|
+
comp => withAPI({
|
|
61
|
+
lifecycle: comp.lifecycle,
|
|
62
|
+
queue
|
|
63
|
+
})(comp)
|
|
64
|
+
)(baseConfig)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error(`Failed to create snackbar: ${error.message}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default createSnackbar
|