adata-ui 2.1.40-beta.1 → 2.1.40-beta.2

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,118 @@
1
+ <script lang="ts" setup>
2
+ type CountColor = 'gray' | 'blue' | 'yellow' | 'red' | 'green' | 'neutral'
3
+
4
+ export interface PillTab {
5
+ key: string
6
+ name: string
7
+ count?: number
8
+ countColor?: CountColor
9
+ disabled?: boolean
10
+ hideCount?: boolean
11
+ }
12
+
13
+ const props = withDefaults(defineProps<{
14
+ modelValue: string
15
+ options: PillTab[]
16
+ showCount?: boolean
17
+ ariaLabel?: string
18
+ block?: boolean
19
+ disabled?: boolean
20
+ size?: 'sm' | 'md'
21
+ }>(), {
22
+ showCount: false,
23
+ block: false,
24
+ disabled: false,
25
+ size: 'sm',
26
+ })
27
+
28
+ const emit = defineEmits<{ (e: 'update:modelValue', value: string): void }>()
29
+
30
+ const SIZE_CLASS: Record<'sm' | 'md', string> = {
31
+ sm: 'text-xs',
32
+ md: 'text-sm',
33
+ }
34
+
35
+ const COUNT_COLOR_CLASS: Record<CountColor, string> = {
36
+ gray: 'bg-gray-200/70 text-gray-600 dark:bg-white/[0.08] dark:text-gray-300',
37
+ blue: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
38
+ yellow: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300',
39
+ red: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',
40
+ green: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
41
+ neutral: 'bg-gray-100 text-gray-500 dark:bg-white/[0.06] dark:text-gray-400',
42
+ }
43
+
44
+ const { t, te } = useI18n()
45
+
46
+ function localize(label: string) {
47
+ return te(label) ? t(label) : label
48
+ }
49
+
50
+ function isDisabled(option: PillTab) {
51
+ return props.disabled || option.disabled === true
52
+ }
53
+
54
+ function select(option: PillTab) {
55
+ if (isDisabled(option)) return
56
+ emit('update:modelValue', option.key)
57
+ }
58
+ </script>
59
+
60
+ <template>
61
+ <div
62
+ class="pill-tabs-v2 min-w-0 max-w-full rounded-xl border border-gray-200 bg-gray-50 p-1 dark:border-white/10 dark:bg-white/[0.03]"
63
+ :class="[
64
+ block ? 'w-full' : 'w-fit',
65
+ disabled ? 'pill-tabs-v2--disabled opacity-60' : '',
66
+ ]"
67
+ :aria-disabled="disabled || undefined"
68
+ >
69
+ <div
70
+ role="tablist"
71
+ :aria-label="ariaLabel"
72
+ class="pill-tabs-v2__list flex w-full flex-nowrap items-center gap-1 overflow-x-auto"
73
+ >
74
+ <button
75
+ v-for="option in options"
76
+ :key="option.key"
77
+ type="button"
78
+ role="tab"
79
+ :aria-selected="modelValue === option.key"
80
+ :aria-disabled="isDisabled(option) || undefined"
81
+ :disabled="isDisabled(option)"
82
+ :tabindex="modelValue === option.key ? 0 : -1"
83
+ class="inline-flex items-center gap-1.5 whitespace-nowrap rounded-lg px-3 py-1.5 font-medium transition-colors duration-150"
84
+ :class="[
85
+ SIZE_CLASS[size],
86
+ isDisabled(option)
87
+ ? 'cursor-not-allowed text-gray-300 dark:text-gray-600'
88
+ : modelValue === option.key
89
+ ? 'text-deepblue-900 bg-white shadow-sm dark:bg-gray-700 dark:text-gray-100 dark:shadow-none dark:ring-1 dark:ring-inset dark:ring-white/10'
90
+ : 'text-gray-700 hover:bg-white/70 hover:text-gray-900 dark:text-gray-200 dark:hover:bg-white/[0.06] dark:hover:text-gray-100',
91
+ block ? 'flex-1 basis-0 justify-center' : 'shrink-0',
92
+ ]"
93
+ @click="select(option)"
94
+ >
95
+ <slot name="option" :option="option">
96
+ <span>{{ localize(option.name) }}</span>
97
+
98
+ <span
99
+ v-if="showCount && !option.hideCount"
100
+ class="inline-flex min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 text-xs font-semibold leading-5"
101
+ :class="COUNT_COLOR_CLASS[option.countColor ?? 'gray']"
102
+ >
103
+ {{ option.count }}
104
+ </span>
105
+ </slot>
106
+ </button>
107
+ </div>
108
+ </div>
109
+ </template>
110
+
111
+ <style scoped>
112
+ .pill-tabs-v2__list {
113
+ scrollbar-width: none;
114
+ }
115
+ .pill-tabs-v2__list::-webkit-scrollbar {
116
+ display: none;
117
+ }
118
+ </style>
@@ -0,0 +1,388 @@
1
+ <script setup lang="ts">
2
+ import { onKeyStroke, useScrollLock, useWindowSize } from '@vueuse/core'
3
+
4
+ type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'container' | 'fullscreen'
5
+
6
+ const props = withDefaults(defineProps<{
7
+ title?: string
8
+ size?: ModalSize
9
+ width?: string
10
+ closeOnOverlay?: boolean
11
+ closeOnEsc?: boolean
12
+ lockScroll?: boolean
13
+ persistent?: boolean
14
+ transition?: boolean
15
+ contentClass?: string
16
+ hideHeader?: boolean
17
+ blur?: boolean | 'sm' | 'md' | 'lg' | 'xl'
18
+ mobileSheet?: boolean
19
+ swipeToClose?: boolean
20
+ swipeThreshold?: number
21
+ showDragHandle?: boolean
22
+ }>(), {
23
+ size: 'md',
24
+ closeOnOverlay: true,
25
+ closeOnEsc: true,
26
+ lockScroll: true,
27
+ persistent: false,
28
+ transition: true,
29
+ hideHeader: false,
30
+ blur: false,
31
+ mobileSheet: true,
32
+ swipeToClose: true,
33
+ swipeThreshold: 65,
34
+ showDragHandle: true,
35
+ })
36
+
37
+ const emit = defineEmits<{
38
+ (e: 'close'): void
39
+ (e: 'open'): void
40
+ }>()
41
+
42
+ const open = defineModel<boolean>({ required: true })
43
+
44
+ const contentRef = ref<HTMLElement | null>(null)
45
+ const overlayRef = ref<HTMLElement | null>(null)
46
+ const swipeClosing = ref(false)
47
+ const bodyScrollLocked = useScrollLock(import.meta.client ? document.body : null)
48
+ const { width: windowWidth } = useWindowSize()
49
+
50
+ const isMobile = computed(() => windowWidth.value < 1025)
51
+ const isSheet = computed(() => isMobile.value && props.mobileSheet)
52
+
53
+ const sizeStyle = computed(() => {
54
+ if (isSheet.value) return { width: '100%' }
55
+ if (props.width) return { width: props.width }
56
+
57
+ switch (props.size) {
58
+ case 'sm': return { width: '380px' }
59
+ case 'md': return { width: '520px' }
60
+ case 'lg': return { width: '720px' }
61
+ case 'xl': return { width: '1100px' }
62
+ case 'container': return {}
63
+ case 'fullscreen': return { width: '100vw', height: '100vh' }
64
+ default: return { width: '520px' }
65
+ }
66
+ })
67
+
68
+ const blurClass = computed(() => {
69
+ if (!props.blur) return ''
70
+
71
+ switch (props.blur) {
72
+ case 'sm': return 'backdrop-blur-sm'
73
+ case 'md': return 'backdrop-blur-md'
74
+ case 'lg': return 'backdrop-blur-lg'
75
+ case 'xl': return 'backdrop-blur-xl'
76
+ default: return 'backdrop-blur'
77
+ }
78
+ })
79
+
80
+ const overlayClass = computed(() => {
81
+ const base = ['modal-v2-overlay fixed inset-0 z-[10000] flex bg-black/40 dark:bg-black/60']
82
+
83
+ if (isSheet.value) {
84
+ base.push('items-end justify-stretch')
85
+ }
86
+ else {
87
+ base.push(props.size === 'container' ? 'items-center justify-center' : 'items-center justify-center p-4')
88
+ }
89
+
90
+ if (blurClass.value) base.push(blurClass.value)
91
+
92
+ return base.join(' ')
93
+ })
94
+
95
+ const contentBaseClass = computed(() => {
96
+ const base = ['modal-v2-content flex flex-col overflow-hidden border bg-white shadow-2xl dark:bg-gray-900']
97
+
98
+ if (isSheet.value) {
99
+ base.push('w-full max-w-none max-h-[100dvh] rounded-t-2xl rounded-b-none border-x-0 border-b-0 border-t border-gray-200 dark:border-gray-800')
100
+ }
101
+ else {
102
+ const radius = props.size === 'fullscreen' ? 'rounded-none' : 'rounded-2xl'
103
+ const widthCls = props.size === 'container'
104
+ ? ' w-full max-w-screen-sm md:max-w-screen-md lg:max-w-screen-lg xl:max-w-screen-xl 2xl:max-w-screen-2xl'
105
+ : ''
106
+ base.push(`max-h-[calc(100vh-2rem)] border-gray-200 dark:border-gray-800 ${radius}${widthCls}`)
107
+ }
108
+
109
+ return base.join(' ')
110
+ })
111
+
112
+ const overlayTransitionName = computed(() => {
113
+ if (swipeClosing.value) return ''
114
+ return props.transition ? 'modal-v2-overlay' : ''
115
+ })
116
+ const contentTransitionName = computed(() => {
117
+ if (swipeClosing.value || !props.transition) return ''
118
+ return isSheet.value ? 'modal-v2-sheet' : 'modal-v2-content'
119
+ })
120
+
121
+ function closeIfAllowed() {
122
+ if (props.persistent) return
123
+ open.value = false
124
+ }
125
+
126
+ function onOverlayClick(event: MouseEvent) {
127
+ if (!props.closeOnOverlay || props.persistent) return
128
+ if (event.target === event.currentTarget) {
129
+ open.value = false
130
+ }
131
+ }
132
+
133
+ onKeyStroke('Escape', () => {
134
+ if (!open.value) return
135
+ if (!props.closeOnEsc || props.persistent) return
136
+ open.value = false
137
+ })
138
+
139
+ function setScrollbarCompensation(active: boolean) {
140
+ if (!import.meta.client) return
141
+
142
+ if (active) {
143
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
144
+ document.body.style.paddingRight = scrollbarWidth > 0 ? `${scrollbarWidth}px` : ''
145
+ }
146
+ else {
147
+ document.body.style.paddingRight = ''
148
+ }
149
+ }
150
+
151
+ watch(open, (value) => {
152
+ if (props.lockScroll) {
153
+ if (value) setScrollbarCompensation(true)
154
+ bodyScrollLocked.value = value
155
+ if (!value) setScrollbarCompensation(false)
156
+ }
157
+
158
+ if (value) emit('open')
159
+ else emit('close')
160
+ })
161
+
162
+ onBeforeUnmount(() => {
163
+ if (bodyScrollLocked.value) {
164
+ bodyScrollLocked.value = false
165
+ }
166
+ setScrollbarCompensation(false)
167
+ })
168
+
169
+ const dragging = ref(false)
170
+ const dragStartY = ref(0)
171
+ const dragDelta = ref(0)
172
+
173
+ function canSwipe() {
174
+ return isSheet.value && props.swipeToClose && !props.persistent
175
+ }
176
+
177
+ function setTransform(value: string) {
178
+ if (contentRef.value) {
179
+ contentRef.value.style.transform = value
180
+ }
181
+ }
182
+
183
+ function onTouchStart(event: TouchEvent) {
184
+ if (!canSwipe()) return
185
+ dragging.value = true
186
+ dragStartY.value = event.touches[0].clientY
187
+ dragDelta.value = 0
188
+
189
+ if (contentRef.value) {
190
+ contentRef.value.style.transition = 'none'
191
+ }
192
+ }
193
+
194
+ function onTouchMove(event: TouchEvent) {
195
+ if (!dragging.value || !canSwipe()) return
196
+
197
+ const delta = event.touches[0].clientY - dragStartY.value
198
+ if (delta < 0) {
199
+ dragDelta.value = 0
200
+ setTransform('')
201
+ return
202
+ }
203
+
204
+ dragDelta.value = delta
205
+ setTransform(`translateY(${delta}px)`)
206
+ }
207
+
208
+ function prefersReducedMotion() {
209
+ return import.meta.client && window.matchMedia('(prefers-reduced-motion: reduce)').matches
210
+ }
211
+
212
+ function animateClose() {
213
+ const content = contentRef.value
214
+
215
+ if (!content || prefersReducedMotion()) {
216
+ open.value = false
217
+ return
218
+ }
219
+
220
+ swipeClosing.value = true
221
+
222
+ let done = false
223
+ const finish = () => {
224
+ if (done) return
225
+ done = true
226
+ content.removeEventListener('transitionend', onTransitionEnd)
227
+ open.value = false
228
+ swipeClosing.value = false
229
+ }
230
+
231
+ function onTransitionEnd(event: TransitionEvent) {
232
+ if (event.target === content && event.propertyName === 'transform') {
233
+ finish()
234
+ }
235
+ }
236
+
237
+ content.addEventListener('transitionend', onTransitionEnd)
238
+ setTimeout(finish, 320)
239
+
240
+ requestAnimationFrame(() => {
241
+ content.style.transition = 'transform 260ms cubic-bezier(0.22, 1, 0.36, 1)'
242
+ content.style.transform = 'translateY(100%)'
243
+
244
+ if (overlayRef.value) {
245
+ overlayRef.value.style.transition = 'opacity 260ms ease'
246
+ overlayRef.value.style.opacity = '0'
247
+ }
248
+ })
249
+ }
250
+
251
+ function onTouchEnd() {
252
+ if (!dragging.value) return
253
+ dragging.value = false
254
+
255
+ const shouldClose = dragDelta.value > props.swipeThreshold && canSwipe()
256
+
257
+ if (shouldClose) {
258
+ animateClose()
259
+ }
260
+ else {
261
+ if (contentRef.value) {
262
+ contentRef.value.style.transition = 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1)'
263
+ contentRef.value.style.transform = ''
264
+ }
265
+ }
266
+
267
+ dragDelta.value = 0
268
+ }
269
+ </script>
270
+
271
+ <template>
272
+ <client-only>
273
+ <teleport to="body">
274
+ <transition :name="overlayTransitionName">
275
+ <div
276
+ v-if="open"
277
+ ref="overlayRef"
278
+ :class="overlayClass"
279
+ @mousedown="onOverlayClick"
280
+ >
281
+ <transition :name="contentTransitionName" appear>
282
+ <div
283
+ v-if="open"
284
+ ref="contentRef"
285
+ :class="contentBaseClass"
286
+ :style="sizeStyle"
287
+ @mousedown.stop
288
+ >
289
+ <div
290
+ v-if="isSheet && showDragHandle"
291
+ class="flex shrink-0 cursor-grab touch-none justify-center py-2"
292
+ @touchstart.passive="onTouchStart"
293
+ @touchmove="onTouchMove"
294
+ @touchend="onTouchEnd"
295
+ @touchcancel="onTouchEnd"
296
+ >
297
+ <div class="h-1 w-10 rounded-full bg-gray-300 dark:bg-gray-600" />
298
+ </div>
299
+
300
+ <header
301
+ v-if="!hideHeader && (title || $slots.header)"
302
+ class="flex shrink-0 items-center justify-between gap-3 border-b border-gray-200 px-5 py-4 dark:border-gray-800"
303
+ @touchstart.passive="onTouchStart"
304
+ @touchmove="onTouchMove"
305
+ @touchend="onTouchEnd"
306
+ @touchcancel="onTouchEnd"
307
+ >
308
+ <slot name="header">
309
+ <h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">
310
+ {{ title }}
311
+ </h2>
312
+ <button
313
+ type="button"
314
+ class="rounded-md p-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-gray-200"
315
+ @click="closeIfAllowed"
316
+ >
317
+ <a-icon-x-mark class="size-4" />
318
+ </button>
319
+ </slot>
320
+ </header>
321
+
322
+ <div class="min-h-0 flex-1 overflow-auto" :class="contentClass">
323
+ <slot :close="closeIfAllowed" />
324
+ </div>
325
+
326
+ <footer
327
+ v-if="$slots.footer"
328
+ class="shrink-0 border-t border-gray-200 px-5 py-4 dark:border-gray-800"
329
+ >
330
+ <slot name="footer" :close="closeIfAllowed" />
331
+ </footer>
332
+ </div>
333
+ </transition>
334
+ </div>
335
+ </transition>
336
+ </teleport>
337
+ </client-only>
338
+ </template>
339
+
340
+ <style scoped lang="scss">
341
+ .modal-v2-overlay-enter-active {
342
+ transition: opacity 200ms ease-out;
343
+ }
344
+
345
+ .modal-v2-overlay-leave-active {
346
+ transition: opacity 150ms ease-in;
347
+ }
348
+
349
+ .modal-v2-overlay-enter-from,
350
+ .modal-v2-overlay-leave-to {
351
+ opacity: 0;
352
+ }
353
+
354
+ .modal-v2-content-enter-active {
355
+ transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1), opacity 220ms ease-out;
356
+ }
357
+
358
+ .modal-v2-content-leave-active {
359
+ transition: transform 150ms ease-in, opacity 150ms ease-in;
360
+ }
361
+
362
+ .modal-v2-content-enter-from,
363
+ .modal-v2-content-leave-to {
364
+ opacity: 0;
365
+ transform: translateY(8px) scale(0.98);
366
+ }
367
+
368
+ .modal-v2-sheet-enter-active,
369
+ .modal-v2-sheet-leave-active {
370
+ transition: transform 280ms cubic-bezier(0.22, 1, 0.36, 1);
371
+ }
372
+
373
+ .modal-v2-sheet-enter-from,
374
+ .modal-v2-sheet-leave-to {
375
+ transform: translateY(100%);
376
+ }
377
+
378
+ @media (prefers-reduced-motion: reduce) {
379
+ .modal-v2-overlay-enter-active,
380
+ .modal-v2-overlay-leave-active,
381
+ .modal-v2-content-enter-active,
382
+ .modal-v2-content-leave-active,
383
+ .modal-v2-sheet-enter-active,
384
+ .modal-v2-sheet-leave-active {
385
+ transition: none;
386
+ }
387
+ }
388
+ </style>
@@ -0,0 +1,82 @@
1
+ import type { ComputedRef, Ref } from 'vue'
2
+
3
+ interface UseChipOverflowOptions {
4
+ container: Ref<HTMLElement | null>
5
+ measure: Ref<HTMLElement | null>
6
+ count: Ref<number>
7
+ gap?: number
8
+ }
9
+
10
+ interface UseChipOverflow {
11
+ visibleCount: Ref<number>
12
+ hiddenCount: ComputedRef<number>
13
+ }
14
+
15
+ export function useChipOverflow(options: UseChipOverflowOptions): UseChipOverflow {
16
+ const { container, measure, count, gap = 6 } = options
17
+
18
+ const availableWidth = ref(0)
19
+ const visibleCount = ref(0)
20
+
21
+ function recompute() {
22
+ const total = count.value
23
+ if (total <= 0) {
24
+ visibleCount.value = 0
25
+ return
26
+ }
27
+
28
+ const avail = availableWidth.value
29
+ const root = measure.value
30
+
31
+ if (!avail || !root) {
32
+ visibleCount.value = total
33
+ return
34
+ }
35
+
36
+ const chips = (Array.from(root.children) as HTMLElement[]).slice(0, total)
37
+
38
+ let used = 0
39
+ let fit = 0
40
+ for (let i = 0; i < chips.length; i++) {
41
+ const width = chips[i].offsetWidth + (i > 0 ? gap : 0)
42
+ if (used + width > avail) break
43
+ used += width
44
+ fit++
45
+ }
46
+
47
+ if (fit >= total) {
48
+ visibleCount.value = total
49
+ return
50
+ }
51
+
52
+ const badge = root.children[total] as HTMLElement | undefined
53
+ const badgeWidth = (badge?.offsetWidth ?? 0) + gap
54
+ while (fit > 1 && used + badgeWidth > avail) {
55
+ used -= chips[fit - 1].offsetWidth + gap
56
+ fit--
57
+ }
58
+
59
+ visibleCount.value = Math.max(1, fit)
60
+ }
61
+
62
+ let resizeObserver: ResizeObserver | null = null
63
+
64
+ onMounted(() => {
65
+ if (container.value) {
66
+ availableWidth.value = container.value.clientWidth
67
+ resizeObserver = new ResizeObserver((entries) => {
68
+ availableWidth.value = entries[0]?.contentRect.width ?? 0
69
+ })
70
+ resizeObserver.observe(container.value)
71
+ }
72
+ recompute()
73
+ })
74
+
75
+ onUnmounted(() => resizeObserver?.disconnect())
76
+
77
+ watch([availableWidth, count], () => nextTick(recompute))
78
+
79
+ const hiddenCount = computed(() => Math.max(0, count.value - visibleCount.value))
80
+
81
+ return { visibleCount, hiddenCount }
82
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "adata-ui",
3
3
  "type": "module",
4
- "version": "2.1.40-beta.1",
4
+ "version": "2.1.40-beta.2",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
7
7
  "dev": "nuxi dev .playground",