@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.
@@ -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
+ })
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.63",
3
+ "version": "0.0.65",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
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 « Today » (prop `today-btn`) : bord à bord dans le panneau — */
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;