@dnax/ui 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,15 @@
1
- // fixedLayout — offset automatique des barres fixed (q-header, q-back-header)
2
- // et du q-page qui les suit.
1
+ // fixedLayout — offset automatique des barres fixed (q-header, q-back-header,
2
+ // q-footer) et du q-page qui les entoure.
3
3
  //
4
4
  // Problème : les barres fixed sortent du flux (position: fixed) → le contenu de
5
5
  // q-page passe dessous. Ce module :
6
6
  // - q-page (mode "page") reçoit un padding-top = hauteur cumulée des barres
7
- // fixed qui le précèdent (le contenu ne se retrouve plus sous le header)
7
+ // fixed HAUT qui le précèdent ET un padding-bottom = hauteur cumulée des
8
+ // barres fixed BAS qui le suivent (le contenu n'est masqué ni en haut ni en
9
+ // bas au scroll)
8
10
  // - chaque barre fixed suivante (mode "bar") s'empile sous la précédente (top)
11
+ // - chaque barre fixed basse (mode "bar-bottom") s'empile au-dessus de la
12
+ // suivante (bottom)
9
13
  //
10
14
  // Les barres fixed sont cherchées dans TOUT le sous-arbre de l'enveloppe q-app
11
15
  // (pas seulement parmi les frères directs) et ordonnées par position dans le
@@ -15,7 +19,12 @@
15
19
  import { onBeforeUnmount, onMounted, watch } from "vue"
16
20
  import type { Ref } from "vue"
17
21
 
18
- export const FIXED_BAR_SELECTOR = ".q-header--fixed, .q-back-header--fixed"
22
+ /** Barres fixed en haut de l'écran */
23
+ export const FIXED_TOP_SELECTOR = ".q-header--fixed, .q-back-header--fixed"
24
+ /** Barres fixed en bas de l'écran */
25
+ export const FIXED_BOTTOM_SELECTOR = ".q-footer--fixed"
26
+ /** Toutes les barres fixed (haut + bas) */
27
+ export const FIXED_BAR_SELECTOR = `${FIXED_TOP_SELECTOR}, ${FIXED_BOTTOM_SELECTOR}`
19
28
 
20
29
  /** Racine du layout : le .q-app le plus proche, sinon le parent direct. */
