@dnax/ui 0.0.25 → 0.0.27

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.
@@ -2,8 +2,9 @@
2
2
  // QBottomSheetHeader — header du panneau façon barre d'app (équivalent QHeader) :
3
3
  // le contenu est embarqué dans un <q-toolbar> (min-height 50px, padding 0 12px),
4
4
  // avec titre + description + bouton fermer (équivalent DrawerHeader/DrawerTitle).
5
- // no-padding retire le padding horizontal du toolbar.
6
- import { inject } from "vue"
5
+ // Si un CONTENU custom est fourni (slot par défaut, sans title/description),
6
+ // il n'est PAS enveloppé dans un toolbar — l'utilisateur fournit son layout.
7
+ import { Comment, Fragment, Text, computed, inject, onUpdated, ref, useSlots } from "vue"
7
8
  import { Icon } from "@iconify/vue"
8
9
  import { icons } from "../lib/icons"
9
10
  import { qBottomSheetKey } from "./QBottomSheet.vue"
@@ -17,11 +18,44 @@ interface Props {
17
18
  noPadding?: boolean
18
19
  }
19
20
 
20
- withDefaults(defineProps<Props>(), {
21
+ const props = withDefaults(defineProps<Props>(), {
21
22
  noPadding: false,
22
23
  })
23
24
 
24
25
  const sheet = inject(qBottomSheetKey, null)
26
+
27
+ const slots = useSlots()
28
+
29
+ const refresh = ref(0)
30
+ onUpdated(() => {
31
+ refresh.value++
32
+ })
33
+
34
+ function slotHasContent(nodes: unknown[]): boolean {
35
+ for (const n of nodes as any[]) {
36
+ if (!n) continue
37
+ if (n.type === Comment) continue
38
+ if (n.type === Text) {
39
+ if (String(n.children ?? "").trim() !== "") return true
40
+ continue
41
+ }
42
+ if (n.type === Fragment) {
43
+ if (slotHasContent(n.children ?? [])) return true
44
+ continue
45
+ }
46
+ return true
47
+ }
48
+ return false
49
+ }
50
+
51
+ const isStandard = computed(
52
+ () => props.title !== undefined || props.description !== undefined,
53
+ )
54
+ const hasCustomContent = computed(() => {
55
+ void refresh.value
56
+ return slotHasContent(slots.default?.() ?? [])
57
+ })
58
+ const useCustom = computed(() => !isStandard.value && hasCustomContent.value)
25
59
  </script>
26
60
 
27
61
  <template>
@@ -30,7 +64,9 @@ const sheet = inject(qBottomSheetKey, null)
30
64
  :class="{ 'q-bottom-sheet__header--no-padding': noPadding }"
31
65
  v-bind="$attrs"
32
66
  >
33
- <q-toolbar>
67
+ <slot v-if="useCustom" />
68
+
69
+ <q-toolbar v-else>
34
70
  <div class="q-bottom-sheet__header-text">
35
71
  <h2 v-if="title" class="q-bottom-sheet__title">{{ title }}</h2>
36
72
  <slot name="title" />
@@ -2,11 +2,19 @@
2
2
  // QBottomSheetProvider — rend la pile de bottom sheets programmatiques ($q.bottomSheet).
3
3
  // Intégré dans QConfigProvider (rendu automatiquement par le plus externe),
4
4
  // utilisable aussi en autonome : <q-bottom-sheet-provider><slot /></q-bottom-sheet-provider>.
5
+ //
6
+ // Deux patterns (comme le plugin dialog) :
7
+ // - SANS title/description → le composant passé EST le sheet (racine
8
+ // <q-bottom-sheet v-model="open"> pilotée par useBottomSheetPluginComponent,
9
+ // open déjà true) — rendu tel quel via QBottomSheetHost.
10
+ // - AVEC title/description → le provider enveloppe dans un q-bottom-sheet +
11
+ // header ; le composant fournit le corps et émet @ok/@cancel/@dismiss.
5
12
  import { computed, inject } from "vue"
6
13
  import { bottomSheetStack, closeBottomSheet } from "../lib/q"
7
14
  import type { BottomSheetController } from "../lib/q"
8
15
  import { qConfigKey } from "../lib/config"
9
16
  import { themeVarsStyle } from "../lib/themeVars"
17
+ import QBottomSheetHost from "./internal/QBottomSheetHost.vue"
10
18
 
11
19
  const sheets = bottomSheetStack
12
20
 
@@ -31,34 +39,49 @@ const handleDismiss = (ctrl: BottomSheetController) => {
31
39
 
32
40
  <template>
33
41
  <slot />
34
- <q-bottom-sheet
35
- v-for="(sheet, idx) in sheets"
36
- :key="idx"
37
- :model-value="true"
38
- :persistent="sheet._opts.persistent"
39
- :width="sheet._opts.width"
40
- :height="sheet._opts.height"
41
- :rounded="sheet._opts.rounded"
42
- :dark="sheet._opts.dark"
43
- :translucent="sheet._opts.translucent"
44
- :drag-threshold="sheet._opts.dragThreshold"
45
- :transition="sheet._opts.transition"
46
- :transition-duration="sheet._opts.transitionDuration"
47
- :content-style="sheetThemeStyle"
48
- @update:model-value="(v: boolean) => !v && handleDismiss(sheet)"
49
- >
50
- <q-bottom-sheet-header
51
- v-if="sheet._opts.title || sheet._opts.description"
52
- :title="sheet._opts.title"
53
- :description="sheet._opts.description"
54
- />
55
- <component
56
- :is="sheet._opts.component"
57
- v-bind="sheet._opts.componentProps"
42
+ <template v-for="(sheet, idx) in sheets" :key="idx">
43
+ <!-- Pattern auto : le composant est lui-même un <q-bottom-sheet> -->
44
+ <q-bottom-sheet-host
45
+ v-if="!sheet._opts.title && !sheet._opts.description"
46
+ :ctrl="sheet"
47
+ :on-ok="(data?: unknown) => handleOk(sheet, data)"
48
+ :on-cancel="() => handleCancel(sheet)"
49
+ :on-hide="() => handleDismiss(sheet)"
58
50
  @ok="(data?: unknown) => handleOk(sheet, data)"
59
51
  @cancel="handleCancel(sheet)"
60
52
  @dismiss="handleDismiss(sheet)"
61
53
  @close="handleDismiss(sheet)"
62
54
  />
63
- </q-bottom-sheet>
55
+ <!-- Pattern enveloppe : le provider rend le panneau + header, le composant
56
+ fournit le corps (events legacy ok/cancel/dismiss/close) -->
57
+ <q-bottom-sheet
58
+ v-else
59
+ :model-value="true"
60
+ :persistent="sheet._opts.persistent"
61
+ :width="sheet._opts.width"
62
+ :height="sheet._opts.height"
63
+ :rounded="sheet._opts.rounded"
64
+ :dark="sheet._opts.dark"
65
+ :translucent="sheet._opts.translucent"
66
+ :drag-threshold="sheet._opts.dragThreshold"
67
+ :transition="sheet._opts.transition"
68
+ :transition-duration="sheet._opts.transitionDuration"
69
+ :content-style="sheetThemeStyle"
70
+ @update:model-value="(v: boolean) => !v && handleDismiss(sheet)"
71
+ >
72
+ <q-bottom-sheet-header
73
+ v-if="sheet._opts.title || sheet._opts.description"
74
+ :title="sheet._opts.title"
75
+ :description="sheet._opts.description"
76
+ />
77
+ <component
78
+ :is="sheet._opts.component"
79
+ v-bind="sheet._opts.componentProps"
80
+ @ok="(data?: unknown) => handleOk(sheet, data)"
81
+ @cancel="handleCancel(sheet)"
82
+ @dismiss="handleDismiss(sheet)"
83
+ @close="handleDismiss(sheet)"
84
+ />
85
+ </q-bottom-sheet>
86
+ </template>
64
87
  </template>
@@ -86,6 +86,11 @@ onMounted(() => {
86
86
  })
87
87
  onBeforeUnmount(() => {
88
88
  mql?.removeEventListener("change", updateSystem)
89
+ // Retire les variables de thème posées sur <html> (provider racine)
90
+ if (isProvidersRoot.value && typeof document !== "undefined") {
91
+ for (const key of appliedThemeKeys) document.documentElement.style.removeProperty(key)
92
+ appliedThemeKeys = []
93
+ }
89
94
  })
90
95
 
91
96
  /** Mode effectif : light | dark | system résolu */
@@ -144,6 +149,26 @@ const themeStyle = computed<Record<string, string>>(() => {
144
149
  if (isRadiusScale(global)) style["--q-radius"] = RADIUS_VALUES[global]
145
150
  return style
146
151
  })
152
+
153
+ // Thème GLOBAL (provider racine) : pose aussi les variables sur <html> — les
154
+ // overlays téléportés au body (dialogs $q.dialog, …) perdent l'héritage du div
155
+ // .q-config-provider et retomberaient sur :root (couleurs par défaut). Comme la
156
+ // classe .dark déjà posée sur <html>, les téléports héritent du thème réel.
157
+ // Les providers imbriqués (non-root) ne touchent pas <html> : leur div local prime.
158
+ let appliedThemeKeys: string[] = []
159
+ watch(
160
+ [themeStyle, isProvidersRoot],
161
+ ([style, root]) => {
162
+ if (typeof document === "undefined" || !root) return
163
+ for (const key of appliedThemeKeys) document.documentElement.style.removeProperty(key)
164
+ appliedThemeKeys = []
165
+ for (const [k, v] of Object.entries(style)) {
166
+ document.documentElement.style.setProperty(k, v)
167
+ appliedThemeKeys.push(k)
168
+ }
169
+ },
170
+ { immediate: true },
171
+ )
147
172
  </script>
148
173
 
149
174
  <template>
@@ -2,8 +2,10 @@
2
2
  // QDialogHeader — header de la modale façon barre d'app (équivalent QHeader) :
3
3
  // le contenu est embarqué dans un <q-toolbar> (min-height 50px, padding 0 12px),
4
4
  // avec titre + description + bouton fermer OPT-IN (show-close).
5
- // no-padding retire le padding horizontal du toolbar.
6
- import { inject } from "vue"
5
+ // Si un CONTENU custom est fourni (slot par défaut, sans title/description/
6
+ // show-close), il n'est PAS enveloppé dans un toolbar — l'utilisateur fournit
7
+ // son propre layout (ex. <q-toolbar>) directement dans la barre.
8
+ import { Comment, Fragment, Text, computed, inject, onUpdated, ref, useSlots } from "vue"
7
9
  import { Icon } from "@iconify/vue"
8
10
  import { icons } from "../lib/icons"
9
11
  import { qDialogKey } from "./QDialog.vue"
@@ -25,6 +27,43 @@ const props = withDefaults(defineProps<Props>(), {
25
27
  })
26
28
 
27
29
  const dialog = inject(qDialogKey, null)
30
+
31
+ const slots = useSlots()
32
+
33
+ // Force le recalcul de la détection à chaque update (le contenu du slot peut
34
+ // changer dynamiquement côté parent)
35
+ const refresh = ref(0)
36
+ onUpdated(() => {
37
+ refresh.value++
38
+ })
39
+
40
+ function slotHasContent(nodes: unknown[]): boolean {
41
+ for (const n of nodes as any[]) {
42
+ if (!n) continue
43
+ if (n.type === Comment) continue
44
+ if (n.type === Text) {
45
+ if (String(n.children ?? "").trim() !== "") return true
46
+ continue
47
+ }
48
+ if (n.type === Fragment) {
49
+ if (slotHasContent(n.children ?? [])) return true
50
+ continue
51
+ }
52
+ return true
53
+ }
54
+ return false
55
+ }
56
+
57
+ // Mode « custom » : pas de title/description/close et un contenu de slot → on
58
+ // rend le slot directement (sans toolbar d'enveloppe)
59
+ const isStandard = computed(
60
+ () => props.title !== undefined || props.description !== undefined || props.showClose,
61
+ )
62
+ const hasCustomContent = computed(() => {
63
+ void refresh.value
64
+ return slotHasContent(slots.default?.() ?? [])
65
+ })
66
+ const useCustom = computed(() => !isStandard.value && hasCustomContent.value)
28
67
  </script>
29
68
 
30
69
  <template>
@@ -33,7 +72,11 @@ const dialog = inject(qDialogKey, null)
33
72
  :class="{ 'q-dialog__header--no-padding': noPadding }"
34
73
  v-bind="$attrs"
35
74
  >
36
- <q-toolbar>
75
+ <!-- Contenu custom : l'utilisateur fournit son propre layout (q-toolbar…) -->
76
+ <slot v-if="useCustom" />
77
+
78
+ <!-- Mode standard : toolbar embarqué (title / description / close) -->
79
+ <q-toolbar v-else>
37
80
  <div class="q-dialog__header-text">
38
81
  <h2 v-if="title" class="q-dialog__title">{{ title }}</h2>
39
82
  <slot name="title" />
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ // QBottomSheetHost — host interne du $q.bottomSheet (pattern Quasar, comme
3
+ // QDialogHost) : rend le composant passé tel quel et lui fournit le contexte
4
+ // plugin. Le composant DOIT contenir un <q-bottom-sheet v-model="open"
5
+ // @hide="onDialogHide"> comme racine, piloté via
6
+ // useBottomSheetPluginComponent() — open est déjà true (pas de v-model à
7
+ // mettre). À la fermeture (@hide) ou via onDialogOK / onDialogCancel, les
8
+ // résolveurs sont appelés et l'entrée retirée.
9
+ import { provide, ref } from "vue"
10
+ import { qBottomSheetPluginKey } from "../../lib/q"
11
+ import type { BottomSheetController } from "../../lib/q"
12
+
13
+ const props = defineProps<{
14
+ ctrl: BottomSheetController
15
+ onOk: (data?: unknown) => void
16
+ onCancel: () => void
17
+ onHide: () => void
18
+ }>()
19
+
20
+ const open = ref(true)
21
+
22
+ provide(qBottomSheetPluginKey, {
23
+ open,
24
+ onDialogOK: (data?: unknown) => props.onOk(data),
25
+ onDialogCancel: () => props.onCancel(),
26
+ onDialogHide: () => props.onHide(),
27
+ })
28
+
29
+ // Le composant émet aussi les events legacy (ok/cancel/dismiss/close)
30
+ const emit = defineEmits<{
31
+ ok: [data?: unknown]
32
+ cancel: []
33
+ dismiss: []
34
+ close: []
35
+ }>()
36
+ </script>
37
+
38
+ <template>
39
+ <component
40
+ :is="props.ctrl._opts.component"
41
+ v-bind="props.ctrl._opts.componentProps"
42
+ @ok="emit('ok', $event)"
43
+ @cancel="emit('cancel')"
44
+ @dismiss="emit('dismiss')"
45
+ @close="emit('close')"
46
+ />
47
+ </template>
package/index.ts CHANGED
@@ -128,12 +128,13 @@ export { default as QUploader } from "./components/QUploader.vue"
128
128
  export { default as QVideo } from "./components/QVideo.vue"
129
129
  export { default as QVirtualScroll } from "./components/QVirtualScroll.vue"
130
130
 
131
- export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet, useDialogPluginComponent } from "./lib/q"
131
+ export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet, useDialogPluginComponent, useBottomSheetPluginComponent } from "./lib/q"
132
132
  export type {
133
133
  DialogOptions,
134
134
  DialogController,
135
135
  BottomSheetOptions,
136
136
  BottomSheetController,
137
+ BottomSheetPluginContext,
137
138
  NotifyOptions,
138
139
  NotifyController,
139
140
  } from "./lib/q"
package/lib/q.ts CHANGED
@@ -112,7 +112,7 @@ export interface BottomSheetOptions {
112
112
  component: Component | string
113
113
  /** Props passées au composant */
114
114
  componentProps?: Record<string, any>
115
- /** Titre affiché par QBottomSheetHeader (optionnel) */
115
+ /** Titre affiché par QBottomSheetHeader (optionnel — sinon le composant rend son propre header) */
116
116
  title?: string
117
117
  description?: string
118
118
  /** Largeur max du panneau (défaut : 640px) */
@@ -134,6 +134,22 @@ export interface BottomSheetOptions {
134
134
  transitionDuration?: number
135
135
  }
136
136
 
137
+ /**
138
+ * Contexte plugin bottom sheet — fourni par QBottomSheetHost quand le composant
139
+ * passé à $q.bottomSheet.open() est lui-même un <q-bottom-sheet> (pattern
140
+ * Quasar, identique à $q.dialog) : open est déjà true, pas besoin de v-model.
141
+ */
142
+ export interface BottomSheetPluginContext {
143
+ /** Ref d'ouverture (v-model du <q-bottom-sheet> du composant, déjà true) */
144
+ open: Ref<boolean>
145
+ onDialogOK: (data?: unknown) => void
146
+ onDialogCancel: () => void
147
+ onDialogHide: () => void
148
+ }
149
+
150
+ export const qBottomSheetPluginKey: InjectionKey<BottomSheetPluginContext> =
151
+ Symbol("q-bottom-sheet-plugin")
152
+
137
153
  export interface BottomSheetController {
138
154
  onOK: (cb: (data?: any) => void | Promise<void>) => BottomSheetController
139
155
  onCancel: (cb: () => void | Promise<void>) => BottomSheetController
@@ -146,6 +162,22 @@ export interface BottomSheetController {
146
162
  _opts: BottomSheetOptions
147
163
  }
148
164
 
165
+ /**
166
+ * Composable Quasar-style pour les composants passés à $q.bottomSheet.open() :
167
+ * le composant rend un <q-bottom-sheet v-model="open" @hide="onDialogHide">
168
+ * comme racine. open est déjà true (le panneau s'affiche à l'ouverture), et
169
+ * onDialogOK / onDialogCancel résolvent les callbacks du controller.
170
+ */
171
+ export function useBottomSheetPluginComponent() {
172
+ const ctx = inject(qBottomSheetPluginKey, null)
173
+ return {
174
+ open: ctx?.open ?? ref(false),
175
+ onDialogHide: () => ctx?.onDialogHide(),
176
+ onDialogOK: (data?: unknown) => ctx?.onDialogOK(data),
177
+ onDialogCancel: () => ctx?.onDialogCancel(),
178
+ }
179
+ }
180
+
149
181
  // — Pile de bottom sheets (singleton module-level, client-side) —
150
182
  const bottomSheets: Ref<BottomSheetController[]> = ref([])
151
183
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
@@ -26,12 +26,13 @@ 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, useDialogPluginComponent } from "./lib/q"
29
+ export { $q, usePlugin, useQ, QPlugin, dialogStack, closeDialog, bottomSheetStack, closeBottomSheet, useDialogPluginComponent, useBottomSheetPluginComponent } from "./lib/q"
30
30
  export type {
31
31
  DialogOptions,
32
32
  DialogController,
33
33
  BottomSheetOptions,
34
34
  BottomSheetController,
35
+ BottomSheetPluginContext,
35
36
  NotifyOptions,
36
37
  NotifyController,
37
38
  } from "./lib/q"