@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.
@@ -1,9 +1,14 @@
1
1
  <script setup lang="ts">
2
- // QBottomSheetFooter — zone d'actions en bas du panneau (équivalent DrawerFooter).
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
- <slot />
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 : titre + description + bouton fermer (équivalent DrawerHeader/DrawerTitle).
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
- <div class="q-bottom-sheet__header-text">
20
- <h2 v-if="title" class="q-bottom-sheet__title">{{ title }}</h2>
21
- <slot name="title" />
22
- <p v-if="description" class="q-bottom-sheet__description">{{ description }}</p>
23
- <slot name="description" />
24
- </div>
25
- <button
26
- type="button"
27
- class="q-bottom-sheet__close"
28
- aria-label="Fermer"
29
- @click="sheet?.setOpen(false)"
30
- >
31
- <Icon :icon="icons.x" aria-hidden="true" />
32
- </button>
33
- <slot />
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"
@@ -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 — zone d'actions en bas de la modale (équivalent DialogFooter).
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
- <slot />
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 : titre + description + bouton fermer (équivalent DialogHeader/Title/Close).
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
- <div class="q-dialog__header-text">
25
- <h2 v-if="title" class="q-dialog__title">{{ title }}</h2>
26
- <slot name="title" />
27
- <p v-if="description" class="q-dialog__description">{{ description }}</p>
28
- <slot name="description" />
29
- </div>
30
- <button
31
- v-if="showClose"
32
- type="button"
33
- class="q-dialog__close"
34
- aria-label="Fermer"
35
- @click="dialog?.setOpen(false)"
36
- >
37
- <Icon :icon="icons.x" aria-hidden="true" />
38
- </button>
39
- <slot />
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
- // Chaque entrée de la pile est rendue dans un <q-dialog> ; quand le composant
6
- // émet ok / cancel / dismiss / close, le résolveur correspondant est appelé
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 les tokens du thème
19
- // sur leur contenu pour qu'ils héritent des couleurs/radius du provider.
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
- <q-dialog
43
+ <div
42
44
  v-for="(dialog, idx) in dialogs"
43
45
  :key="idx"
44
- :model-value="true"
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-header
52
- v-if="dialog._opts.title || dialog._opts.description"
53
- :title="dialog._opts.title"
54
- :description="dialog._opts.description"
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
- </q-dialog>
58
+ </div>
65
59
  </template>
@@ -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,
@@ -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 automatique quand des barres fixed (q-header / q-back-header)
5
- // précèdent la page : le contenu reste sous les barres (jamais masqué).
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"
@@ -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
- tabs?.inlineLabel.value && "q-tab--inline-label",
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
 
@@ -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,