21
30
  function layoutRoot(el: HTMLElement): HTMLElement | null {
@@ -23,13 +32,13 @@ function layoutRoot(el: HTMLElement): HTMLElement | null {
23
32
  }
24
33
 
25
34
  /**
26
- * Hauteur cumulée des barres fixed qui précèdent `el` dans l'ordre du document,
27
- * sous la même enveloppe q-app. `el` lui-même (cas mode "bar") est exclu.
35
+ * Hauteur cumulée des barres fixed HAUT qui précèdent `el` dans l'ordre du
36
+ * document, sous la même enveloppe q-app. `el` lui-même (cas mode "bar") est exclu.
28
37
  */
29
38
  export function fixedBarsHeightBefore(el: HTMLElement): number {
30
39
  const root = layoutRoot(el)
31
40
  if (!root) return 0
32
- const bars = Array.from(root.querySelectorAll<HTMLElement>(FIXED_BAR_SELECTOR))
41
+ const bars = Array.from(root.querySelectorAll<HTMLElement>(FIXED_TOP_SELECTOR))
33
42
  let total = 0
34
43
  for (const bar of bars) {
35
44
  if (bar === el) continue
@@ -41,6 +50,26 @@ export function fixedBarsHeightBefore(el: HTMLElement): number {
41
50
  return total
42
51
  }
43
52
 
53
+ /**
54
+ * Hauteur cumulée des barres fixed BAS qui SUIVENT `el` dans l'ordre du
55
+ * document, sous la même enveloppe q-app. `el` lui-même (cas mode "bar-bottom")
56
+ * est exclu.
57
+ */
58
+ export function fixedBarsHeightAfter(el: HTMLElement): number {
59
+ const root = layoutRoot(el)
60
+ if (!root) return 0
61
+ const bars = Array.from(root.querySelectorAll<HTMLElement>(FIXED_BOTTOM_SELECTOR))
62
+ let total = 0
63
+ for (const bar of bars) {
64
+ if (bar === el) continue
65
+ // DOCUMENT_POSITION_PRECEDING = la barre est APRÈS el dans le document
66
+ if (bar.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING) {
67
+ total += bar.offsetHeight
68
+ }
69
+ }
70
+ return total
71
+ }
72
+
44
73
  /** Toutes les barres fixed sous l'enveloppe q-app (pour l'observation). */
45
74
  function fixedBarsInRoot(el: HTMLElement): HTMLElement[] {
46
75
  const root = layoutRoot(el)
@@ -49,15 +78,19 @@ function fixedBarsInRoot(el: HTMLElement): HTMLElement[] {
49
78
  }
50
79
 
51
80
  /**
52
- * Applique l'offset lié aux barres fixed qui précèdent `el` :
53
- * - mode "page" : padding-top = hauteur cumulée (q-page) + variable --q-page-offset
54
- * - mode "bar" : top = hauteur cumulée (empilement des barres fixed)
81
+ * Applique l'offset lié aux barres fixed qui entourent `el` :
82
+ * - mode "page" : padding-top = barres HAUT précédentes,
83
+ * padding-bottom = barres BAS suivantes
84
+ * (+ variables --q-page-offset / --q-page-offset-bottom)
85
+ * - mode "bar" : top = hauteur cumulée (empilement des barres fixed HAUT)
86
+ * - mode "bar-bottom" : bottom = hauteur cumulée (empilement des footers fixed
87
+ * depuis le bas de l'écran)
55
88
  *
56
89
  * `enabled` permet de n'activer que dans certaines conditions (ex. props.fixed).
57
90
  */
58
91
  export function useFixedBarOffset(
59
92
  el: Ref<HTMLElement | null>,
60
- mode: "page" | "bar",
93
+ mode: "page" | "bar" | "bar-bottom",
61
94
  enabled: () => boolean = () => true,
62
95
  ) {
63
96
  let ro: ResizeObserver | null = null
@@ -75,14 +108,22 @@ export function useFixedBarOffset(
75
108
  const apply = () => {
76
109
  const node = el.value
77
110
  if (!node || !enabled()) return
78
- const h = fixedBarsHeightBefore(node)
79
111
  if (mode === "page") {
80
- node.style.setProperty("--q-page-offset", h ? `${h}px` : "0px")
81
- node.style.paddingTop = h ? `${h}px` : ""
112
+ const top = fixedBarsHeightBefore(node)
113
+ const bottom = fixedBarsHeightAfter(node)
114
+ node.style.setProperty("--q-page-offset", top ? `${top}px` : "0px")
115
+ node.style.setProperty("--q-page-offset-bottom", bottom ? `${bottom}px` : "0px")
116
+ node.style.paddingTop = top ? `${top}px` : ""
117
+ node.style.paddingBottom = bottom ? `${bottom}px` : ""
82
118
  }
83
- else {
119
+ else if (mode === "bar") {
120
+ const h = fixedBarsHeightBefore(node)
84
121
  node.style.top = h ? `${h}px` : ""
85
122
  }
123
+ else {
124
+ const h = fixedBarsHeightAfter(node)
125
+ node.style.bottom = h ? `${h}px` : ""
126
+ }
86
127
  }
87
128
 
88
129
  const clear = () => {
@@ -90,11 +131,16 @@ export function useFixedBarOffset(
90
131
  if (!node) return
91
132
  if (mode === "page") {
92
133
  node.style.removeProperty("--q-page-offset")
134
+ node.style.removeProperty("--q-page-offset-bottom")
93
135
  node.style.paddingTop = ""
136
+ node.style.paddingBottom = ""
94
137
  }
95
- else {
138
+ else if (mode === "bar") {
96
139
  node.style.top = ""
97
140
  }
141
+ else {
142
+ node.style.bottom = ""
143
+ }
98
144
  }
99
145
 
100
146
  onMounted(() => {
package/lib/platform.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // $q.platform — détection de plateforme (API groupée sous `is`)
2
2
  // + breakpoints réactifs (VueUse useBreakpoints, tailles Quasar).
3
+ // API alignée sur Quasar : https://quasar.dev/options/platform-detection
3
4
  import { useBreakpoints } from "@vueuse/core"
4
5
 
5
6
  export interface QPlatformIs {
@@ -9,6 +10,8 @@ export interface QPlatformIs {
9
10
  desktop: boolean
10
11
  /** mobile ET Capacitor/Cordova (app native) */
11
12
  nativeMobile: boolean
13
+ /** nom du wrapper natif : 'cordova' | 'capacitor' | undefined */
14
+ nativeMobileWrapper?: "cordova" | "capacitor"
12
15
  ios: boolean
13
16
  android: boolean
14
17
  iphone: boolean
@@ -22,12 +25,40 @@ export interface QPlatformIs {
22
25
  firefox: boolean
23
26
  opera: boolean
24
27
  safari: boolean
28
+ vivaldi: boolean
25
29
  ie: boolean
30
+ /** moteur WebKit (Safari, Chrome, Edge…) */
31
+ webkit: boolean
32
+ /** extension navigateur (BEX — Chrome/Firefox) */
33
+ bex: boolean
34
+ /** Chrome OS (Chromebook) */
35
+ cros: boolean
36
+ blackberry: boolean
37
+ winphone: boolean
38
+ /** Amazon Silk (Kindle) */
39
+ silk: boolean
26
40
  cordova: boolean
27
41
  capacitor: boolean
28
42
  electron: boolean
43
+ /** écran tactile */
29
44
  touch: boolean
45
+ /** pointeur précis (souris) disponible */
30
46
  mouse: boolean
47
+ /** nom du navigateur : 'chrome' | 'firefox' | 'safari' | … | 'generic' */
48
+ name: string
49
+ /** version complète du navigateur ('70.0.3538.110') */
50
+ version: string
51
+ /** version majeure du navigateur (70, ou -1 si inconnue) */
52
+ versionNumber: number
53
+ /** nom de l'OS : 'mac' | 'win' | 'linux' | 'ios' | 'android' | 'cros' | … */
54
+ platform: string
55
+ }
56
+
57
+ export interface QPlatformHas {
58
+ /** écran tactile capable */
59
+ touch: boolean
60
+ /** localStorage / sessionStorage disponibles */
61
+ webStorage: boolean
31
62
  }
32
63
 
33
64
  export interface QPlatform {
@@ -38,25 +69,111 @@ export interface QPlatform {
38
69
  isClient: boolean
39
70
  /** booléens de détection groupés : $q.platform.is.mobile, is.ios, … */
40
71
  is: QPlatformIs
72
+ /** capacités : has.touch, has.webStorage */
73
+ has: QPlatformHas
41
74
  within: { iframe: boolean }
42
75
  }
43
76
 
77
+ function emptyIs(): QPlatformIs {
78
+ return {
79
+ mobile: false,
80
+ tablet: false,
81
+ desktop: false,
82
+ nativeMobile: false,
83
+ nativeMobileWrapper: undefined,
84
+ ios: false,
85
+ android: false,
86
+ iphone: false,
87
+ ipad: false,
88
+ ipod: false,
89
+ mac: false,
90
+ win: false,
91
+ linux: false,
92
+ chrome: false,
93
+ edge: false,
94
+ firefox: false,
95
+ opera: false,
96
+ safari: false,
97
+ vivaldi: false,
98
+ ie: false,
99
+ webkit: false,
100
+ bex: false,
101
+ cros: false,
102
+ blackberry: false,
103
+ winphone: false,
104
+ silk: false,
105
+ cordova: false,
106
+ capacitor: false,
107
+ electron: false,
108
+ touch: false,
109
+ mouse: false,
110
+ name: "",
111
+ version: "",
112
+ versionNumber: -1,
113
+ platform: "",
114
+ }
115
+ }
116
+
117
+ /** Nom + version du navigateur depuis l'UA (ordre = spécificité) */
118
+ function detectBrowser(ua: string) {
119
+ const m = (re: RegExp) => ua.match(re)?.[1] ?? ""
120
+ let name = "generic"
121
+ let version = ""
122
+
123
+ if (/vivaldi\/([\d.]+)/i.test(ua)) {
124
+ name = "vivaldi"
125
+ version = m(/vivaldi\/([\d.]+)/i)
126
+ }
127
+ else if (/edg\/([\d.]+)/i.test(ua)) {
128
+ name = "edge"
129
+ version = m(/edg\/([\d.]+)/i)
130
+ }
131
+ else if (/edge\/([\d.]+)/i.test(ua)) {
132
+ name = "edge"
133
+ version = m(/edge\/([\d.]+)/i)
134
+ }
135
+ else if (/opr\/([\d.]+)/i.test(ua)) {
136
+ name = "opera"
137
+ version = m(/opr\/([\d.]+)/i)
138
+ }
139
+ else if (/opera\/([\d.]+)/i.test(ua)) {
140
+ name = "opera"
141
+ version = m(/opera\/([\d.]+)/i)
142
+ }
143
+ else if (/(?:chrome|crios)\/([\d.]+)/i.test(ua)) {
144
+ name = "chrome"
145
+ version = m(/(?:chrome|crios)\/([\d.]+)/i)
146
+ }
147
+ else if (/(?:firefox|fxios)\/([\d.]+)/i.test(ua)) {
148
+ name = "firefox"
149
+ version = m(/(?:firefox|fxios)\/([\d.]+)/i)
150
+ }
151
+ else if (/msie ([\d.]+)/i.test(ua)) {
152
+ name = "ie"
153
+ version = m(/msie ([\d.]+)/i)
154
+ }
155
+ else if (/trident\/.*rv:([\d.]+)/i.test(ua)) {
156
+ name = "ie"
157
+ version = m(/trident\/.*rv:([\d.]+)/i)
158
+ }
159
+ else if (/version\/([\d.]+)/i.test(ua)) {
160
+ name = "safari"
161
+ version = m(/version\/([\d.]+)/i)
162
+ }
163
+
164
+ const versionNumber = version ? parseInt(version.split(".")[0]!, 10) || -1 : -1
165
+ return { name, version, versionNumber }
166
+ }
167
+
44
168
  function detect(): QPlatform {
45
169
  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
170
  return {
55
171
  userAgent: "",
56
172
  platform: "",
57
173
  isServer: true,
58
174
  isClient: false,
59
- is: empty,
175
+ is: emptyIs(),
176
+ has: { touch: false, webStorage: false },
60
177
  within: { iframe: false },
61
178
  }
62
179
  }
@@ -71,17 +188,54 @@ function detect(): QPlatform {
71
188
  // iPad modernes : MacIntel + écran tactile multi-points
72
189
  const ipad = /ipad/i.test(ua) || (/macintosh|macintel/i.test(platform) && (nav.maxTouchPoints ?? 0) > 1)
73
190
  const android = /android/i.test(ua)
191
+ const blackberry = /blackberry/i.test(ua)
192
+ const winphone = /windows phone/i.test(ua)
193
+ // Chromebooks : navigator.platform vaut "CrOS x86_64" (ou "Linux x86_64" ancien)
194
+ const cros = /cros/i.test(platform)
195
+ const silk = /silk/i.test(ua)
74
196
 
75
197
  const touch = "ontouchstart" in window || (nav.maxTouchPoints ?? 0) > 0
76
198
  const mobileUA = /mobile|iphone|ipod|android|windows phone|blackberry/i.test(ua)
77
199
  const tablet = ipad || (android && !mobileUA)
78
200
  const mobile = mobileUA || (tablet ? false : touch && /android|ipad|iphone/i.test(ua))
79
201
 
202
+ const cordova = "cordova" in window
203
+ const capacitor = !!(window as any).Capacitor?.isNativePlatform?.()
204
+ const nativeMobileWrapper: "cordova" | "capacitor" | undefined = capacitor
205
+ ? "capacitor"
206
+ : cordova
207
+ ? "cordova"
208
+ : undefined
209
+
210
+ // nom de l'OS (miroir de l'API Quasar : is.platform)
211
+ let osName = "generic"
212
+ if (android) osName = "android"
213
+ else if (ios) osName = "ios"
214
+ else if (blackberry) osName = "blackberry"
215
+ else if (winphone) osName = "winphone"
216
+ else if (cros) osName = "cros"
217
+ else if (silk) osName = "silk"
218
+ else if (/mac|iphone|ipad|ipod/i.test(ua)) osName = "mac"
219
+ else if (/win/i.test(ua)) osName = "win"
220
+ else if (/linux/i.test(platform)) osName = "linux"
221
+
222
+ let webStorage = false
223
+ try {
224
+ webStorage = typeof window.localStorage !== "undefined"
225
+ }
226
+ catch {
227
+ webStorage = false
228
+ }
229
+
230
+ const browser = detectBrowser(ua)
231
+ const notChromium = !/(edg|opr|vivaldi)/i.test(ua)
232
+
80
233
  const is: QPlatformIs = {
81
234
  mobile,
82
235
  tablet,
83
236
  desktop: !mobileUA && !tablet,
84
- nativeMobile: false, // affiné après (Capacitor/Cordova)
237
+ nativeMobile: mobile && (capacitor || cordova),
238
+ nativeMobileWrapper,
85
239
  ios,
86
240
  android,
87
241
  iphone,
@@ -90,28 +244,40 @@ function detect(): QPlatform {
90
244
  mac: /mac|iphone|ipad|ipod/i.test(ua),
91
245
  win: /win/i.test(ua),
92
246
  linux: /linux/i.test(platform),
93
- chrome: /chrome|crios/i.test(ua) && !/edg/i.test(ua),
94
- edge: /edg/i.test(ua),
247
+ chrome: /chrome|crios/i.test(ua) && notChromium,
248
+ edge: /edg|edge/i.test(ua),
95
249
  firefox: /firefox|fxios/i.test(ua),
96
250
  opera: /opr|opera/i.test(ua),
97
- safari: /safari/i.test(ua) && !/chrome|crios|edg|opr/i.test(ua),
251
+ safari: /safari/i.test(ua) && notChromium && !/chrome|crios/i.test(ua),
252
+ vivaldi: /vivaldi/i.test(ua),
98
253
  ie: /msie|trident/i.test(ua),
99
- cordova: typeof window !== "undefined" && "cordova" in window,
100
- capacitor: typeof window !== "undefined" && !!(window as any).Capacitor?.isNativePlatform?.(),
254
+ webkit: /applewebkit/i.test(ua),
255
+ bex:
256
+ typeof window !== "undefined" &&
257
+ !!((window as any).chrome?.runtime?.id || (globalThis as any).browser?.runtime?.id),
258
+ cros,
259
+ blackberry,
260
+ winphone,
261
+ silk,
262
+ cordova,
263
+ capacitor,
101
264
  electron: /electron/i.test(ua),
102
265
  touch,
103
- mouse: typeof window.matchMedia === "function" && window.matchMedia("(pointer: fine)").matches,
266
+ mouse:
267
+ typeof window.matchMedia === "function" && window.matchMedia("(pointer: fine)").matches,
268
+ name: browser.name,
269
+ version: browser.version,
270
+ versionNumber: browser.versionNumber,
271
+ platform: osName,
104
272
  }
105
273
 
106
- // nativeMobile : mobile + Capacitor/Cordova
107
- is.nativeMobile = is.mobile && (is.capacitor || is.cordova)
108
-
109
274
  return {
110
275
  userAgent: ua,
111
276
  platform,
112
277
  isServer: false,
113
278
  isClient: true,
114
279
  is,
280
+ has: { touch, webStorage },
115
281
  within: { iframe: window.self !== window.top },
116
282
  }
117
283
  }
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.10",
3
+ "version": "0.0.12",
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,