@dnax/ui 0.0.64 → 0.0.66

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,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.64",
3
+ "version": "0.0.66",
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
@@ -832,12 +832,46 @@ html.dark body {
832
832
  }
833
833
 
834
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). */
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. */
836
837
  .q-tabs--arrows .q-tabs__content {
838
+ --q-tabs-fade-start: 0px;
839
+ --q-tabs-fade-end: 0px;
840
+
837
841
  overflow-x: auto;
838
842
  overflow-y: hidden;
839
843
  scrollbar-width: none;
840
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;
841
875
  }
842
876
 
843
877
  .q-tabs--arrows .q-tabs__content::-webkit-scrollbar {
@@ -847,34 +881,71 @@ html.dark body {
847
881
  .q-tabs--vertical.q-tabs--arrows .q-tabs__content {
848
882
  overflow-x: hidden;
849
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
+ );
850
900
  }
851
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. */
852
911
  .q-tabs__arrow {
853
- flex: 0 0 auto;
912
+ position: absolute;
913
+ top: 50%;
914
+ z-index: 2;
854
915
  display: inline-flex;
855
916
  align-items: center;
856
917
  justify-content: center;
857
- width: 28px;
918
+ width: 32px;
919
+ height: 32px;
858
920
  padding: 0;
859
- border: none;
860
- border-radius: 0;
861
- background: transparent;
921
+ border: 1px solid var(--border);
922
+ border-radius: 50%;
923
+ background-color: var(--card, #fff);
862
924
  color: inherit;
863
- opacity: 0.6;
925
+ box-shadow: 0 1px 3px rgb(0 0 0 / 0.12);
864
926
  cursor: pointer;
927
+ transform: translateY(-50%);
865
928
  transition:
866
929
  opacity 0.15s ease,
867
930
  background-color 0.15s ease;
868
931
  }
869
932
 
870
- .q-tabs__arrow:hover {
871
- opacity: 1;
872
- background-color: rgb(0 0 0 / 0.04);
933
+ .q-tabs__arrow--start {
934
+ left: 8px;
873
935
  }
874
936
 
875
- /* Thème sombre : le hover noir 4 % est invisible (cf. convention des hovers sombres). */
876
- .dark .q-tabs__arrow:hover {
877
- background-color: rgb(255 255 255 / 0.08);
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;
878
949
  }
879
950
 
880
951
  .q-tabs__arrow svg {
@@ -882,9 +953,20 @@ html.dark body {
882
953
  height: 16px;
883
954
  }
884
955
 
956
+ /* Vertical : flèches en haut / en bas, centrées horizontalement. */
885
957
  .q-tabs--vertical .q-tabs__arrow {
886
- width: 100%;
887
- height: 24px;
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;
888
970
  }
889
971
 
890
972
  .q-tabs--dense {
@@ -3533,13 +3615,27 @@ html.dark body {
3533
3615
  color: rgb(255 255 255 / 0.6);
3534
3616
  }
3535
3617
 
3536
- .q-table__rows-select select {
3537
- padding: 2px 6px;
3538
- border: 1px solid var(--q-table-border);
3539
- border-radius: 4px;
3540
- background: transparent;
3541
- color: inherit;
3542
- font: inherit;
3618
+ /* Sélecteur « lignes par page » : QSelect compact — le footer n'a pas de place
3619
+ pour la ligne d'aide (hint/error) ni pour la hauteur de champ standard. */
3620
+ .q-table__rows-field {
3621
+ width: 72px;
3622
+ flex: 0 0 auto;
3623
+ }
3624
+
3625
+ .q-table__rows-field .q-field__control {
3626
+ min-height: 28px;
3627
+ padding: 0 8px;
3628
+ font-size: 13px;
3629
+ }
3630
+
3631
+ .q-table__rows-field .q-field__bottom {
3632
+ display: none;
3633
+ }
3634
+
3635
+ .q-table--dark .q-table__rows-field .q-field__control {
3636
+ background-color: #1d1d1d;
3637
+ border-color: var(--q-table-border);
3638
+ color: #fff;
3543
3639
  }
3544
3640
 
3545
3641
  .q-table__page-nav {
@@ -6123,7 +6219,7 @@ a.q-breadcrumbs__el:hover {
6123
6219
  background-color: var(--primary);
6124
6220
  }
6125
6221
 
6126
- /* — raccourci « Today » (prop `today-btn`) : bord à bord dans le panneau — */
6222
+ /* — raccourci « aujourd'hui » (prop `today-btn`) : bord à bord dans le panneau — */
6127
6223
  .q-date-calendar__footer {
6128
6224
  display: flex;
6129
6225
  justify-content: flex-end;