@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.
- package/README.md +15 -0
- package/components/QAccordion.vue +68 -0
- package/components/QAccordionContent.vue +62 -0
- package/components/QAccordionItem.vue +24 -0
- package/components/QAccordionTrigger.vue +51 -0
- package/components/QActionSheet.vue +159 -0
- package/components/QApp.vue +8 -0
- package/components/QAutocomplete.vue +316 -0
- package/components/QAvatar.vue +92 -0
- package/components/QBackHeader.vue +69 -0
- package/components/QBackTop.vue +125 -0
- package/components/QBadge.vue +55 -0
- package/components/QBottomSheet.vue +177 -0
- package/components/QBottomSheetFooter.vue +9 -0
- package/components/QBottomSheetHeader.vue +35 -0
- package/components/QBottomSheetProvider.vue +62 -0
- package/components/QBottomSheetTrigger.vue +18 -0
- package/components/QBtn.vue +144 -0
- package/components/QBtnGroup.vue +31 -0
- package/components/QBubble.vue +36 -0
- package/components/QBubbleContent.vue +28 -0
- package/components/QBubbleGroup.vue +25 -0
- package/components/QBubbleReactions.vue +40 -0
- package/components/QCard.vue +63 -0
- package/components/QCardActions.vue +27 -0
- package/components/QCardContent.vue +29 -0
- package/components/QCarousel.vue +115 -0
- package/components/QCarouselContent.vue +23 -0
- package/components/QCarouselItem.vue +20 -0
- package/components/QCarouselNav.vue +51 -0
- package/components/QCheckbox.vue +146 -0
- package/components/QChip.vue +91 -0
- package/components/QCircularProgress.vue +122 -0
- package/components/QCol.vue +52 -0
- package/components/QCollapse.vue +141 -0
- package/components/QConfigProvider.vue +164 -0
- package/components/QContainer.vue +201 -0
- package/components/QCountDown.vue +164 -0
- package/components/QCountryPicker.vue +593 -0
- package/components/QDataGrid.vue +491 -0
- package/components/QDatePicker.vue +250 -0
- package/components/QDialog.vue +163 -0
- package/components/QDialogFooter.vue +9 -0
- package/components/QDialogHeader.vue +41 -0
- package/components/QDialogProvider.vue +65 -0
- package/components/QDialogTrigger.vue +18 -0
- package/components/QFab.vue +80 -0
- package/components/QFabAction.vue +48 -0
- package/components/QFilePicker.vue +294 -0
- package/components/QFooter.vue +72 -0
- package/components/QGallery.vue +280 -0
- package/components/QGrid.vue +54 -0
- package/components/QHeader.vue +38 -0
- package/components/QIcon.vue +53 -0
- package/components/QImagePicker.vue +248 -0
- package/components/QImagePreview.vue +225 -0
- package/components/QImagePreviewProvider.vue +35 -0
- package/components/QImg.vue +142 -0
- package/components/QInfiniteScroll.vue +109 -0
- package/components/QInnerLoading.vue +67 -0
- package/components/QInput.vue +289 -0
- package/components/QInputOtp.vue +238 -0
- package/components/QInputPassword.vue +67 -0
- package/components/QInputTag.vue +281 -0
- package/components/QIntersection.vue +119 -0
- package/components/QItem.vue +60 -0
- package/components/QItemSection.vue +47 -0
- package/components/QLinearProgress.vue +94 -0
- package/components/QList.vue +39 -0
- package/components/QLoading.vue +77 -0
- package/components/QLoadingProvider.vue +41 -0
- package/components/QMarquee.vue +56 -0
- package/components/QMessageScroller.vue +10 -0
- package/components/QMessageScrollerButton.vue +48 -0
- package/components/QMessageScrollerContent.vue +31 -0
- package/components/QMessageScrollerItem.vue +49 -0
- package/components/QMessageScrollerProvider.vue +198 -0
- package/components/QMessageScrollerViewport.vue +109 -0
- package/components/QNavMenu.vue +75 -0
- package/components/QNavMenuContent.vue +10 -0
- package/components/QNavMenuItem.vue +60 -0
- package/components/QNavMenuTrigger.vue +96 -0
- package/components/QNotifyProvider.vue +33 -0
- package/components/QNotifyToast.vue +60 -0
- package/components/QPage.vue +56 -0
- package/components/QPagination.vue +195 -0
- package/components/QParallax.vue +124 -0
- package/components/QPullToRefresh.vue +138 -0
- package/components/QRadio.vue +99 -0
- package/components/QRating.vue +105 -0
- package/components/QReorder.vue +191 -0
- package/components/QRollingText.vue +225 -0
- package/components/QRow.vue +23 -0
- package/components/QSafeArea.vue +39 -0
- package/components/QScrollArea.vue +118 -0
- package/components/QSelect.vue +698 -0
- package/components/QSeparator.vue +39 -0
- package/components/QSidebar.vue +153 -0
- package/components/QSidebarContent.vue +14 -0
- package/components/QSidebarFooter.vue +14 -0
- package/components/QSidebarHeader.vue +14 -0
- package/components/QSidebarMenu.vue +9 -0
- package/components/QSidebarMenuButton.vue +59 -0
- package/components/QSidebarMenuItem.vue +9 -0
- package/components/QSidebarTrigger.vue +32 -0
- package/components/QSkeleton.vue +95 -0
- package/components/QSlider.vue +205 -0
- package/components/QSpace.vue +10 -0
- package/components/QSpinner.vue +172 -0
- package/components/QSplitter.vue +145 -0
- package/components/QSticky.vue +181 -0
- package/components/QSwipeCell.vue +217 -0
- package/components/QSwiper.vue +102 -0
- package/components/QSwiperSlide.vue +19 -0
- package/components/QSyntax.vue +171 -0
- package/components/QTab.vue +175 -0
- package/components/QTabPanel.vue +50 -0
- package/components/QTabPanels.vue +106 -0
- package/components/QTable.vue +575 -0
- package/components/QTabs.vue +188 -0
- package/components/QText.vue +316 -0
- package/components/QTextCaption.vue +122 -0
- package/components/QToolbar.vue +28 -0
- package/components/QTooltip.vue +313 -0
- package/components/QUploader.vue +473 -0
- package/components/QVideo.vue +289 -0
- package/components/QVirtualScroll.vue +190 -0
- package/components/internal/QDateCalendar.vue +153 -0
- package/index.ts +151 -0
- package/lib/breakpoints.ts +44 -0
- package/lib/colors.ts +24 -0
- package/lib/config.ts +34 -0
- package/lib/countries.ts +261 -0
- package/lib/formula.ts +630 -0
- package/lib/icons.ts +38 -0
- package/lib/loading.ts +75 -0
- package/lib/overlayBack.ts +124 -0
- package/lib/platform.ts +146 -0
- package/lib/primitive.ts +25 -0
- package/lib/q.ts +287 -0
- package/lib/screen.ts +105 -0
- package/lib/search.ts +35 -0
- package/lib/themeVars.ts +22 -0
- package/lib/useComponentProps.ts +54 -0
- package/lib/utils.ts +7 -0
- package/module.ts +89 -0
- package/package.json +41 -0
- package/scripts/generate-exports.ts +56 -0
- package/styles/main.css +7627 -0
- 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
|
+
}
|
package/lib/platform.ts
ADDED
|
@@ -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
|
+
}
|
package/lib/primitive.ts
ADDED
|
@@ -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 }
|