@dnax/ui 0.0.23 → 0.0.25

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.
@@ -16,6 +16,7 @@ export const qBottomSheetKey: InjectionKey<BottomSheetContext> = Symbol("q-botto
16
16
  import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from "vue"
17
17
  import { cn } from "../lib/utils"
18
18
  import { useOverlayBack } from "../lib/overlayBack"
19
+ import { markOverlayClose } from "../lib/closeOverlay"
19
20
 
20
21
  interface Props {
21
22
  /** Ouvert (v-model) */
@@ -33,6 +34,10 @@ interface Props {
33
34
  translucent?: boolean | number
34
35
  /** Seuil de drag (px) au-delà duquel on ferme */
35
36
  dragThreshold?: number
37
+ /** Animation d'ouverture : slide-up (défaut) | fade | zoom */
38
+ transition?: "slide-up" | "fade" | "zoom"
39
+ /** Durée des transitions d'entrée/sortie en ms */
40
+ transitionDuration?: number
36
41
  /** Style du panneau (variables de thème pour les contenus téléportés) */
37
42
  contentStyle?: Record<string, string>
38
43
  }
@@ -63,6 +68,11 @@ provide<BottomSheetContext>(qBottomSheetKey, {
63
68
  // « Retour » navigateur → ferme le bottom sheet au lieu de naviguer
64
69
  useOverlayBack(open, () => { open.value = false }, "QBottomSheet")
65
70
 
71
+ // v-close : marque l'overlay comme fermable (la directive remonte jusqu'ici)
72
+ const markOverlay = (el: unknown) => {
73
+ markOverlayClose(el as HTMLElement | null, () => { open.value = false })
74
+ }
75
+
66
76
  // Échap + verrouillage du scroll du body
67
77
  const onDocKeydown = (e: KeyboardEvent) => {
68
78
  if (e.key === "Escape" && open.value && !props.persistent) open.value = false
@@ -120,6 +130,21 @@ const radiusStyle = computed<Record<string, string> | undefined>(() => {
120
130
  return undefined
121
131
  })
122
132
 
133
+ // Nom de transition : q-bs (défaut slide-up) | q-bs-fade | q-bs-zoom
134
+ const transitionName = computed(() =>
135
+ props.transition ? `q-bs-${props.transition}` : "q-bs",
136
+ )
137
+
138
+ // Durée (ms) → variables consommées par les transitions/animations CSS
139
+ const transitionStyle = computed<Record<string, string> | undefined>(() =>
140
+ props.transitionDuration
141
+ ? {
142
+ "--q-bs-duration-enter": `${props.transitionDuration}ms`,
143
+ "--q-bs-duration-leave": `${props.transitionDuration}ms`,
144
+ }
145
+ : undefined,
146
+ )
147
+
123
148
  const panelClasses = computed(() =>
124
149
  cn(
125
150
  "q-bottom-sheet__panel",
@@ -143,9 +168,10 @@ const translucentStyle = computed<Record<string, string> | undefined>(() =>
143
168
  </span>
144
169
 
145
170
  <Teleport to="body">
146
- <Transition name="q-bs">
171
+ <Transition :name="transitionName" :duration="transitionDuration">
147
172
  <div
148
173
  v-if="open"
174
+ :ref="markOverlay"
149
175
  class="q-bottom-sheet__overlay"
150
176
  @click="!persistent && (open = false)"
151
177
  >
@@ -153,7 +179,7 @@ const translucentStyle = computed<Record<string, string> | undefined>(() =>
153
179
  ref="panelRef"
154
180
  class="q-bottom-sheet__panel"
155
181
  :class="panelClasses"
156
- :style="[panelStyle, radiusStyle, translucentStyle, props.contentStyle]"
182
+ :style="[panelStyle, radiusStyle, translucentStyle, transitionStyle, props.contentStyle]"
157
183
  role="dialog"
158
184
  aria-modal="true"
159
185
  @click.stop
@@ -42,6 +42,8 @@ const handleDismiss = (ctrl: BottomSheetController) => {
42
42
  :dark="sheet._opts.dark"
43
43
  :translucent="sheet._opts.translucent"
44
44
  :drag-threshold="sheet._opts.dragThreshold"
45
+ :transition="sheet._opts.transition"
46
+ :transition-duration="sheet._opts.transitionDuration"
45
47
  :content-style="sheetThemeStyle"
46
48
  @update:model-value="(v: boolean) => !v && handleDismiss(sheet)"
47
49
  >
@@ -17,6 +17,7 @@ import { cn } from "../lib/utils"
17
17
  import { radiusStyle, useRadius } from "../lib/useComponentProps"
18
18
  import type { RadiusProp } from "../lib/useComponentProps"
19
19
  import { useOverlayBack } from "../lib/overlayBack"
20
+ import { markOverlayClose } from "../lib/closeOverlay"
20
21
 
21
22
  type DialogPosition = "standard" | "top" | "right" | "bottom" | "left"
22
23
 
@@ -88,6 +89,11 @@ const onDocKeydown = (e: KeyboardEvent) => {
88
89
  // « Retour » navigateur → ferme le dialog au lieu de naviguer
89
90
  useOverlayBack(open, () => { open.value = false }, "QDialog")
90
91
 
92
+ // v-close : marque l'overlay comme fermable (la directive remonte jusqu'ici)
93
+ const markOverlay = (el: unknown) => {
94
+ markOverlayClose(el as HTMLElement | null, () => { open.value = false })
95
+ }
96
+
91
97
  // — Verrou du scroll de la page derrière le dialog —
92
98
  // Compteur module-level + suivi par instance : chaque dialog verrouille au plus
93
99
  // une fois et déverrouille au plus une fois → pile de dialogs ($q.dialog) sans
@@ -169,6 +175,7 @@ const durationStyle = computed<StyleValue>(() =>
169
175
  <Transition :name="transitionName" @after-enter="emit('show')" @after-leave="emit('hide')">
170
176
  <div
171
177
  v-if="open"
178
+ :ref="markOverlay"
172
179
  class="q-dialog__overlay"
173
180
  :class="overlayClasses"
174
181
  :style="durationStyle"
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  // QDialogHeader — header de la modale façon barre d'app (équivalent QHeader) :
3
3
  // le contenu est embarqué dans un <q-toolbar> (min-height 50px, padding 0 12px),
4
- // avec titre + description + bouton fermer (équivalent DialogHeader/Title/Close).
4
+ // avec titre + description + bouton fermer OPT-IN (show-close).
5
5
  // no-padding retire le padding horizontal du toolbar.
6
6
  import { inject } from "vue"
7
7
  import { Icon } from "@iconify/vue"
@@ -13,14 +13,14 @@ import QSpace from "./QSpace.vue"
13
13
  interface Props {
14
14
  title?: string
15
15
  description?: string
16
- /** Bouton fermer */
16
+ /** Affiche le bouton fermer (opt-in — sans la prop, aucun ×) */
17
17
  showClose?: boolean
18
18
  /** Supprime le padding du toolbar (contenu collé aux bords) */
19
19
  noPadding?: boolean
20
20
  }
21
21
 
22
22
  const props = withDefaults(defineProps<Props>(), {
23
- showClose: true,
23
+ showClose: false,
24
24
  noPadding: false,
25
25
  })
26
26
 
@@ -31,19 +31,6 @@ 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
-
47
34
  const rootEl = ref<HTMLElement | null>(null)
48
35
  // ref fonctionnel : sur un élément → l'élément ; sur q-virtual-scroll (composant) → .$el
49
36
  const setRoot = (el: unknown) => {
@@ -71,7 +58,7 @@ useFixedBarOffset(rootEl, "page")
71
58
  v-bind="$attrs"
72
59
  >
73
60
  <template #default="scope">
74
- <slot v-bind="forwardScope(scope)" />
61
+ <slot v-bind="scope" />
75
62
  </template>
76
63
  <template #before>
77
64
  <slot name="before" />
@@ -24,7 +24,7 @@ export const qTabsKey: InjectionKey<QTabContext> = Symbol("q-tabs")
24
24
  </script>
25
25
 
26
26
  <script setup lang="ts">
27
- import { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, shallowRef, watch } from "vue"
27
+ import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, provide, ref, shallowRef, watch } from "vue"
28
28
  import { cva } from "class-variance-authority"
29
29
  import { cn } from "../lib/utils"
30
30
  import { colorValue } from "../lib/colors"
@@ -117,6 +117,24 @@ const props = withDefaults(defineProps<Props>(), {
117
117
 
118
118
  const emit = defineEmits<{ "update:modelValue": [value: string | number | null] }>()
119
119
 
120
+ // — Mode non-contrôlé : sans v-model (pas de listener update:modelValue), QTabs
121
+ // maintient son propre état interne — indispensable pour QRouteTab (le tab
122
+ // actif suit la route sans v-model parent) et pour tout usage sans v-model.
123
+ const instance = getCurrentInstance()
124
+ const controlled = computed(
125
+ () => !!instance && "onUpdate:modelValue" in (instance.vnode.props ?? {}),
126
+ )
127
+ const internal = ref<string | number | null>(null)
128
+
129
+ const activeName = computed<string | number | null>(() =>
130
+ controlled.value ? (props.modelValue ?? null) : internal.value,
131
+ )
132
+
133
+ const setActive = (name: string | number) => {
134
+ if (controlled.value) emit("update:modelValue", name)
135
+ else internal.value = name
136
+ }
137
+
120
138
  // Position effective de l'indicateur : "top" | "bottom" (défaut) | "none" (masqué)
121
139
  const indicatorPos = computed<"top" | "bottom" | "none">(() => {
122
140
  const p = props.switchIndicatorPosition
@@ -145,7 +163,7 @@ const onCollapseEnd = (e: Event) => {
145
163
  measure()
146
164
  }
147
165
 
148
- watch([() => props.modelValue, tabRegs], measure)
166
+ watch([activeName, tabRegs], measure)
149
167
 
150
168
  onMounted(() => {
151
169
  measure()
@@ -158,8 +176,8 @@ onBeforeUnmount(() => {
158
176
  })
159
177
 
160
178
  const context: QTabContext = {
161
- activeName: computed(() => props.modelValue),
162
- setActive: (name) => emit("update:modelValue", name),
179
+ activeName,
180
+ setActive,
163
181
  register: (tab) => {
164
182
  tabRegs.value = [...tabRegs.value, tab]
165
183
  },
@@ -175,7 +193,7 @@ const context: QTabContext = {
175
193
  provide(qTabsKey, context)
176
194
 
177
195
  const activeTabEl = computed(
178
- () => tabRegs.value.find((t) => t.name === props.modelValue)?.el.value ?? null,
196
+ () => tabRegs.value.find((t) => t.name === activeName.value)?.el.value ?? null,
179
197
  )
180
198
 
181
199
  const indicatorStyle = computed<Record<string, string>>(() => {
package/index.ts CHANGED
@@ -152,3 +152,4 @@ export {
152
152
  installOverlayBackHandler,
153
153
  } from "./lib/overlayBack"
154
154
  export type { OverlayHandle } from "./lib/overlayBack"
155
+ export { markOverlayClose, closeParentOverlay, vClose } from "./lib/closeOverlay"
@@ -0,0 +1,48 @@
1
+ // v-close — directive : ferme l'overlay (dialog, bottom sheet, …) le plus
2
+ // proche au clic (équivalent Quasar v-close-popup).
3
+ // Les overlays s'enregistrent en posant el.__qClose sur leur élément racine
4
+ // (markOverlayClose) ; la directive remonte depuis l'élément cliqué.
5
+
6
+ interface QCloseMarker extends HTMLElement {
7
+ __qClose?: () => void
8
+ __qVClose?: () => void
9
+ }
10
+
11
+ /** Marque un élément comme « overlay fermable » (posé par QDialog, QBottomSheet…) */
12
+ export function markOverlayClose(el: HTMLElement | null, close: () => void) {
13
+ if (!el) return
14
+ ;(el as QCloseMarker).__qClose = close
15
+ }
16
+
17
+ /** Remonte jusqu'à l'overlay le plus proche portant __qClose */
18
+ function closestOverlay(el: Element): QCloseMarker | null {
19
+ let node: Element | null = el
20
+ while (node) {
21
+ const marker = node as QCloseMarker
22
+ if (typeof marker.__qClose === "function") return marker
23
+ node = node.parentElement
24
+ }
25
+ return null
26
+ }
27
+
28
+ /** Ferme l'overlay parent le plus proche de `el` */
29
+ export function closeParentOverlay(el: Element) {
30
+ closestOverlay(el)?.__qClose?.()
31
+ }
32
+
33
+ /** Directive : <q-btn v-close> → ferme le dialog/sheet parent au clic.
34
+ * v-close="false" désactive la fermeture. */
35
+ export const vClose = {
36
+ mounted(el: HTMLElement, binding: { value?: unknown }) {
37
+ const onClick = () => {
38
+ if (binding.value === false) return
39
+ closeParentOverlay(el)
40
+ }
41
+ el.addEventListener("click", onClick)
42
+ ;(el as QCloseMarker).__qVClose = onClick
43
+ },
44
+ unmounted(el: HTMLElement) {
45
+ const fn = (el as QCloseMarker).__qVClose
46
+ if (fn) el.removeEventListener("click", fn)
47
+ },
48
+ }
package/lib/q.ts CHANGED
@@ -128,6 +128,10 @@ export interface BottomSheetOptions {
128
128
  persistent?: boolean
129
129
  /** Seuil de drag (px) au-delà duquel on ferme */
130
130
  dragThreshold?: number
131
+ /** Animation d'ouverture : slide-up (défaut) | fade | zoom */
132
+ transition?: "slide-up" | "fade" | "zoom"
133
+ /** Durée des transitions d'entrée/sortie en ms */
134
+ transitionDuration?: number
131
135
  }
132
136
 
133
137
  export interface BottomSheetController {
package/module.ts CHANGED
@@ -85,5 +85,19 @@ export default defineNuxtPlugin((nuxtApp) => {
85
85
  `,
86
86
  })
87
87
  }
88
+
89
+ // Directive globale v-close : <q-btn v-close> ferme le dialog/sheet parent
90
+ addPluginTemplate({
91
+ filename: "dnax-ui-directives.mjs",
92
+ mode: "client",
93
+ getContents: () => `
94
+ import { defineNuxtPlugin } from "#imports"
95
+ import { vClose } from "@dnax/ui/runtime"
96
+
97
+ export default defineNuxtPlugin((nuxtApp) => {
98
+ nuxtApp.vueApp.directive("close", vClose)
99
+ })
100
+ `,
101
+ })
88
102
  },
89
103
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
@@ -50,6 +50,7 @@ export {
50
50
  installOverlayBackHandler,
51
51
  } from "./lib/overlayBack"
52
52
  export type { OverlayHandle } from "./lib/overlayBack"
53
+ export { markOverlayClose, closeParentOverlay, vClose } from "./lib/closeOverlay"
53
54
  `
54
55
 
55
56
  await Bun.write(indexFile, `${exports}\n${manualExports}`)
package/styles/main.css CHANGED
@@ -4868,25 +4868,37 @@ html.dark body {
4868
4868
  border-color: rgb(255 255 255 / 0.12);
4869
4869
  }
4870
4870
 
4871
- /* — transitions (slide vertical type vaul) — */
4871
+ /* — transitions (slide vertical type vaul) — durées configurables via
4872
+ transition-duration (vars --q-bs-duration-enter/leave) */
4872
4873
  .q-bs-enter-active,
4873
- .q-bs-leave-active {
4874
- transition: opacity 0.25s ease;
4874
+ .q-bs-leave-active,
4875
+ .q-bs-fade-enter-active,
4876
+ .q-bs-fade-leave-active,
4877
+ .q-bs-zoom-enter-active,
4878
+ .q-bs-zoom-leave-active {
4879
+ transition: opacity var(--q-bs-duration-enter, 0.25s) ease;
4880
+ }
4881
+ .q-bs-leave-active,
4882
+ .q-bs-fade-leave-active,
4883
+ .q-bs-zoom-leave-active {
4884
+ transition: opacity var(--q-bs-duration-leave, 0.2s) ease;
4875
4885
  }
4876
-
4877
4886
  .q-bs-enter-from,
4878
- .q-bs-leave-to {
4887
+ .q-bs-leave-to,
4888
+ .q-bs-fade-enter-from,
4889
+ .q-bs-fade-leave-to,
4890
+ .q-bs-zoom-enter-from,
4891
+ .q-bs-zoom-leave-to {
4879
4892
  opacity: 0;
4880
4893
  }
4881
4894
 
4895
+ /* slide-up (défaut) : le panneau glisse depuis le bas */
4882
4896
  .q-bs-enter-active .q-bottom-sheet__panel {
4883
- animation: q-bs-in 0.25s ease;
4897
+ animation: q-bs-in var(--q-bs-duration-enter, 0.25s) ease;
4884
4898
  }
4885
-
4886
4899
  .q-bs-leave-active .q-bottom-sheet__panel {
4887
- animation: q-bs-out 0.2s ease forwards;
4900
+ animation: q-bs-out var(--q-bs-duration-leave, 0.2s) ease forwards;
4888
4901
  }
4889
-
4890
4902
  @keyframes q-bs-in {
4891
4903
  from {
4892
4904
  transform: translateY(100%);
@@ -4895,13 +4907,36 @@ html.dark body {
4895
4907
  transform: none;
4896
4908
  }
4897
4909
  }
4898
-
4899
4910
  @keyframes q-bs-out {
4900
4911
  to {
4901
4912
  transform: translateY(100%);
4902
4913
  }
4903
4914
  }
4904
4915
 
4916
+ /* zoom : le panneau scale légèrement depuis le centre-bas */
4917
+ .q-bs-zoom-enter-active .q-bottom-sheet__panel {
4918
+ animation: q-bs-zoom-in var(--q-bs-duration-enter, 0.25s) ease;
4919
+ }
4920
+ .q-bs-zoom-leave-active .q-bottom-sheet__panel {
4921
+ animation: q-bs-zoom-out var(--q-bs-duration-leave, 0.2s) ease forwards;
4922
+ }
4923
+ @keyframes q-bs-zoom-in {
4924
+ from {
4925
+ opacity: 0;
4926
+ transform: scale(0.96) translateY(8px);
4927
+ }
4928
+ to {
4929
+ opacity: 1;
4930
+ transform: none;
4931
+ }
4932
+ }
4933
+ @keyframes q-bs-zoom-out {
4934
+ to {
4935
+ opacity: 0;
4936
+ transform: scale(0.96) translateY(8px);
4937
+ }
4938
+ }
4939
+
4905
4940
  /* ===== QBackHeader ===== */
4906
4941
  .q-back-header {
4907
4942
  position: relative;