@dnax/ui 0.0.10 → 0.0.12
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/QBottomSheetFooter.vue +7 -2
- package/components/QBottomSheetHeader.vue +23 -16
- package/components/QContainer.vue +47 -1
- package/components/QDialog.vue +2 -2
- package/components/QDialogContent.vue +20 -0
- package/components/QDialogFooter.vue +7 -2
- package/components/QDialogHeader.vue +24 -17
- package/components/QDialogProvider.vue +20 -26
- package/components/QFooter.vue +7 -0
- package/components/QPage.vue +3 -2
- package/components/QTab.vue +4 -2
- package/components/QTabs.vue +39 -0
- package/components/internal/QDialogHost.vue +45 -0
- package/index.ts +2 -1
- package/lib/fixedLayout.ts +62 -16
- package/lib/platform.ts +185 -19
- package/lib/q.ts +35 -2
- package/package.json +1 -1
- package/scripts/generate-exports.ts +1 -1
- package/styles/main.css +273 -32
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// QBottomSheetFooter —
|
|
2
|
+
// QBottomSheetFooter — pied du panneau façon barre d'app (équivalent QFooter) :
|
|
3
|
+
// les actions sont embarquées dans un <q-toolbar> (min-height 50px, padding
|
|
4
|
+
// 0 12px) et alignées à droite (justify-content: flex-end — styles/main.css).
|
|
5
|
+
import QToolbar from "./QToolbar.vue"
|
|
3
6
|
</script>
|
|
4
7
|
|
|
5
8
|
<template>
|
|
6
9
|
<div class="q-bottom-sheet__footer">
|
|
7
|
-
<
|
|
10
|
+
<q-toolbar>
|
|
11
|
+
<slot />
|
|
12
|
+
</q-toolbar>
|
|
8
13
|
</div>
|
|
9
14
|
</template>
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// QBottomSheetHeader — header du panneau
|
|
2
|
+
// QBottomSheetHeader — header du panneau façon barre d'app (équivalent QHeader) :
|
|
3
|
+
// le contenu est embarqué dans un <q-toolbar> (min-height 50px, padding 0 12px),
|
|
4
|
+
// avec titre + description + bouton fermer (équivalent DrawerHeader/DrawerTitle).
|
|
3
5
|
import { inject } from "vue"
|
|
4
6
|
import { Icon } from "@iconify/vue"
|
|
5
7
|
import { icons } from "../lib/icons"
|
|
6
8
|
import { qBottomSheetKey } from "./QBottomSheet.vue"
|
|
9
|
+
import QToolbar from "./QToolbar.vue"
|
|
10
|
+
import QSpace from "./QSpace.vue"
|
|
7
11
|
|
|
8
12
|
interface Props {
|
|
9
13
|
title?: string
|
|
@@ -16,20 +20,23 @@ const sheet = inject(qBottomSheetKey, null)
|
|
|
16
20
|
|
|
17
21
|
<template>
|
|
18
22
|
<div class="q-bottom-sheet__header">
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
<q-toolbar>
|
|
24
|
+
<div class="q-bottom-sheet__header-text">
|
|
25
|
+
<h2 v-if="title" class="q-bottom-sheet__title">{{ title }}</h2>
|
|
26
|
+
<slot name="title" />
|
|
27
|
+
<p v-if="description" class="q-bottom-sheet__description">{{ description }}</p>
|
|
28
|
+
<slot name="description" />
|
|
29
|
+
</div>
|
|
30
|
+
<q-space />
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
class="q-bottom-sheet__close"
|
|
34
|
+
aria-label="Fermer"
|
|
35
|
+
@click="sheet?.setOpen(false)"
|
|
36
|
+
>
|
|
37
|
+
<Icon :icon="icons.x" aria-hidden="true" />
|
|
38
|
+
</button>
|
|
39
|
+
<slot />
|
|
40
|
+
</q-toolbar>
|
|
34
41
|
</div>
|
|
35
42
|
</template>
|
|
@@ -38,6 +38,19 @@ interface Props {
|
|
|
38
38
|
waveColor?: string
|
|
39
39
|
/** Couleur de la seconde vague (background-effect="wave", défaut violet) */
|
|
40
40
|
waveColor2?: string
|
|
41
|
+
/** Style glassmorphism : fond translucide + flou d'arrière-plan + bordure claire */
|
|
42
|
+
glass?: boolean
|
|
43
|
+
/** URL d'une image de fond — rendue en couche derrière le contenu (cover) */
|
|
44
|
+
backgroundImage?: string
|
|
45
|
+
/** Taille de l'image : cover | contain | fill | none | scale-down, ou valeur
|
|
46
|
+
* CSS ("50%", "400px" → boîte centrée, image entière visible) */
|
|
47
|
+
backgroundImageSize?: string
|
|
48
|
+
/** Anime l'image de fond (Ken Burns indéterminé : zoom + panoramique lents en boucle) */
|
|
49
|
+
backgroundAnimated?: boolean
|
|
50
|
+
/** Direction de la boucle Ken Burns : alternate (défaut) | alternate-reverse | normal | reverse */
|
|
51
|
+
backgroundAnimationDirection?: "alternate" | "alternate-reverse" | "normal" | "reverse"
|
|
52
|
+
/** Durée d'une itération du Ken Burns (défaut 24s) */
|
|
53
|
+
backgroundAnimationDuration?: string
|
|
41
54
|
class?: string
|
|
42
55
|
}
|
|
43
56
|
|
|
@@ -47,6 +60,13 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
47
60
|
padding: "16px",
|
|
48
61
|
})
|
|
49
62
|
|
|
63
|
+
const IMG_FIT = new Set(["cover", "contain", "fill", "none", "scale-down"])
|
|
64
|
+
|
|
65
|
+
// Taille CSS libre ("50%", "400px"…) → boîte centrée (object-fit: contain)
|
|
66
|
+
const imgSized = computed(
|
|
67
|
+
() => !!props.backgroundImageSize && !IMG_FIT.has(props.backgroundImageSize),
|
|
68
|
+
)
|
|
69
|
+
|
|
50
70
|
const containerStyle = computed<Record<string, string>>(() => {
|
|
51
71
|
const style: Record<string, string> = {
|
|
52
72
|
"--q-container-max": typeof props.maxWidth === "number" ? `${props.maxWidth}px` : props.maxWidth,
|
|
@@ -61,6 +81,18 @@ const containerStyle = computed<Record<string, string>>(() => {
|
|
|
61
81
|
if (props.auroraColor2) style["--q-aurora-color-2"] = props.auroraColor2
|
|
62
82
|
if (props.waveColor) style["--q-wave-color"] = props.waveColor
|
|
63
83
|
if (props.waveColor2) style["--q-wave-color-2"] = props.waveColor2
|
|
84
|
+
if (props.backgroundImageSize && IMG_FIT.has(props.backgroundImageSize)) {
|
|
85
|
+
style["--q-container-img-fit"] = props.backgroundImageSize
|
|
86
|
+
}
|
|
87
|
+
if (imgSized.value && props.backgroundImageSize) {
|
|
88
|
+
style["--q-container-img-size"] = props.backgroundImageSize
|
|
89
|
+
}
|
|
90
|
+
if (props.backgroundAnimationDirection) {
|
|
91
|
+
style["--q-container-kenburns-direction"] = props.backgroundAnimationDirection
|
|
92
|
+
}
|
|
93
|
+
if (props.backgroundAnimationDuration) {
|
|
94
|
+
style["--q-container-kenburns-duration"] = props.backgroundAnimationDuration
|
|
95
|
+
}
|
|
64
96
|
return style
|
|
65
97
|
})
|
|
66
98
|
|
|
@@ -68,7 +100,8 @@ const containerClasses = computed(() =>
|
|
|
68
100
|
cn(
|
|
69
101
|
"q-container",
|
|
70
102
|
props.fluid && "q-container--fluid",
|
|
71
|
-
props.backgroundEffect && "q-container--bg",
|
|
103
|
+
(props.backgroundEffect || props.backgroundImage) && "q-container--bg",
|
|
104
|
+
props.glass && "q-container--glass",
|
|
72
105
|
props.class,
|
|
73
106
|
),
|
|
74
107
|
)
|
|
@@ -145,6 +178,19 @@ onBeforeUnmount(() => rootEl.value?.removeEventListener("mousemove", onMouseMove
|
|
|
145
178
|
|
|
146
179
|
<template>
|
|
147
180
|
<div ref="rootEl" class="q-container" :class="containerClasses" :style="containerStyle">
|
|
181
|
+
<img
|
|
182
|
+
v-if="backgroundImage"
|
|
183
|
+
:src="backgroundImage"
|
|
184
|
+
class="q-container__img"
|
|
185
|
+
:class="{
|
|
186
|
+
'q-container__img--glass': glass,
|
|
187
|
+
'q-container__img--animated': backgroundAnimated,
|
|
188
|
+
'q-container__img--sized': imgSized,
|
|
189
|
+
}"
|
|
190
|
+
alt=""
|
|
191
|
+
aria-hidden="true"
|
|
192
|
+
draggable="false"
|
|
193
|
+
/>
|
|
148
194
|
<div
|
|
149
195
|
v-if="backgroundEffect"
|
|
150
196
|
class="q-container__bg"
|
package/components/QDialog.vue
CHANGED
|
@@ -40,8 +40,8 @@ interface Props {
|
|
|
40
40
|
contentStyle?: StyleValue
|
|
41
41
|
noBackdropDismiss?: boolean
|
|
42
42
|
noEscDismiss?: boolean
|
|
43
|
-
/** Animation : fade | zoom | slide-up | slide-down | slide-left | slide-right (sinon basée sur position) */
|
|
44
|
-
transition?: "fade" | "zoom" | "slide-up" | "slide-down" | "slide-left" | "slide-right"
|
|
43
|
+
/** Animation : fade | zoom | slide-up | slide-down | slide-left | slide-right | swipe-left | swipe-right (sinon basée sur position) */
|
|
44
|
+
transition?: "fade" | "zoom" | "slide-up" | "slide-down" | "slide-left" | "slide-right" | "swipe-left" | "swipe-right"
|
|
45
45
|
/** Durée des transitions d'entrée/sortie en ms (défaut CSS : ~200ms entrée, ~150ms sortie) */
|
|
46
46
|
transitionDuration?: number
|
|
47
47
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QDialogContent — section de corps de la modale (équivalent QCardSection).
|
|
3
|
+
// Le panneau <q-dialog> est en flex-column (max-height 90vh) : avec `scrollable`,
|
|
4
|
+
// le corps prend l'espace restant et défile en interne, entre un header et un
|
|
5
|
+
// footer fixes (essentiel en mode maximized, où le panneau est overflow: hidden).
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Rendu scrollable : le corps défile entre header et footer fixes */
|
|
8
|
+
scrollable?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
withDefaults(defineProps<Props>(), {
|
|
12
|
+
scrollable: false,
|
|
13
|
+
})
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<div class="q-dialog__body" :class="{ 'q-dialog__body--scrollable': scrollable }">
|
|
18
|
+
<slot />
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// QDialogFooter —
|
|
2
|
+
// QDialogFooter — pied de la modale façon barre d'app (équivalent QFooter) :
|
|
3
|
+
// les actions sont embarquées dans un <q-toolbar> (min-height 50px, padding
|
|
4
|
+
// 0 12px) et alignées à droite (justify-content: flex-end — styles/main.css).
|
|
5
|
+
import QToolbar from "./QToolbar.vue"
|
|
3
6
|
</script>
|
|
4
7
|
|
|
5
8
|
<template>
|
|
6
9
|
<div class="q-dialog__footer">
|
|
7
|
-
<
|
|
10
|
+
<q-toolbar>
|
|
11
|
+
<slot />
|
|
12
|
+
</q-toolbar>
|
|
8
13
|
</div>
|
|
9
14
|
</template>
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// QDialogHeader — header de la modale
|
|
2
|
+
// QDialogHeader — header de la modale façon barre d'app (équivalent QHeader) :
|
|
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).
|
|
3
5
|
import { inject } from "vue"
|
|
4
6
|
import { Icon } from "@iconify/vue"
|
|
5
7
|
import { icons } from "../lib/icons"
|
|
6
8
|
import { qDialogKey } from "./QDialog.vue"
|
|
9
|
+
import QToolbar from "./QToolbar.vue"
|
|
10
|
+
import QSpace from "./QSpace.vue"
|
|
7
11
|
|
|
8
12
|
interface Props {
|
|
9
13
|
title?: string
|
|
@@ -21,21 +25,24 @@ const dialog = inject(qDialogKey, null)
|
|
|
21
25
|
|
|
22
26
|
<template>
|
|
23
27
|
<div class="q-dialog__header">
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
<q-toolbar>
|
|
29
|
+
<div class="q-dialog__header-text">
|
|
30
|
+
<h2 v-if="title" class="q-dialog__title">{{ title }}</h2>
|
|
31
|
+
<slot name="title" />
|
|
32
|
+
<p v-if="description" class="q-dialog__description">{{ description }}</p>
|
|
33
|
+
<slot name="description" />
|
|
34
|
+
</div>
|
|
35
|
+
<q-space />
|
|
36
|
+
<button
|
|
37
|
+
v-if="showClose"
|
|
38
|
+
type="button"
|
|
39
|
+
class="q-dialog__close"
|
|
40
|
+
aria-label="Fermer"
|
|
41
|
+
@click="dialog?.setOpen(false)"
|
|
42
|
+
>
|
|
43
|
+
<Icon :icon="icons.x" aria-hidden="true" />
|
|
44
|
+
</button>
|
|
45
|
+
<slot />
|
|
46
|
+
</q-toolbar>
|
|
40
47
|
</div>
|
|
41
48
|
</template>
|
|
@@ -2,26 +2,28 @@
|
|
|
2
2
|
// QDialogProvider — rend la pile de dialogues programmatiques ($q.dialog).
|
|
3
3
|
// Intégré dans QConfigProvider (rendu automatiquement par le plus externe),
|
|
4
4
|
// mais utilisable aussi en autonome : <q-dialog-provider><slot /></q-dialog-provider>.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
//
|
|
6
|
+
// Pattern Quasar : le composant passé à $q.dialog.open({ component }) DOIT contenir
|
|
7
|
+
// un <q-dialog v-model="open" @hide="onDialogHide"> à sa racine, piloté via le
|
|
8
|
+
// composable useDialogPluginComponent() (exporté par @dnax/ui). Le provider fournit
|
|
9
|
+
// le contexte (open, onOK, onCancel) via QDialogHost ; à la fermeture (backdrop,
|
|
10
|
+
// Échap, ×, onDialogOK, onDialogCancel), le résolveur correspondant est appelé
|
|
7
11
|
// puis l'entrée est retirée de la pile.
|
|
8
12
|
import { computed, inject } from "vue"
|
|
9
13
|
import { dialogStack, closeDialog } from "../lib/q"
|
|
10
14
|
import type { DialogController } from "../lib/q"
|
|
11
15
|
import { qConfigKey } from "../lib/config"
|
|
12
16
|
import { themeVarsStyle } from "../lib/themeVars"
|
|
17
|
+
import QDialogHost from "./internal/QDialogHost.vue"
|
|
13
18
|
|
|
14
19
|
const dialogs = dialogStack
|
|
15
20
|
|
|
16
21
|
const config = inject(qConfigKey, null)
|
|
17
22
|
|
|
18
|
-
// Les dialogs sont téléportés au body (QDialog) : on repose
|
|
19
|
-
// sur
|
|
23
|
+
// Les dialogs sont téléportés au body (QDialog du composant passé) : on repose
|
|
24
|
+
// les tokens du thème sur le host pour qu'ils héritent des couleurs/radius du provider.
|
|
20
25
|
const dialogThemeStyle = themeVarsStyle(computed(() => config?.theme.value ?? {}))
|
|
21
26
|
|
|
22
|
-
const contentStyle = (ctrl: DialogController) =>
|
|
23
|
-
typeof ctrl._opts.fullscreen === "string" ? { width: ctrl._opts.fullscreen } : undefined
|
|
24
|
-
|
|
25
27
|
const handleOk = (ctrl: DialogController, data?: unknown) => {
|
|
26
28
|
ctrl._resolvers.ok?.(data)
|
|
27
29
|
closeDialog(ctrl)
|
|
@@ -38,28 +40,20 @@ const handleDismiss = (ctrl: DialogController) => {
|
|
|
38
40
|
|
|
39
41
|
<template>
|
|
40
42
|
<slot />
|
|
41
|
-
<
|
|
43
|
+
<div
|
|
42
44
|
v-for="(dialog, idx) in dialogs"
|
|
43
45
|
:key="idx"
|
|
44
|
-
:
|
|
45
|
-
:maximized="dialog._opts.fullscreen === true"
|
|
46
|
-
:content-style="[contentStyle(dialog), dialogThemeStyle]"
|
|
47
|
-
:content-class="dialog._opts.class"
|
|
48
|
-
:persistent="dialog._opts.persistent"
|
|
49
|
-
@update:model-value="(v: boolean) => !v && handleDismiss(dialog)"
|
|
46
|
+
:style="dialogThemeStyle"
|
|
50
47
|
>
|
|
51
|
-
<q-dialog-
|
|
52
|
-
|
|
53
|
-
:
|
|
54
|
-
:
|
|
55
|
-
|
|
56
|
-
<component
|
|
57
|
-
:is="dialog._opts.component"
|
|
58
|
-
v-bind="dialog._opts.componentProps"
|
|
48
|
+
<q-dialog-host
|
|
49
|
+
:ctrl="dialog"
|
|
50
|
+
:on-ok="(data?: unknown) => handleOk(dialog, data)"
|
|
51
|
+
:on-cancel="() => handleCancel(dialog)"
|
|
52
|
+
:on-hide="() => handleDismiss(dialog)"
|
|
59
53
|
@ok="(data?: unknown) => handleOk(dialog, data)"
|
|
60
|
-
@cancel="handleCancel(dialog)"
|
|
61
|
-
@dismiss="handleDismiss(dialog)"
|
|
62
|
-
@close="handleDismiss(dialog)"
|
|
54
|
+
@cancel="() => handleCancel(dialog)"
|
|
55
|
+
@dismiss="() => handleDismiss(dialog)"
|
|
56
|
+
@close="() => handleDismiss(dialog)"
|
|
63
57
|
/>
|
|
64
|
-
</
|
|
58
|
+
</div>
|
|
65
59
|
</template>
|
package/components/QFooter.vue
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// QFooter — barre basse (safe-area bottom appliquée par styles/main.css : .q-app .q-footer)
|
|
3
3
|
import { computed, onBeforeUnmount, onMounted, ref } from "vue"
|
|
4
|
+
import { useFixedBarOffset } from "../lib/fixedLayout"
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
6
7
|
/** Fixe la barre en bas de l'écran (sort du flux) */
|
|
@@ -24,6 +25,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
24
25
|
bordered: false,
|
|
25
26
|
})
|
|
26
27
|
|
|
28
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
29
|
+
|
|
30
|
+
// Empilement : bottom = hauteur cumulée des footers fixed suivants (si fixed)
|
|
31
|
+
useFixedBarOffset(rootEl, "bar-bottom", () => props.fixed)
|
|
32
|
+
|
|
27
33
|
const translucentStyle = computed<Record<string, string> | undefined>(() =>
|
|
28
34
|
props.translucent === true || typeof props.translucent === "number"
|
|
29
35
|
? { "--q-translucent-opacity": `${typeof props.translucent === "number" ? props.translucent : 70}%` }
|
|
@@ -55,6 +61,7 @@ onBeforeUnmount(() => {
|
|
|
55
61
|
|
|
56
62
|
<template>
|
|
57
63
|
<footer
|
|
64
|
+
ref="rootEl"
|
|
58
65
|
class="q-footer"
|
|
59
66
|
:class="{
|
|
60
67
|
'q-footer--fixed': fixed,
|
package/components/QPage.vue
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// QPage — zone de contenu. Prop `virtual` : active le virtual scroll
|
|
3
3
|
// (rendu fenêtré via QVirtualScroll) quand on a beaucoup d'items.
|
|
4
|
-
// Padding-top
|
|
5
|
-
//
|
|
4
|
+
// Padding-top/padding-bottom automatiques quand des barres fixed
|
|
5
|
+
// (q-header / q-back-header avant, q-footer après) entourent la page : le
|
|
6
|
+
// contenu reste visible, jamais masqué par les barres au scroll.
|
|
6
7
|
import { onBeforeUnmount, onMounted, ref } from "vue"
|
|
7
8
|
import { useFixedBarOffset } from "../lib/fixedLayout"
|
|
8
9
|
import QVirtualScroll from "./QVirtualScroll.vue"
|
package/components/QTab.vue
CHANGED
|
@@ -95,9 +95,11 @@ const tabClasses = computed(() =>
|
|
|
95
95
|
isActive.value && tabs?.activeClass.value,
|
|
96
96
|
(props.noCaps || tabs?.noCaps.value) && "q-tab--no-caps",
|
|
97
97
|
props.disable && "q-tab--disabled",
|
|
98
|
-
|
|
98
|
+
// Mode replié : le label s'étend à droite de l'icône → forcer l'inline-label
|
|
99
|
+
// (et neutraliser le padding stacked-icon + le fallback :has) */
|
|
100
|
+
(tabs?.inlineLabel.value || tabs?.collapseInactive.value) && "q-tab--inline-label",
|
|
99
101
|
// Icône empilée au-dessus du label : padding vertical pour l'aérer du cadre
|
|
100
|
-
props.icon && !tabs?.inlineLabel.value && "q-tab--stacked-icon",
|
|
102
|
+
props.icon && !tabs?.inlineLabel.value && !tabs?.collapseInactive.value && "q-tab--stacked-icon",
|
|
101
103
|
),
|
|
102
104
|
)
|
|
103
105
|
|
package/components/QTabs.vue
CHANGED
|
@@ -16,6 +16,8 @@ export interface QTabContext {
|
|
|
16
16
|
inlineLabel: Readonly<Ref<boolean>>
|
|
17
17
|
noCaps: Readonly<Ref<boolean>>
|
|
18
18
|
activeClass: Readonly<Ref<string | undefined>>
|
|
19
|
+
/** Mode replié : seul le tab actif montre icône + label (les autres : icône seule) */
|
|
20
|
+
collapseInactive: Readonly<Ref<boolean>>
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export const qTabsKey: InjectionKey<QTabContext> = Symbol("q-tabs")
|
|
@@ -49,6 +51,8 @@ interface Props {
|
|
|
49
51
|
activeColor?: string
|
|
50
52
|
/** Couleur de fond du tab actif */
|
|
51
53
|
activeBgColor?: string
|
|
54
|
+
/** Couleur des tabs inactifs (token ou hex — défaut : couleur héritée) */
|
|
55
|
+
inactiveColor?: string
|
|
52
56
|
/** Classe ajoutée au tab actif */
|
|
53
57
|
activeClass?: string
|
|
54
58
|
/** Couleur de l'indicateur (défaut : activeColor → primary) */
|
|
@@ -74,6 +78,19 @@ interface Props {
|
|
|
74
78
|
contentClass?: string
|
|
75
79
|
/** Anime les tabs au clic (appui scale + pop du tab actif) */
|
|
76
80
|
animated?: boolean
|
|
81
|
+
/**
|
|
82
|
+
* Transition du changement de tab (requiert `animated`) :
|
|
83
|
+
* spring (défaut, rebond léger) | smooth (expo-out velouté, label qui monte)
|
|
84
|
+
* | elastic (rebond marqué).
|
|
85
|
+
*/
|
|
86
|
+
transition?: "spring" | "smooth" | "elastic"
|
|
87
|
+
/** Durée des transitions en ms (indicateur, labels collapse, pop/rise) */
|
|
88
|
+
transitionDuration?: number
|
|
89
|
+
/**
|
|
90
|
+
* Mode replié (pattern mobile) : les tabs inactifs n'affichent que l'icône ;
|
|
91
|
+
* le tab actif s'étend (icône + label) avec une largeur animée.
|
|
92
|
+
*/
|
|
93
|
+
collapseInactive?: boolean
|
|
77
94
|
}
|
|
78
95
|
|
|
79
96
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -81,6 +98,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
81
98
|
align: "center",
|
|
82
99
|
activeColor: "",
|
|
83
100
|
activeBgColor: "",
|
|
101
|
+
inactiveColor: "",
|
|
84
102
|
activeClass: "",
|
|
85
103
|
indicatorColor: "",
|
|
86
104
|
dense: false,
|
|
@@ -92,6 +110,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
92
110
|
stretch: false,
|
|
93
111
|
contentClass: "",
|
|
94
112
|
animated: false,
|
|
113
|
+
transition: "spring",
|
|
114
|
+
transitionDuration: undefined,
|
|
115
|
+
collapseInactive: false,
|
|
95
116
|
})
|
|
96
117
|
|
|
97
118
|
const emit = defineEmits<{ "update:modelValue": [value: string | number | null] }>()
|
|
@@ -107,6 +128,7 @@ const indicatorPos = computed<"top" | "bottom" | "none">(() => {
|
|
|
107
128
|
// shallowRef : on ne veut PAS que Vue dé-unwrappe les Ref contenues dans les enregistrements
|
|
108
129
|
const tabRegs = shallowRef<QTabRegistration[]>([])
|
|
109
130
|
const tick = ref(0)
|
|
131
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
110
132
|
|
|
111
133
|
// Recalcule la position de l'indicateur après chaque changement de DOM
|
|
112
134
|
const measure = async () => {
|
|
@@ -114,14 +136,25 @@ const measure = async () => {
|
|
|
114
136
|
tick.value++
|
|
115
137
|
}
|
|
116
138
|
|
|
139
|
+
// collapse-inactive : le label s'étend/retombe via une transition max-width
|
|
140
|
+
// (0.3s) — à la fin, la largeur du tab actif change encore → re-mesurer,
|
|
141
|
+
// sinon l'indicateur reste calé sur la largeur repliée (mal positionné).
|
|
142
|
+
// transitionend bulle depuis le label jusqu'à la racine : un seul écouteur.
|
|
143
|
+
const onCollapseEnd = (e: Event) => {
|
|
144
|
+
if ((e as TransitionEvent).propertyName !== "max-width") return
|
|
145
|
+
measure()
|
|
146
|
+
}
|
|
147
|
+
|
|
117
148
|
watch([() => props.modelValue, tabRegs], measure)
|
|
118
149
|
|
|
119
150
|
onMounted(() => {
|
|
120
151
|
measure()
|
|
121
152
|
if (typeof window !== "undefined") window.addEventListener("resize", measure)
|
|
153
|
+
rootEl.value?.addEventListener("transitionend", onCollapseEnd)
|
|
122
154
|
})
|
|
123
155
|
onBeforeUnmount(() => {
|
|
124
156
|
if (typeof window !== "undefined") window.removeEventListener("resize", measure)
|
|
157
|
+
rootEl.value?.removeEventListener("transitionend", onCollapseEnd)
|
|
125
158
|
})
|
|
126
159
|
|
|
127
160
|
const context: QTabContext = {
|
|
@@ -136,6 +169,7 @@ const context: QTabContext = {
|
|
|
136
169
|
inlineLabel: computed(() => props.inlineLabel),
|
|
137
170
|
noCaps: computed(() => props.noCaps),
|
|
138
171
|
activeClass: computed(() => (props.activeClass ? props.activeClass : undefined)),
|
|
172
|
+
collapseInactive: computed(() => props.collapseInactive),
|
|
139
173
|
}
|
|
140
174
|
|
|
141
175
|
provide(qTabsKey, context)
|
|
@@ -170,6 +204,8 @@ const containerStyle = computed<Record<string, string>>(() => {
|
|
|
170
204
|
// Token (primary…) → var(--primary) ; sinon valeur directe (hex, rgb…)
|
|
171
205
|
if (props.activeColor) style["--q-tabs-active-color"] = colorValue(props.activeColor)
|
|
172
206
|
if (props.activeBgColor) style["--q-tabs-active-bg"] = colorValue(props.activeBgColor)
|
|
207
|
+
if (props.inactiveColor) style["--q-tabs-inactive-color"] = colorValue(props.inactiveColor)
|
|
208
|
+
if (props.transitionDuration) style["--q-tabs-duration"] = `${props.transitionDuration}ms`
|
|
173
209
|
return style
|
|
174
210
|
})
|
|
175
211
|
|
|
@@ -184,12 +220,15 @@ const tabsClasses = computed(() =>
|
|
|
184
220
|
props.shrink && "q-tabs--shrink",
|
|
185
221
|
props.stretch && "q-tabs--stretch",
|
|
186
222
|
props.animated && "q-tabs--animated",
|
|
223
|
+
props.animated && `q-tabs--anim-${props.transition}`,
|
|
224
|
+
props.collapseInactive && "q-tabs--collapse-inactive",
|
|
187
225
|
),
|
|
188
226
|
)
|
|
189
227
|
</script>
|
|
190
228
|
|
|
191
229
|
<template>
|
|
192
230
|
<div
|
|
231
|
+
ref="rootEl"
|
|
193
232
|
class="q-tabs"
|
|
194
233
|
:class="tabsClasses"
|
|
195
234
|
:style="containerStyle"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QDialogHost — host interne du $q.dialog : rend le composant passé tel quel et
|
|
3
|
+
// lui fournit le contexte plugin (pattern Quasar). Le composant passé DOIT
|
|
4
|
+
// contenir un <q-dialog v-model="open" @hide="onDialogHide"> comme racine, piloté
|
|
5
|
+
// via useDialogPluginComponent(). À la fermeture (backdrop/Échap/× → @hide), ou
|
|
6
|
+
// via onDialogOK / onDialogCancel, les résolveurs sont appelés et l'entrée retirée.
|
|
7
|
+
import { provide, ref } from "vue"
|
|
8
|
+
import { qDialogPluginKey } from "../../lib/q"
|
|
9
|
+
import type { DialogController } from "../../lib/q"
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
ctrl: DialogController
|
|
13
|
+
onOk: (data?: unknown) => void
|
|
14
|
+
onCancel: () => void
|
|
15
|
+
onHide: () => void
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
const open = ref(true)
|
|
19
|
+
|
|
20
|
+
provide(qDialogPluginKey, {
|
|
21
|
+
open,
|
|
22
|
+
onOK: (data?: unknown) => props.onOk(data),
|
|
23
|
+
onCancel: () => props.onCancel(),
|
|
24
|
+
onHide: () => props.onHide(),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Le composant émet aussi les events legacy (ok/cancel/dismiss/close)
|
|
28
|
+
const emit = defineEmits<{
|
|
29
|
+
ok: [data?: unknown]
|
|
30
|
+
cancel: []
|
|
31
|
+
dismiss: []
|
|
32
|
+
close: []
|
|
33
|
+
}>()
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<component
|
|
38
|
+
:is="props.ctrl._opts.component"
|
|
39
|
+
v-bind="props.ctrl._opts.componentProps"
|
|
40
|
+
@ok="emit('ok', $event)"
|
|
41
|
+
@cancel="emit('cancel')"
|
|
42
|
+
@dismiss="emit('dismiss')"
|
|
43
|
+
@close="emit('close')"
|
|
44
|
+
/>
|
|
45
|
+
</template>
|
package/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export { default as QCountryPicker } from "./components/QCountryPicker.vue"
|
|
|
39
39
|
export { default as QDataGrid } from "./components/QDataGrid.vue"
|
|
40
40
|
export { default as QDatePicker } from "./components/QDatePicker.vue"
|
|
41
41
|
export { default as QDialog } from "./components/QDialog.vue"
|
|
42
|
+
export { default as QDialogContent } from "./components/QDialogContent.vue"
|
|
42
43
|
export { default as QDialogFooter } from "./components/QDialogFooter.vue"
|
|
43
44
|
export { default as QDialogHeader } from "./components/QDialogHeader.vue"
|
|
44
45
|
export { default as QDialogProvider } from "./components/QDialogProvider.vue"
|
|
@@ -126,7 +127,7 @@ export { default as QUploader } from "./components/QUploader.vue"
|
|
|
126
127
|
export { default as QVideo } from "./components/QVideo.vue"
|
|
127
128
|
export { default as QVirtualScroll } from "./components/QVirtualScroll.vue"
|
|
128
129
|
|
|
129
|
-
export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet } from "./lib/q"
|
|
130
|
+
export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet, useDialogPluginComponent } from "./lib/q"
|
|
130
131
|
export type {
|
|
131
132
|
DialogOptions,
|
|
132
133
|
DialogController,
|