free-astro-components 0.0.42 → 1.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/package.json +1 -1
- package/src/components/Modal.astro +256 -0
- package/src/components/ModalBody.astro +20 -0
- package/src/components/ModalFooter.astro +22 -0
- package/src/components/ModalHeader.astro +18 -0
- package/src/components/ThemeSwitch.astro +1 -0
- package/src/css/main.css +1 -0
- package/src/index.js +4 -0
- package/src/types/index.d.ts +17 -1
- package/src/utils/modal.ts +107 -0
- package/src/utils/utils.ts +5 -1
package/package.json
CHANGED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
---
|
|
2
|
+
import '../css/main.css'
|
|
3
|
+
import Icon from './Icon.astro'
|
|
4
|
+
import type { ModalHeader, ModalBody, ModalFooter } from '../../.'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
id: string
|
|
8
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | 'full'
|
|
9
|
+
class?: string
|
|
10
|
+
children: ModalHeader | ModalBody | ModalFooter
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { id, size = 'md', class: className } = Astro.props
|
|
14
|
+
const sizeClasses = {
|
|
15
|
+
xs: 'ac-modal--xs',
|
|
16
|
+
sm: 'ac-modal--sm',
|
|
17
|
+
md: 'ac-modal--md',
|
|
18
|
+
lg: 'ac-modal--lg',
|
|
19
|
+
xl: 'ac-modal--xl',
|
|
20
|
+
'2xl': 'ac-modal--2xl',
|
|
21
|
+
'3xl': 'ac-modal--3xl',
|
|
22
|
+
'4xl': 'ac-modal--4xl',
|
|
23
|
+
full: 'ac-modal--full',
|
|
24
|
+
}[size]
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<dialog
|
|
28
|
+
id={id}
|
|
29
|
+
class:list={[
|
|
30
|
+
'ac-modal ac-modal--close ac-modal--animated',
|
|
31
|
+
sizeClasses,
|
|
32
|
+
className,
|
|
33
|
+
]}
|
|
34
|
+
data-modal
|
|
35
|
+
>
|
|
36
|
+
<button class="ac-modal-close" data-modal-close>
|
|
37
|
+
<Icon icon="clear" />
|
|
38
|
+
</button>
|
|
39
|
+
<slot />
|
|
40
|
+
</dialog>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
:root {
|
|
44
|
+
--ac-modal-backdrop-color: rgba(var(--ac-color-700), 0.15);
|
|
45
|
+
--ac-modal-backdrop-filter: blur(var(--ac-spacing-3));
|
|
46
|
+
--ac-modal-background-color: rgb(var(--ac-color-100));
|
|
47
|
+
--ac-modal-border-radius: var(--ac-rounded-2xl);
|
|
48
|
+
--ac-modal-width-xs: calc(var(--ac-spacing-16) * 5);
|
|
49
|
+
--ac-modal-width-sm: calc(var(--ac-spacing-16) * 6);
|
|
50
|
+
--ac-modal-width-md: calc(var(--ac-spacing-16) * 7);
|
|
51
|
+
--ac-modal-width-lg: calc(var(--ac-spacing-16) * 8);
|
|
52
|
+
--ac-modal-width-xl: calc(var(--ac-spacing-16) * 9);
|
|
53
|
+
--ac-modal-width-2xl: calc(var(--ac-spacing-16) * 10);
|
|
54
|
+
--ac-modal-width-3xl: calc(var(--ac-spacing-16) * 11);
|
|
55
|
+
--ac-modal-width-4xl: calc(var(--ac-spacing-16) * 12);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@keyframes slide-in-up {
|
|
59
|
+
0% {
|
|
60
|
+
transform: translateY(100%);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
100% {
|
|
64
|
+
transform: translateY(0);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@keyframes fade-in {
|
|
69
|
+
0% {
|
|
70
|
+
opacity: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
100% {
|
|
74
|
+
opacity: 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@keyframes slide-out-down {
|
|
79
|
+
100% {
|
|
80
|
+
transform: translateY(100%);
|
|
81
|
+
display: none;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@keyframes fade-out {
|
|
86
|
+
0% {
|
|
87
|
+
opacity: 1;
|
|
88
|
+
display: flex;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
100% {
|
|
92
|
+
opacity: 0;
|
|
93
|
+
display: none;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ac-modal {
|
|
98
|
+
background-color: var(--ac-modal-background-color);
|
|
99
|
+
border-top-left-radius: var(--ac-modal-border-radius);
|
|
100
|
+
border-top-right-radius: var(--ac-modal-border-radius);
|
|
101
|
+
display: flex;
|
|
102
|
+
flex-direction: column;
|
|
103
|
+
font-family: var(--ac-font-sans);
|
|
104
|
+
height: auto;
|
|
105
|
+
inset-block-end: 0;
|
|
106
|
+
inset-block-start: auto;
|
|
107
|
+
max-height: calc(100% - var(--ac-spacing-16));
|
|
108
|
+
max-width: 100%;
|
|
109
|
+
overflow: hidden;
|
|
110
|
+
padding-bottom: 0;
|
|
111
|
+
padding-inline: 0;
|
|
112
|
+
padding-top: var(--ac-spacing-4);
|
|
113
|
+
position: fixed;
|
|
114
|
+
width: 100%;
|
|
115
|
+
|
|
116
|
+
&::backdrop {
|
|
117
|
+
-webkit-backdrop-filter: var(--ac-modal-backdrop-filter);
|
|
118
|
+
backdrop-filter: var(--ac-modal-backdrop-filter);
|
|
119
|
+
background-color: var(--ac-modal-backdrop-color);
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
&:before {
|
|
124
|
+
background-color: rgba(var(--ac-color-300), 0.5);
|
|
125
|
+
border-radius: var(--ac-rounded-full);
|
|
126
|
+
content: '';
|
|
127
|
+
display: block;
|
|
128
|
+
height: var(--ac-spacing-1);
|
|
129
|
+
left: calc(50% - var(--ac-spacing-5));
|
|
130
|
+
position: absolute;
|
|
131
|
+
top: var(--ac-spacing-3);
|
|
132
|
+
width: var(--ac-spacing-10);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
&.ac-modal--close {
|
|
136
|
+
display: none;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
&.ac-modal--full {
|
|
140
|
+
border-radius: 0;
|
|
141
|
+
height: 100%;
|
|
142
|
+
width: 100%;
|
|
143
|
+
max-width: 100%;
|
|
144
|
+
max-height: 100%;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
&.ac-modal--animated {
|
|
148
|
+
animation: slide-out-down 0.2s forwards;
|
|
149
|
+
|
|
150
|
+
&[open] {
|
|
151
|
+
animation: slide-in-up 0.3s linear;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.ac-modal-close {
|
|
157
|
+
appearance: none;
|
|
158
|
+
background-color: var(--ac-transparent);
|
|
159
|
+
border-radius: var(--ac-rounded-full);
|
|
160
|
+
color: rgb(var(--ac-color-500));
|
|
161
|
+
cursor: pointer;
|
|
162
|
+
height: var(--ac-spacing-8);
|
|
163
|
+
padding: var(--ac-spacing-1);
|
|
164
|
+
position: absolute;
|
|
165
|
+
right: var(--ac-spacing-2);
|
|
166
|
+
top: var(--ac-spacing-2);
|
|
167
|
+
width: var(--ac-spacing-8);
|
|
168
|
+
|
|
169
|
+
&:hover {
|
|
170
|
+
background-color: rgb(var(--ac-color-200));
|
|
171
|
+
color: rgb(var(--ac-color-700));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
& > svg {
|
|
175
|
+
height: var(--ac-spacing-6);
|
|
176
|
+
width: var(--ac-spacing-6);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@media (min-width: 640px) {
|
|
181
|
+
.ac-modal {
|
|
182
|
+
border-bottom-left-radius: var(--ac-modal-border-radius);
|
|
183
|
+
border-bottom-right-radius: var(--ac-modal-border-radius);
|
|
184
|
+
inset-block-start: 0;
|
|
185
|
+
max-width: calc(100% - var(--ac-spacing-16));
|
|
186
|
+
padding-top: 0;
|
|
187
|
+
|
|
188
|
+
&:before {
|
|
189
|
+
content: none;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
&.ac-modal--sm {
|
|
193
|
+
width: var(--ac-modal-width-sm);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
&.ac-modal--md {
|
|
197
|
+
width: var(--ac-modal-width-md);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
&.ac-modal--lg {
|
|
201
|
+
width: var(--ac-modal-width-lg);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
&.ac-modal--xl {
|
|
205
|
+
width: var(--ac-modal-width-xl);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
&.ac-modal--2xl {
|
|
209
|
+
width: var(--ac-modal-width-2xl);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
&.ac-modal--3xl {
|
|
213
|
+
width: var(--ac-modal-width-3xl);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
&.ac-modal--4xl {
|
|
217
|
+
width: var(--ac-modal-width-4xl);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
&.ac-modal--animated {
|
|
221
|
+
animation: fade-out 0.2s forwards;
|
|
222
|
+
|
|
223
|
+
&[open] {
|
|
224
|
+
animation: fade-in 0.3s linear;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
</style>
|
|
230
|
+
|
|
231
|
+
<script>
|
|
232
|
+
import { DOMLoaded } from '../utils/utils'
|
|
233
|
+
import { openModal, closeModal } from '../utils/modal'
|
|
234
|
+
|
|
235
|
+
DOMLoaded(() => {
|
|
236
|
+
const modalTriggers = document.querySelectorAll(
|
|
237
|
+
'[data-modal-trigger]'
|
|
238
|
+
) as NodeListOf<HTMLButtonElement>
|
|
239
|
+
const modalCloses = document.querySelectorAll(
|
|
240
|
+
'[data-modal-close]'
|
|
241
|
+
) as NodeListOf<HTMLButtonElement>
|
|
242
|
+
|
|
243
|
+
modalTriggers.forEach((trigger) => {
|
|
244
|
+
const modalId = trigger.dataset.modalTrigger
|
|
245
|
+
const modal = document.querySelector(`#${modalId}`) as HTMLDialogElement
|
|
246
|
+
|
|
247
|
+
trigger.addEventListener('click', () => openModal(modal))
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
modalCloses.forEach((close) => {
|
|
251
|
+
const modal = close.closest('[data-modal]') as HTMLDialogElement
|
|
252
|
+
|
|
253
|
+
close.addEventListener('click', () => closeModal(modal))
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
</script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: string
|
|
4
|
+
children: any
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { class: className } = Astro.props
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<div class:list={['ac-modal-body', className]}>
|
|
11
|
+
<slot />
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.ac-modal-body {
|
|
16
|
+
flex: 1 1 0%;
|
|
17
|
+
overflow-y: auto;
|
|
18
|
+
padding: var(--ac-spacing-4) var(--ac-spacing-6);
|
|
19
|
+
}
|
|
20
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: string
|
|
4
|
+
children: any
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { class: className } = Astro.props
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<footer class:list={['ac-modal-footer', className]}>
|
|
11
|
+
<slot />
|
|
12
|
+
</footer>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.ac-modal-footer {
|
|
16
|
+
align-items: center;
|
|
17
|
+
display: flex;
|
|
18
|
+
gap: var(--ac-spacing-4);
|
|
19
|
+
justify-content: flex-end;
|
|
20
|
+
padding: var(--ac-spacing-4) var(--ac-spacing-6);
|
|
21
|
+
}
|
|
22
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: string
|
|
4
|
+
children: any
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { class: className } = Astro.props
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<header class:list={['ac-modal-header', className]}>
|
|
11
|
+
<slot />
|
|
12
|
+
</header>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.ac-modal-header {
|
|
16
|
+
padding: var(--ac-spacing-4) var(--ac-spacing-6);
|
|
17
|
+
}
|
|
18
|
+
</style>
|
package/src/css/main.css
CHANGED
package/src/index.js
CHANGED
|
@@ -9,3 +9,7 @@ export { default as Select } from './components/Select.astro'
|
|
|
9
9
|
export { default as Tab } from './components/Tab.astro'
|
|
10
10
|
export { default as TabItem } from './components/TabItem.astro'
|
|
11
11
|
export { default as ThemeSwitch } from './components/ThemeSwitch.astro'
|
|
12
|
+
export { default as Modal } from './components/Modal.astro'
|
|
13
|
+
export { default as ModaHeader } from './components/ModalHeader.astro'
|
|
14
|
+
export { default as ModalBody } from './components/ModalBody.astro'
|
|
15
|
+
export { default as ModalFooter } from './components/ModalFooter.astro'
|
package/src/types/index.d.ts
CHANGED
|
@@ -40,4 +40,20 @@ export const TabItem: TabItem
|
|
|
40
40
|
|
|
41
41
|
// ThemeSwitch component
|
|
42
42
|
export type ThemeSwitch = typeof import('../index.js').ThemeSwitch
|
|
43
|
-
export const ThemeSwitch: ThemeSwitch
|
|
43
|
+
export const ThemeSwitch: ThemeSwitch
|
|
44
|
+
|
|
45
|
+
// Modal component
|
|
46
|
+
export type Modal = typeof import('../index.js').Modal
|
|
47
|
+
export const Modal: Modal
|
|
48
|
+
|
|
49
|
+
// ModalHeader component
|
|
50
|
+
export type ModalHeader = typeof import('../index.js').ModalHeader
|
|
51
|
+
export const ModalHeader: ModalHeader
|
|
52
|
+
|
|
53
|
+
// ModalBody component
|
|
54
|
+
export type ModalBody = typeof import('../index.js').ModalBody
|
|
55
|
+
export const ModalBody: ModalBody
|
|
56
|
+
|
|
57
|
+
// ModalFooter component
|
|
58
|
+
export type ModalFooter = typeof import('../index.js').ModalFooter
|
|
59
|
+
export const ModalFooter: ModalFooter
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { isTouchDevice } from '../utils/utils'
|
|
2
|
+
|
|
3
|
+
export const openModal = (modal: HTMLDialogElement) => {
|
|
4
|
+
const body = document.body
|
|
5
|
+
|
|
6
|
+
body.style.overflow = 'hidden'
|
|
7
|
+
modal.classList.remove('ac-modal--close')
|
|
8
|
+
modal.showModal()
|
|
9
|
+
|
|
10
|
+
if (isTouchDevice()) {
|
|
11
|
+
enableTouchControls(modal)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
modal.addEventListener(
|
|
15
|
+
'click',
|
|
16
|
+
(event) => {
|
|
17
|
+
if (event.target === modal) {
|
|
18
|
+
closeModal(modal)
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{ once: true },
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const closeModal = (modal: HTMLDialogElement) => {
|
|
26
|
+
const body = document.body
|
|
27
|
+
modal.close()
|
|
28
|
+
if (isTouchDevice()) {
|
|
29
|
+
disableTouchControls(modal)
|
|
30
|
+
}
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
modal.classList.add('ac-modal--close')
|
|
33
|
+
body.style.overflow = 'auto'
|
|
34
|
+
}, 200)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const enableTouchControls = (modal: HTMLDialogElement) => {
|
|
38
|
+
let startY = 0
|
|
39
|
+
let currentY = 0
|
|
40
|
+
let isScrolling = false
|
|
41
|
+
|
|
42
|
+
const isScrollable = (element: HTMLElement) => {
|
|
43
|
+
return element.scrollHeight > element.clientHeight
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const getClosestScrollableElement = (element: HTMLElement) => {
|
|
47
|
+
while (element && element !== modal) {
|
|
48
|
+
if (isScrollable(element)) {
|
|
49
|
+
return element
|
|
50
|
+
}
|
|
51
|
+
element = element.parentElement as HTMLElement
|
|
52
|
+
}
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const isScrollAtTop = (element: HTMLElement) => {
|
|
57
|
+
return element.scrollTop === 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handleTouchStart = (event: TouchEvent) => {
|
|
61
|
+
startY = event.touches[0].clientY
|
|
62
|
+
const targetElement = event.target as HTMLElement
|
|
63
|
+
const scrollableElement = getClosestScrollableElement(targetElement)
|
|
64
|
+
|
|
65
|
+
if (scrollableElement && !isScrollAtTop(scrollableElement)) {
|
|
66
|
+
isScrolling = true
|
|
67
|
+
} else {
|
|
68
|
+
isScrolling = false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const handleTouchMove = (event: TouchEvent) => {
|
|
73
|
+
if (isScrolling) return
|
|
74
|
+
currentY = event.touches[0].clientY
|
|
75
|
+
const translateY = Math.max(0, currentY - startY)
|
|
76
|
+
|
|
77
|
+
modal.style.transition = 'none'
|
|
78
|
+
modal.style.opacity = `${1 - translateY / 250}`
|
|
79
|
+
modal.classList.remove('ac-modal--animated')
|
|
80
|
+
modal.style.transform = `translateY(${translateY}px)`
|
|
81
|
+
|
|
82
|
+
if (translateY > 250) {
|
|
83
|
+
modal.close()
|
|
84
|
+
modal.classList.add('ac-modal--animated')
|
|
85
|
+
modal.classList.add('ac-modal--close')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const handleTouchEnd = () => {
|
|
90
|
+
modal.style.transform = 'translateY(0)'
|
|
91
|
+
modal.style.opacity = '1'
|
|
92
|
+
modal.style.transition = 'transform 0.2s ease-in-out'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
modal.addEventListener('touchstart', handleTouchStart, { passive: true })
|
|
96
|
+
modal.addEventListener('touchmove', handleTouchMove, { passive: true })
|
|
97
|
+
modal.addEventListener('touchend', handleTouchEnd, { passive: true })
|
|
98
|
+
;(modal as any).handleTouchStart = handleTouchStart
|
|
99
|
+
;(modal as any).handleTouchMove = handleTouchMove
|
|
100
|
+
;(modal as any).handleTouchEnd = handleTouchEnd
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const disableTouchControls = (modal: HTMLDialogElement) => {
|
|
104
|
+
modal.removeEventListener('touchstart', (modal as any).handleTouchStart)
|
|
105
|
+
modal.removeEventListener('touchmove', (modal as any).handleTouchMove)
|
|
106
|
+
modal.removeEventListener('touchend', (modal as any).handleTouchEnd)
|
|
107
|
+
}
|