@dnax/ui 0.0.23 → 0.0.26
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/components/QBottomSheet.vue +28 -2
- package/components/QBottomSheetHeader.vue +40 -4
- package/components/QBottomSheetProvider.vue +48 -23
- package/components/QDialog.vue +7 -0
- package/components/QDialogHeader.vue +49 -6
- package/components/QPage.vue +1 -14
- package/components/QTabs.vue +23 -5
- package/components/internal/QBottomSheetHost.vue +47 -0
- package/index.ts +3 -1
- package/lib/closeOverlay.ts +48 -0
- package/lib/q.ts +37 -1
- package/module.ts +14 -0
- package/package.json +1 -1
- package/scripts/generate-exports.ts +3 -1
- package/styles/main.css +45 -10
|
@@ -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="
|
|
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
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// QBottomSheetHeader — header du panneau façon barre d'app (équivalent QHeader) :
|
|
3
3
|
// le contenu est embarqué dans un <q-toolbar> (min-height 50px, padding 0 12px),
|
|
4
4
|
// avec titre + description + bouton fermer (équivalent DrawerHeader/DrawerTitle).
|
|
5
|
-
//
|
|
6
|
-
|
|
5
|
+
// Si un CONTENU custom est fourni (slot par défaut, sans title/description),
|
|
6
|
+
// il n'est PAS enveloppé dans un toolbar — l'utilisateur fournit son layout.
|
|
7
|
+
import { Comment, Fragment, Text, computed, inject, onUpdated, ref, useSlots } from "vue"
|
|
7
8
|
import { Icon } from "@iconify/vue"
|
|
8
9
|
import { icons } from "../lib/icons"
|
|
9
10
|
import { qBottomSheetKey } from "./QBottomSheet.vue"
|
|
@@ -17,11 +18,44 @@ interface Props {
|
|
|
17
18
|
noPadding?: boolean
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
withDefaults(defineProps<Props>(), {
|
|
21
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
22
|
noPadding: false,
|
|
22
23
|
})
|
|
23
24
|
|
|
24
25
|
const sheet = inject(qBottomSheetKey, null)
|
|
26
|
+
|
|
27
|
+
const slots = useSlots()
|
|
28
|
+
|
|
29
|
+
const refresh = ref(0)
|
|
30
|
+
onUpdated(() => {
|
|
31
|
+
refresh.value++
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function slotHasContent(nodes: unknown[]): boolean {
|
|
35
|
+
for (const n of nodes as any[]) {
|
|
36
|
+
if (!n) continue
|
|
37
|
+
if (n.type === Comment) continue
|
|
38
|
+
if (n.type === Text) {
|
|
39
|
+
if (String(n.children ?? "").trim() !== "") return true
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if (n.type === Fragment) {
|
|
43
|
+
if (slotHasContent(n.children ?? [])) return true
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const isStandard = computed(
|
|
52
|
+
() => props.title !== undefined || props.description !== undefined,
|
|
53
|
+
)
|
|
54
|
+
const hasCustomContent = computed(() => {
|
|
55
|
+
void refresh.value
|
|
56
|
+
return slotHasContent(slots.default?.() ?? [])
|
|
57
|
+
})
|
|
58
|
+
const useCustom = computed(() => !isStandard.value && hasCustomContent.value)
|
|
25
59
|
</script>
|
|
26
60
|
|
|
27
61
|
<template>
|
|
@@ -30,7 +64,9 @@ const sheet = inject(qBottomSheetKey, null)
|
|
|
30
64
|
:class="{ 'q-bottom-sheet__header--no-padding': noPadding }"
|
|
31
65
|
v-bind="$attrs"
|
|
32
66
|
>
|
|
33
|
-
<
|
|
67
|
+
<slot v-if="useCustom" />
|
|
68
|
+
|
|
69
|
+
<q-toolbar v-else>
|
|
34
70
|
<div class="q-bottom-sheet__header-text">
|
|
35
71
|
<h2 v-if="title" class="q-bottom-sheet__title">{{ title }}</h2>
|
|
36
72
|
<slot name="title" />
|
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
// QBottomSheetProvider — rend la pile de bottom sheets programmatiques ($q.bottomSheet).
|
|
3
3
|
// Intégré dans QConfigProvider (rendu automatiquement par le plus externe),
|
|
4
4
|
// utilisable aussi en autonome : <q-bottom-sheet-provider><slot /></q-bottom-sheet-provider>.
|
|
5
|
+
//
|
|
6
|
+
// Deux patterns (comme le plugin dialog) :
|
|
7
|
+
// - SANS title/description → le composant passé EST le sheet (racine
|
|
8
|
+
// <q-bottom-sheet v-model="open"> pilotée par useBottomSheetPluginComponent,
|
|
9
|
+
// open déjà true) — rendu tel quel via QBottomSheetHost.
|
|
10
|
+
// - AVEC title/description → le provider enveloppe dans un q-bottom-sheet +
|
|
11
|
+
// header ; le composant fournit le corps et émet @ok/@cancel/@dismiss.
|
|
5
12
|
import { computed, inject } from "vue"
|
|
6
13
|
import { bottomSheetStack, closeBottomSheet } from "../lib/q"
|
|
7
14
|
import type { BottomSheetController } from "../lib/q"
|
|
8
15
|
import { qConfigKey } from "../lib/config"
|
|
9
16
|
import { themeVarsStyle } from "../lib/themeVars"
|
|
17
|
+
import QBottomSheetHost from "./internal/QBottomSheetHost.vue"
|
|
10
18
|
|
|
11
19
|
const sheets = bottomSheetStack
|
|
12
20
|
|
|
@@ -31,32 +39,49 @@ const handleDismiss = (ctrl: BottomSheetController) => {
|
|
|
31
39
|
|
|
32
40
|
<template>
|
|
33
41
|
<slot />
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
:dark="sheet._opts.dark"
|
|
43
|
-
:translucent="sheet._opts.translucent"
|
|
44
|
-
:drag-threshold="sheet._opts.dragThreshold"
|
|
45
|
-
:content-style="sheetThemeStyle"
|
|
46
|
-
@update:model-value="(v: boolean) => !v && handleDismiss(sheet)"
|
|
47
|
-
>
|
|
48
|
-
<q-bottom-sheet-header
|
|
49
|
-
v-if="sheet._opts.title || sheet._opts.description"
|
|
50
|
-
:title="sheet._opts.title"
|
|
51
|
-
:description="sheet._opts.description"
|
|
52
|
-
/>
|
|
53
|
-
<component
|
|
54
|
-
:is="sheet._opts.component"
|
|
55
|
-
v-bind="sheet._opts.componentProps"
|
|
42
|
+
<template v-for="(sheet, idx) in sheets" :key="idx">
|
|
43
|
+
<!-- Pattern auto : le composant est lui-même un <q-bottom-sheet> -->
|
|
44
|
+
<q-bottom-sheet-host
|
|
45
|
+
v-if="!sheet._opts.title && !sheet._opts.description"
|
|
46
|
+
:ctrl="sheet"
|
|
47
|
+
:on-ok="(data?: unknown) => handleOk(sheet, data)"
|
|
48
|
+
:on-cancel="() => handleCancel(sheet)"
|
|
49
|
+
:on-hide="() => handleDismiss(sheet)"
|
|
56
50
|
@ok="(data?: unknown) => handleOk(sheet, data)"
|
|
57
51
|
@cancel="handleCancel(sheet)"
|
|
58
52
|
@dismiss="handleDismiss(sheet)"
|
|
59
53
|
@close="handleDismiss(sheet)"
|
|
60
54
|
/>
|
|
61
|
-
|
|
55
|
+
<!-- Pattern enveloppe : le provider rend le panneau + header, le composant
|
|
56
|
+
fournit le corps (events legacy ok/cancel/dismiss/close) -->
|
|
57
|
+
<q-bottom-sheet
|
|
58
|
+
v-else
|
|
59
|
+
:model-value="true"
|
|
60
|
+
:persistent="sheet._opts.persistent"
|
|
61
|
+
:width="sheet._opts.width"
|
|
62
|
+
:height="sheet._opts.height"
|
|
63
|
+
:rounded="sheet._opts.rounded"
|
|
64
|
+
:dark="sheet._opts.dark"
|
|
65
|
+
:translucent="sheet._opts.translucent"
|
|
66
|
+
:drag-threshold="sheet._opts.dragThreshold"
|
|
67
|
+
:transition="sheet._opts.transition"
|
|
68
|
+
:transition-duration="sheet._opts.transitionDuration"
|
|
69
|
+
:content-style="sheetThemeStyle"
|
|
70
|
+
@update:model-value="(v: boolean) => !v && handleDismiss(sheet)"
|
|
71
|
+
>
|
|
72
|
+
<q-bottom-sheet-header
|
|
73
|
+
v-if="sheet._opts.title || sheet._opts.description"
|
|
74
|
+
:title="sheet._opts.title"
|
|
75
|
+
:description="sheet._opts.description"
|
|
76
|
+
/>
|
|
77
|
+
<component
|
|
78
|
+
:is="sheet._opts.component"
|
|
79
|
+
v-bind="sheet._opts.componentProps"
|
|
80
|
+
@ok="(data?: unknown) => handleOk(sheet, data)"
|
|
81
|
+
@cancel="handleCancel(sheet)"
|
|
82
|
+
@dismiss="handleDismiss(sheet)"
|
|
83
|
+
@close="handleDismiss(sheet)"
|
|
84
|
+
/>
|
|
85
|
+
</q-bottom-sheet>
|
|
86
|
+
</template>
|
|
62
87
|
</template>
|
package/components/QDialog.vue
CHANGED
|
@@ -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,9 +1,11 @@
|
|
|
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 (
|
|
5
|
-
//
|
|
6
|
-
|
|
4
|
+
// avec titre + description + bouton fermer OPT-IN (show-close).
|
|
5
|
+
// Si un CONTENU custom est fourni (slot par défaut, sans title/description/
|
|
6
|
+
// show-close), il n'est PAS enveloppé dans un toolbar — l'utilisateur fournit
|
|
7
|
+
// son propre layout (ex. <q-toolbar>) directement dans la barre.
|
|
8
|
+
import { Comment, Fragment, Text, computed, inject, onUpdated, ref, useSlots } from "vue"
|
|
7
9
|
import { Icon } from "@iconify/vue"
|
|
8
10
|
import { icons } from "../lib/icons"
|
|
9
11
|
import { qDialogKey } from "./QDialog.vue"
|
|
@@ -13,18 +15,55 @@ import QSpace from "./QSpace.vue"
|
|
|
13
15
|
interface Props {
|
|
14
16
|
title?: string
|
|
15
17
|
description?: string
|
|
16
|
-
/**
|
|
18
|
+
/** Affiche le bouton fermer (opt-in — sans la prop, aucun ×) */
|
|
17
19
|
showClose?: boolean
|
|
18
20
|
/** Supprime le padding du toolbar (contenu collé aux bords) */
|
|
19
21
|
noPadding?: boolean
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
const props = withDefaults(defineProps<Props>(), {
|
|
23
|
-
showClose:
|
|
25
|
+
showClose: false,
|
|
24
26
|
noPadding: false,
|
|
25
27
|
})
|
|
26
28
|
|
|
27
29
|
const dialog = inject(qDialogKey, null)
|
|
30
|
+
|
|
31
|
+
const slots = useSlots()
|
|
32
|
+
|
|
33
|
+
// Force le recalcul de la détection à chaque update (le contenu du slot peut
|
|
34
|
+
// changer dynamiquement côté parent)
|
|
35
|
+
const refresh = ref(0)
|
|
36
|
+
onUpdated(() => {
|
|
37
|
+
refresh.value++
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
function slotHasContent(nodes: unknown[]): boolean {
|
|
41
|
+
for (const n of nodes as any[]) {
|
|
42
|
+
if (!n) continue
|
|
43
|
+
if (n.type === Comment) continue
|
|
44
|
+
if (n.type === Text) {
|
|
45
|
+
if (String(n.children ?? "").trim() !== "") return true
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
if (n.type === Fragment) {
|
|
49
|
+
if (slotHasContent(n.children ?? [])) return true
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Mode « custom » : pas de title/description/close et un contenu de slot → on
|
|
58
|
+
// rend le slot directement (sans toolbar d'enveloppe)
|
|
59
|
+
const isStandard = computed(
|
|
60
|
+
() => props.title !== undefined || props.description !== undefined || props.showClose,
|
|
61
|
+
)
|
|
62
|
+
const hasCustomContent = computed(() => {
|
|
63
|
+
void refresh.value
|
|
64
|
+
return slotHasContent(slots.default?.() ?? [])
|
|
65
|
+
})
|
|
66
|
+
const useCustom = computed(() => !isStandard.value && hasCustomContent.value)
|
|
28
67
|
</script>
|
|
29
68
|
|
|
30
69
|
<template>
|
|
@@ -33,7 +72,11 @@ const dialog = inject(qDialogKey, null)
|
|
|
33
72
|
:class="{ 'q-dialog__header--no-padding': noPadding }"
|
|
34
73
|
v-bind="$attrs"
|
|
35
74
|
>
|
|
36
|
-
|
|
75
|
+
<!-- Contenu custom : l'utilisateur fournit son propre layout (q-toolbar…) -->
|
|
76
|
+
<slot v-if="useCustom" />
|
|
77
|
+
|
|
78
|
+
<!-- Mode standard : toolbar embarqué (title / description / close) -->
|
|
79
|
+
<q-toolbar v-else>
|
|
37
80
|
<div class="q-dialog__header-text">
|
|
38
81
|
<h2 v-if="title" class="q-dialog__title">{{ title }}</h2>
|
|
39
82
|
<slot name="title" />
|
package/components/QPage.vue
CHANGED
|
@@ -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="
|
|
61
|
+
<slot v-bind="scope" />
|
|
75
62
|
</template>
|
|
76
63
|
<template #before>
|
|
77
64
|
<slot name="before" />
|
package/components/QTabs.vue
CHANGED
|
@@ -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([
|
|
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
|
|
162
|
-
setActive
|
|
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 ===
|
|
196
|
+
() => tabRegs.value.find((t) => t.name === activeName.value)?.el.value ?? null,
|
|
179
197
|
)
|
|
180
198
|
|
|
181
199
|
const indicatorStyle = computed<Record<string, string>>(() => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QBottomSheetHost — host interne du $q.bottomSheet (pattern Quasar, comme
|
|
3
|
+
// QDialogHost) : rend le composant passé tel quel et lui fournit le contexte
|
|
4
|
+
// plugin. Le composant DOIT contenir un <q-bottom-sheet v-model="open"
|
|
5
|
+
// @hide="onDialogHide"> comme racine, piloté via
|
|
6
|
+
// useBottomSheetPluginComponent() — open est déjà true (pas de v-model à
|
|
7
|
+
// mettre). À la fermeture (@hide) ou via onDialogOK / onDialogCancel, les
|
|
8
|
+
// résolveurs sont appelés et l'entrée retirée.
|
|
9
|
+
import { provide, ref } from "vue"
|
|
10
|
+
import { qBottomSheetPluginKey } from "../../lib/q"
|
|
11
|
+
import type { BottomSheetController } from "../../lib/q"
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
ctrl: BottomSheetController
|
|
15
|
+
onOk: (data?: unknown) => void
|
|
16
|
+
onCancel: () => void
|
|
17
|
+
onHide: () => void
|
|
18
|
+
}>()
|
|
19
|
+
|
|
20
|
+
const open = ref(true)
|
|
21
|
+
|
|
22
|
+
provide(qBottomSheetPluginKey, {
|
|
23
|
+
open,
|
|
24
|
+
onDialogOK: (data?: unknown) => props.onOk(data),
|
|
25
|
+
onDialogCancel: () => props.onCancel(),
|
|
26
|
+
onDialogHide: () => props.onHide(),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Le composant émet aussi les events legacy (ok/cancel/dismiss/close)
|
|
30
|
+
const emit = defineEmits<{
|
|
31
|
+
ok: [data?: unknown]
|
|
32
|
+
cancel: []
|
|
33
|
+
dismiss: []
|
|
34
|
+
close: []
|
|
35
|
+
}>()
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<component
|
|
40
|
+
:is="props.ctrl._opts.component"
|
|
41
|
+
v-bind="props.ctrl._opts.componentProps"
|
|
42
|
+
@ok="emit('ok', $event)"
|
|
43
|
+
@cancel="emit('cancel')"
|
|
44
|
+
@dismiss="emit('dismiss')"
|
|
45
|
+
@close="emit('close')"
|
|
46
|
+
/>
|
|
47
|
+
</template>
|
package/index.ts
CHANGED
|
@@ -128,12 +128,13 @@ export { default as QUploader } from "./components/QUploader.vue"
|
|
|
128
128
|
export { default as QVideo } from "./components/QVideo.vue"
|
|
129
129
|
export { default as QVirtualScroll } from "./components/QVirtualScroll.vue"
|
|
130
130
|
|
|
131
|
-
export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet, useDialogPluginComponent } from "./lib/q"
|
|
131
|
+
export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet, useDialogPluginComponent, useBottomSheetPluginComponent } from "./lib/q"
|
|
132
132
|
export type {
|
|
133
133
|
DialogOptions,
|
|
134
134
|
DialogController,
|
|
135
135
|
BottomSheetOptions,
|
|
136
136
|
BottomSheetController,
|
|
137
|
+
BottomSheetPluginContext,
|
|
137
138
|
NotifyOptions,
|
|
138
139
|
NotifyController,
|
|
139
140
|
} from "./lib/q"
|
|
@@ -152,3 +153,4 @@ export {
|
|
|
152
153
|
installOverlayBackHandler,
|
|
153
154
|
} from "./lib/overlayBack"
|
|
154
155
|
export type { OverlayHandle } from "./lib/overlayBack"
|
|
156
|
+
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
|
@@ -112,7 +112,7 @@ export interface BottomSheetOptions {
|
|
|
112
112
|
component: Component | string
|
|
113
113
|
/** Props passées au composant */
|
|
114
114
|
componentProps?: Record<string, any>
|
|
115
|
-
/** Titre affiché par QBottomSheetHeader (optionnel) */
|
|
115
|
+
/** Titre affiché par QBottomSheetHeader (optionnel — sinon le composant rend son propre header) */
|
|
116
116
|
title?: string
|
|
117
117
|
description?: string
|
|
118
118
|
/** Largeur max du panneau (défaut : 640px) */
|
|
@@ -128,8 +128,28 @@ 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
|
|
|
137
|
+
/**
|
|
138
|
+
* Contexte plugin bottom sheet — fourni par QBottomSheetHost quand le composant
|
|
139
|
+
* passé à $q.bottomSheet.open() est lui-même un <q-bottom-sheet> (pattern
|
|
140
|
+
* Quasar, identique à $q.dialog) : open est déjà true, pas besoin de v-model.
|
|
141
|
+
*/
|
|
142
|
+
export interface BottomSheetPluginContext {
|
|
143
|
+
/** Ref d'ouverture (v-model du <q-bottom-sheet> du composant, déjà true) */
|
|
144
|
+
open: Ref<boolean>
|
|
145
|
+
onDialogOK: (data?: unknown) => void
|
|
146
|
+
onDialogCancel: () => void
|
|
147
|
+
onDialogHide: () => void
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const qBottomSheetPluginKey: InjectionKey<BottomSheetPluginContext> =
|
|
151
|
+
Symbol("q-bottom-sheet-plugin")
|
|
152
|
+
|
|
133
153
|
export interface BottomSheetController {
|
|
134
154
|
onOK: (cb: (data?: any) => void | Promise<void>) => BottomSheetController
|
|
135
155
|
onCancel: (cb: () => void | Promise<void>) => BottomSheetController
|
|
@@ -142,6 +162,22 @@ export interface BottomSheetController {
|
|
|
142
162
|
_opts: BottomSheetOptions
|
|
143
163
|
}
|
|
144
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Composable Quasar-style pour les composants passés à $q.bottomSheet.open() :
|
|
167
|
+
* le composant rend un <q-bottom-sheet v-model="open" @hide="onDialogHide">
|
|
168
|
+
* comme racine. open est déjà true (le panneau s'affiche à l'ouverture), et
|
|
169
|
+
* onDialogOK / onDialogCancel résolvent les callbacks du controller.
|
|
170
|
+
*/
|
|
171
|
+
export function useBottomSheetPluginComponent() {
|
|
172
|
+
const ctx = inject(qBottomSheetPluginKey, null)
|
|
173
|
+
return {
|
|
174
|
+
open: ctx?.open ?? ref(false),
|
|
175
|
+
onDialogHide: () => ctx?.onDialogHide(),
|
|
176
|
+
onDialogOK: (data?: unknown) => ctx?.onDialogOK(data),
|
|
177
|
+
onDialogCancel: () => ctx?.onDialogCancel(),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
145
181
|
// — Pile de bottom sheets (singleton module-level, client-side) —
|
|
146
182
|
const bottomSheets: Ref<BottomSheetController[]> = ref([])
|
|
147
183
|
|
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
|
@@ -26,12 +26,13 @@ const exports = files
|
|
|
26
26
|
|
|
27
27
|
// Exports manuels (composables / helpers) — conservés à chaque régénération
|
|
28
28
|
const manualExports = `
|
|
29
|
-
export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet, useDialogPluginComponent } from "./lib/q"
|
|
29
|
+
export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet, useDialogPluginComponent, useBottomSheetPluginComponent } from "./lib/q"
|
|
30
30
|
export type {
|
|
31
31
|
DialogOptions,
|
|
32
32
|
DialogController,
|
|
33
33
|
BottomSheetOptions,
|
|
34
34
|
BottomSheetController,
|
|
35
|
+
BottomSheetPluginContext,
|
|
35
36
|
NotifyOptions,
|
|
36
37
|
NotifyController,
|
|
37
38
|
} from "./lib/q"
|
|
@@ -50,6 +51,7 @@ export {
|
|
|
50
51
|
installOverlayBackHandler,
|
|
51
52
|
} from "./lib/overlayBack"
|
|
52
53
|
export type { OverlayHandle } from "./lib/overlayBack"
|
|
54
|
+
export { markOverlayClose, closeParentOverlay, vClose } from "./lib/closeOverlay"
|
|
53
55
|
`
|
|
54
56
|
|
|
55
57
|
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
|
-
|
|
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;
|