@dnax/ui 0.0.63 → 0.0.65
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/components/QDatePicker.vue +12 -5
- package/components/QTabs.vue +79 -2
- package/components/internal/QDateCalendar.vue +25 -23
- package/index.ts +16 -0
- package/lib/dateI18n.test.ts +91 -0
- package/lib/dateI18n.ts +85 -0
- package/lib/network.test.ts +199 -0
- package/lib/network.ts +147 -0
- package/lib/q.ts +6 -1
- package/lib/styleTokens.test.ts +32 -0
- package/lib/tabs.test.ts +35 -0
- package/lib/tabs.ts +38 -0
- package/lib/wakeLock.test.ts +219 -0
- package/lib/wakeLock.ts +202 -0
- package/package.json +1 -1
- package/runtime.ts +7 -0
- package/styles/main.css +143 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// $q.wakeLock — Screen Wake Lock : logique pure + fabrique testable hors navigateur.
|
|
2
|
+
import { describe, expect, it } from "bun:test"
|
|
3
|
+
import {
|
|
4
|
+
createWakeLock,
|
|
5
|
+
isDocumentVisible,
|
|
6
|
+
isWakeLockCapable,
|
|
7
|
+
shouldReacquire,
|
|
8
|
+
} from "./wakeLock"
|
|
9
|
+
|
|
10
|
+
class FakeSentinel {
|
|
11
|
+
released = false
|
|
12
|
+
releaseCalls = 0
|
|
13
|
+
private listeners = new Set<() => void>()
|
|
14
|
+
|
|
15
|
+
async release() {
|
|
16
|
+
this.releaseCalls++
|
|
17
|
+
this.released = true
|
|
18
|
+
for (const cb of [...this.listeners]) cb()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
addEventListener(_type: "release", cb: () => void) {
|
|
22
|
+
this.listeners.add(cb)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
removeEventListener(_type: "release", cb: () => void) {
|
|
26
|
+
this.listeners.delete(cb)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Simule le relâchement décidé par le navigateur (page masquée, veille…). */
|
|
30
|
+
autoRelease() {
|
|
31
|
+
this.released = true
|
|
32
|
+
for (const cb of [...this.listeners]) cb()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fakeApi(options: { fail?: Error } = {}) {
|
|
37
|
+
const sentinels: FakeSentinel[] = []
|
|
38
|
+
return {
|
|
39
|
+
calls: 0,
|
|
40
|
+
sentinels,
|
|
41
|
+
async request(_type: "screen") {
|
|
42
|
+
this.calls++
|
|
43
|
+
if (options.fail) throw options.fail
|
|
44
|
+
const s = new FakeSentinel()
|
|
45
|
+
sentinels.push(s)
|
|
46
|
+
return s
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fakeVisibility(state = "visible") {
|
|
52
|
+
const listeners = new Set<() => void>()
|
|
53
|
+
return {
|
|
54
|
+
visibilityState: state as string,
|
|
55
|
+
addEventListener(_type: "visibilitychange", cb: () => void) {
|
|
56
|
+
listeners.add(cb)
|
|
57
|
+
},
|
|
58
|
+
removeEventListener(_type: "visibilitychange", cb: () => void) {
|
|
59
|
+
listeners.delete(cb)
|
|
60
|
+
},
|
|
61
|
+
set(next: string) {
|
|
62
|
+
this.visibilityState = next
|
|
63
|
+
for (const cb of [...listeners]) cb()
|
|
64
|
+
},
|
|
65
|
+
count() {
|
|
66
|
+
return listeners.size
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function client(options: Partial<Parameters<typeof createWakeLock>[0]> = {}) {
|
|
72
|
+
return createWakeLock({
|
|
73
|
+
server: false,
|
|
74
|
+
deferCapability: false,
|
|
75
|
+
api: fakeApi(),
|
|
76
|
+
visibility: fakeVisibility(),
|
|
77
|
+
...options,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("isWakeLockCapable", () => {
|
|
82
|
+
it("exige une API avec request()", () => {
|
|
83
|
+
expect(isWakeLockCapable(null)).toBe(false)
|
|
84
|
+
expect(isWakeLockCapable(undefined)).toBe(false)
|
|
85
|
+
expect(isWakeLockCapable({} as never)).toBe(false)
|
|
86
|
+
expect(isWakeLockCapable({ request: async () => new FakeSentinel() })).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe("isDocumentVisible", () => {
|
|
91
|
+
it("visible sauf `visibilityState: 'hidden'` (et sans document injecté)", () => {
|
|
92
|
+
expect(isDocumentVisible(null)).toBe(true)
|
|
93
|
+
expect(isDocumentVisible({ addEventListener() {} })).toBe(true)
|
|
94
|
+
expect(
|
|
95
|
+
isDocumentVisible({ visibilityState: "visible", addEventListener() {} }),
|
|
96
|
+
).toBe(true)
|
|
97
|
+
expect(
|
|
98
|
+
isDocumentVisible({ visibilityState: "hidden", addEventListener() {} }),
|
|
99
|
+
).toBe(false)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe("shouldReacquire", () => {
|
|
104
|
+
it("uniquement si demandé, visible et rien de tenu", () => {
|
|
105
|
+
expect(shouldReacquire(true, true, false)).toBe(true)
|
|
106
|
+
expect(shouldReacquire(false, true, false)).toBe(false)
|
|
107
|
+
expect(shouldReacquire(true, false, false)).toBe(false)
|
|
108
|
+
expect(shouldReacquire(true, true, true)).toBe(false)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe("createWakeLock", () => {
|
|
113
|
+
it("au serveur : méthodes sans effet, rien de capable ni d'actif", async () => {
|
|
114
|
+
const lock = createWakeLock({ server: true })
|
|
115
|
+
expect(lock.isCapable).toBe(false)
|
|
116
|
+
expect(lock.isActive).toBe(false)
|
|
117
|
+
await expect(lock.request()).resolves.toBeUndefined()
|
|
118
|
+
await expect(lock.release()).resolves.toBeUndefined()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("sans API : `Not capable`", async () => {
|
|
122
|
+
const lock = client({ api: null })
|
|
123
|
+
expect(lock.isCapable).toBe(false)
|
|
124
|
+
await expect(lock.request()).rejects.toThrow("Not capable")
|
|
125
|
+
expect(lock.isActive).toBe(false)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it("request() acquiert le verrou, une seule fois (idempotent)", async () => {
|
|
129
|
+
const api = fakeApi()
|
|
130
|
+
const lock = client({ api })
|
|
131
|
+
|
|
132
|
+
await lock.request()
|
|
133
|
+
expect(lock.isActive).toBe(true)
|
|
134
|
+
expect(api.calls).toBe(1)
|
|
135
|
+
|
|
136
|
+
await lock.request()
|
|
137
|
+
expect(api.calls).toBe(1)
|
|
138
|
+
expect(lock.isActive).toBe(true)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("release() rend la main au système", async () => {
|
|
142
|
+
const api = fakeApi()
|
|
143
|
+
const lock = client({ api })
|
|
144
|
+
|
|
145
|
+
await lock.request()
|
|
146
|
+
await lock.release()
|
|
147
|
+
|
|
148
|
+
expect(lock.isActive).toBe(false)
|
|
149
|
+
expect(api.sentinels[0]!.releaseCalls).toBe(1)
|
|
150
|
+
|
|
151
|
+
// Une nouvelle demande fonctionne.
|
|
152
|
+
await lock.request()
|
|
153
|
+
expect(api.calls).toBe(2)
|
|
154
|
+
expect(lock.isActive).toBe(true)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it("le navigateur relâche → isActive retombe, et le verrou est redemandé au retour", async () => {
|
|
158
|
+
const api = fakeApi()
|
|
159
|
+
const visibility = fakeVisibility("visible")
|
|
160
|
+
const lock = client({ api, visibility })
|
|
161
|
+
|
|
162
|
+
await lock.request()
|
|
163
|
+
expect(lock.isActive).toBe(true)
|
|
164
|
+
|
|
165
|
+
// Onglet masqué : le navigateur relâche de lui-même.
|
|
166
|
+
visibility.set("hidden")
|
|
167
|
+
api.sentinels[0]!.autoRelease()
|
|
168
|
+
expect(lock.isActive).toBe(false)
|
|
169
|
+
expect(api.calls).toBe(1)
|
|
170
|
+
|
|
171
|
+
// Retour de visibilité : la demande mémorisée est rejouée.
|
|
172
|
+
visibility.set("visible")
|
|
173
|
+
await Promise.resolve()
|
|
174
|
+
expect(api.calls).toBe(2)
|
|
175
|
+
expect(lock.isActive).toBe(true)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it("après release(), plus aucune redemande au changement de visibilité", async () => {
|
|
179
|
+
const api = fakeApi()
|
|
180
|
+
const visibility = fakeVisibility("visible")
|
|
181
|
+
const lock = client({ api, visibility })
|
|
182
|
+
|
|
183
|
+
await lock.request()
|
|
184
|
+
await lock.release()
|
|
185
|
+
|
|
186
|
+
visibility.set("hidden")
|
|
187
|
+
visibility.set("visible")
|
|
188
|
+
await Promise.resolve()
|
|
189
|
+
|
|
190
|
+
expect(api.calls).toBe(1)
|
|
191
|
+
expect(lock.isActive).toBe(false)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it("le refus du navigateur est propagé (échec doux côté appelant)", async () => {
|
|
195
|
+
const api = fakeApi({ fail: new Error("denied") })
|
|
196
|
+
const lock = client({ api })
|
|
197
|
+
|
|
198
|
+
await expect(lock.request()).rejects.toThrow("denied")
|
|
199
|
+
expect(lock.isActive).toBe(false)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it("destroy() détache l'écouteur de visibilité", async () => {
|
|
203
|
+
const visibility = fakeVisibility()
|
|
204
|
+
const lock = client({ visibility })
|
|
205
|
+
|
|
206
|
+
expect(visibility.count()).toBe(1)
|
|
207
|
+
lock.destroy()
|
|
208
|
+
expect(visibility.count()).toBe(0)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it("isCapable n'est révélé qu'après hydratation (report d'un tick)", async () => {
|
|
212
|
+
const api = fakeApi()
|
|
213
|
+
const lock = createWakeLock({ server: false, api, visibility: null })
|
|
214
|
+
|
|
215
|
+
expect(lock.isCapable).toBe(false)
|
|
216
|
+
await new Promise((resolve) => setTimeout(resolve, 1))
|
|
217
|
+
expect(lock.isCapable).toBe(true)
|
|
218
|
+
})
|
|
219
|
+
})
|
package/lib/wakeLock.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// $q.wakeLock — Screen Wake Lock façon Quasar (`AppWakeLockPlugin`).
|
|
2
|
+
// https://quasar.dev/quasar-plugins/app-wake-lock
|
|
3
|
+
//
|
|
4
|
+
// - `request()` garde l'écran allumé ; `release()` rend la main au délai système.
|
|
5
|
+
// - `isActive` ne vaut `true` que **tant que le verrou est réellement tenu** : le
|
|
6
|
+
// navigateur le relâche de lui-même quand la page est masquée (onglet, veille…).
|
|
7
|
+
// La demande est alors **mémorisée** et rejouée au retour de visibilité, jusqu'à
|
|
8
|
+
// `release()`.
|
|
9
|
+
// - `isCapable` vaut `false` au serveur **et au premier rendu client**, puis prend sa
|
|
10
|
+
// valeur réelle après hydratation (sinon le `v-if` d'un bouton « garder l'écran
|
|
11
|
+
// allumé » divergerait du HTML SSR).
|
|
12
|
+
// - Sans API (navigateur non supporté), les méthodes rejettent `Not capable` ; au
|
|
13
|
+
// serveur elles se résolvent sans effet.
|
|
14
|
+
//
|
|
15
|
+
// Logique pure (`isWakeLockCapable`, `isDocumentVisible`, `shouldReacquire`) et fabrique
|
|
16
|
+
// `createWakeLock` à dépendances injectables : testable sans DOM.
|
|
17
|
+
import { reactive, ref } from "vue"
|
|
18
|
+
|
|
19
|
+
/** Sentinel minimal du Screen Wake Lock API (injectable pour les tests). */
|
|
20
|
+
export interface WakeLockSentinelLike {
|
|
21
|
+
/** Passe à `true` quand le navigateur relâche le verrou de lui-même */
|
|
22
|
+
released?: boolean
|
|
23
|
+
release: () => Promise<void>
|
|
24
|
+
addEventListener?: (type: "release", listener: () => void) => void
|
|
25
|
+
removeEventListener?: (type: "release", listener: () => void) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Sous-ensemble de `navigator.wakeLock`. */
|
|
29
|
+
export interface WakeLockApiLike {
|
|
30
|
+
request: (type: "screen") => Promise<WakeLockSentinelLike>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Sous-ensemble de `document` utilisé pour suivre la visibilité. */
|
|
34
|
+
export interface WakeLockVisibilityLike {
|
|
35
|
+
visibilityState?: string
|
|
36
|
+
addEventListener: (type: "visibilitychange", listener: () => void) => void
|
|
37
|
+
removeEventListener?: (type: "visibilitychange", listener: () => void) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface QWakeLock {
|
|
41
|
+
/** Un verrou est réellement tenu (réactif, lecture sans `.value`) */
|
|
42
|
+
readonly isActive: boolean
|
|
43
|
+
/** Screen Wake Lock disponible — `false` au serveur et avant hydratation */
|
|
44
|
+
readonly isCapable: boolean
|
|
45
|
+
/** Demande le verrou (idempotent). Rejette si non supporté (`Not capable`). */
|
|
46
|
+
request: () => Promise<void>
|
|
47
|
+
/** Relâche le verrou et cesse les redemandes automatiques */
|
|
48
|
+
release: () => Promise<void>
|
|
49
|
+
/** Détache l'écouteur de visibilité (instance de test / usage hors singleton) */
|
|
50
|
+
destroy: () => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface WakeLockOptions {
|
|
54
|
+
/** API Screen Wake Lock (défaut : `navigator.wakeLock` ; `null` si absente) */
|
|
55
|
+
api?: WakeLockApiLike | null
|
|
56
|
+
/** Document pour la visibilité (défaut : `document` ; `null` au serveur) */
|
|
57
|
+
visibility?: WakeLockVisibilityLike | null
|
|
58
|
+
/** Force le mode serveur : aucune API lue, méthodes sans effet */
|
|
59
|
+
server?: boolean
|
|
60
|
+
/**
|
|
61
|
+
* Diffère `isCapable` après l'hydratation (défaut : `true` côté client) — évite un
|
|
62
|
+
* décalage entre le HTML SSR (`isCapable = false`) et le premier rendu client.
|
|
63
|
+
*/
|
|
64
|
+
deferCapability?: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `true` seulement si l'API Screen Wake Lock est exposée par le navigateur. */
|
|
68
|
+
export function isWakeLockCapable(api?: WakeLockApiLike | null): boolean {
|
|
69
|
+
return !!api && typeof api.request === "function"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Une page est considérée visible sauf si le document dit explicitement `hidden`. */
|
|
73
|
+
export function isDocumentVisible(visibility?: WakeLockVisibilityLike | null): boolean {
|
|
74
|
+
return visibility?.visibilityState !== "hidden"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Faut-il redemander le verrou ? (demandé, page visible, et rien de tenu) */
|
|
78
|
+
export function shouldReacquire(wanted: boolean, visible: boolean, held: boolean): boolean {
|
|
79
|
+
return wanted && visible && !held
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function notCapableError(): Error {
|
|
83
|
+
const message = "Not capable"
|
|
84
|
+
return typeof DOMException === "function"
|
|
85
|
+
? new DOMException(message, "NotSupportedError")
|
|
86
|
+
: new Error(message)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Crée le gestionnaire de verrou. Sans `visibility`, aucune écoute n'est posée ;
|
|
91
|
+
* en mode serveur, `request()` / `release()` se résolvent sans effet.
|
|
92
|
+
*/
|
|
93
|
+
export function createWakeLock(options: WakeLockOptions = {}): QWakeLock {
|
|
94
|
+
const server = options.server ?? typeof window === "undefined"
|
|
95
|
+
const api = "api" in options
|
|
96
|
+
? (options.api ?? null)
|
|
97
|
+
: typeof navigator !== "undefined"
|
|
98
|
+
? ((navigator as Navigator & { wakeLock?: WakeLockApiLike }).wakeLock ?? null)
|
|
99
|
+
: null
|
|
100
|
+
const visibility = "visibility" in options
|
|
101
|
+
? (options.visibility ?? null)
|
|
102
|
+
: typeof document !== "undefined"
|
|
103
|
+
? document
|
|
104
|
+
: null
|
|
105
|
+
|
|
106
|
+
const active = ref(false)
|
|
107
|
+
const capable = ref(false)
|
|
108
|
+
|
|
109
|
+
// `isCapable` n'est révélé qu'après le rendu SSR/hydratation (cf. en-tête).
|
|
110
|
+
if (!server) {
|
|
111
|
+
if (options.deferCapability === false) {
|
|
112
|
+
capable.value = isWakeLockCapable(api)
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
capable.value = isWakeLockCapable(api)
|
|
117
|
+
}, 0)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** L'utilisateur veut garder l'écran allumé (survit aux relâchements du navigateur). */
|
|
122
|
+
let wanted = false
|
|
123
|
+
let sentinel: WakeLockSentinelLike | null = null
|
|
124
|
+
let pending: Promise<void> | null = null
|
|
125
|
+
|
|
126
|
+
const bindRelease = (s: WakeLockSentinelLike) => {
|
|
127
|
+
s.addEventListener?.("release", () => {
|
|
128
|
+
if (sentinel !== s) return
|
|
129
|
+
sentinel = null
|
|
130
|
+
active.value = false
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const request = async (): Promise<void> => {
|
|
135
|
+
if (server) return
|
|
136
|
+
if (!isWakeLockCapable(api)) throw notCapableError()
|
|
137
|
+
|
|
138
|
+
wanted = true
|
|
139
|
+
if (sentinel && sentinel.released !== true) {
|
|
140
|
+
active.value = true
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
if (pending) return pending
|
|
144
|
+
|
|
145
|
+
pending = (async () => {
|
|
146
|
+
const s = await api!.request("screen")
|
|
147
|
+
if (!wanted) {
|
|
148
|
+
// `release()` a été appelé pendant la demande : ne pas reprendre le verrou.
|
|
149
|
+
await s.release().catch(() => {})
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
sentinel = s
|
|
153
|
+
active.value = true
|
|
154
|
+
bindRelease(s)
|
|
155
|
+
})()
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await pending
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
pending = null
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const release = async (): Promise<void> => {
|
|
166
|
+
wanted = false
|
|
167
|
+
const s = sentinel
|
|
168
|
+
sentinel = null
|
|
169
|
+
active.value = false
|
|
170
|
+
if (s) await s.release().catch(() => {})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const onVisibilityChange = () => {
|
|
174
|
+
if (sentinel?.released === true) {
|
|
175
|
+
sentinel = null
|
|
176
|
+
active.value = false
|
|
177
|
+
}
|
|
178
|
+
const held = sentinel !== null && sentinel.released !== true
|
|
179
|
+
if (!shouldReacquire(wanted, isDocumentVisible(visibility), held)) return
|
|
180
|
+
// Refus possible (batterie faible, iframe…) : la prochaine visibilité réessaiera.
|
|
181
|
+
void request().catch(() => {})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
visibility?.addEventListener("visibilitychange", onVisibilityChange)
|
|
185
|
+
|
|
186
|
+
return reactive({
|
|
187
|
+
get isActive() {
|
|
188
|
+
return active.value
|
|
189
|
+
},
|
|
190
|
+
get isCapable() {
|
|
191
|
+
return capable.value
|
|
192
|
+
},
|
|
193
|
+
request,
|
|
194
|
+
release,
|
|
195
|
+
destroy: () => {
|
|
196
|
+
visibility?.removeEventListener?.("visibilitychange", onVisibilityChange)
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Singleton exposé sous `$q.wakeLock` (et importable directement depuis `@dnax/ui`). */
|
|
202
|
+
export const wakeLock: QWakeLock = createWakeLock()
|
package/package.json
CHANGED
package/runtime.ts
CHANGED
|
@@ -39,6 +39,13 @@ export {
|
|
|
39
39
|
useDialogPluginComponent,
|
|
40
40
|
useBottomSheetPluginComponent,
|
|
41
41
|
} from "./lib/q"
|
|
42
|
+
// `$q.network` (état de connexion réactif) — accessible aussi par import direct,
|
|
43
|
+
// sans évaluer le barrel ni les composants.
|
|
44
|
+
export { network, createNetwork } from "./lib/network"
|
|
45
|
+
export type { QNetwork, NetworkOptions } from "./lib/network"
|
|
46
|
+
// `$q.wakeLock` (écran allumé) — Screen Wake Lock API.
|
|
47
|
+
export { wakeLock, createWakeLock } from "./lib/wakeLock"
|
|
48
|
+
export type { QWakeLock, WakeLockOptions } from "./lib/wakeLock"
|
|
42
49
|
|
|
43
50
|
// — Providers de `$q` (montés au boot par l'app hôte) —
|
|
44
51
|
export { default as QDialogProvider } from "./components/QDialogProvider.vue"
|
package/styles/main.css
CHANGED
|
@@ -831,6 +831,144 @@ html.dark body {
|
|
|
831
831
|
align-items: stretch;
|
|
832
832
|
}
|
|
833
833
|
|
|
834
|
+
/* Flèches de défilement (`show-arrows`, comportement Quasar) : le contenu devient
|
|
835
|
+
défilable (barre de défilement masquée, le défilement est piloté par les flèches).
|
|
836
|
+
`scroll-padding` : le focus / `scrollIntoView` ne colle pas un onglet sous une flèche. */
|
|
837
|
+
.q-tabs--arrows .q-tabs__content {
|
|
838
|
+
--q-tabs-fade-start: 0px;
|
|
839
|
+
--q-tabs-fade-end: 0px;
|
|
840
|
+
|
|
841
|
+
overflow-x: auto;
|
|
842
|
+
overflow-y: hidden;
|
|
843
|
+
scrollbar-width: none;
|
|
844
|
+
-ms-overflow-style: none;
|
|
845
|
+
scroll-padding-inline: 48px;
|
|
846
|
+
/* Fondu sous les flèches : masque **alpha** (transparent → blanc opaque). Le blanc est
|
|
847
|
+
opaque en mode `alpha` **et** `luminance` (ancien WebKit), et un masque ne dépend
|
|
848
|
+
d'aucune couleur de thème : le rendu est **identique en clair et en sombre** (contrairement
|
|
849
|
+
à un dégradé `--card` qui supposerait la couleur de surface du parent).
|
|
850
|
+
Le côté déjà entièrement visible garde `--q-tabs-fade-*: 0px` → aucun fondu. */
|
|
851
|
+
-webkit-mask-image: linear-gradient(
|
|
852
|
+
to right,
|
|
853
|
+
transparent 0,
|
|
854
|
+
#fff var(--q-tabs-fade-start),
|
|
855
|
+
#fff calc(100% - var(--q-tabs-fade-end)),
|
|
856
|
+
transparent 100%
|
|
857
|
+
);
|
|
858
|
+
mask-image: linear-gradient(
|
|
859
|
+
to right,
|
|
860
|
+
transparent 0,
|
|
861
|
+
#fff var(--q-tabs-fade-start),
|
|
862
|
+
#fff calc(100% - var(--q-tabs-fade-end)),
|
|
863
|
+
transparent 100%
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/* Côtés à fondre — posés par le composant d'après les mesures de défilement
|
|
868
|
+
(`tabArrowState`) : à droite tant qu'il reste du contenu, à gauche dès qu'on a défilé. */
|
|
869
|
+
.q-tabs--arrows.q-tabs--scroll-start .q-tabs__content {
|
|
870
|
+
--q-tabs-fade-start: 48px;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.q-tabs--arrows.q-tabs--scroll-end .q-tabs__content {
|
|
874
|
+
--q-tabs-fade-end: 48px;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.q-tabs--arrows .q-tabs__content::-webkit-scrollbar {
|
|
878
|
+
display: none;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.q-tabs--vertical.q-tabs--arrows .q-tabs__content {
|
|
882
|
+
overflow-x: hidden;
|
|
883
|
+
overflow-y: auto;
|
|
884
|
+
scroll-padding-inline: 0;
|
|
885
|
+
scroll-padding-block: 40px;
|
|
886
|
+
-webkit-mask-image: linear-gradient(
|
|
887
|
+
to bottom,
|
|
888
|
+
transparent 0,
|
|
889
|
+
#fff var(--q-tabs-fade-start),
|
|
890
|
+
#fff calc(100% - var(--q-tabs-fade-end)),
|
|
891
|
+
transparent 100%
|
|
892
|
+
);
|
|
893
|
+
mask-image: linear-gradient(
|
|
894
|
+
to bottom,
|
|
895
|
+
transparent 0,
|
|
896
|
+
#fff var(--q-tabs-fade-start),
|
|
897
|
+
#fff calc(100% - var(--q-tabs-fade-end)),
|
|
898
|
+
transparent 100%
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.q-tabs--vertical.q-tabs--arrows.q-tabs--scroll-start .q-tabs__content {
|
|
903
|
+
--q-tabs-fade-start: 40px;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.q-tabs--vertical.q-tabs--arrows.q-tabs--scroll-end .q-tabs__content {
|
|
907
|
+
--q-tabs-fade-end: 40px;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/* Bouton **circulaire en surimpression** : les onglets défilent dessous. */
|
|
911
|
+
.q-tabs__arrow {
|
|
912
|
+
position: absolute;
|
|
913
|
+
top: 50%;
|
|
914
|
+
z-index: 2;
|
|
915
|
+
display: inline-flex;
|
|
916
|
+
align-items: center;
|
|
917
|
+
justify-content: center;
|
|
918
|
+
width: 32px;
|
|
919
|
+
height: 32px;
|
|
920
|
+
padding: 0;
|
|
921
|
+
border: 1px solid var(--border);
|
|
922
|
+
border-radius: 50%;
|
|
923
|
+
background-color: var(--card, #fff);
|
|
924
|
+
color: inherit;
|
|
925
|
+
box-shadow: 0 1px 3px rgb(0 0 0 / 0.12);
|
|
926
|
+
cursor: pointer;
|
|
927
|
+
transform: translateY(-50%);
|
|
928
|
+
transition:
|
|
929
|
+
opacity 0.15s ease,
|
|
930
|
+
background-color 0.15s ease;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
.q-tabs__arrow--start {
|
|
934
|
+
left: 8px;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.q-tabs__arrow--end {
|
|
938
|
+
right: 8px;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
.q-tabs__arrow:hover:not(:disabled) {
|
|
942
|
+
background-color: var(--muted, rgb(0 0 0 / 0.04));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/* Côté sans défilement possible : flèche **estompée** (toujours visible, comme la référence). */
|
|
946
|
+
.q-tabs__arrow:disabled {
|
|
947
|
+
opacity: 0.35;
|
|
948
|
+
cursor: default;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
.q-tabs__arrow svg {
|
|
952
|
+
width: 16px;
|
|
953
|
+
height: 16px;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/* Vertical : flèches en haut / en bas, centrées horizontalement. */
|
|
957
|
+
.q-tabs--vertical .q-tabs__arrow {
|
|
958
|
+
top: auto;
|
|
959
|
+
left: 50%;
|
|
960
|
+
right: auto;
|
|
961
|
+
transform: translateX(-50%);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
.q-tabs--vertical .q-tabs__arrow--start {
|
|
965
|
+
top: 8px;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
.q-tabs--vertical .q-tabs__arrow--end {
|
|
969
|
+
bottom: 8px;
|
|
970
|
+
}
|
|
971
|
+
|
|
834
972
|
.q-tabs--dense {
|
|
835
973
|
--q-tabs-h: 32px;
|
|
836
974
|
}
|
|
@@ -841,10 +979,14 @@ html.dark body {
|
|
|
841
979
|
|
|
842
980
|
.q-tabs--center .q-tabs__content {
|
|
843
981
|
justify-content: center;
|
|
982
|
+
/* `safe` : en cas de débordement, retombe sur `flex-start` au lieu de rendre le
|
|
983
|
+
début inaccessible au défilement (bug classique du flex centré + overflow). */
|
|
984
|
+
justify-content: safe center;
|
|
844
985
|
}
|
|
845
986
|
|
|
846
987
|
.q-tabs--right .q-tabs__content {
|
|
847
988
|
justify-content: flex-end;
|
|
989
|
+
justify-content: safe flex-end;
|
|
848
990
|
}
|
|
849
991
|
|
|
850
992
|
.q-tabs--justify .q-tab {
|
|
@@ -6063,7 +6205,7 @@ a.q-breadcrumbs__el:hover {
|
|
|
6063
6205
|
background-color: var(--primary);
|
|
6064
6206
|
}
|
|
6065
6207
|
|
|
6066
|
-
/* — raccourci «
|
|
6208
|
+
/* — raccourci « aujourd'hui » (prop `today-btn`) : bord à bord dans le panneau — */
|
|
6067
6209
|
.q-date-calendar__footer {
|
|
6068
6210
|
display: flex;
|
|
6069
6211
|
justify-content: flex-end;
|