@dnax/ui 0.0.1

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.
Files changed (150) hide show
  1. package/README.md +15 -0
  2. package/components/QAccordion.vue +68 -0
  3. package/components/QAccordionContent.vue +62 -0
  4. package/components/QAccordionItem.vue +24 -0
  5. package/components/QAccordionTrigger.vue +51 -0
  6. package/components/QActionSheet.vue +159 -0
  7. package/components/QApp.vue +8 -0
  8. package/components/QAutocomplete.vue +316 -0
  9. package/components/QAvatar.vue +92 -0
  10. package/components/QBackHeader.vue +69 -0
  11. package/components/QBackTop.vue +125 -0
  12. package/components/QBadge.vue +55 -0
  13. package/components/QBottomSheet.vue +177 -0
  14. package/components/QBottomSheetFooter.vue +9 -0
  15. package/components/QBottomSheetHeader.vue +35 -0
  16. package/components/QBottomSheetProvider.vue +62 -0
  17. package/components/QBottomSheetTrigger.vue +18 -0
  18. package/components/QBtn.vue +144 -0
  19. package/components/QBtnGroup.vue +31 -0
  20. package/components/QBubble.vue +36 -0
  21. package/components/QBubbleContent.vue +28 -0
  22. package/components/QBubbleGroup.vue +25 -0
  23. package/components/QBubbleReactions.vue +40 -0
  24. package/components/QCard.vue +63 -0
  25. package/components/QCardActions.vue +27 -0
  26. package/components/QCardContent.vue +29 -0
  27. package/components/QCarousel.vue +115 -0
  28. package/components/QCarouselContent.vue +23 -0
  29. package/components/QCarouselItem.vue +20 -0
  30. package/components/QCarouselNav.vue +51 -0
  31. package/components/QCheckbox.vue +146 -0
  32. package/components/QChip.vue +91 -0
  33. package/components/QCircularProgress.vue +122 -0
  34. package/components/QCol.vue +52 -0
  35. package/components/QCollapse.vue +141 -0
  36. package/components/QConfigProvider.vue +164 -0
  37. package/components/QContainer.vue +201 -0
  38. package/components/QCountDown.vue +164 -0
  39. package/components/QCountryPicker.vue +593 -0
  40. package/components/QDataGrid.vue +491 -0
  41. package/components/QDatePicker.vue +250 -0
  42. package/components/QDialog.vue +163 -0
  43. package/components/QDialogFooter.vue +9 -0
  44. package/components/QDialogHeader.vue +41 -0
  45. package/components/QDialogProvider.vue +65 -0
  46. package/components/QDialogTrigger.vue +18 -0
  47. package/components/QFab.vue +80 -0
  48. package/components/QFabAction.vue +48 -0
  49. package/components/QFilePicker.vue +294 -0
  50. package/components/QFooter.vue +72 -0
  51. package/components/QGallery.vue +280 -0
  52. package/components/QGrid.vue +54 -0
  53. package/components/QHeader.vue +38 -0
  54. package/components/QIcon.vue +53 -0
  55. package/components/QImagePicker.vue +248 -0
  56. package/components/QImagePreview.vue +225 -0
  57. package/components/QImagePreviewProvider.vue +35 -0
  58. package/components/QImg.vue +142 -0
  59. package/components/QInfiniteScroll.vue +109 -0
  60. package/components/QInnerLoading.vue +67 -0
  61. package/components/QInput.vue +289 -0
  62. package/components/QInputOtp.vue +238 -0
  63. package/components/QInputPassword.vue +67 -0
  64. package/components/QInputTag.vue +281 -0
  65. package/components/QIntersection.vue +119 -0
  66. package/components/QItem.vue +60 -0
  67. package/components/QItemSection.vue +47 -0
  68. package/components/QLinearProgress.vue +94 -0
  69. package/components/QList.vue +39 -0
  70. package/components/QLoading.vue +77 -0
  71. package/components/QLoadingProvider.vue +41 -0
  72. package/components/QMarquee.vue +56 -0
  73. package/components/QMessageScroller.vue +10 -0
  74. package/components/QMessageScrollerButton.vue +48 -0
  75. package/components/QMessageScrollerContent.vue +31 -0
  76. package/components/QMessageScrollerItem.vue +49 -0
  77. package/components/QMessageScrollerProvider.vue +198 -0
  78. package/components/QMessageScrollerViewport.vue +109 -0
  79. package/components/QNavMenu.vue +75 -0
  80. package/components/QNavMenuContent.vue +10 -0
  81. package/components/QNavMenuItem.vue +60 -0
  82. package/components/QNavMenuTrigger.vue +96 -0
  83. package/components/QNotifyProvider.vue +33 -0
  84. package/components/QNotifyToast.vue +60 -0
  85. package/components/QPage.vue +56 -0
  86. package/components/QPagination.vue +195 -0
  87. package/components/QParallax.vue +124 -0
  88. package/components/QPullToRefresh.vue +138 -0
  89. package/components/QRadio.vue +99 -0
  90. package/components/QRating.vue +105 -0
  91. package/components/QReorder.vue +191 -0
  92. package/components/QRollingText.vue +225 -0
  93. package/components/QRow.vue +23 -0
  94. package/components/QSafeArea.vue +39 -0
  95. package/components/QScrollArea.vue +118 -0
  96. package/components/QSelect.vue +698 -0
  97. package/components/QSeparator.vue +39 -0
  98. package/components/QSidebar.vue +153 -0
  99. package/components/QSidebarContent.vue +14 -0
  100. package/components/QSidebarFooter.vue +14 -0
  101. package/components/QSidebarHeader.vue +14 -0
  102. package/components/QSidebarMenu.vue +9 -0
  103. package/components/QSidebarMenuButton.vue +59 -0
  104. package/components/QSidebarMenuItem.vue +9 -0
  105. package/components/QSidebarTrigger.vue +32 -0
  106. package/components/QSkeleton.vue +95 -0
  107. package/components/QSlider.vue +205 -0
  108. package/components/QSpace.vue +10 -0
  109. package/components/QSpinner.vue +172 -0
  110. package/components/QSplitter.vue +145 -0
  111. package/components/QSticky.vue +181 -0
  112. package/components/QSwipeCell.vue +217 -0
  113. package/components/QSwiper.vue +102 -0
  114. package/components/QSwiperSlide.vue +19 -0
  115. package/components/QSyntax.vue +171 -0
  116. package/components/QTab.vue +175 -0
  117. package/components/QTabPanel.vue +50 -0
  118. package/components/QTabPanels.vue +106 -0
  119. package/components/QTable.vue +575 -0
  120. package/components/QTabs.vue +188 -0
  121. package/components/QText.vue +316 -0
  122. package/components/QTextCaption.vue +122 -0
  123. package/components/QToolbar.vue +28 -0
  124. package/components/QTooltip.vue +313 -0
  125. package/components/QUploader.vue +473 -0
  126. package/components/QVideo.vue +289 -0
  127. package/components/QVirtualScroll.vue +190 -0
  128. package/components/internal/QDateCalendar.vue +153 -0
  129. package/index.ts +151 -0
  130. package/lib/breakpoints.ts +44 -0
  131. package/lib/colors.ts +24 -0
  132. package/lib/config.ts +34 -0
  133. package/lib/countries.ts +261 -0
  134. package/lib/formula.ts +630 -0
  135. package/lib/icons.ts +38 -0
  136. package/lib/loading.ts +75 -0
  137. package/lib/overlayBack.ts +124 -0
  138. package/lib/platform.ts +146 -0
  139. package/lib/primitive.ts +25 -0
  140. package/lib/q.ts +287 -0
  141. package/lib/screen.ts +105 -0
  142. package/lib/search.ts +35 -0
  143. package/lib/themeVars.ts +22 -0
  144. package/lib/useComponentProps.ts +54 -0
  145. package/lib/utils.ts +7 -0
  146. package/module.ts +89 -0
  147. package/package.json +41 -0
  148. package/scripts/generate-exports.ts +56 -0
  149. package/styles/main.css +7627 -0
  150. package/tsconfig.json +33 -0
