@dnax/ui 0.0.21 → 0.0.23

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.
@@ -162,8 +162,11 @@ const sheetTitle = computed(
162
162
 
163
163
  // — Swipe down pour fermer (mode sheet, opt-in swipe-to-close) —
164
164
  const dragging = ref(false)
165
+ const swipeClosing = ref(false)
165
166
  let dragStartY = 0
166
167
  let dragDy = 0
168
+ let swipeCloseTimer: number | undefined
169
+
167
170
  const canSwipe = computed(() => props.swipeToClose && props.mode === "sheet")
168
171
 
169
172
  const onDragStart = (e: PointerEvent) => {
@@ -183,16 +186,39 @@ const onDragMove = (e: PointerEvent) => {
183
186
  const onDragEnd = () => {
184
187
  if (!dragging.value) return
185
188
  dragging.value = false
186
- if (dragDy > 80) closePopup()
187
- else if (sheetRef.value) sheetRef.value.style.transform = ""
189
+ if (dragDy > 80) {
190
+ // Au-delà du seuil : on CONTINUE le glissement jusqu'à sortie complète
191
+ // (translateY(100%)) puis on ferme à la fin de la transition — au lieu de
192
+ // laisser le panneau revenir en place avant le fade (saut visuel).
193
+ swipeClosing.value = true
194
+ if (sheetRef.value) sheetRef.value.style.transform = "translateY(100%)"
195
+ window.clearTimeout(swipeCloseTimer)
196
+ swipeCloseTimer = window.setTimeout(() => {
197
+ swipeClosing.value = false
198
+ closePopup()
199
+ }, 400) // fallback si la transition ne se déclenche pas (déjà à 100%, reduced-motion)
200
+ }
201
+ else if (sheetRef.value) {
202
+ // Sous le seuil : retour en douceur à la position initiale
203
+ sheetRef.value.style.transform = ""
204
+ }
188
205
  dragDy = 0
189
206
  }
190
207
 
208
+ // Ferme dès que la transition de glissement se termine (plus précis que le timeout)
209
+ const onSheetTransitionEnd = (e: TransitionEvent) => {
210
+ if (e.propertyName !== "transform" || !swipeClosing.value) return
211
+ window.clearTimeout(swipeCloseTimer)
212
+ swipeClosing.value = false
213
+ closePopup()
214
+ }
215
+
191
216
  const sheetClasses = computed(() => [
192
217
  `q-autocomplete__sheet--${props.mode}`,
193
218
  modeOptions.value?.class,
194
219
  dragging.value && "q-autocomplete__sheet--dragging",
195
220
  canSwipe.value && "q-autocomplete__sheet--swipeable",
221
+ swipeClosing.value && "q-autocomplete__sheet--swipe-closing",
196
222
  ])
197
223
 
198
224
  // — Valeurs des options —
@@ -371,6 +397,7 @@ onMounted(() => {
371
397
  })
372
398
  onBeforeUnmount(() => {
373
399
  if (typeof document !== "undefined") document.removeEventListener("mousedown", onDocMousedown)
400
+ window.clearTimeout(swipeCloseTimer)
374
401
  })
375
402
 
376
403
  // Verrou du scroll de la page derrière le panneau (modal/sheet)
@@ -480,6 +507,7 @@ watch(open, (v) => {
480
507
  role="dialog"
481
508
  aria-modal="true"
482
509
  :aria-label="sheetTitle"
510
+ @transitionend="onSheetTransitionEnd"
483
511
  >
484
512
  <div
485
513
  class="q-autocomplete__sheet-header"
@@ -31,6 +31,19 @@ interface Props {
31
31
 
32
32
  defineProps<Props>()
33
33
 
34
+ // Slots typés (mode virtual) : même contrat que QVirtualScroll
35
+ // (item, index, ref pour la mesure ; before/after pour les zones sticky)
36
+ defineSlots<{
37
+ default?: (props: { item: any; index: number; ref: (el: unknown) => void }) => any
38
+ before?: () => any
39
+ after?: () => any
40
+ }>()
41
+
42
+ // L'inférence du scope ne traverse pas la branche v-if → cast explicite
43
+ // (le scope reçu EST le scope du QVirtualScroll)
44
+ type VirtualScope = { item: any; index: number; ref: (el: unknown) => void }
45
+ const forwardScope = (scope: unknown) => scope as VirtualScope
46
+
34
47
  const rootEl = ref<HTMLElement | null>(null)
35
48
  // ref fonctionnel : sur un élément → l'élément ; sur q-virtual-scroll (composant) → .$el
36
49
  const setRoot = (el: unknown) => {
@@ -58,7 +71,7 @@ useFixedBarOffset(rootEl, "page")
58
71
  v-bind="$attrs"
59
72
  >
60
73
  <template #default="scope">
61
- <slot v-bind="scope" />
74
+ <slot v-bind="forwardScope(scope)" />
62
75
  </template>
63
76
  <template #before>
64
77
  <slot name="before" />
@@ -0,0 +1,106 @@
1
+ <script setup lang="ts">
2
+ // QRouteTab — QTab lié à la route (pattern Quasar QRouteTab) :
3
+ // <q-route-tab to="/docs/components/tabs" label="Tabs" exact />
4
+ // Le tab devient actif automatiquement quand la route courante matche `to`
5
+ // (le v-model du QTabs suit), et cliquer navigue vers la destination.
6
+ import { computed, inject, watch } from "vue"
7
+ import type { RouteLocationRaw } from "vue-router"
8
+ import { useRoute, useRouter } from "vue-router"
9
+ import QTab from "./QTab.vue"
10
+ import { qTabsKey } from "./QTabs.vue"
11
+
12
+ let seq = 0
13
+
14
+ interface Props {
15
+ /** Destination de route (même API que RouterLink `to`) */
16
+ to: RouteLocationRaw
17
+ /** Match exact : path (et hash) identiques — sinon préfixe de chemin */
18
+ exact?: boolean
19
+ /** remplace l'entrée d'historique au clic (router.replace) */
20
+ replace?: boolean
21
+ // — API QTab (transmise) —
22
+ name?: string | number
23
+ icon?: string
24
+ label?: string | number
25
+ alert?: boolean | string
26
+ alertIcon?: string
27
+ count?: number
28
+ countColor?: string
29
+ countMax?: number
30
+ noCaps?: boolean
31
+ tabindex?: string | number
32
+ disable?: boolean
33
+ contentClass?: string
34
+ }
35
+
36
+ const props = withDefaults(defineProps<Props>(), {
37
+ exact: false,
38
+ replace: false,
39
+ alert: false,
40
+ noCaps: false,
41
+ disable: false,
42
+ countMax: 99,
43
+ })
44
+
45
+ const tabs = inject(qTabsKey, null)
46
+
47
+ const resolvedName = props.name ?? `rt_${seq++}`
48
+
49
+ // Router optionnel : sans app router, QRouteTab se comporte comme un QTab
50
+ let router: ReturnType<typeof useRouter> | undefined
51
+ let route: ReturnType<typeof useRoute> | undefined
52
+ try {
53
+ router = useRouter()
54
+ route = useRoute()
55
+ }
56
+ catch {
57
+ router = undefined
58
+ route = undefined
59
+ }
60
+
61
+ const matches = computed(() => {
62
+ if (!router || !route || !props.to) return false
63
+ const resolved = router.resolve(props.to)
64
+ if (props.exact) {
65
+ return resolved.path === route.path && resolved.hash === route.hash
66
+ }
67
+ // non-exact : la route courante commence par le path de `to` (préfixe de segment)
68
+ const prefix = resolved.path.endsWith("/") ? resolved.path : resolved.path + "/"
69
+ return route.path === resolved.path || route.path.startsWith(prefix)
70
+ })
71
+
72
+ // La route change → le tab qui matche devient actif (sync du v-model QTabs)
73
+ watch(
74
+ () => route?.fullPath,
75
+ () => {
76
+ if (matches.value) tabs?.setActive(resolvedName)
77
+ },
78
+ { immediate: true },
79
+ )
80
+
81
+ const onClick = () => {
82
+ if (props.disable || !router) return
83
+ if (props.replace) router.replace(props.to)
84
+ else router.push(props.to)
85
+ }
86
+ </script>
87
+
88
+ <template>
89
+ <q-tab
90
+ :name="resolvedName"
91
+ :label="label"
92
+ :icon="icon"
93
+ :alert="alert"
94
+ :alert-icon="alertIcon"
95
+ :count="count"
96
+ :count-color="countColor"
97
+ :count-max="countMax"
98
+ :no-caps="noCaps"
99
+ :tabindex="tabindex"
100
+ :disable="disable"
101
+ :content-class="contentClass"
102
+ @click="onClick"
103
+ >
104
+ <slot />
105
+ </q-tab>
106
+ </template>
@@ -35,7 +35,7 @@ const props = withDefaults(defineProps<Props>(), {
35
35
  })
36
36
 
37
37
  defineSlots<{
38
- default?: (props: { item: any; index: number; ref: (el: Element | null) => void }) => any
38
+ default?: (props: { item: any; index: number; ref: (el: unknown) => void }) => any
39
39
  before?: () => any
40
40
  after?: () => any
41
41
  }>()
@@ -49,7 +49,7 @@ const scrollTop = ref(0)
49
49
  // — Hauteurs mesurées par index (celles non mesurées → taille estimée) —
50
50
  const itemSizes = new Map<number, number>()
51
51
  const observers = new Map<number, ResizeObserver>()
52
- const refFns = new Map<number, (el: Element | null) => void>()
52
+ const refFns = new Map<number, (el: unknown) => void>()
53
53
 
54
54
  // Préfixe cumulé des offsets (reconstruit quand les tailles changent, pas au scroll)
55
55
  let prefix: number[] = [0]
@@ -103,7 +103,7 @@ const observeItem = (index: number, el: Element | null) => {
103
103
  const itemRef = (index: number) => {
104
104
  let fn = refFns.get(index)
105
105
  if (!fn) {
106
- fn = (el) => observeItem(index, el)
106
+ fn = (el: unknown) => observeItem(index, el as Element | null)
107
107
  refFns.set(index, fn)
108
108
  }
109
109
  return fn
package/index.ts CHANGED
@@ -91,6 +91,7 @@ export { default as QRadio } from "./components/QRadio.vue"
91
91
  export { default as QRating } from "./components/QRating.vue"
92
92
  export { default as QReorder } from "./components/QReorder.vue"
93
93
  export { default as QRollingText } from "./components/QRollingText.vue"
94
+ export { default as QRouteTab } from "./components/QRouteTab.vue"
94
95
  export { default as QRow } from "./components/QRow.vue"
95
96
  export { default as QSafeArea } from "./components/QSafeArea.vue"
96
97
  export { default as QScrollArea } from "./components/QScrollArea.vue"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
package/styles/main.css CHANGED
@@ -3474,6 +3474,12 @@ html.dark body {
3474
3474
  transition: none;
3475
3475
  }
3476
3476
 
3477
+ /* fermeture par swipe : le glissement (translateY 100%) gère la sortie —
3478
+ l'animation leave (pop-out 24px) serait un saut depuis la position glissée */
3479
+ .q-autocomplete-modal-leave-active .q-autocomplete__sheet--swipe-closing {
3480
+ animation: none;
3481
+ }
3482
+
3477
3483
  /* le header est la zone qui attrape le swipe (cursor grab) — seulement si
3478
3484
  swipe-to-close est actif (classe --swipeable) */
3479
3485
  .q-autocomplete__sheet--swipeable .q-autocomplete__sheet-header {