@dnax/ui 0.0.11 → 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/QDialogContent.vue +20 -0
- package/components/QDialogFooter.vue +7 -2
- package/components/QDialogHeader.vue +24 -17
- 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/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 +221 -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"
|
|
@@ -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>
|
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"
|
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"
|
package/lib/fixedLayout.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
// fixedLayout — offset automatique des barres fixed (q-header, q-back-header
|
|
2
|
-
// et du q-page qui les
|
|
1
|
+
// fixedLayout — offset automatique des barres fixed (q-header, q-back-header,
|
|
2
|
+
// q-footer) et du q-page qui les entoure.
|
|
3
3
|
//
|
|
4
4
|
// Problème : les barres fixed sortent du flux (position: fixed) → le contenu de
|
|
5
5
|
// q-page passe dessous. Ce module :
|
|
6
6
|
// - q-page (mode "page") reçoit un padding-top = hauteur cumulée des barres
|
|
7
|
-
// fixed qui le précèdent
|
|
7
|
+
// fixed HAUT qui le précèdent ET un padding-bottom = hauteur cumulée des
|
|
8
|
+
// barres fixed BAS qui le suivent (le contenu n'est masqué ni en haut ni en
|
|
9
|
+
// bas au scroll)
|
|
8
10
|
// - chaque barre fixed suivante (mode "bar") s'empile sous la précédente (top)
|
|
11
|
+
// - chaque barre fixed basse (mode "bar-bottom") s'empile au-dessus de la
|
|
12
|
+
// suivante (bottom)
|
|
9
13
|
//
|
|
10
14
|
// Les barres fixed sont cherchées dans TOUT le sous-arbre de l'enveloppe q-app
|
|
11
15
|
// (pas seulement parmi les frères directs) et ordonnées par position dans le
|
|
@@ -15,7 +19,12 @@
|
|
|
15
19
|
import { onBeforeUnmount, onMounted, watch } from "vue"
|
|
16
20
|
import type { Ref } from "vue"
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
/** Barres fixed en haut de l'écran */
|
|
23
|
+
export const FIXED_TOP_SELECTOR = ".q-header--fixed, .q-back-header--fixed"
|
|
24
|
+
/** Barres fixed en bas de l'écran */
|
|
25
|
+
export const FIXED_BOTTOM_SELECTOR = ".q-footer--fixed"
|
|
26
|
+
/** Toutes les barres fixed (haut + bas) */
|
|
27
|
+
export const FIXED_BAR_SELECTOR = `${FIXED_TOP_SELECTOR}, ${FIXED_BOTTOM_SELECTOR}`
|
|
19
28
|
|
|
20
29
|
/** Racine du layout : le .q-app le plus proche, sinon le parent direct. */
|
|
21
30
|
function layoutRoot(el: HTMLElement): HTMLElement | null {
|
|
@@ -23,13 +32,13 @@ function layoutRoot(el: HTMLElement): HTMLElement | null {
|
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
/**
|
|
26
|
-
* Hauteur cumulée des barres fixed qui précèdent `el` dans l'ordre du
|
|
27
|
-
* sous la même enveloppe q-app. `el` lui-même (cas mode "bar") est exclu.
|
|
35
|
+
* Hauteur cumulée des barres fixed HAUT qui précèdent `el` dans l'ordre du
|
|
36
|
+
* document, sous la même enveloppe q-app. `el` lui-même (cas mode "bar") est exclu.
|
|
28
37
|
*/
|
|
29
38
|
export function fixedBarsHeightBefore(el: HTMLElement): number {
|
|
30
39
|
const root = layoutRoot(el)
|
|
31
40
|
if (!root) return 0
|
|
32
|
-
const bars = Array.from(root.querySelectorAll<HTMLElement>(
|
|
41
|
+
const bars = Array.from(root.querySelectorAll<HTMLElement>(FIXED_TOP_SELECTOR))
|
|
33
42
|
let total = 0
|
|
34
43
|
for (const bar of bars) {
|
|
35
44
|
if (bar === el) continue
|
|
@@ -41,6 +50,26 @@ export function fixedBarsHeightBefore(el: HTMLElement): number {
|
|
|
41
50
|
return total
|
|
42
51
|
}
|
|
43
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Hauteur cumulée des barres fixed BAS qui SUIVENT `el` dans l'ordre du
|
|
55
|
+
* document, sous la même enveloppe q-app. `el` lui-même (cas mode "bar-bottom")
|
|
56
|
+
* est exclu.
|
|
57
|
+
*/
|
|
58
|
+
export function fixedBarsHeightAfter(el: HTMLElement): number {
|
|
59
|
+
const root = layoutRoot(el)
|
|
60
|
+
if (!root) return 0
|
|
61
|
+
const bars = Array.from(root.querySelectorAll<HTMLElement>(FIXED_BOTTOM_SELECTOR))
|
|
62
|
+
let total = 0
|
|
63
|
+
for (const bar of bars) {
|
|
64
|
+
if (bar === el) continue
|
|
65
|
+
// DOCUMENT_POSITION_PRECEDING = la barre est APRÈS el dans le document
|
|
66
|
+
if (bar.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING) {
|
|
67
|
+
total += bar.offsetHeight
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return total
|
|
71
|
+
}
|
|
72
|
+
|
|
44
73
|
/** Toutes les barres fixed sous l'enveloppe q-app (pour l'observation). */
|
|
45
74
|
function fixedBarsInRoot(el: HTMLElement): HTMLElement[] {
|
|
46
75
|
const root = layoutRoot(el)
|
|
@@ -49,15 +78,19 @@ function fixedBarsInRoot(el: HTMLElement): HTMLElement[] {
|
|
|
49
78
|
}
|
|
50
79
|
|
|
51
80
|
/**
|
|
52
|
-
* Applique l'offset lié aux barres fixed qui
|
|
53
|
-
* - mode "page"
|
|
54
|
-
*
|
|
81
|
+
* Applique l'offset lié aux barres fixed qui entourent `el` :
|
|
82
|
+
* - mode "page" : padding-top = barres HAUT précédentes,
|
|
83
|
+
* padding-bottom = barres BAS suivantes
|
|
84
|
+
* (+ variables --q-page-offset / --q-page-offset-bottom)
|
|
85
|
+
* - mode "bar" : top = hauteur cumulée (empilement des barres fixed HAUT)
|
|
86
|
+
* - mode "bar-bottom" : bottom = hauteur cumulée (empilement des footers fixed
|
|
87
|
+
* depuis le bas de l'écran)
|
|
55
88
|
*
|
|
56
89
|
* `enabled` permet de n'activer que dans certaines conditions (ex. props.fixed).
|
|
57
90
|
*/
|
|
58
91
|
export function useFixedBarOffset(
|
|
59
92
|
el: Ref<HTMLElement | null>,
|
|
60
|
-
mode: "page" | "bar",
|
|
93
|
+
mode: "page" | "bar" | "bar-bottom",
|
|
61
94
|
enabled: () => boolean = () => true,
|
|
62
95
|
) {
|
|
63
96
|
let ro: ResizeObserver | null = null
|
|
@@ -75,14 +108,22 @@ export function useFixedBarOffset(
|
|
|
75
108
|
const apply = () => {
|
|
76
109
|
const node = el.value
|
|
77
110
|
if (!node || !enabled()) return
|
|
78
|
-
const h = fixedBarsHeightBefore(node)
|
|
79
111
|
if (mode === "page") {
|
|
80
|
-
|
|
81
|
-
|
|
112
|
+
const top = fixedBarsHeightBefore(node)
|
|
113
|
+
const bottom = fixedBarsHeightAfter(node)
|
|
114
|
+
node.style.setProperty("--q-page-offset", top ? `${top}px` : "0px")
|
|
115
|
+
node.style.setProperty("--q-page-offset-bottom", bottom ? `${bottom}px` : "0px")
|
|
116
|
+
node.style.paddingTop = top ? `${top}px` : ""
|
|
117
|
+
node.style.paddingBottom = bottom ? `${bottom}px` : ""
|
|
82
118
|
}
|
|
83
|
-
else {
|
|
119
|
+
else if (mode === "bar") {
|
|
120
|
+
const h = fixedBarsHeightBefore(node)
|
|
84
121
|
node.style.top = h ? `${h}px` : ""
|
|
85
122
|
}
|
|
123
|
+
else {
|
|
124
|
+
const h = fixedBarsHeightAfter(node)
|
|
125
|
+
node.style.bottom = h ? `${h}px` : ""
|
|
126
|
+
}
|
|
86
127
|
}
|
|
87
128
|
|
|
88
129
|
const clear = () => {
|
|
@@ -90,11 +131,16 @@ export function useFixedBarOffset(
|
|
|
90
131
|
if (!node) return
|
|
91
132
|
if (mode === "page") {
|
|
92
133
|
node.style.removeProperty("--q-page-offset")
|
|
134
|
+
node.style.removeProperty("--q-page-offset-bottom")
|
|
93
135
|
node.style.paddingTop = ""
|
|
136
|
+
node.style.paddingBottom = ""
|
|
94
137
|
}
|
|
95
|
-
else {
|
|
138
|
+
else if (mode === "bar") {
|
|
96
139
|
node.style.top = ""
|
|
97
140
|
}
|
|
141
|
+
else {
|
|
142
|
+
node.style.bottom = ""
|
|
143
|
+
}
|
|
98
144
|
}
|
|
99
145
|
|
|
100
146
|
onMounted(() => {
|
package/lib/platform.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// $q.platform — détection de plateforme (API groupée sous `is`)
|
|
2
2
|
// + breakpoints réactifs (VueUse useBreakpoints, tailles Quasar).
|
|
3
|
+
// API alignée sur Quasar : https://quasar.dev/options/platform-detection
|
|
3
4
|
import { useBreakpoints } from "@vueuse/core"
|
|
4
5
|
|
|
5
6
|
export interface QPlatformIs {
|
|
@@ -9,6 +10,8 @@ export interface QPlatformIs {
|
|
|
9
10
|
desktop: boolean
|
|
10
11
|
/** mobile ET Capacitor/Cordova (app native) */
|
|
11
12
|
nativeMobile: boolean
|
|
13
|
+
/** nom du wrapper natif : 'cordova' | 'capacitor' | undefined */
|
|
14
|
+
nativeMobileWrapper?: "cordova" | "capacitor"
|
|
12
15
|
ios: boolean
|
|
13
16
|
android: boolean
|
|
14
17
|
iphone: boolean
|
|
@@ -22,12 +25,40 @@ export interface QPlatformIs {
|
|
|
22
25
|
firefox: boolean
|
|
23
26
|
opera: boolean
|
|
24
27
|
safari: boolean
|
|
28
|
+
vivaldi: boolean
|
|
25
29
|
ie: boolean
|
|
30
|
+
/** moteur WebKit (Safari, Chrome, Edge…) */
|
|
31
|
+
webkit: boolean
|
|
32
|
+
/** extension navigateur (BEX — Chrome/Firefox) */
|
|
33
|
+
bex: boolean
|
|
34
|
+
/** Chrome OS (Chromebook) */
|
|
35
|
+
cros: boolean
|
|
36
|
+
blackberry: boolean
|
|
37
|
+
winphone: boolean
|
|
38
|
+
/** Amazon Silk (Kindle) */
|
|
39
|
+
silk: boolean
|
|
26
40
|
cordova: boolean
|
|
27
41
|
capacitor: boolean
|
|
28
42
|
electron: boolean
|
|
43
|
+
/** écran tactile */
|
|
29
44
|
touch: boolean
|
|
45
|
+
/** pointeur précis (souris) disponible */
|
|
30
46
|
mouse: boolean
|
|
47
|
+
/** nom du navigateur : 'chrome' | 'firefox' | 'safari' | … | 'generic' */
|
|
48
|
+
name: string
|
|
49
|
+
/** version complète du navigateur ('70.0.3538.110') */
|
|
50
|
+
version: string
|
|
51
|
+
/** version majeure du navigateur (70, ou -1 si inconnue) */
|
|
52
|
+
versionNumber: number
|
|
53
|
+
/** nom de l'OS : 'mac' | 'win' | 'linux' | 'ios' | 'android' | 'cros' | … */
|
|
54
|
+
platform: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface QPlatformHas {
|
|
58
|
+
/** écran tactile capable */
|
|
59
|
+
touch: boolean
|
|
60
|
+
/** localStorage / sessionStorage disponibles */
|
|
61
|
+
webStorage: boolean
|
|
31
62
|
}
|
|
32
63
|
|
|
33
64
|
export interface QPlatform {
|
|
@@ -38,25 +69,111 @@ export interface QPlatform {
|
|
|
38
69
|
isClient: boolean
|
|
39
70
|
/** booléens de détection groupés : $q.platform.is.mobile, is.ios, … */
|
|
40
71
|
is: QPlatformIs
|
|
72
|
+
/** capacités : has.touch, has.webStorage */
|
|
73
|
+
has: QPlatformHas
|
|
41
74
|
within: { iframe: boolean }
|
|
42
75
|
}
|
|
43
76
|
|
|
77
|
+
function emptyIs(): QPlatformIs {
|
|
78
|
+
return {
|
|
79
|
+
mobile: false,
|
|
80
|
+
tablet: false,
|
|
81
|
+
desktop: false,
|
|
82
|
+
nativeMobile: false,
|
|
83
|
+
nativeMobileWrapper: undefined,
|
|
84
|
+
ios: false,
|
|
85
|
+
android: false,
|
|
86
|
+
iphone: false,
|
|
87
|
+
ipad: false,
|
|
88
|
+
ipod: false,
|
|
89
|
+
mac: false,
|
|
90
|
+
win: false,
|
|
91
|
+
linux: false,
|
|
92
|
+
chrome: false,
|
|
93
|
+
edge: false,
|
|
94
|
+
firefox: false,
|
|
95
|
+
opera: false,
|
|
96
|
+
safari: false,
|
|
97
|
+
vivaldi: false,
|
|
98
|
+
ie: false,
|
|
99
|
+
webkit: false,
|
|
100
|
+
bex: false,
|
|
101
|
+
cros: false,
|
|
102
|
+
blackberry: false,
|
|
103
|
+
winphone: false,
|
|
104
|
+
silk: false,
|
|
105
|
+
cordova: false,
|
|
106
|
+
capacitor: false,
|
|
107
|
+
electron: false,
|
|
108
|
+
touch: false,
|
|
109
|
+
mouse: false,
|
|
110
|
+
name: "",
|
|
111
|
+
version: "",
|
|
112
|
+
versionNumber: -1,
|
|
113
|
+
platform: "",
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Nom + version du navigateur depuis l'UA (ordre = spécificité) */
|
|
118
|
+
function detectBrowser(ua: string) {
|
|
119
|
+
const m = (re: RegExp) => ua.match(re)?.[1] ?? ""
|
|
120
|
+
let name = "generic"
|
|
121
|
+
let version = ""
|
|
122
|
+
|
|
123
|
+
if (/vivaldi\/([\d.]+)/i.test(ua)) {
|
|
124
|
+
name = "vivaldi"
|
|
125
|
+
version = m(/vivaldi\/([\d.]+)/i)
|
|
126
|
+
}
|
|
127
|
+
else if (/edg\/([\d.]+)/i.test(ua)) {
|
|
128
|
+
name = "edge"
|
|
129
|
+
version = m(/edg\/([\d.]+)/i)
|
|
130
|
+
}
|
|
131
|
+
else if (/edge\/([\d.]+)/i.test(ua)) {
|
|
132
|
+
name = "edge"
|
|
133
|
+
version = m(/edge\/([\d.]+)/i)
|
|
134
|
+
}
|
|
135
|
+
else if (/opr\/([\d.]+)/i.test(ua)) {
|
|
136
|
+
name = "opera"
|
|
137
|
+
version = m(/opr\/([\d.]+)/i)
|
|
138
|
+
}
|
|
139
|
+
else if (/opera\/([\d.]+)/i.test(ua)) {
|
|
140
|
+
name = "opera"
|
|
141
|
+
version = m(/opera\/([\d.]+)/i)
|
|
142
|
+
}
|
|
143
|
+
else if (/(?:chrome|crios)\/([\d.]+)/i.test(ua)) {
|
|
144
|
+
name = "chrome"
|
|
145
|
+
version = m(/(?:chrome|crios)\/([\d.]+)/i)
|
|
146
|
+
}
|
|
147
|
+
else if (/(?:firefox|fxios)\/([\d.]+)/i.test(ua)) {
|
|
148
|
+
name = "firefox"
|
|
149
|
+
version = m(/(?:firefox|fxios)\/([\d.]+)/i)
|
|
150
|
+
}
|
|
151
|
+
else if (/msie ([\d.]+)/i.test(ua)) {
|
|
152
|
+
name = "ie"
|
|
153
|
+
version = m(/msie ([\d.]+)/i)
|
|
154
|
+
}
|
|
155
|
+
else if (/trident\/.*rv:([\d.]+)/i.test(ua)) {
|
|
156
|
+
name = "ie"
|
|
157
|
+
version = m(/trident\/.*rv:([\d.]+)/i)
|
|
158
|
+
}
|
|
159
|
+
else if (/version\/([\d.]+)/i.test(ua)) {
|
|
160
|
+
name = "safari"
|
|
161
|
+
version = m(/version\/([\d.]+)/i)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const versionNumber = version ? parseInt(version.split(".")[0]!, 10) || -1 : -1
|
|
165
|
+
return { name, version, versionNumber }
|
|
166
|
+
}
|
|
167
|
+
|
|
44
168
|
function detect(): QPlatform {
|
|
45
169
|
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
46
|
-
const empty = Object.fromEntries(
|
|
47
|
-
[
|
|
48
|
-
"mobile", "tablet", "desktop", "nativeMobile", "ios", "android", "iphone",
|
|
49
|
-
"ipad", "ipod", "mac", "win", "linux", "chrome", "edge", "firefox",
|
|
50
|
-
"opera", "safari", "ie", "cordova", "capacitor", "electron", "touch", "mouse",
|
|
51
|
-
].map((k) => [k, false]),
|
|
52
|
-
) as unknown as QPlatformIs
|
|
53
|
-
|
|
54
170
|
return {
|
|
55
171
|
userAgent: "",
|
|
56
172
|
platform: "",
|
|
57
173
|
isServer: true,
|
|
58
174
|
isClient: false,
|
|
59
|
-
is:
|
|
175
|
+
is: emptyIs(),
|
|
176
|
+
has: { touch: false, webStorage: false },
|
|
60
177
|
within: { iframe: false },
|
|
61
178
|
}
|
|
62
179
|
}
|
|
@@ -71,17 +188,54 @@ function detect(): QPlatform {
|
|
|
71
188
|
// iPad modernes : MacIntel + écran tactile multi-points
|
|
72
189
|
const ipad = /ipad/i.test(ua) || (/macintosh|macintel/i.test(platform) && (nav.maxTouchPoints ?? 0) > 1)
|
|
73
190
|
const android = /android/i.test(ua)
|
|
191
|
+
const blackberry = /blackberry/i.test(ua)
|
|
192
|
+
const winphone = /windows phone/i.test(ua)
|
|
193
|
+
// Chromebooks : navigator.platform vaut "CrOS x86_64" (ou "Linux x86_64" ancien)
|
|
194
|
+
const cros = /cros/i.test(platform)
|
|
195
|
+
const silk = /silk/i.test(ua)
|
|
74
196
|
|
|
75
197
|
const touch = "ontouchstart" in window || (nav.maxTouchPoints ?? 0) > 0
|
|
76
198
|
const mobileUA = /mobile|iphone|ipod|android|windows phone|blackberry/i.test(ua)
|
|
77
199
|
const tablet = ipad || (android && !mobileUA)
|
|
78
200
|
const mobile = mobileUA || (tablet ? false : touch && /android|ipad|iphone/i.test(ua))
|
|
79
201
|
|
|
202
|
+
const cordova = "cordova" in window
|
|
203
|
+
const capacitor = !!(window as any).Capacitor?.isNativePlatform?.()
|
|
204
|
+
const nativeMobileWrapper: "cordova" | "capacitor" | undefined = capacitor
|
|
205
|
+
? "capacitor"
|
|
206
|
+
: cordova
|
|
207
|
+
? "cordova"
|
|
208
|
+
: undefined
|
|
209
|
+
|
|
210
|
+
// nom de l'OS (miroir de l'API Quasar : is.platform)
|
|
211
|
+
let osName = "generic"
|
|
212
|
+
if (android) osName = "android"
|
|
213
|
+
else if (ios) osName = "ios"
|
|
214
|
+
else if (blackberry) osName = "blackberry"
|
|
215
|
+
else if (winphone) osName = "winphone"
|
|
216
|
+
else if (cros) osName = "cros"
|
|
217
|
+
else if (silk) osName = "silk"
|
|
218
|
+
else if (/mac|iphone|ipad|ipod/i.test(ua)) osName = "mac"
|
|
219
|
+
else if (/win/i.test(ua)) osName = "win"
|
|
220
|
+
else if (/linux/i.test(platform)) osName = "linux"
|
|
221
|
+
|
|
222
|
+
let webStorage = false
|
|
223
|
+
try {
|
|
224
|
+
webStorage = typeof window.localStorage !== "undefined"
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
webStorage = false
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const browser = detectBrowser(ua)
|
|
231
|
+
const notChromium = !/(edg|opr|vivaldi)/i.test(ua)
|
|
232
|
+
|
|
80
233
|
const is: QPlatformIs = {
|
|
81
234
|
mobile,
|
|
82
235
|
tablet,
|
|
83
236
|
desktop: !mobileUA && !tablet,
|
|
84
|
-
nativeMobile:
|
|
237
|
+
nativeMobile: mobile && (capacitor || cordova),
|
|
238
|
+
nativeMobileWrapper,
|
|
85
239
|
ios,
|
|
86
240
|
android,
|
|
87
241
|
iphone,
|
|
@@ -90,28 +244,40 @@ function detect(): QPlatform {
|
|
|
90
244
|
mac: /mac|iphone|ipad|ipod/i.test(ua),
|
|
91
245
|
win: /win/i.test(ua),
|
|
92
246
|
linux: /linux/i.test(platform),
|
|
93
|
-
chrome: /chrome|crios/i.test(ua) &&
|
|
94
|
-
edge: /edg/i.test(ua),
|
|
247
|
+
chrome: /chrome|crios/i.test(ua) && notChromium,
|
|
248
|
+
edge: /edg|edge/i.test(ua),
|
|
95
249
|
firefox: /firefox|fxios/i.test(ua),
|
|
96
250
|
opera: /opr|opera/i.test(ua),
|
|
97
|
-
safari: /safari/i.test(ua) && !/chrome|crios
|
|
251
|
+
safari: /safari/i.test(ua) && notChromium && !/chrome|crios/i.test(ua),
|
|
252
|
+
vivaldi: /vivaldi/i.test(ua),
|
|
98
253
|
ie: /msie|trident/i.test(ua),
|
|
99
|
-
|
|
100
|
-
|
|
254
|
+
webkit: /applewebkit/i.test(ua),
|
|
255
|
+
bex:
|
|
256
|
+
typeof window !== "undefined" &&
|
|
257
|
+
!!((window as any).chrome?.runtime?.id || (globalThis as any).browser?.runtime?.id),
|
|
258
|
+
cros,
|
|
259
|
+
blackberry,
|
|
260
|
+
winphone,
|
|
261
|
+
silk,
|
|
262
|
+
cordova,
|
|
263
|
+
capacitor,
|
|
101
264
|
electron: /electron/i.test(ua),
|
|
102
265
|
touch,
|
|
103
|
-
mouse:
|
|
266
|
+
mouse:
|
|
267
|
+
typeof window.matchMedia === "function" && window.matchMedia("(pointer: fine)").matches,
|
|
268
|
+
name: browser.name,
|
|
269
|
+
version: browser.version,
|
|
270
|
+
versionNumber: browser.versionNumber,
|
|
271
|
+
platform: osName,
|
|
104
272
|
}
|
|
105
273
|
|
|
106
|
-
// nativeMobile : mobile + Capacitor/Cordova
|
|
107
|
-
is.nativeMobile = is.mobile && (is.capacitor || is.cordova)
|
|
108
|
-
|
|
109
274
|
return {
|
|
110
275
|
userAgent: ua,
|
|
111
276
|
platform,
|
|
112
277
|
isServer: false,
|
|
113
278
|
isClient: true,
|
|
114
279
|
is,
|
|
280
|
+
has: { touch, webStorage },
|
|
115
281
|
within: { iframe: window.self !== window.top },
|
|
116
282
|
}
|
|
117
283
|
}
|
package/package.json
CHANGED
package/styles/main.css
CHANGED
|
@@ -66,9 +66,14 @@ body {
|
|
|
66
66
|
/* Safe-area intégrées à la règle de base .q-footer (plus bas) */
|
|
67
67
|
|
|
68
68
|
/* Contenu de page : pas de padding par défaut (barres dans le flux).
|
|
69
|
-
Compensation UNIQUEMENT si une barre est fixée (:has) : 50px + safe-area
|
|
69
|
+
Compensation UNIQUEMENT si une barre est fixée (:has) : 50px + safe-area.
|
|
70
|
+
Safe-area bas : le contenu scroillé jusqu'au bout ne passe pas sous la barre
|
|
71
|
+
d'accueil iOS. */
|
|
70
72
|
.q-page {
|
|
71
73
|
flex: 1 1 auto; /* prend l'espace restant */
|
|
74
|
+
padding-bottom: 0;
|
|
75
|
+
padding-bottom: constant(safe-area-inset-bottom);
|
|
76
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
.q-app:has(.q-header--fixed) .q-page {
|
|
@@ -708,11 +713,30 @@ html.dark body {
|
|
|
708
713
|
height: 2px;
|
|
709
714
|
background-color: var(--q-tabs-active-color);
|
|
710
715
|
transition:
|
|
711
|
-
transform 0.25s ease,
|
|
712
|
-
width 0.25s ease;
|
|
716
|
+
transform var(--q-tabs-duration, 0.25s) ease,
|
|
717
|
+
width var(--q-tabs-duration, 0.25s) ease;
|
|
713
718
|
will-change: transform, width;
|
|
714
719
|
}
|
|
715
720
|
|
|
721
|
+
/* — transitions animées (prop animated + transition) —
|
|
722
|
+
L'indicateur est la star du changement de tab : chaque preset lui donne une
|
|
723
|
+
courbe différente (spring = léger overshoot, smooth = expo-out velouté,
|
|
724
|
+
elastic = rebond marqué). */
|
|
725
|
+
.q-tabs--animated .q-tabs__indicator {
|
|
726
|
+
transition-duration: var(--q-tabs-duration, 0.45s);
|
|
727
|
+
}
|
|
728
|
+
.q-tabs--anim-spring .q-tabs__indicator {
|
|
729
|
+
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* back-out : dépasse puis se pose */
|
|
730
|
+
}
|
|
731
|
+
.q-tabs--anim-smooth .q-tabs__indicator {
|
|
732
|
+
transition-duration: var(--q-tabs-duration, 0.5s);
|
|
733
|
+
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1); /* expo-out : glisse velouté */
|
|
734
|
+
}
|
|
735
|
+
.q-tabs--anim-elastic .q-tabs__indicator {
|
|
736
|
+
transition-duration: var(--q-tabs-duration, 0.6s);
|
|
737
|
+
transition-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55); /* rebond prononcé */
|
|
738
|
+
}
|
|
739
|
+
|
|
716
740
|
/* ===== QTabPanels / QTabPanel ===== */
|
|
717
741
|
.q-tab-panels {
|
|
718
742
|
position: relative;
|
|
@@ -801,7 +825,9 @@ html.dark body {
|
|
|
801
825
|
padding: 0 16px;
|
|
802
826
|
border: none;
|
|
803
827
|
background: transparent;
|
|
804
|
-
|
|
828
|
+
/* Couleur des tabs inactifs : inactive-color (QTabs) sinon couleur héritée.
|
|
829
|
+
.q-tab--active (plus bas) reprend la main par ordre de source. */
|
|
830
|
+
color: var(--q-tabs-inactive-color, inherit);
|
|
805
831
|
font: inherit;
|
|
806
832
|
font-size: 14px;
|
|
807
833
|
font-weight: 500;
|
|
@@ -822,9 +848,15 @@ html.dark body {
|
|
|
822
848
|
transform: scale(0.94);
|
|
823
849
|
}
|
|
824
850
|
|
|
825
|
-
/* pop du tab actif (overshoot
|
|
826
|
-
.q-tabs--
|
|
827
|
-
|
|
851
|
+
/* pop du tab actif (overshoot) — spring (défaut) et elastic */
|
|
852
|
+
.q-tabs--anim-spring .q-tab--active,
|
|
853
|
+
.q-tabs--anim-elastic .q-tab--active {
|
|
854
|
+
animation: q-tab-pop var(--q-tabs-duration, 0.3s) cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/* smooth : le contenu du tab actif monte et apparaît (label + icône) */
|
|
858
|
+
.q-tabs--anim-smooth .q-tab--active .q-tab__content {
|
|
859
|
+
animation: q-tab-rise var(--q-tabs-duration, 0.32s) cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
828
860
|
}
|
|
829
861
|
|
|
830
862
|
.q-tab--active {
|
|
@@ -837,8 +869,12 @@ html.dark body {
|
|
|
837
869
|
outline-offset: -2px;
|
|
838
870
|
}
|
|
839
871
|
|
|
840
|
-
|
|
841
|
-
|
|
872
|
+
/* Mobile : le :hover reste collé après le tap (pas de souris) → réservé aux
|
|
873
|
+
devices avec hover réel (souris / trackpad) */
|
|
874
|
+
@media (hover: hover) and (pointer: fine) {
|
|
875
|
+
.q-tab:hover {
|
|
876
|
+
background-color: rgb(0 0 0 / 0.04);
|
|
877
|
+
}
|
|
842
878
|
}
|
|
843
879
|
|
|
844
880
|
.q-tab:disabled,
|
|
@@ -928,6 +964,63 @@ html.dark body {
|
|
|
928
964
|
}
|
|
929
965
|
}
|
|
930
966
|
|
|
967
|
+
@keyframes q-tab-rise {
|
|
968
|
+
from {
|
|
969
|
+
opacity: 0;
|
|
970
|
+
transform: translateY(6px);
|
|
971
|
+
}
|
|
972
|
+
to {
|
|
973
|
+
opacity: 1;
|
|
974
|
+
transform: none;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/* — collapse-inactive : les tabs inactifs ne montrent que l'icône ; le tab
|
|
979
|
+
actif s'étend (icône + label) avec une largeur animée (pattern mobile
|
|
980
|
+
bottom-nav). max-width 0 → 999px : la largeur utilisée = min(naturelle, max)
|
|
981
|
+
→ le label se déplie jusqu'à sa largeur réelle, le tab suit.
|
|
982
|
+
PAS d'overflow: hidden : le texte n'est pas tronqué pendant l'ouverture —
|
|
983
|
+
il apparaît entier en FONDU (opacity), plus joli que le glissement. */
|
|
984
|
+
.q-tabs--collapse-inactive .q-tab__content {
|
|
985
|
+
gap: 0;
|
|
986
|
+
}
|
|
987
|
+
.q-tabs--collapse-inactive .q-tab__label {
|
|
988
|
+
max-width: 0;
|
|
989
|
+
opacity: 0;
|
|
990
|
+
white-space: nowrap; /* pas de retour à la ligne pendant l'ouverture */
|
|
991
|
+
transition:
|
|
992
|
+
max-width var(--q-tabs-duration, 0.3s) cubic-bezier(0.16, 1, 0.3, 1),
|
|
993
|
+
opacity var(--q-tabs-duration, 0.25s) ease;
|
|
994
|
+
}
|
|
995
|
+
.q-tabs--collapse-inactive .q-tab--active .q-tab__content {
|
|
996
|
+
gap: 6px;
|
|
997
|
+
}
|
|
998
|
+
.q-tabs--collapse-inactive .q-tab--active .q-tab__label {
|
|
999
|
+
max-width: 999px;
|
|
1000
|
+
opacity: 1;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/* Crossfade : pendant que le label du tab inactif s'estompe, celui du tab
|
|
1004
|
+
actif apparaît en fondu — simultanément. Les animations d'entrée du tab
|
|
1005
|
+
(pop/rise) sont neutralisées en mode collapse pour ne pas geler ou masquer
|
|
1006
|
+
le fade du label entrant ; l'indicateur garde son spring (transition width). */
|
|
1007
|
+
.q-tabs--collapse-inactive.q-tabs--anim-spring .q-tab--active,
|
|
1008
|
+
.q-tabs--collapse-inactive.q-tabs--anim-elastic .q-tab--active,
|
|
1009
|
+
.q-tabs--collapse-inactive.q-tabs--anim-smooth .q-tab--active .q-tab__content {
|
|
1010
|
+
animation: none;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1014
|
+
.q-tabs--animated .q-tabs__indicator,
|
|
1015
|
+
.q-tabs--anim-spring .q-tab--active,
|
|
1016
|
+
.q-tabs--anim-elastic .q-tab--active,
|
|
1017
|
+
.q-tabs--anim-smooth .q-tab--active .q-tab__content,
|
|
1018
|
+
.q-tabs--collapse-inactive .q-tab__label {
|
|
1019
|
+
transition-duration: 0s;
|
|
1020
|
+
animation: none;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
931
1024
|
.q-tab__content {
|
|
932
1025
|
display: flex;
|
|
933
1026
|
flex-direction: inherit;
|
|
@@ -4492,11 +4585,8 @@ html.dark body {
|
|
|
4492
4585
|
}
|
|
4493
4586
|
|
|
4494
4587
|
.q-bottom-sheet__header {
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
justify-content: space-between;
|
|
4498
|
-
gap: 12px;
|
|
4499
|
-
padding: 12px 16px 8px;
|
|
4588
|
+
padding: 0; /* le padding est porté par le <q-toolbar> embarqué */
|
|
4589
|
+
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
4500
4590
|
}
|
|
4501
4591
|
|
|
4502
4592
|
.q-bottom-sheet__header-text {
|
|
@@ -4546,11 +4636,17 @@ html.dark body {
|
|
|
4546
4636
|
}
|
|
4547
4637
|
|
|
4548
4638
|
.q-bottom-sheet__footer {
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4639
|
+
padding: 0; /* le padding est porté par le <q-toolbar> embarqué */
|
|
4640
|
+
border-top: 1px solid rgb(0 0 0 / 0.08);
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
.q-bottom-sheet__footer .q-toolbar {
|
|
4644
|
+
justify-content: flex-end; /* actions alignées à droite (défaut) */
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
.q-bottom-sheet__panel--dark .q-bottom-sheet__header,
|
|
4648
|
+
.q-bottom-sheet__panel--dark .q-bottom-sheet__footer {
|
|
4649
|
+
border-color: rgb(255 255 255 / 0.12);
|
|
4554
4650
|
}
|
|
4555
4651
|
|
|
4556
4652
|
/* — transitions (slide vertical type vaul) — */
|
|
@@ -5815,14 +5911,28 @@ html.dark body {
|
|
|
5815
5911
|
|
|
5816
5912
|
/* safe-area : l'encoche en haut est gérée par le header, la barre d'accueil en bas par le footer */
|
|
5817
5913
|
.q-dialog__content--maximized .q-dialog__header,
|
|
5818
|
-
.q-dialog__content--top .q-dialog__header
|
|
5914
|
+
.q-dialog__content--top .q-dialog__header,
|
|
5915
|
+
.q-dialog__content--left .q-dialog__header,
|
|
5916
|
+
.q-dialog__content--right .q-dialog__header {
|
|
5819
5917
|
padding-top: 0;
|
|
5820
5918
|
padding-top: constant(safe-area-inset-top);
|
|
5821
5919
|
padding-top: env(safe-area-inset-top);
|
|
5822
5920
|
}
|
|
5823
5921
|
|
|
5922
|
+
/* fullscreen en paysage : l'encoche latérale passe dans le header (comme QHeader) */
|
|
5923
|
+
.q-dialog__content--maximized .q-dialog__header {
|
|
5924
|
+
padding-left: 0;
|
|
5925
|
+
padding-left: constant(safe-area-inset-left);
|
|
5926
|
+
padding-left: env(safe-area-inset-left);
|
|
5927
|
+
padding-right: 0;
|
|
5928
|
+
padding-right: constant(safe-area-inset-right);
|
|
5929
|
+
padding-right: env(safe-area-inset-right);
|
|
5930
|
+
}
|
|
5931
|
+
|
|
5824
5932
|
.q-dialog__content--maximized .q-dialog__footer,
|
|
5825
|
-
.q-dialog__content--bottom .q-dialog__footer
|
|
5933
|
+
.q-dialog__content--bottom .q-dialog__footer,
|
|
5934
|
+
.q-dialog__content--left .q-dialog__footer,
|
|
5935
|
+
.q-dialog__content--right .q-dialog__footer {
|
|
5826
5936
|
padding-bottom: 0;
|
|
5827
5937
|
padding-bottom: constant(safe-area-inset-bottom);
|
|
5828
5938
|
padding-bottom: env(safe-area-inset-bottom);
|
|
@@ -5844,11 +5954,7 @@ html.dark body {
|
|
|
5844
5954
|
position: sticky;
|
|
5845
5955
|
top: 0;
|
|
5846
5956
|
z-index: 1;
|
|
5847
|
-
|
|
5848
|
-
align-items: flex-start;
|
|
5849
|
-
justify-content: space-between;
|
|
5850
|
-
gap: 12px;
|
|
5851
|
-
padding: 16px;
|
|
5957
|
+
padding: 0; /* le padding est porté par le <q-toolbar> embarqué */
|
|
5852
5958
|
border-bottom: 1px solid rgb(0 0 0 / 0.08);
|
|
5853
5959
|
background-color: inherit; /* reste opaque pendant le scroll du body */
|
|
5854
5960
|
}
|
|
@@ -5899,15 +6005,26 @@ html.dark body {
|
|
|
5899
6005
|
position: sticky;
|
|
5900
6006
|
bottom: 0;
|
|
5901
6007
|
z-index: 1;
|
|
5902
|
-
|
|
5903
|
-
align-items: center;
|
|
5904
|
-
justify-content: flex-end;
|
|
5905
|
-
gap: 8px;
|
|
5906
|
-
padding: 12px 16px 16px;
|
|
6008
|
+
padding: 0; /* le padding est porté par le <q-toolbar> embarqué */
|
|
5907
6009
|
border-top: 1px solid rgb(0 0 0 / 0.08);
|
|
5908
6010
|
background-color: inherit;
|
|
5909
6011
|
}
|
|
5910
6012
|
|
|
6013
|
+
.q-dialog__footer .q-toolbar {
|
|
6014
|
+
justify-content: flex-end; /* actions alignées à droite (défaut) */
|
|
6015
|
+
}
|
|
6016
|
+
|
|
6017
|
+
.q-dialog__body {
|
|
6018
|
+
min-height: 0;
|
|
6019
|
+
padding: 4px 16px 16px;
|
|
6020
|
+
}
|
|
6021
|
+
|
|
6022
|
+
.q-dialog__body--scrollable {
|
|
6023
|
+
flex: 1 1 auto;
|
|
6024
|
+
overflow-y: auto;
|
|
6025
|
+
overscroll-behavior: contain;
|
|
6026
|
+
}
|
|
6027
|
+
|
|
5911
6028
|
.q-dialog__trigger {
|
|
5912
6029
|
display: inline-flex;
|
|
5913
6030
|
align-items: center;
|
|
@@ -6321,11 +6438,80 @@ html.dark body {
|
|
|
6321
6438
|
z-index: 0;
|
|
6322
6439
|
pointer-events: none;
|
|
6323
6440
|
}
|
|
6324
|
-
.q-container--bg > :not(.q-container__bg) {
|
|
6441
|
+
.q-container--bg > :not(.q-container__bg, .q-container__img) {
|
|
6325
6442
|
position: relative;
|
|
6326
6443
|
z-index: 1;
|
|
6327
6444
|
}
|
|
6328
6445
|
|
|
6446
|
+
/* — image de fond (prop background-image) : couche cover derrière le contenu — */
|
|
6447
|
+
.q-container__img {
|
|
6448
|
+
position: absolute;
|
|
6449
|
+
inset: 0;
|
|
6450
|
+
z-index: 0;
|
|
6451
|
+
width: 100%;
|
|
6452
|
+
height: 100%;
|
|
6453
|
+
object-fit: var(--q-container-img-fit, cover);
|
|
6454
|
+
pointer-events: none;
|
|
6455
|
+
user-select: none;
|
|
6456
|
+
}
|
|
6457
|
+
|
|
6458
|
+
/* taille CSS libre ("50%", "400px"…) : boîte centrée (translate séparé de
|
|
6459
|
+
transform → compose avec le Ken Burns), image entière visible */
|
|
6460
|
+
.q-container__img--sized {
|
|
6461
|
+
width: var(--q-container-img-size);
|
|
6462
|
+
height: var(--q-container-img-size);
|
|
6463
|
+
left: 50%;
|
|
6464
|
+
top: 50%;
|
|
6465
|
+
translate: -50% -50%;
|
|
6466
|
+
object-fit: contain;
|
|
6467
|
+
}
|
|
6468
|
+
|
|
6469
|
+
/* glassmorphisée : floutée + saturée + léger zoom pour masquer les bords du blur */
|
|
6470
|
+
.q-container__img--glass {
|
|
6471
|
+
filter: blur(14px) saturate(1.4);
|
|
6472
|
+
transform: scale(1.15);
|
|
6473
|
+
}
|
|
6474
|
+
|
|
6475
|
+
/* — Ken Burns indéterminé (prop background-animated) : zoom + panoramique
|
|
6476
|
+
lents qui bouclent — direction et durée configurables (défaut alternate 24s) */
|
|
6477
|
+
.q-container__img--animated {
|
|
6478
|
+
animation:
|
|
6479
|
+
q-container-kenburns var(--q-container-kenburns-duration, 24s) ease-in-out
|
|
6480
|
+
var(--q-container-kenburns-direction, alternate) infinite;
|
|
6481
|
+
will-change: transform;
|
|
6482
|
+
}
|
|
6483
|
+
@keyframes q-container-kenburns {
|
|
6484
|
+
0% {
|
|
6485
|
+
transform: scale(1.06) translate(0%, 0%);
|
|
6486
|
+
}
|
|
6487
|
+
30% {
|
|
6488
|
+
transform: scale(1.18) translate(-2%, -1.5%);
|
|
6489
|
+
}
|
|
6490
|
+
60% {
|
|
6491
|
+
transform: scale(1.14) translate(2%, 1.5%);
|
|
6492
|
+
}
|
|
6493
|
+
100% {
|
|
6494
|
+
transform: scale(1.22) translate(-1%, 2%);
|
|
6495
|
+
}
|
|
6496
|
+
}
|
|
6497
|
+
|
|
6498
|
+
/* — glassmorphism du conteneur (prop glass) : fond translucide + flou de ce
|
|
6499
|
+
qui est derrière + bordure claire (variables personnalisables) — */
|
|
6500
|
+
.q-container--glass {
|
|
6501
|
+
--q-container-glass-bg: rgb(255 255 255 / 0.14);
|
|
6502
|
+
--q-container-glass-border: rgb(255 255 255 / 0.3);
|
|
6503
|
+
--q-container-glass-blur: 14px;
|
|
6504
|
+
background-color: var(--q-container-glass-bg);
|
|
6505
|
+
border: 1px solid var(--q-container-glass-border);
|
|
6506
|
+
-webkit-backdrop-filter: blur(var(--q-container-glass-blur)) saturate(1.4);
|
|
6507
|
+
backdrop-filter: blur(var(--q-container-glass-blur)) saturate(1.4);
|
|
6508
|
+
box-shadow: 0 8px 32px rgb(0 0 0 / 0.12);
|
|
6509
|
+
}
|
|
6510
|
+
.dark .q-container--glass {
|
|
6511
|
+
--q-container-glass-bg: rgb(255 255 255 / 0.06);
|
|
6512
|
+
--q-container-glass-border: rgb(255 255 255 / 0.14);
|
|
6513
|
+
}
|
|
6514
|
+
|
|
6329
6515
|
/* grid : grille de carrés qui s'estompe vers les bords */
|
|
6330
6516
|
.q-container__bg--grid {
|
|
6331
6517
|
background-image:
|
|
@@ -6537,6 +6723,9 @@ html.dark body {
|
|
|
6537
6723
|
.q-container__wave {
|
|
6538
6724
|
animation: none;
|
|
6539
6725
|
}
|
|
6726
|
+
.q-container__img--animated {
|
|
6727
|
+
animation: none;
|
|
6728
|
+
}
|
|
6540
6729
|
}
|
|
6541
6730
|
|
|
6542
6731
|
/* ===== QMarquee — bandeau défilant infini ===== */
|