@saasmakers/ui 1.4.48 → 1.4.50
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.
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { NuxtLinkLocale } from '#components'
|
|
3
|
+
import type { BaseCard } from '../../types/bases'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(defineProps<BaseCard>(), {
|
|
6
|
+
avatar: '',
|
|
7
|
+
clickable: true,
|
|
8
|
+
color: 'gray',
|
|
9
|
+
description: '',
|
|
10
|
+
details: '',
|
|
11
|
+
detailsIcon: undefined,
|
|
12
|
+
direction: 'horizontal',
|
|
13
|
+
emoji: undefined,
|
|
14
|
+
hasBackground: true,
|
|
15
|
+
hasChevron: false,
|
|
16
|
+
icon: undefined,
|
|
17
|
+
id: undefined,
|
|
18
|
+
image: '',
|
|
19
|
+
isSelected: false,
|
|
20
|
+
title: undefined,
|
|
21
|
+
titleIcon: undefined,
|
|
22
|
+
to: undefined,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits<{
|
|
26
|
+
click: [event: MouseEvent, id?: number | string]
|
|
27
|
+
}>()
|
|
28
|
+
|
|
29
|
+
defineSlots<{
|
|
30
|
+
default?: () => VNode[]
|
|
31
|
+
detailsLeft?: () => VNode[]
|
|
32
|
+
innerBoxLeft?: () => VNode[]
|
|
33
|
+
innerBoxRight?: () => VNode[]
|
|
34
|
+
outerBoxLeft?: () => VNode[]
|
|
35
|
+
outerBoxRight?: () => VNode[]
|
|
36
|
+
right?: () => VNode[]
|
|
37
|
+
}>()
|
|
38
|
+
|
|
39
|
+
const { getIcon } = useLayerIcons()
|
|
40
|
+
const hasAvatarBox = computed<boolean>(() => !!(props.avatar || props.emoji || props.icon || props.image))
|
|
41
|
+
|
|
42
|
+
const isClickable = computed(() => {
|
|
43
|
+
return props.to || props.clickable
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function onClick(event: MouseEvent) {
|
|
47
|
+
emit('click', event, props.id)
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<component
|
|
53
|
+
:is="to ? NuxtLinkLocale : 'div'"
|
|
54
|
+
class="group flex flex-col"
|
|
55
|
+
:class="{
|
|
56
|
+
'cursor-pointer': isClickable,
|
|
57
|
+
'border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm p-1.5 pr-2.5': hasBackground,
|
|
58
|
+
'hover:border-gray-300 dark:hover:border-gray-700': hasBackground && isClickable,
|
|
59
|
+
'rounded-xl': hasBackground,
|
|
60
|
+
}"
|
|
61
|
+
:to="to"
|
|
62
|
+
@click="onClick"
|
|
63
|
+
>
|
|
64
|
+
<div
|
|
65
|
+
class="flex overflow-hidden"
|
|
66
|
+
:class="{
|
|
67
|
+
'flex-row items-center text-left': direction === 'horizontal',
|
|
68
|
+
'flex-col text-center': direction === 'vertical',
|
|
69
|
+
}"
|
|
70
|
+
>
|
|
71
|
+
<span class="flex justify-center">
|
|
72
|
+
<slot name="outerBoxLeft" />
|
|
73
|
+
|
|
74
|
+
<span
|
|
75
|
+
v-if="hasAvatarBox"
|
|
76
|
+
class="relative z-10 h-9 w-9 flex items-center justify-center flex-initial"
|
|
77
|
+
:class="{
|
|
78
|
+
'border shadow-inner': !avatar && !image,
|
|
79
|
+
'rounded-lg': !avatar && !image,
|
|
80
|
+
'border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-900': !avatar && !image,
|
|
81
|
+
'mr-2': direction === 'horizontal',
|
|
82
|
+
'mb-1': direction === 'vertical',
|
|
83
|
+
}"
|
|
84
|
+
>
|
|
85
|
+
<slot name="innerBoxLeft" />
|
|
86
|
+
|
|
87
|
+
<BaseAvatar
|
|
88
|
+
v-if="avatar"
|
|
89
|
+
class="h-10 w-10 rounded-lg flex-initial"
|
|
90
|
+
:src="avatar"
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
<BaseEmoji
|
|
94
|
+
v-else-if="emoji"
|
|
95
|
+
class="m-0.5"
|
|
96
|
+
:emoji="emoji"
|
|
97
|
+
:has-box="false"
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
<BaseIcon
|
|
101
|
+
v-else-if="icon"
|
|
102
|
+
class="text-lg"
|
|
103
|
+
:color="color"
|
|
104
|
+
:icon="icon"
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
<img
|
|
108
|
+
v-else-if="image"
|
|
109
|
+
class="h-9 w-9 rounded-lg object-cover shadow-sm drag-none flex-initial"
|
|
110
|
+
:alt="title"
|
|
111
|
+
loading="lazy"
|
|
112
|
+
:src="image"
|
|
113
|
+
>
|
|
114
|
+
|
|
115
|
+
<slot name="innerBoxRight" />
|
|
116
|
+
</span>
|
|
117
|
+
|
|
118
|
+
<slot name="outerBoxRight" />
|
|
119
|
+
</span>
|
|
120
|
+
|
|
121
|
+
<div
|
|
122
|
+
class="min-w-0 flex flex-1 flex-col justify-center leading-snug"
|
|
123
|
+
:class="{
|
|
124
|
+
'mr-4': direction === 'horizontal',
|
|
125
|
+
'items-center': direction === 'vertical',
|
|
126
|
+
}"
|
|
127
|
+
>
|
|
128
|
+
<div class="w-full flex flex-col gap-0">
|
|
129
|
+
<BaseIcon
|
|
130
|
+
v-if="title"
|
|
131
|
+
class="w-full"
|
|
132
|
+
:class="{ 'self-center': !hasAvatarBox }"
|
|
133
|
+
:icon="titleIcon"
|
|
134
|
+
size="base"
|
|
135
|
+
:text="title"
|
|
136
|
+
truncate
|
|
137
|
+
/>
|
|
138
|
+
|
|
139
|
+
<BaseText
|
|
140
|
+
v-if="description"
|
|
141
|
+
block
|
|
142
|
+
class="text-gray-600 leading-4 dark:text-gray-400"
|
|
143
|
+
size="2xs"
|
|
144
|
+
:text="description"
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div class="flex items-center">
|
|
149
|
+
<slot name="detailsLeft" />
|
|
150
|
+
|
|
151
|
+
<BaseIcon
|
|
152
|
+
v-if="details"
|
|
153
|
+
class="mt-0.5 whitespace-nowrap text-gray-700 font-semibold tracking-tighter dark:text-gray-300"
|
|
154
|
+
:icon="detailsIcon"
|
|
155
|
+
size="2xs"
|
|
156
|
+
:text="details"
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<BaseIcon
|
|
162
|
+
v-if="isSelected"
|
|
163
|
+
class="mr-1.5 self-center text-2xl flex-initial"
|
|
164
|
+
color="green"
|
|
165
|
+
:icon="getIcon('checkCircle')"
|
|
166
|
+
/>
|
|
167
|
+
|
|
168
|
+
<BaseIcon
|
|
169
|
+
v-else-if="to || hasChevron"
|
|
170
|
+
class="self-center text-xl text-gray-500 flex-initial dark:text-gray-500 group-hover:text-gray-900 dark:group-hover:text-gray-100"
|
|
171
|
+
:icon="getIcon('chevronRight')"
|
|
172
|
+
/>
|
|
173
|
+
|
|
174
|
+
<slot name="right" />
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<slot />
|
|
178
|
+
</component>
|
|
179
|
+
</template>
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { LayoutBottomSheet } from '../../types/layout'
|
|
3
|
+
|
|
4
|
+
withDefaults(defineProps<LayoutBottomSheet>(), { title: undefined })
|
|
5
|
+
|
|
6
|
+
defineSlots<{
|
|
7
|
+
default?: () => VNode[]
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const closeThresholdRatio = 0.25
|
|
11
|
+
const dragMoveThreshold = 8
|
|
12
|
+
const focusableSelector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
13
|
+
const visible = defineModel<boolean>({ default: false })
|
|
14
|
+
const contentRef = ref<HTMLElement>()
|
|
15
|
+
const focusTrigger = ref<HTMLElement>()
|
|
16
|
+
const panelRef = ref<HTMLElement>()
|
|
17
|
+
|
|
18
|
+
const drag = reactive({
|
|
19
|
+
closing: false,
|
|
20
|
+
isDragging: false,
|
|
21
|
+
offset: 0,
|
|
22
|
+
pointerId: undefined as number | undefined,
|
|
23
|
+
startedFromContent: false,
|
|
24
|
+
startY: 0,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
let savedBodyOverflow = ''
|
|
28
|
+
let savedBodyPaddingRight = ''
|
|
29
|
+
|
|
30
|
+
const fixedElementPadding = new Map<HTMLElement, string>()
|
|
31
|
+
|
|
32
|
+
const closeThreshold = computed(() => {
|
|
33
|
+
return (panelRef.value?.offsetHeight ?? 320) * closeThresholdRatio
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
function getFocusableElements(container: HTMLElement) {
|
|
37
|
+
return Array.from(container.querySelectorAll<HTMLElement>(focusableSelector))
|
|
38
|
+
.filter((element) => {
|
|
39
|
+
return element.offsetParent !== null || getComputedStyle(element).position === 'fixed'
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function lockBodyScroll() {
|
|
44
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
|
|
45
|
+
|
|
46
|
+
savedBodyOverflow = document.body.style.overflow
|
|
47
|
+
savedBodyPaddingRight = document.body.style.paddingRight
|
|
48
|
+
document.body.style.overflow = 'hidden'
|
|
49
|
+
|
|
50
|
+
if (scrollbarWidth > 0) {
|
|
51
|
+
document.body.style.paddingRight = `${scrollbarWidth}px`
|
|
52
|
+
|
|
53
|
+
for (const element of document.querySelectorAll('body *')) {
|
|
54
|
+
if (!(element instanceof HTMLElement)) {
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (element.classList.contains('layout-bottom-sheet')) {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { position } = getComputedStyle(element)
|
|
63
|
+
|
|
64
|
+
if (position !== 'fixed' && position !== 'sticky') {
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fixedElementPadding.set(element, element.style.paddingRight)
|
|
69
|
+
|
|
70
|
+
const currentPadding = Number.parseFloat(getComputedStyle(element).paddingRight) || 0
|
|
71
|
+
|
|
72
|
+
element.style.paddingRight = `${currentPadding + scrollbarWidth}px`
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onClose() {
|
|
78
|
+
visible.value = false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function onContentPointerDown(event: PointerEvent) {
|
|
82
|
+
if (drag.closing) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if ((contentRef.value?.scrollTop ?? 0) > 0) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
drag.startedFromContent = true
|
|
91
|
+
drag.pointerId = event.pointerId
|
|
92
|
+
drag.startY = event.clientY
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function onFocusTrap(event: KeyboardEvent) {
|
|
96
|
+
if (event.key !== 'Tab' || !panelRef.value) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const focusableElements = getFocusableElements(panelRef.value)
|
|
101
|
+
|
|
102
|
+
if (focusableElements.length === 0) {
|
|
103
|
+
event.preventDefault()
|
|
104
|
+
panelRef.value.focus()
|
|
105
|
+
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const firstElement = focusableElements[0]
|
|
110
|
+
const lastElement = focusableElements[focusableElements.length - 1]
|
|
111
|
+
const activeElement = document.activeElement
|
|
112
|
+
|
|
113
|
+
if (event.shiftKey && activeElement === firstElement) {
|
|
114
|
+
event.preventDefault()
|
|
115
|
+
lastElement.focus()
|
|
116
|
+
}
|
|
117
|
+
else if (!event.shiftKey && activeElement === lastElement) {
|
|
118
|
+
event.preventDefault()
|
|
119
|
+
firstElement.focus()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function onHeaderPointerDown(event: PointerEvent) {
|
|
124
|
+
if (drag.closing) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
drag.startedFromContent = false
|
|
129
|
+
drag.isDragging = true
|
|
130
|
+
drag.pointerId = event.pointerId
|
|
131
|
+
drag.startY = event.clientY
|
|
132
|
+
drag.offset = 0
|
|
133
|
+
|
|
134
|
+
const handle = event.currentTarget as HTMLElement
|
|
135
|
+
|
|
136
|
+
handle.setPointerCapture(event.pointerId)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function onKeydown(event: KeyboardEvent) {
|
|
140
|
+
if (!visible.value) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (event.key === 'Escape') {
|
|
145
|
+
onClose()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function onPanelAfterLeave() {
|
|
150
|
+
if (import.meta.client) {
|
|
151
|
+
unlockBodyScroll()
|
|
152
|
+
window.removeEventListener('keydown', onFocusTrap)
|
|
153
|
+
focusTrigger.value?.focus()
|
|
154
|
+
|
|
155
|
+
focusTrigger.value = undefined
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function onPanelTransitionEnd(event: TransitionEvent) {
|
|
160
|
+
if (event.propertyName !== 'transform' || !drag.closing) {
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
onClose()
|
|
165
|
+
resetDragState()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function onPointerMove(event: PointerEvent) {
|
|
169
|
+
if (drag.startedFromContent && !drag.isDragging) {
|
|
170
|
+
if (event.pointerId !== drag.pointerId) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if ((contentRef.value?.scrollTop ?? 0) > 0) {
|
|
175
|
+
resetPendingDrag()
|
|
176
|
+
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const offset = event.clientY - drag.startY
|
|
181
|
+
|
|
182
|
+
if (offset < 0) {
|
|
183
|
+
resetPendingDrag()
|
|
184
|
+
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (offset < dragMoveThreshold) {
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
drag.isDragging = true
|
|
193
|
+
drag.offset = offset
|
|
194
|
+
|
|
195
|
+
const handle = event.currentTarget as HTMLElement
|
|
196
|
+
|
|
197
|
+
handle.setPointerCapture(event.pointerId)
|
|
198
|
+
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!drag.isDragging) {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const offset = event.clientY - drag.startY
|
|
207
|
+
|
|
208
|
+
if (drag.startedFromContent && offset < 0) {
|
|
209
|
+
drag.isDragging = false
|
|
210
|
+
|
|
211
|
+
resetPendingDrag()
|
|
212
|
+
|
|
213
|
+
drag.offset = 0
|
|
214
|
+
|
|
215
|
+
const handle = event.currentTarget as HTMLElement
|
|
216
|
+
|
|
217
|
+
if (handle.hasPointerCapture(event.pointerId)) {
|
|
218
|
+
handle.releasePointerCapture(event.pointerId)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
drag.offset = Math.max(0, offset)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function onPointerUp(event: PointerEvent) {
|
|
228
|
+
if (!drag.isDragging) {
|
|
229
|
+
resetPendingDrag()
|
|
230
|
+
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
drag.isDragging = false
|
|
235
|
+
|
|
236
|
+
resetPendingDrag()
|
|
237
|
+
|
|
238
|
+
const handle = event.currentTarget as HTMLElement
|
|
239
|
+
|
|
240
|
+
handle.releasePointerCapture(event.pointerId)
|
|
241
|
+
|
|
242
|
+
if (drag.offset >= closeThreshold.value) {
|
|
243
|
+
drag.closing = true
|
|
244
|
+
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
drag.offset = 0
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resetDragState() {
|
|
252
|
+
drag.closing = false
|
|
253
|
+
drag.isDragging = false
|
|
254
|
+
drag.offset = 0
|
|
255
|
+
drag.pointerId = undefined
|
|
256
|
+
drag.startY = 0
|
|
257
|
+
drag.startedFromContent = false
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function resetPendingDrag() {
|
|
261
|
+
drag.pointerId = undefined
|
|
262
|
+
drag.startedFromContent = false
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function unlockBodyScroll() {
|
|
266
|
+
document.body.style.overflow = savedBodyOverflow
|
|
267
|
+
document.body.style.paddingRight = savedBodyPaddingRight
|
|
268
|
+
|
|
269
|
+
for (const [element, paddingRight] of fixedElementPadding) {
|
|
270
|
+
element.style.paddingRight = paddingRight
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fixedElementPadding.clear()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
watch(visible, async (isVisible) => {
|
|
277
|
+
if (import.meta.client && isVisible) {
|
|
278
|
+
focusTrigger.value = document.activeElement instanceof HTMLElement
|
|
279
|
+
? document.activeElement
|
|
280
|
+
: undefined
|
|
281
|
+
|
|
282
|
+
lockBodyScroll()
|
|
283
|
+
|
|
284
|
+
await nextTick()
|
|
285
|
+
|
|
286
|
+
if (panelRef.value) {
|
|
287
|
+
const focusableElements = getFocusableElements(panelRef.value)
|
|
288
|
+
|
|
289
|
+
if (focusableElements.length > 0) {
|
|
290
|
+
focusableElements[0].focus()
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
panelRef.value.focus()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
window.addEventListener('keydown', onFocusTrap)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!isVisible) {
|
|
301
|
+
resetDragState()
|
|
302
|
+
}
|
|
303
|
+
}, { immediate: true })
|
|
304
|
+
|
|
305
|
+
onMounted(() => window.addEventListener('keydown', onKeydown))
|
|
306
|
+
|
|
307
|
+
onUnmounted(() => {
|
|
308
|
+
window.removeEventListener('keydown', onKeydown)
|
|
309
|
+
window.removeEventListener('keydown', onFocusTrap)
|
|
310
|
+
|
|
311
|
+
if (import.meta.client) {
|
|
312
|
+
unlockBodyScroll()
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
</script>
|
|
316
|
+
|
|
317
|
+
<template>
|
|
318
|
+
<ClientOnly>
|
|
319
|
+
<Teleport to="body">
|
|
320
|
+
<Transition
|
|
321
|
+
enter-active-class="transition-opacity duration-300 ease motion-reduce:transition-none motion-reduce:duration-0"
|
|
322
|
+
enter-from-class="opacity-0 motion-reduce:opacity-100"
|
|
323
|
+
enter-to-class="opacity-100"
|
|
324
|
+
leave-active-class="transition-opacity duration-300 ease motion-reduce:transition-none motion-reduce:duration-0"
|
|
325
|
+
leave-from-class="opacity-100"
|
|
326
|
+
leave-to-class="opacity-0 motion-reduce:opacity-100"
|
|
327
|
+
>
|
|
328
|
+
<div
|
|
329
|
+
v-if="visible"
|
|
330
|
+
aria-hidden="true"
|
|
331
|
+
class="layout-bottom-sheet fixed inset-0 z-[60] bg-black/50"
|
|
332
|
+
:class="{
|
|
333
|
+
'transition-none motion-reduce:transition-none': drag.isDragging,
|
|
334
|
+
'transition-opacity duration-200 ease motion-reduce:transition-none motion-reduce:duration-0': !drag.isDragging && (drag.closing || drag.offset > 0),
|
|
335
|
+
}"
|
|
336
|
+
:style="{
|
|
337
|
+
opacity: drag.closing ? 0 : drag.offset > 0 ? Math.max(0, 1 - drag.offset / (panelRef?.offsetHeight ?? 320)) : undefined,
|
|
338
|
+
}"
|
|
339
|
+
@click="onClose"
|
|
340
|
+
/>
|
|
341
|
+
</Transition>
|
|
342
|
+
|
|
343
|
+
<Transition
|
|
344
|
+
enter-active-class="transition-transform duration-300 ease motion-reduce:transition-none motion-reduce:duration-0"
|
|
345
|
+
enter-from-class="translate-y-full motion-reduce:translate-y-0"
|
|
346
|
+
enter-to-class="translate-y-0"
|
|
347
|
+
leave-active-class="transition-transform duration-300 ease motion-reduce:transition-none motion-reduce:duration-0"
|
|
348
|
+
leave-from-class="translate-y-0"
|
|
349
|
+
leave-to-class="translate-y-full motion-reduce:translate-y-0"
|
|
350
|
+
@after-leave="onPanelAfterLeave"
|
|
351
|
+
>
|
|
352
|
+
<div
|
|
353
|
+
v-if="visible"
|
|
354
|
+
ref="panelRef"
|
|
355
|
+
:aria-label="title"
|
|
356
|
+
aria-modal="true"
|
|
357
|
+
class="layout-bottom-sheet fixed bottom-0 left-0 right-0 z-[60] mx-auto max-h-[85dvh] max-w-screen-md flex flex-col rounded-t-2xl bg-white text-gray-900 shadow-lg safe-bottom dark:bg-gray-900 dark:text-gray-100"
|
|
358
|
+
:class="{
|
|
359
|
+
'transition-none motion-reduce:transition-none': drag.isDragging,
|
|
360
|
+
'transition-transform duration-200 ease motion-reduce:transition-none motion-reduce:duration-0': !drag.isDragging && (drag.closing || drag.offset > 0),
|
|
361
|
+
}"
|
|
362
|
+
role="dialog"
|
|
363
|
+
:style="{
|
|
364
|
+
transform: drag.closing ? 'translateY(100%)' : drag.offset > 0 ? `translateY(${drag.offset}px)` : undefined,
|
|
365
|
+
}"
|
|
366
|
+
tabindex="-1"
|
|
367
|
+
@transitionend="onPanelTransitionEnd"
|
|
368
|
+
>
|
|
369
|
+
<div
|
|
370
|
+
class="flex shrink-0 cursor-grab touch-none justify-center px-5 pb-2 pt-3 active:cursor-grabbing"
|
|
371
|
+
@pointercancel="onPointerUp"
|
|
372
|
+
@pointerdown="onHeaderPointerDown"
|
|
373
|
+
@pointermove="onPointerMove"
|
|
374
|
+
@pointerup="onPointerUp"
|
|
375
|
+
>
|
|
376
|
+
<div class="h-1.5 w-10 rounded-full bg-gray-300 dark:bg-gray-700" />
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<div
|
|
380
|
+
ref="contentRef"
|
|
381
|
+
class="min-h-0 flex-1 overflow-y-auto px-5 pb-4"
|
|
382
|
+
@pointercancel="onPointerUp"
|
|
383
|
+
@pointerdown="onContentPointerDown"
|
|
384
|
+
@pointermove="onPointerMove"
|
|
385
|
+
@pointerup="onPointerUp"
|
|
386
|
+
>
|
|
387
|
+
<slot />
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
</Transition>
|
|
391
|
+
</Teleport>
|
|
392
|
+
</ClientOnly>
|
|
393
|
+
</template>
|
package/app/types/bases.d.ts
CHANGED
|
@@ -72,6 +72,28 @@ export type BaseButtonSize
|
|
|
72
72
|
|
|
73
73
|
export type BaseButtonType = 'button' | 'reset' | 'submit'
|
|
74
74
|
|
|
75
|
+
export interface BaseCard {
|
|
76
|
+
avatar?: string
|
|
77
|
+
clickable?: boolean
|
|
78
|
+
color?: BaseColor
|
|
79
|
+
description?: string
|
|
80
|
+
details?: string
|
|
81
|
+
detailsIcon?: string
|
|
82
|
+
direction?: BaseCardDirection
|
|
83
|
+
emoji?: string
|
|
84
|
+
hasBackground?: boolean
|
|
85
|
+
hasChevron?: boolean
|
|
86
|
+
icon?: string
|
|
87
|
+
id?: number | string
|
|
88
|
+
image?: string
|
|
89
|
+
isSelected?: boolean
|
|
90
|
+
title?: string
|
|
91
|
+
titleIcon?: string
|
|
92
|
+
to?: RouteLocationNamedI18n
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type BaseCardDirection = 'horizontal' | 'vertical'
|
|
96
|
+
|
|
75
97
|
export interface BaseCharacter {
|
|
76
98
|
character?: BaseCharacterCharacter
|
|
77
99
|
size?: BaseCharacterSize
|
package/app/types/global.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ declare global {
|
|
|
12
12
|
type BaseButtonRounded = import('./bases').BaseButtonRounded
|
|
13
13
|
type BaseButtonSize = import('./bases').BaseButtonSize
|
|
14
14
|
type BaseButtonType = import('./bases').BaseButtonType
|
|
15
|
+
type BaseCard = import('./bases').BaseCard
|
|
16
|
+
type BaseCardDirection = import('./bases').BaseCardDirection
|
|
15
17
|
type BaseCharacter = import('./bases').BaseCharacter
|
|
16
18
|
type BaseCharacterCharacter = import('./bases').BaseCharacterCharacter
|
|
17
19
|
type BaseCharacterSize = import('./bases').BaseCharacterSize
|
|
@@ -90,6 +92,9 @@ declare global {
|
|
|
90
92
|
type FieldTextarea = import('./fields').FieldTextarea
|
|
91
93
|
type FieldTime = import('./fields').FieldTime
|
|
92
94
|
|
|
95
|
+
// Layout
|
|
96
|
+
type LayoutBottomSheet = import('./layout').LayoutBottomSheet
|
|
97
|
+
|
|
93
98
|
// Project
|
|
94
99
|
type LayerIconIcon = import('../composables/useLayerIcons').LayerIconIcon
|
|
95
100
|
type LayerIconValue = import('../composables/useLayerIcons').LayerIconValue
|