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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "A collection of free Astro components",
4
4
  "author": "Denis Ventura",
5
5
  "type": "module",
6
- "version": "0.0.42",
6
+ "version": "1.0.0",
7
7
  "exports": {
8
8
  ".": {
9
9
  "import": {
@@ -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>
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import '../css/main.css'
2
3
  interface Props {
3
4
  label?: string
4
5
  class?: string
package/src/css/main.css CHANGED
@@ -77,6 +77,7 @@
77
77
  --ac-spacing-9: 2.25rem;
78
78
  --ac-spacing-10: 2.5rem;
79
79
  --ac-spacing-12: 3rem;
80
+ --ac-spacing-16: 4rem;
80
81
 
81
82
  /* Border width */
82
83
  --ac-border-0: 0;
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'
@@ -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
+ }
@@ -4,4 +4,8 @@ export const DOMLoaded = (callback: () => void) => {
4
4
  } else {
5
5
  callback()
6
6
  }
7
- }
7
+ }
8
+
9
+ export const isTouchDevice = () => {
10
+ return window.matchMedia('(pointer: coarse)').matches
11
+ }