@dnax/ui 0.0.11 → 0.0.13
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 +21 -3
- package/components/QBottomSheetHeader.vue +34 -18
- package/components/QContainer.vue +47 -1
- package/components/QDialog.vue +31 -3
- package/components/QDialogContent.vue +20 -0
- package/components/QDialogFooter.vue +21 -3
- package/components/QDialogHeader.vue +32 -18
- package/components/QFooter.vue +11 -0
- package/components/QPage.vue +3 -2
- package/components/QTab.vue +4 -2
- package/components/QTabs.vue +39 -0
- package/index.ts +1 -0
- package/lib/fixedLayout.ts +62 -16
- package/lib/platform.ts +185 -19
- package/package.json +1 -1
- package/styles/main.css +247 -32
|
@@ -1,9 +1,27 @@
|
|
|
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
|
+
// no-padding retire le padding horizontal du toolbar.
|
|
6
|
+
import QToolbar from "./QToolbar.vue"
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Supprime le padding du toolbar (actions collées aux bords) */
|
|
10
|
+
noPadding?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
withDefaults(defineProps<Props>(), {
|
|
14
|
+
noPadding: false,
|
|
15
|
+
})
|
|
3
16
|
</script>
|
|
4
17
|
|
|
5
18
|
<template>
|
|
6
|
-
<div
|
|
7
|
-
|
|
19
|
+
<div
|
|
20
|
+
class="q-bottom-sheet__footer"
|
|
21
|
+
:class="{ 'q-bottom-sheet__footer--no-padding': noPadding }"
|
|
22
|
+
>
|
|
23
|
+
<q-toolbar>
|
|
24
|
+
<slot />
|
|
25
|
+
</q-toolbar>
|
|
8
26
|
</div>
|
|
9
27
|
</template>
|
|
@@ -1,35 +1,51 @@
|
|
|
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).
|
|
5
|
+
// no-padding retire le padding horizontal du toolbar.
|
|
3
6
|
import { inject } from "vue"
|
|
4
7
|
import { Icon } from "@iconify/vue"
|
|
5
8
|
import { icons } from "../lib/icons"
|
|
6
9
|
import { qBottomSheetKey } from "./QBottomSheet.vue"
|
|
10
|
+
import QToolbar from "./QToolbar.vue"
|
|
11
|
+
import QSpace from "./QSpace.vue"
|
|
7
12
|
|
|
8
13
|
interface Props {
|
|
9
14
|
title?: string
|
|
10
15
|
description?: string
|
|
16
|
+
/** Supprime le padding du toolbar (contenu collé aux bords) */
|
|
17
|
+
noPadding?: boolean
|
|
11
18
|
}
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
withDefaults(defineProps<Props>(), {
|
|
21
|
+
noPadding: false,
|
|
22
|
+
})
|
|
23
|
+
|
|
14
24
|
const sheet = inject(qBottomSheetKey, null)
|
|
15
25
|
</script>
|
|
16
26
|
|
|
17
27
|
<template>
|
|
18
|
-
<div
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
<div
|
|
29
|
+
class="q-bottom-sheet__header"
|
|
30
|
+
:class="{ 'q-bottom-sheet__header--no-padding': noPadding }"
|
|
31
|
+
>
|
|
32
|
+
<q-toolbar>
|
|
33
|
+
<div class="q-bottom-sheet__header-text">
|
|
34
|
+
<h2 v-if="title" class="q-bottom-sheet__title">{{ title }}</h2>
|
|
35
|
+
<slot name="title" />
|
|
36
|
+
<p v-if="description" class="q-bottom-sheet__description">{{ description }}</p>
|
|
37
|
+
<slot name="description" />
|
|
38
|
+
</div>
|
|
39
|
+
<q-space />
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
class="q-bottom-sheet__close"
|
|
43
|
+
aria-label="Fermer"
|
|
44
|
+
@click="sheet?.setOpen(false)"
|
|
45
|
+
>
|
|
46
|
+
<Icon :icon="icons.x" aria-hidden="true" />
|
|
47
|
+
</button>
|
|
48
|
+
<slot />
|
|
49
|
+
</q-toolbar>
|
|
34
50
|
</div>
|
|
35
51
|
</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
|
@@ -88,8 +88,36 @@ const onDocKeydown = (e: KeyboardEvent) => {
|
|
|
88
88
|
// « Retour » navigateur → ferme le dialog au lieu de naviguer
|
|
89
89
|
useOverlayBack(open, () => { open.value = false }, "QDialog")
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
// — Verrou du scroll de la page derrière le dialog —
|
|
92
|
+
// Compteur module-level + suivi par instance : chaque dialog verrouille au plus
|
|
93
|
+
// une fois et déverrouille au plus une fois → pile de dialogs ($q.dialog) sans
|
|
94
|
+
// déverrouillage prématuré. Verrouille html + body (iOS Safari ignore
|
|
95
|
+
// overflow:hidden sur body seul).
|
|
96
|
+
let scrollLockCount = 0
|
|
97
|
+
function setBodyScrollLock(locked: boolean) {
|
|
98
|
+
if (typeof document === "undefined") return
|
|
99
|
+
scrollLockCount = Math.max(0, scrollLockCount + (locked ? 1 : -1))
|
|
100
|
+
const on = scrollLockCount > 0
|
|
101
|
+
document.documentElement.style.overflow = on ? "hidden" : ""
|
|
102
|
+
document.body.style.overflow = on ? "hidden" : ""
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let didLock = false
|
|
106
|
+
const lockIf = (v: boolean) => {
|
|
107
|
+
if (v && !didLock) {
|
|
108
|
+
didLock = true
|
|
109
|
+
setBodyScrollLock(true)
|
|
110
|
+
}
|
|
111
|
+
else if (!v && didLock) {
|
|
112
|
+
didLock = false
|
|
113
|
+
setBodyScrollLock(false)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
onMounted(() => lockIf(props.modelValue))
|
|
118
|
+
watch(open, lockIf)
|
|
119
|
+
onBeforeUnmount(() => {
|
|
120
|
+
if (didLock) setBodyScrollLock(false)
|
|
93
121
|
})
|
|
94
122
|
|
|
95
123
|
onMounted(() => {
|
|
@@ -98,8 +126,8 @@ onMounted(() => {
|
|
|
98
126
|
onBeforeUnmount(() => {
|
|
99
127
|
if (typeof document !== "undefined") {
|
|
100
128
|
document.removeEventListener("keydown", onDocKeydown)
|
|
101
|
-
document.body.style.overflow = ""
|
|
102
129
|
}
|
|
130
|
+
if (didLock) setBodyScrollLock(false)
|
|
103
131
|
})
|
|
104
132
|
|
|
105
133
|
const overlayClasses = computed(() => cn("q-dialog__overlay", `q-dialog__overlay--${props.position}`))
|
|
@@ -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,27 @@
|
|
|
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
|
+
// no-padding retire le padding horizontal du toolbar.
|
|
6
|
+
import QToolbar from "./QToolbar.vue"
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Supprime le padding du toolbar (actions collées aux bords) */
|
|
10
|
+
noPadding?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
withDefaults(defineProps<Props>(), {
|
|
14
|
+
noPadding: false,
|
|
15
|
+
})
|
|
3
16
|
</script>
|
|
4
17
|
|
|
5
18
|
<template>
|
|
6
|
-
<div
|
|
7
|
-
|
|
19
|
+
<div
|
|
20
|
+
class="q-dialog__footer"
|
|
21
|
+
:class="{ 'q-dialog__footer--no-padding': noPadding }"
|
|
22
|
+
>
|
|
23
|
+
<q-toolbar>
|
|
24
|
+
<slot />
|
|
25
|
+
</q-toolbar>
|
|
8
26
|
</div>
|
|
9
27
|
</template>
|
|
@@ -1,41 +1,55 @@
|
|
|
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).
|
|
5
|
+
// no-padding retire le padding horizontal du toolbar.
|
|
3
6
|
import { inject } from "vue"
|
|
4
7
|
import { Icon } from "@iconify/vue"
|
|
5
8
|
import { icons } from "../lib/icons"
|
|
6
9
|
import { qDialogKey } from "./QDialog.vue"
|
|
10
|
+
import QToolbar from "./QToolbar.vue"
|
|
11
|
+
import QSpace from "./QSpace.vue"
|
|
7
12
|
|
|
8
13
|
interface Props {
|
|
9
14
|
title?: string
|
|
10
15
|
description?: string
|
|
11
16
|
/** Bouton fermer */
|
|
12
17
|
showClose?: boolean
|
|
18
|
+
/** Supprime le padding du toolbar (contenu collé aux bords) */
|
|
19
|
+
noPadding?: boolean
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
const props = withDefaults(defineProps<Props>(), {
|
|
16
23
|
showClose: true,
|
|
24
|
+
noPadding: false,
|
|
17
25
|
})
|
|
18
26
|
|
|
19
27
|
const dialog = inject(qDialogKey, null)
|
|
20
28
|
</script>
|
|
21
29
|
|
|
22
30
|
<template>
|
|
23
|
-
<div
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
<div
|
|
32
|
+
class="q-dialog__header"
|
|
33
|
+
:class="{ 'q-dialog__header--no-padding': noPadding }"
|
|
34
|
+
>
|
|
35
|
+
<q-toolbar>
|
|
36
|
+
<div class="q-dialog__header-text">
|
|
37
|
+
<h2 v-if="title" class="q-dialog__title">{{ title }}</h2>
|
|
38
|
+
<slot name="title" />
|
|
39
|
+
<p v-if="description" class="q-dialog__description">{{ description }}</p>
|
|
40
|
+
<slot name="description" />
|
|
41
|
+
</div>
|
|
42
|
+
<q-space />
|
|
43
|
+
<button
|
|
44
|
+
v-if="showClose"
|
|
45
|
+
type="button"
|
|
46
|
+
class="q-dialog__close"
|
|
47
|
+
aria-label="Fermer"
|
|
48
|
+
@click="dialog?.setOpen(false)"
|
|
49
|
+
>
|
|
50
|
+
<Icon :icon="icons.x" aria-hidden="true" />
|
|
51
|
+
</button>
|
|
52
|
+
<slot />
|
|
53
|
+
</q-toolbar>
|
|
40
54
|
</div>
|
|
41
55
|
</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) */
|
|
@@ -15,6 +16,8 @@ interface Props {
|
|
|
15
16
|
translucent?: boolean | number
|
|
16
17
|
/** Glassmorphism marqué : fond très translucide + flou fort + bordure claire */
|
|
17
18
|
glass?: boolean
|
|
19
|
+
/** Supprime le padding horizontal (éléments collés aux bords, safe-area conservée) */
|
|
20
|
+
noPadding?: boolean
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -22,8 +25,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
22
25
|
elevated: false,
|
|
23
26
|
reveal: false,
|
|
24
27
|
bordered: false,
|
|
28
|
+
noPadding: false,
|
|
25
29
|
})
|
|
26
30
|
|
|
31
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
32
|
+
|
|
33
|
+
// Empilement : bottom = hauteur cumulée des footers fixed suivants (si fixed)
|
|
34
|
+
useFixedBarOffset(rootEl, "bar-bottom", () => props.fixed)
|
|
35
|
+
|
|
27
36
|
const translucentStyle = computed<Record<string, string> | undefined>(() =>
|
|
28
37
|
props.translucent === true || typeof props.translucent === "number"
|
|
29
38
|
? { "--q-translucent-opacity": `${typeof props.translucent === "number" ? props.translucent : 70}%` }
|
|
@@ -55,6 +64,7 @@ onBeforeUnmount(() => {
|
|
|
55
64
|
|
|
56
65
|
<template>
|
|
57
66
|
<footer
|
|
67
|
+
ref="rootEl"
|
|
58
68
|
class="q-footer"
|
|
59
69
|
:class="{
|
|
60
70
|
'q-footer--fixed': fixed,
|
|
@@ -64,6 +74,7 @@ onBeforeUnmount(() => {
|
|
|
64
74
|
'q-footer--reveal-hidden': props.reveal && !visible,
|
|
65
75
|
'q-footer--translucent': props.translucent === true || typeof props.translucent === 'number',
|
|
66
76
|
'q-footer--glass': props.glass,
|
|
77
|
+
'q-footer--no-padding': props.noPadding,
|
|
67
78
|
}"
|
|
68
79
|
:style="translucentStyle"
|
|
69
80
|
>
|
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"
|
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"
|