@dnax/ui 0.0.9 → 0.0.11

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.
@@ -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
  }
@@ -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>
@@ -113,6 +113,18 @@ onBeforeUnmount(() => {
113
113
  if (typeof document !== "undefined") document.removeEventListener("keydown", onDocKeydown)
114
114
  })
115
115
 
116
+ // Verrouille le scroll du body quand la sidebar offcanvas est ouverte : le body
117
+ // ne bouge plus en arrière-plan (et le swipe ne part pas sur la page)
118
+ watch(
119
+ () => !isStatic.value && open.value,
120
+ (locked) => {
121
+ if (typeof document !== "undefined") {
122
+ document.body.style.overflow = locked ? "hidden" : ""
123
+ }
124
+ },
125
+ { immediate: true },
126
+ )
127
+
116
128
  const rootClasses = computed(() =>
117
129
  cn(
118
130
  "q-sidebar",
@@ -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
@@ -126,7 +126,7 @@ export { default as QUploader } from "./components/QUploader.vue"
126
126
  export { default as QVideo } from "./components/QVideo.vue"
127
127
  export { default as QVirtualScroll } from "./components/QVirtualScroll.vue"
128
128
 
129
- export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet } from "./lib/q"
129
+ export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet, useDialogPluginComponent } from "./lib/q"
130
130
  export type {
131
131
  DialogOptions,
132
132
  DialogController,
package/lib/q.ts CHANGED
@@ -5,9 +5,9 @@
5
5
  // app.use(QPlugin) → accès $q via this.$q / getCurrentInstance().proxy.$q
6
6
  // ou import direct :
7
7
  // import { $q } from "@dnax/ui" → $q.dialog.open(...)
8
- import { ref } from "vue"
8
+ import { inject, ref } from "vue"
9
9
  import { h } from "vue"
10
- import type { App, Component, Ref } from "vue"
10
+ import type { App, Component, InjectionKey, Ref } from "vue"
11
11
  import { toast } from "vue-sonner"
12
12
  import type { ExternalToast } from "vue-sonner"
13
13
  import QNotifyToast from "../components/QNotifyToast.vue"
@@ -44,6 +44,39 @@ export interface DialogController {
44
44
  _opts: DialogOptions
45
45
  }
46
46
 
47
+ // — Contexte plugin dialog (pattern Quasar) —
48
+ // Le composant passé à $q.dialog.open({ component }) DOIT contenir un
49
+ // <q-dialog v-model="open" @hide="onDialogHide"> à sa racine ; il pilote
50
+ // l'ouverture via le composable useDialogPluginComponent().
51
+ export interface DialogPluginContext {
52
+ /** Ref d'ouverture (v-model du <q-dialog> du composant) */
53
+ open: Ref<boolean>
54
+ /** Ferme en validant — le résolveur onOK est appelé */
55
+ onOK: (data?: unknown) => void
56
+ /** Ferme en annulant — le résolveur onCancel est appelé */
57
+ onCancel: () => void
58
+ /** Fermé par backdrop / Échap / × — le résolveur onDismiss est appelé */
59
+ onHide: () => void
60
+ }
61
+
62
+ export const qDialogPluginKey: InjectionKey<DialogPluginContext> = Symbol("q-dialog-plugin")
63
+
64
+ /**
65
+ * Composable Quasar-style pour les composants passés à $q.dialog.open().
66
+ * Le composant doit rendre un <q-dialog v-model="open" @hide="onDialogHide">
67
+ * comme racine. Expose aussi onDialogOK / onDialogCancel pour fermer en validant
68
+ * ou en annulant, et onDialogHide à brancher sur @hide du q-dialog.
69
+ */
70
+ export function useDialogPluginComponent() {
71
+ const ctx = inject(qDialogPluginKey, null)
72
+ return {
73
+ open: ctx?.open ?? ref(false),
74
+ onDialogHide: () => ctx?.onHide(),
75
+ onDialogOK: (data?: unknown) => ctx?.onOK(data),
76
+ onDialogCancel: () => ctx?.onCancel(),
77
+ }
78
+ }
79
+
47
80
  // — Pile de dialogues (singleton module-level, client-side) —
48
81
  const dialogs: Ref<DialogController[]> = ref([])
49
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
@@ -26,7 +26,7 @@ const exports = files
26
26
 
27
27
  // Exports manuels (composables / helpers) — conservés à chaque régénération
28
28
  const manualExports = `
29
- export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet } from "./lib/q"
29
+ export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet, useDialogPluginComponent } from "./lib/q"
30
30
  export type {
31
31
  DialogOptions,
32
32
  DialogController,
package/styles/main.css CHANGED
@@ -3621,8 +3621,13 @@ html.dark body {
3621
3621
  transition:
3622
3622
  transform 0.25s ease,
3623
3623
  visibility 0.25s;
3624
- /* Scroll vertical natif autorisé, gestes horizontaux (swipe pour fermer) au JS */
3624
+ /* Scroll vertical natif autorisé, gestes horizontaux (swipe pour fermer) au JS.
3625
+ touch-action n'est PAS hérité → on l'applique à tous les descendants : le
3626
+ geste peut démarrer sur le contenu scrollable (.q-sidebar__content) sans que
3627
+ le navigateur ne prenne le swipe pour le body. */
3625
3628
  touch-action: pan-y;
3629
+ /* Empêche le scroll-chaining : swiper la sidebar ne fait plus bouger le body */
3630
+ overscroll-behavior: contain;
3626
3631
  /* Safe-area : encoche, barre d'accueil et côtés en paysage */
3627
3632
  padding-top: 0;
3628
3633
  padding-top: constant(safe-area-inset-top);
@@ -3635,6 +3640,13 @@ html.dark body {
3635
3640
  padding-left: env(safe-area-inset-left);
3636
3641
  }
3637
3642
 
3643
+ /* Tous les descendants (contenu scrollable, menu, boutons…) : gestes horizontaux
3644
+ au JS, scroll vertical natif uniquement */
3645
+ .q-sidebar--offcanvas,
3646
+ .q-sidebar--offcanvas * {
3647
+ touch-action: pan-y;
3648
+ }
3649
+
3638
3650
  .q-sidebar--offcanvas.q-sidebar--right {
3639
3651
  left: auto;
3640
3652
  right: 0;
@@ -3702,6 +3714,8 @@ html.dark body {
3702
3714
  overflow-y: auto;
3703
3715
  padding: 8px;
3704
3716
  scrollbar-width: thin;
3717
+ /* Le scroll du contenu ne doit pas se propager au body (swipe-to-close) */
3718
+ overscroll-behavior: contain;
3705
3719
  }
3706
3720
 
3707
3721
  .q-sidebar__footer {
@@ -6223,6 +6237,58 @@ html.dark body {
6223
6237
  }
6224
6238
  }
6225
6239
 
6240
+ /* swipe-* : le panneau glisse depuis un bord et y REVIENT à la fermeture
6241
+ (aller-retour, comme la sidebar) — swipe-left : vient de la droite et y
6242
+ retourne ; swipe-right : vient de la gauche et y retourne. */
6243
+ .q-dialog-swipe-left-enter-active,
6244
+ .q-dialog-swipe-left-leave-active,
6245
+ .q-dialog-swipe-right-enter-active,
6246
+ .q-dialog-swipe-right-leave-active {
6247
+ transition: opacity var(--q-dialog-duration-enter, 0.2s) ease;
6248
+ }
6249
+
6250
+ .q-dialog-swipe-left-enter-active .q-dialog__content,
6251
+ .q-dialog-swipe-left-leave-active .q-dialog__content {
6252
+ --q-dialog-swipe-x: 100vw;
6253
+ }
6254
+
6255
+ .q-dialog-swipe-right-enter-active .q-dialog__content,
6256
+ .q-dialog-swipe-right-leave-active .q-dialog__content {
6257
+ --q-dialog-swipe-x: -100vw;
6258
+ }
6259
+
6260
+ .q-dialog-swipe-left-enter-active .q-dialog__content,
6261
+ .q-dialog-swipe-right-enter-active .q-dialog__content {
6262
+ animation: q-dialog-swipe-in var(--q-dialog-duration-enter, 0.3s) cubic-bezier(0.32, 0.72, 0, 1);
6263
+ }
6264
+
6265
+ .q-dialog-swipe-left-leave-active .q-dialog__content,
6266
+ .q-dialog-swipe-right-leave-active .q-dialog__content {
6267
+ animation: q-dialog-swipe-out var(--q-dialog-duration-leave, 0.2s) ease-in forwards;
6268
+ }
6269
+
6270
+ @keyframes q-dialog-swipe-in {
6271
+ from {
6272
+ opacity: 0.6;
6273
+ transform: translateX(var(--q-dialog-swipe-x, 100vw));
6274
+ }
6275
+ to {
6276
+ opacity: 1;
6277
+ transform: none;
6278
+ }
6279
+ }
6280
+
6281
+ @keyframes q-dialog-swipe-out {
6282
+ from {
6283
+ opacity: 1;
6284
+ transform: none;
6285
+ }
6286
+ to {
6287
+ opacity: 0;
6288
+ transform: translateX(var(--q-dialog-swipe-x, 100vw));
6289
+ }
6290
+ }
6291
+
6226
6292
  /* ===== QContainer ===== */
6227
6293
  .q-container {
6228
6294
  width: 100%;