@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.
- package/components/QDatePicker.vue +12 -5
- package/components/QSpreadsheet.vue +51 -6
- package/components/QTable.vue +20 -6
- package/components/QTabs.vue +11 -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/formula.test.ts +186 -1
- package/lib/formula.ts +442 -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 +46 -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 +119 -23
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
|
@@ -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
|
-
|
|
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:
|
|
918
|
+
width: 32px;
|
|
919
|
+
height: 32px;
|
|
858
920
|
padding: 0;
|
|
859
|
-
border:
|
|
860
|
-
border-radius:
|
|
861
|
-
background:
|
|
921
|
+
border: 1px solid var(--border);
|
|
922
|
+
border-radius: 50%;
|
|
923
|
+
background-color: var(--card, #fff);
|
|
862
924
|
color: inherit;
|
|
863
|
-
|
|
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
|
|
871
|
-
|
|
872
|
-
background-color: rgb(0 0 0 / 0.04);
|
|
933
|
+
.q-tabs__arrow--start {
|
|
934
|
+
left: 8px;
|
|
873
935
|
}
|
|
874
936
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
887
|
-
|
|
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
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
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 «
|
|
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;
|