package/lib/loading.ts ADDED
@@ -0,0 +1,75 @@
1
+ // $q.loading — overlay de chargement plein écran façon Quasar
2
+ // (https://quasar.dev/quasar-plugins/loading) : $q.loading.show(opts) / hide() / isActive.
3
+ import { reactive, ref } from "vue"
4
+ import type { Ref } from "vue"
5
+
6
+ export interface LoadingOptions {
7
+ /** Message sous le spinner */
8
+ message?: string
9
+ /** Taille du spinner en px (défaut : 48) */
10
+ spinnerSize?: number
11
+ /** Couleur du spinner : token (primary…) ou hex (défaut : primary) */
12
+ spinnerColor?: string
13
+ /** Couleur du message (défaut : #1d1d1d) */
14
+ messageColor?: string
15
+ /** Fond de l'overlay (défaut : rgb(0 0 0 / 0.3)) */
16
+ backgroundColor?: string
17
+ /** Délai avant affichage en ms (défaut : 0) */
18
+ delay?: number
19
+ /** show()/hide() comptés — hide() ne cache que quand tous les show sont retirés */
20
+ group?: boolean
21
+ /** Icône Iconify affichée à la place du spinner (ex. : "lucide:loader-circle") */
22
+ icon?: string
23
+ /** Couleur de l'icône (défaut : spinnerColor) */
24
+ iconColor?: string
25
+ /** Garde la carte (fond blanc + ombre) même avec une icône (défaut : transparent) */
26
+ boxed?: boolean
27
+ }
28
+
29
+ export interface QLoadingPlugin {
30
+ show: (options?: LoadingOptions) => void
31
+ hide: () => void
32
+ /** true tant que le loading est affiché */
33
+ isActive: Ref<boolean>
34
+ }
35
+
36
+ const visible = ref(false)
37
+ const opts = reactive<Required<Pick<LoadingOptions, "spinnerSize" | "spinnerColor" | "messageColor" | "backgroundColor" | "delay" | "group">> & LoadingOptions>({
38
+ spinnerSize: 48,
39
+ spinnerColor: "primary",
40
+ messageColor: "#1d1d1d",
41
+ backgroundColor: "rgb(0 0 0 / 0.3)",
42
+ delay: 0,
43
+ group: false,
44
+ icon: undefined,
45
+ iconColor: undefined,
46
+ boxed: false,
47
+ })
48
+
49
+ let groupCount = 0
50
+ let timer: ReturnType<typeof setTimeout> | undefined
51
+
52
+ function show(options?: LoadingOptions) {
53
+ if (options) Object.assign(opts, options)
54
+ if (opts.group) groupCount++
55
+ if (timer) clearTimeout(timer)
56
+ timer = setTimeout(() => {
57
+ visible.value = true
58
+ }, opts.delay ?? 0)
59
+ }
60
+
61
+ function hide() {
62
+ if (opts.group) {
63
+ groupCount = Math.max(0, groupCount - 1)
64
+ if (groupCount > 0) return
65
+ }
66
+ if (timer) clearTimeout(timer)
67
+ visible.value = false
68
+ }
69
+
70
+ export const loading: QLoadingPlugin & { _opts: typeof opts } = {
71
+ show,
72
+ hide,
73
+ isActive: visible,
74
+ _opts: opts,
75
+ }
@@ -0,0 +1,124 @@
1
+ // overlayBack — ferme l'overlay ouvert quand l'utilisateur appuie sur « retour »
2
+ // (bouton back du navigateur, geste iOS, bouton back Android via Capacitor).
3
+ //
4
+ // Le principe : chaque composant overlay (dialog, bottom sheet, action sheet,
5
+ // image preview, sidebar, select/date/country en mode sheet/modal/dialog…)
6
+ // s'enregistre dans une pile LIFO tant qu'il est ouvert, via le composable
7
+ // useOverlayBack(). Un handler global écoute popstate en phase de capture (avant
8
+ // le listener de vue-router) : si un overlay est ouvert, il restaure l'URL
9
+ // courante (history.pushState) et ferme l'overlay au lieu de naviguer.
10
+ //
11
+ // Installation automatique côté Nuxt (module) ou manuelle :
12
+ // import { installOverlayBackHandler } from "@dnax/ui"
13
+ // installOverlayBackHandler() // ou avec un router : installOverlayBackHandler(router)
14
+ import { onBeforeUnmount, watch } from "vue"
15
+ import type { Ref } from "vue"
16
+ import type { Router } from "vue-router"
17
+
18
+ export interface OverlayHandle {
19
+ /** Ferme l'overlay (set v-model=false, close(), dismiss()…) */
20
+ close: () => void
21
+ /** Nom de l'overlay pour le debug (ex. "QDialog") */
22
+ label?: string
23
+ }
24
+
25
+ // — Pile LIFO des overlays ouverts (module-level, client-side) —
26
+ const overlayStack: OverlayHandle[] = []
27
+
28
+ /** Enregistre un overlay ouvert — retourne un unregister */
29
+ export function registerOverlay(handle: OverlayHandle): () => void {
30
+ overlayStack.push(handle)
31
+ return () => unregisterOverlay(handle)
32
+ }
33
+
34
+ export function unregisterOverlay(handle: OverlayHandle) {
35
+ const idx = overlayStack.indexOf(handle)
36
+ if (idx !== -1) overlayStack.splice(idx, 1)
37
+ }
38
+
39
+ export function hasOpenOverlays(): boolean {
40
+ return overlayStack.length > 0
41
+ }
42
+
43
+ /** Ferme le dernier overlay ouvert (le plus haut de la pile) */
44
+ export function closeTopmostOverlay(): boolean {
45
+ const top = overlayStack[overlayStack.length - 1]
46
+ if (!top) return false
47
+ top.close()
48
+ return true
49
+ }
50
+
51
+ /**
52
+ * Composable pour les composants overlay : s'enregistre dans la pile quand
53
+ * `open` devient vrai, se désenregistre quand il se ferme ou à l'unmount.
54
+ *
55
+ * useOverlayBack(open, () => { open.value = false }, "QDialog")
56
+ */
57
+ export function useOverlayBack(
58
+ open: Ref<boolean>,
59
+ close: () => void,
60
+ label?: string,
61
+ ) {
62
+ let unregister: (() => void) | null = null
63
+
64
+ const sync = (v: boolean) => {
65
+ if (v) {
66
+ unregister?.()
67
+ unregister = registerOverlay({ close, label })
68
+ }
69
+ else {
70
+ unregister?.()
71
+ unregister = null
72
+ }
73
+ }
74
+
75
+ watch(open, sync, { immediate: true })
76
+ onBeforeUnmount(() => unregister?.())
77
+ }
78
+
79
+ // — Handler global « retour » (popstate) —
80
+ let installed = false
81
+
82
+ /**
83
+ * Installe le handler qui ferme l'overlay ouvert sur « retour ».
84
+ * - Sans router : restaure l'URL courante (pushState) pour annuler le back.
85
+ * - Avec router : en plus, une garde beforeEach bloque la navigation quand le
86
+ * popstate a été consommé par un overlay.
87
+ *
88
+ * Idempotent (un seul handler par session). Retourne une fonction de désinstallation.
89
+ */
90
+ export function installOverlayBackHandler(router?: Router): () => void {
91
+ if (typeof window === "undefined" || installed) return () => {}
92
+
93
+ let consumedPop = false
94
+
95
+ const onPopState = () => {
96
+ if (!hasOpenOverlays()) return
97
+ // Annule le back du navigateur : on re-pousse l'URL courante avant que le
98
+ // listener (bubble) de vue-router ne lise la nouvelle URL → navigation no-op.
99
+ consumedPop = true
100
+ history.pushState(null, "", window.location.href)
101
+ closeTopmostOverlay()
102
+ }
103
+ // Phase de capture : s'exécute avant le listener bubble du router.
104
+ window.addEventListener("popstate", onPopState, true)
105
+
106
+ let removeGuard = () => {}
107
+ if (router) {
108
+ removeGuard = router.beforeEach(() => {
109
+ if (consumedPop) {
110
+ consumedPop = false
111
+ return false // navigation annulée (l'URL a déjà été restaurée)
112
+ }
113
+ return true
114
+ })
115
+ }
116
+
117
+ installed = true
118
+
119
+ return () => {
120
+ window.removeEventListener("popstate", onPopState, true)
121
+ removeGuard()
122
+ installed = false
123
+ }
124
+ }
@@ -0,0 +1,146 @@
1
+ // $q.platform — détection de plateforme (API groupée sous `is`)
2
+ // + breakpoints réactifs (VueUse useBreakpoints, tailles Quasar).
3
+ import { useBreakpoints } from "@vueuse/core"
4
+
5
+ export interface QPlatformIs {
6
+ /** appareil mobile (web) */
7
+ mobile: boolean
8
+ tablet: boolean
9
+ desktop: boolean
10
+ /** mobile ET Capacitor/Cordova (app native) */
11
+ nativeMobile: boolean
12
+ ios: boolean
13
+ android: boolean
14
+ iphone: boolean
15
+ ipad: boolean
16
+ ipod: boolean
17
+ mac: boolean
18
+ win: boolean
19
+ linux: boolean
20
+ chrome: boolean
21
+ edge: boolean
22
+ firefox: boolean
23
+ opera: boolean
24
+ safari: boolean
25
+ ie: boolean
26
+ cordova: boolean
27
+ capacitor: boolean
28
+ electron: boolean
29
+ touch: boolean
30
+ mouse: boolean
31
+ }
32
+
33
+ export interface QPlatform {
34
+ userAgent: string
35
+ /** navigator.platform ("iPhone", "Linux armv81", …) */
36
+ platform: string
37
+ isServer: boolean
38
+ isClient: boolean
39
+ /** booléens de détection groupés : $q.platform.is.mobile, is.ios, … */
40
+ is: QPlatformIs
41
+ within: { iframe: boolean }
42
+ }
43
+
44
+ function detect(): QPlatform {
45
+ 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
+ return {
55
+ userAgent: "",
56
+ platform: "",
57
+ isServer: true,
58
+ isClient: false,
59
+ is: empty,
60
+ within: { iframe: false },
61
+ }
62
+ }
63
+
64
+ const ua = navigator.userAgent
65
+ const nav = navigator as Navigator & { maxTouchPoints?: number; platform?: string }
66
+ const platform = nav.platform ?? ""
67
+
68
+ const ios = /iphone|ipad|ipod/i.test(ua)
69
+ const iphone = /iphone/i.test(ua)
70
+ const ipod = /ipod/i.test(ua)
71
+ // iPad modernes : MacIntel + écran tactile multi-points
72
+ const ipad = /ipad/i.test(ua) || (/macintosh|macintel/i.test(platform) && (nav.maxTouchPoints ?? 0) > 1)
73
+ const android = /android/i.test(ua)
74
+
75
+ const touch = "ontouchstart" in window || (nav.maxTouchPoints ?? 0) > 0
76
+ const mobileUA = /mobile|iphone|ipod|android|windows phone|blackberry/i.test(ua)
77
+ const tablet = ipad || (android && !mobileUA)
78
+ const mobile = mobileUA || (tablet ? false : touch && /android|ipad|iphone/i.test(ua))
79
+
80
+ const is: QPlatformIs = {
81
+ mobile,
82
+ tablet,
83
+ desktop: !mobileUA && !tablet,
84
+ nativeMobile: false, // affiné après (Capacitor/Cordova)
85
+ ios,
86
+ android,
87
+ iphone,
88
+ ipad,
89
+ ipod,
90
+ mac: /mac|iphone|ipad|ipod/i.test(ua),
91
+ win: /win/i.test(ua),
92
+ linux: /linux/i.test(platform),
93
+ chrome: /chrome|crios/i.test(ua) && !/edg/i.test(ua),
94
+ edge: /edg/i.test(ua),
95
+ firefox: /firefox|fxios/i.test(ua),
96
+ opera: /opr|opera/i.test(ua),
97
+ safari: /safari/i.test(ua) && !/chrome|crios|edg|opr/i.test(ua),
98
+ ie: /msie|trident/i.test(ua),
99
+ cordova: typeof window !== "undefined" && "cordova" in window,
100
+ capacitor: typeof window !== "undefined" && !!(window as any).Capacitor?.isNativePlatform?.(),
101
+ electron: /electron/i.test(ua),
102
+ touch,
103
+ mouse: typeof window.matchMedia === "function" && window.matchMedia("(pointer: fine)").matches,
104
+ }
105
+
106
+ // nativeMobile : mobile + Capacitor/Cordova
107
+ is.nativeMobile = is.mobile && (is.capacitor || is.cordova)
108
+
109
+ return {
110
+ userAgent: ua,
111
+ platform,
112
+ isServer: false,
113
+ isClient: true,
114
+ is,
115
+ within: { iframe: window.self !== window.top },
116
+ }
117
+ }
118
+
119
+ /** Détection statique par session (l'UA ne change pas) — SSR-safe */
120
+ export const platform: QPlatform = detect()
121
+
122
+ // — Breakpoints réactifs (tailles Quasar) —
123
+ const QUASAR_BREAKPOINTS = { xs: 0, sm: 600, md: 1024, lg: 1440, xl: 1920 }
124
+
125
+ export const breakpoints = useBreakpoints(QUASAR_BREAKPOINTS)
126
+
127
+ export const qBreakpoints = {
128
+ /** ref réactives : true quand la largeur ≥ le seuil */
129
+ isXs: breakpoints.xs,
130
+ isSm: breakpoints.sm,
131
+ isMd: breakpoints.md,
132
+ isLg: breakpoints.lg,
133
+ isXl: breakpoints.xl,
134
+ /** comparaisons */
135
+ isBelowSm: breakpoints.isSmaller("sm"),
136
+ isBelowMd: breakpoints.isSmaller("md"),
137
+ isBelowLg: breakpoints.isSmaller("lg"),
138
+ isBelowXl: breakpoints.isSmaller("xl"),
139
+ isAboveSm: breakpoints.isGreaterOrEqual("sm"),
140
+ isAboveMd: breakpoints.isGreaterOrEqual("md"),
141
+ isAboveLg: breakpoints.isGreaterOrEqual("lg"),
142
+ isAboveXl: breakpoints.isGreaterOrEqual("xl"),
143
+ between: breakpoints.between.bind(breakpoints),
144
+ /** largeur de fenêtre (pixels) */
145
+ width: breakpoints.current,
146
+ }
@@ -0,0 +1,25 @@
1
+ import { cloneVNode, defineComponent, h, mergeProps } from "vue"
2
+ import type { PropType } from "vue"
3
+
4
+ /**
5
+ * Rendu polymorphe façon reka-ui Primitive : `as` (élément/composant) ou
6
+ * `as-child` (fusion des attributs/classes sur l'enfant du slot par défaut).
7
+ * Les classes/listeners passent par les attrs et sont fusionnés.
8
+ */
9
+ export const QPrimitive = defineComponent({
10
+ name: "QPrimitive",
11
+ props: {
12
+ as: { type: [String, Object] as PropType<string | object>, default: "div" },
13
+ asChild: Boolean,
14
+ },
15
+ setup(props, { slots, attrs }) {
16
+ return () => {
17
+ const children = slots.default?.()
18
+ if (props.asChild && children && children.length > 0) {
19
+ const node = children[0]!
20
+ return cloneVNode(node, mergeProps(node.props ?? {}, attrs))
21
+ }
22
+ return h(props.as as never, { ...attrs }, children)
23
+ }
24
+ },
25
+ })
package/lib/q.ts ADDED
@@ -0,0 +1,287 @@
1
+ // $q — plugin global style Quasar : dialogues programmatiques.
2
+ // $q.dialog.open({ component, componentProps, ... }) pousse un dialogue dans la
3
+ // pile (singleton) ; <q-dialog-provider> le rend et le retire à la fermeture.
4
+ // Installation (optionnel, pattern Quasar) :
5
+ // app.use(QPlugin) → accès $q via this.$q / getCurrentInstance().proxy.$q
6
+ // ou import direct :
7
+ // import { $q } from "@dnax/ui" → $q.dialog.open(...)
8
+ import { ref } from "vue"
9
+ import { h } from "vue"
10
+ import type { App, Component, Ref } from "vue"
11
+ import { toast } from "vue-sonner"
12
+ import type { ExternalToast } from "vue-sonner"
13
+ import QNotifyToast from "../components/QNotifyToast.vue"
14
+ import { platform, qBreakpoints } from "./platform"
15
+ import { screen } from "./screen"
16
+ import { loading } from "./loading"
17
+
18
+ export interface DialogOptions {
19
+ /** Composant de dialogue (SFC importé) ou nom de composant global */
20
+ component: Component | string
21
+ /** Props passées au composant */
22
+ componentProps?: Record<string, any>
23
+ /** Titre affiché par QDialogHeader (optionnel — sinon le composant rend son propre header) */
24
+ title?: string
25
+ description?: string
26
+ /** true = plein écran ; string = largeur CSS ("90%") */
27
+ fullscreen?: boolean | string
28
+ /** Classe appliquée au contenu du dialogue (q-dialog__content) */
29
+ class?: string
30
+ /** Ne se ferme ni au backdrop ni à Échap */
31
+ persistent?: boolean
32
+ }
33
+
34
+ export interface DialogController {
35
+ onOK: (cb: (data?: any) => void | Promise<void>) => DialogController
36
+ onCancel: (cb: () => void | Promise<void>) => DialogController
37
+ onDismiss: (cb: () => void | Promise<void>) => DialogController
38
+ /** Résolveurs enregistrés (consommés par QDialogProvider) */
39
+ _resolvers: {
40
+ ok?: (data?: any) => void | Promise<void>
41
+ cancel?: () => void | Promise<void>
42
+ dismiss?: () => void | Promise<void>
43
+ }
44
+ _opts: DialogOptions
45
+ }
46
+
47
+ // — Pile de dialogues (singleton module-level, client-side) —
48
+ const dialogs: Ref<DialogController[]> = ref([])
49
+
50
+ function openDialog(opts: DialogOptions): DialogController {
51
+ const ctrl: DialogController = {
52
+ _resolvers: {},
53
+ _opts: opts,
54
+ onOK(cb) {
55
+ ctrl._resolvers.ok = cb
56
+ return ctrl
57
+ },
58
+ onCancel(cb) {
59
+ ctrl._resolvers.cancel = cb
60
+ return ctrl
61
+ },
62
+ onDismiss(cb) {
63
+ ctrl._resolvers.dismiss = cb
64
+ return ctrl
65
+ },
66
+ }
67
+ dialogs.value.push(ctrl)
68
+ return ctrl
69
+ }
70
+
71
+ /** Retire un dialogue de la pile (appelé par QDialogProvider à la fermeture) */
72
+ function closeDialog(ctrl: DialogController) {
73
+ const idx = dialogs.value.indexOf(ctrl)
74
+ if (idx !== -1) dialogs.value.splice(idx, 1)
75
+ }
76
+
77
+ export interface BottomSheetOptions {
78
+ /** Composant à afficher (SFC importé) ou nom de composant global */
79
+ component: Component | string
80
+ /** Props passées au composant */
81
+ componentProps?: Record<string, any>
82
+ /** Titre affiché par QBottomSheetHeader (optionnel) */
83
+ title?: string
84
+ description?: string
85
+ /** Largeur max du panneau (défaut : 640px) */
86
+ width?: string
87
+ /** Hauteur du panneau (sinon max-height 90vh) */
88
+ height?: string
89
+ /** Arrondi des coins hauts : true | false | valeur CSS */
90
+ rounded?: boolean | string
91
+ dark?: boolean
92
+ /** Fond translucide (frosted glass) : true = 70%, ou valeur % */
93
+ translucent?: boolean | number
94
+ /** Ne se ferme ni au backdrop ni à Échap */
95
+ persistent?: boolean
96
+ /** Seuil de drag (px) au-delà duquel on ferme */
97
+ dragThreshold?: number
98
+ }
99
+
100
+ export interface BottomSheetController {
101
+ onOK: (cb: (data?: any) => void | Promise<void>) => BottomSheetController
102
+ onCancel: (cb: () => void | Promise<void>) => BottomSheetController
103
+ onDismiss: (cb: () => void | Promise<void>) => BottomSheetController
104
+ _resolvers: {
105
+ ok?: (data?: any) => void | Promise<void>
106
+ cancel?: () => void | Promise<void>
107
+ dismiss?: () => void | Promise<void>
108
+ }
109
+ _opts: BottomSheetOptions
110
+ }
111
+
112
+ // — Pile de bottom sheets (singleton module-level, client-side) —
113
+ const bottomSheets: Ref<BottomSheetController[]> = ref([])
114
+
115
+ function openBottomSheet(opts: BottomSheetOptions): BottomSheetController {
116
+ const ctrl: BottomSheetController = {
117
+ _resolvers: {},
118
+ _opts: opts,
119
+ onOK(cb) {
120
+ ctrl._resolvers.ok = cb
121
+ return ctrl
122
+ },
123
+ onCancel(cb) {
124
+ ctrl._resolvers.cancel = cb
125
+ return ctrl
126
+ },
127
+ onDismiss(cb) {
128
+ ctrl._resolvers.dismiss = cb
129
+ return ctrl
130
+ },
131
+ }
132
+ bottomSheets.value.push(ctrl)
133
+ return ctrl
134
+ }
135
+
136
+ function closeBottomSheet(ctrl: BottomSheetController) {
137
+ const idx = bottomSheets.value.indexOf(ctrl)
138
+ if (idx !== -1) bottomSheets.value.splice(idx, 1)
139
+ }
140
+
141
+ export interface ImagePreviewOptions {
142
+ /** URLs d'images — string[] ou { src, title? }[] */
143
+ images: (string | { src: string; title?: string })[]
144
+ /** Image initiale */
145
+ index?: number
146
+ /** Transition d'ouverture : fade | up | down | zoom */
147
+ transition?: "fade" | "up" | "down" | "zoom"
148
+ closeBtn?: boolean
149
+ counter?: boolean
150
+ closeOnBackdrop?: boolean
151
+ closeOnSwipeDown?: boolean
152
+ nav?: boolean
153
+ /** Rappel à la fermeture (comme onDismiss des dialogs) */
154
+ onDismiss?: () => void
155
+ }
156
+
157
+ export interface ImagePreviewController {
158
+ /** Va à l'image d'index donné */
159
+ goTo: (index: number) => void
160
+ _opts: ImagePreviewOptions
161
+ }
162
+
163
+ // — Pile de visionneuses (singleton module-level, client-side) —
164
+ const imagePreviews: Ref<ImagePreviewController[]> = ref([])
165
+
166
+ function openImagePreview(opts: ImagePreviewOptions): ImagePreviewController {
167
+ const ctrl: ImagePreviewController = {
168
+ _opts: opts,
169
+ goTo(index) {
170
+ ctrl._opts.index = Math.max(0, index)
171
+ },
172
+ }
173
+ imagePreviews.value.push(ctrl)
174
+ return ctrl
175
+ }
176
+
177
+ /** Retire une visionneuse de la pile (appelé par QImagePreviewProvider à la fermeture) */
178
+ function closeImagePreview(ctrl: ImagePreviewController) {
179
+ const idx = imagePreviews.value.indexOf(ctrl)
180
+ if (idx !== -1) imagePreviews.value.splice(idx, 1)
181
+ }
182
+
183
+ export interface NotifyOptions {
184
+ /** Message principal */
185
+ message: string
186
+ /** Sous-texte (caption Quasar) */
187
+ caption?: string
188
+ /** Icône Iconify (ex. : "lucide:info") */
189
+ icon?: string
190
+ /** Couleur de fond : token (primary, negative…) ou hex */
191
+ color?: string
192
+ /** Type → couleur riche sonner (success, error, warning, info) */
193
+ type?: "default" | "success" | "error" | "warning" | "info"
194
+ /** Position (défaut : bottom-right, celle du QNotifyProvider) */
195
+ position?: ExternalToast["position"]
196
+ /** Durée d'affichage en ms (défaut : 4000) */
197
+ timeout?: number
198
+ /** Bouton(s) d'action — la première est affichée */
199
+ actions?: { label: string; handler: () => void }[]
200
+ }
201
+
202
+ export interface NotifyController {
203
+ /** Ferme la notification immédiatement */
204
+ dismiss: () => void
205
+ }
206
+
207
+ function notify(opts: NotifyOptions): NotifyController {
208
+ const { message, caption, icon, color, type = "default", position, timeout, actions } = opts
209
+ const action = actions?.[0]
210
+ const data: ExternalToast = {
211
+ description: caption,
212
+ duration: timeout ?? 4000,
213
+ position,
214
+ icon: icon ? h(icon) : undefined,
215
+ }
216
+
217
+ let id: string | number | undefined
218
+
219
+ if (color && type === "default") {
220
+ // Couleur Quasar custom → rendu custom (QNotifyToast)
221
+ id = toast.custom(QNotifyToast, {
222
+ ...data,
223
+ componentProps: {
224
+ message,
225
+ caption,
226
+ icon,
227
+ color,
228
+ actionLabel: action?.label,
229
+ onAction: action ? () => action.handler() : undefined,
230
+ onDismiss: () => toast.dismiss(id),
231
+ },
232
+ })
233
+ }
234
+ else {
235
+ const toasts = {
236
+ success: toast.success,
237
+ error: toast.error,
238
+ warning: toast.warning,
239
+ info: toast.info,
240
+ default: toast,
241
+ } as const
242
+ id = toasts[type](message, {
243
+ ...data,
244
+ action: action ? { label: action.label, onClick: () => action.handler() } : undefined,
245
+ })
246
+ }
247
+
248
+ return { dismiss: () => toast.dismiss(id) }
249
+ }
250
+
251
+ /** API publique : $q.dialog / $q.bottomSheet / $q.notify / $q.imagePreview / $q.platform / $q.breakpoints / $q.screen / $q.loading */
252
+ export const $q = {
253
+ dialog: {
254
+ open: openDialog,
255
+ },
256
+ bottomSheet: {
257
+ open: openBottomSheet,
258
+ },
259
+ imagePreview: {
260
+ open: openImagePreview,
261
+ },
262
+ notify,
263
+ platform,
264
+ breakpoints: qBreakpoints,
265
+ screen,
266
+ loading,
267
+ }
268
+
269
+ /** Accès programmatique aux plugins ($q.dialog, $q.notify, $q.loading…) */
270
+ export function usePlugin() {
271
+ return $q
272
+ }
273
+
274
+ /** @deprecated Utilisez usePlugin() — conservé pour rétrocompatibilité */
275
+ export const useQ = usePlugin
276
+
277
+ /** Plugin Vue : app.use(QPlugin) → this.$q / proxy.$q (pattern Quasar) */
278
+ export const QPlugin = {
279
+ install(app: App) {
280
+ app.config.globalProperties.$q = $q
281
+ },
282
+ }
283
+
284
+ // — Internes consommés par QDialogProvider / QBottomSheetProvider / QImagePreviewProvider —
285
+ export { dialogs as dialogStack, closeDialog }
286
+ export { bottomSheets as bottomSheetStack, closeBottomSheet }
287
+ export { imagePreviews as imagePreviewStack, closeImagePreview }