@sia-ui/utils 0.1.0
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/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/async.cjs +134 -0
- package/dist/async.d.cts +81 -0
- package/dist/async.d.ts +81 -0
- package/dist/async.js +1 -0
- package/dist/chunk-63IWZEOC.js +51 -0
- package/dist/chunk-6P4YWHCJ.js +47 -0
- package/dist/chunk-BMNZVRFN.js +25 -0
- package/dist/chunk-DKWI2AZG.js +128 -0
- package/dist/chunk-DY6WSWXH.js +60 -0
- package/dist/chunk-ETJDEEF5.js +129 -0
- package/dist/chunk-FX7K6FKW.js +57 -0
- package/dist/chunk-JHYMIU2G.js +121 -0
- package/dist/chunk-L4BLBOLO.js +93 -0
- package/dist/chunk-PTNHOSWH.js +40 -0
- package/dist/chunk-QMPHN3LG.js +78 -0
- package/dist/chunk-RIFUQ7JM.js +47 -0
- package/dist/chunk-YU5NRSDP.js +11 -0
- package/dist/classname.cjs +28 -0
- package/dist/classname.d.cts +15 -0
- package/dist/classname.d.ts +15 -0
- package/dist/classname.js +1 -0
- package/dist/clipboard.cjs +51 -0
- package/dist/clipboard.d.cts +16 -0
- package/dist/clipboard.d.ts +16 -0
- package/dist/clipboard.js +1 -0
- package/dist/collection.cjs +109 -0
- package/dist/collection.d.cts +44 -0
- package/dist/collection.d.ts +44 -0
- package/dist/collection.js +1 -0
- package/dist/currency.cjs +64 -0
- package/dist/currency.d.cts +16 -0
- package/dist/currency.d.ts +16 -0
- package/dist/currency.js +1 -0
- package/dist/date.cjs +140 -0
- package/dist/date.d.cts +32 -0
- package/dist/date.d.ts +32 -0
- package/dist/date.js +1 -0
- package/dist/events.cjs +13 -0
- package/dist/events.d.cts +10 -0
- package/dist/events.d.ts +10 -0
- package/dist/events.js +1 -0
- package/dist/file.cjs +141 -0
- package/dist/file.d.cts +58 -0
- package/dist/file.d.ts +58 -0
- package/dist/file.js +1 -0
- package/dist/index.cjs +977 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/locale.cjs +58 -0
- package/dist/locale.d.cts +51 -0
- package/dist/locale.d.ts +51 -0
- package/dist/locale.js +1 -0
- package/dist/number.cjs +61 -0
- package/dist/number.d.cts +14 -0
- package/dist/number.d.ts +14 -0
- package/dist/number.js +1 -0
- package/dist/object.cjs +87 -0
- package/dist/object.d.cts +48 -0
- package/dist/object.d.ts +48 -0
- package/dist/object.js +1 -0
- package/dist/query.cjs +65 -0
- package/dist/query.d.cts +48 -0
- package/dist/query.d.ts +48 -0
- package/dist/query.js +1 -0
- package/dist/string.cjs +50 -0
- package/dist/string.d.cts +15 -0
- package/dist/string.d.ts +15 -0
- package/dist/string.js +1 -0
- package/dist/validation.cjs +123 -0
- package/dist/validation.d.cts +77 -0
- package/dist/validation.d.ts +77 -0
- package/dist/validation.js +102 -0
- package/package.json +127 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan Mbella
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @sia-ui/utils
|
|
2
|
+
|
|
3
|
+
Les helpers qu'on réécrit d'un projet à l'autre, sans dépendance.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @sia-ui/utils
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { formatCurrency } from "@sia-ui/utils/currency";
|
|
11
|
+
import { formatTimeAgo } from "@sia-ui/utils/date";
|
|
12
|
+
import { cn } from "@sia-ui/utils/classname";
|
|
13
|
+
|
|
14
|
+
formatCurrency(1500000, { currency: "XAF" }); // "1 500 000 FCFA"
|
|
15
|
+
formatTimeAgo(commande.createdAt); // "il y a 10 minutes"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Cinq modules — `classname`, `currency`, `date`, `events`, `number`, `string` —
|
|
19
|
+
importables séparément pour rester léger. Tout repose sur `Intl`, donc rien à
|
|
20
|
+
embarquer : `XOF` et `XAF` sont formatés correctement.
|
|
21
|
+
|
|
22
|
+
## Place dans SIA UI
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
@sia-ui/tokens valeurs : couleurs, espacements, mouvement, vocabulaire
|
|
26
|
+
@sia-ui/utils fonctions sans dépendance
|
|
27
|
+
@sia-ui/api client HTTP — indépendant de l'interface
|
|
28
|
+
@sia-ui/headless comportement, avec React, sans plateforme
|
|
29
|
+
@sia-ui/react primitives de runtime DOM
|
|
30
|
+
@sia-ui/react-web composants DOM
|
|
31
|
+
@sia-ui/react-native composants natifs, à venir
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Une chose s'écrit au niveau le plus bas où elle a du sens, et une seule fois.
|
|
35
|
+
Voir [la documentation](https://github.com/BeatJo/sia-ui/tree/main/docs).
|
|
36
|
+
|
|
37
|
+
## Licence
|
|
38
|
+
|
|
39
|
+
MIT
|
package/dist/async.cjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/async.ts
|
|
4
|
+
function sleep(ms, signal) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
if (signal?.aborted) {
|
|
7
|
+
reject(signal.reason ?? new Error("Annul\xE9"));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const timer = setTimeout(() => {
|
|
11
|
+
signal?.removeEventListener("abort", onAbort);
|
|
12
|
+
resolve();
|
|
13
|
+
}, ms);
|
|
14
|
+
function onAbort() {
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
reject(signal?.reason ?? new Error("Annul\xE9"));
|
|
17
|
+
}
|
|
18
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function debounce(fn, wait = 250, options = {}) {
|
|
22
|
+
const { leading = false, trailing = true } = options;
|
|
23
|
+
let timer;
|
|
24
|
+
let lastArgs;
|
|
25
|
+
const run = () => {
|
|
26
|
+
timer = void 0;
|
|
27
|
+
if (trailing && lastArgs) {
|
|
28
|
+
const args = lastArgs;
|
|
29
|
+
lastArgs = void 0;
|
|
30
|
+
fn(...args);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const debounced = ((...args) => {
|
|
34
|
+
lastArgs = args;
|
|
35
|
+
if (timer === void 0 && leading) {
|
|
36
|
+
lastArgs = void 0;
|
|
37
|
+
fn(...args);
|
|
38
|
+
}
|
|
39
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
40
|
+
timer = setTimeout(run, wait);
|
|
41
|
+
});
|
|
42
|
+
debounced.cancel = () => {
|
|
43
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
44
|
+
timer = void 0;
|
|
45
|
+
lastArgs = void 0;
|
|
46
|
+
};
|
|
47
|
+
debounced.flush = () => {
|
|
48
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
49
|
+
run();
|
|
50
|
+
};
|
|
51
|
+
debounced.pending = () => timer !== void 0;
|
|
52
|
+
return debounced;
|
|
53
|
+
}
|
|
54
|
+
function throttle(fn, wait = 100) {
|
|
55
|
+
let last = 0;
|
|
56
|
+
let timer;
|
|
57
|
+
let lastArgs;
|
|
58
|
+
const invoke = (args) => {
|
|
59
|
+
last = Date.now();
|
|
60
|
+
lastArgs = void 0;
|
|
61
|
+
fn(...args);
|
|
62
|
+
};
|
|
63
|
+
const throttled = ((...args) => {
|
|
64
|
+
const elapsed = Date.now() - last;
|
|
65
|
+
lastArgs = args;
|
|
66
|
+
if (elapsed >= wait) {
|
|
67
|
+
if (timer !== void 0) {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
timer = void 0;
|
|
70
|
+
}
|
|
71
|
+
invoke(args);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (timer === void 0) {
|
|
75
|
+
timer = setTimeout(() => {
|
|
76
|
+
timer = void 0;
|
|
77
|
+
if (lastArgs) invoke(lastArgs);
|
|
78
|
+
}, wait - elapsed);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
throttled.cancel = () => {
|
|
82
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
83
|
+
timer = void 0;
|
|
84
|
+
lastArgs = void 0;
|
|
85
|
+
};
|
|
86
|
+
throttled.flush = () => {
|
|
87
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
88
|
+
timer = void 0;
|
|
89
|
+
if (lastArgs) invoke(lastArgs);
|
|
90
|
+
};
|
|
91
|
+
throttled.pending = () => timer !== void 0;
|
|
92
|
+
return throttled;
|
|
93
|
+
}
|
|
94
|
+
function createRetry(options = {}) {
|
|
95
|
+
const {
|
|
96
|
+
retries = 2,
|
|
97
|
+
delay = 300,
|
|
98
|
+
maxDelay = 8e3,
|
|
99
|
+
shouldRetry = () => true,
|
|
100
|
+
onRetry,
|
|
101
|
+
signal
|
|
102
|
+
} = options;
|
|
103
|
+
return async function run(task) {
|
|
104
|
+
let lastError;
|
|
105
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
106
|
+
try {
|
|
107
|
+
return await task(attempt);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
lastError = error;
|
|
110
|
+
if (attempt === retries || !shouldRetry(error, attempt)) throw error;
|
|
111
|
+
onRetry?.(error, attempt);
|
|
112
|
+
const base = Math.min(delay * 2 ** attempt, maxDelay);
|
|
113
|
+
await sleep(base + Math.random() * 0.1 * base, signal);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw lastError;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function toAsyncState(input) {
|
|
120
|
+
const { data, error, isLoading = false } = input;
|
|
121
|
+
if (error !== void 0 && error !== null) return { status: "error", error };
|
|
122
|
+
if (isLoading) return { status: "loading" };
|
|
123
|
+
if (data === void 0 || data === null) return { status: "idle" };
|
|
124
|
+
if (Array.isArray(data) && data.length === 0) {
|
|
125
|
+
return { status: "empty", data };
|
|
126
|
+
}
|
|
127
|
+
return { status: "success", data };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
exports.createRetry = createRetry;
|
|
131
|
+
exports.debounce = debounce;
|
|
132
|
+
exports.sleep = sleep;
|
|
133
|
+
exports.throttle = throttle;
|
|
134
|
+
exports.toAsyncState = toAsyncState;
|
package/dist/async.d.cts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Le temps et les promesses.
|
|
3
|
+
*
|
|
4
|
+
* `AsyncStatus` et `AsyncState` vivent ici plutôt que dans un paquet
|
|
5
|
+
* d'interface : un état de chargement n'a rien à voir avec le rendu, et le
|
|
6
|
+
* même trio `data / error / isLoading` sort de TanStack Query, de SWR ou
|
|
7
|
+
* d'un `useState` écrit à la main.
|
|
8
|
+
*/
|
|
9
|
+
type AsyncStatus = "idle" | "loading" | "success" | "empty" | "error";
|
|
10
|
+
interface AsyncState<T> {
|
|
11
|
+
status: AsyncStatus;
|
|
12
|
+
data?: T;
|
|
13
|
+
error?: unknown;
|
|
14
|
+
}
|
|
15
|
+
/** Attend, en respectant une annulation. */
|
|
16
|
+
declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
17
|
+
interface Debounced<A extends unknown[]> {
|
|
18
|
+
(...args: A): void;
|
|
19
|
+
/** Abandonne l'appel en attente. */
|
|
20
|
+
cancel: () => void;
|
|
21
|
+
/** Exécute tout de suite l'appel en attente, s'il y en a un. */
|
|
22
|
+
flush: () => void;
|
|
23
|
+
pending: () => boolean;
|
|
24
|
+
}
|
|
25
|
+
interface DebounceOptions {
|
|
26
|
+
/** Exécute au premier appel plutôt qu'au dernier. */
|
|
27
|
+
leading?: boolean;
|
|
28
|
+
/** Exécute aussi à la fin. Avec `leading`, les deux sont possibles. */
|
|
29
|
+
trailing?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Attend que ça se calme avant d'agir.
|
|
33
|
+
*
|
|
34
|
+
* Pour une recherche pendant la frappe : c'est le dernier appel qui compte,
|
|
35
|
+
* pas les douze précédents. `cancel` est indispensable au démontage — sans
|
|
36
|
+
* lui, la requête part alors que le composant n'existe plus.
|
|
37
|
+
*/
|
|
38
|
+
declare function debounce<A extends unknown[]>(fn: (...args: A) => void, wait?: number, options?: DebounceOptions): Debounced<A>;
|
|
39
|
+
/**
|
|
40
|
+
* Limite la cadence, sans rien perdre du dernier appel.
|
|
41
|
+
*
|
|
42
|
+
* Pour un défilement ou un redimensionnement : on veut une réaction
|
|
43
|
+
* régulière, pas une par image. Contrairement au `debounce`, le premier
|
|
44
|
+
* appel passe tout de suite.
|
|
45
|
+
*/
|
|
46
|
+
declare function throttle<A extends unknown[]>(fn: (...args: A) => void, wait?: number): Debounced<A>;
|
|
47
|
+
interface RetryOptions {
|
|
48
|
+
/** Nombre de tentatives supplémentaires après la première. */
|
|
49
|
+
retries?: number;
|
|
50
|
+
/** Délai de base, doublé à chaque tentative. */
|
|
51
|
+
delay?: number;
|
|
52
|
+
/** Plafond du délai, pour que l'attente reste supportable. */
|
|
53
|
+
maxDelay?: number;
|
|
54
|
+
/** Décide si l'erreur mérite une nouvelle tentative. */
|
|
55
|
+
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
56
|
+
onRetry?: (error: unknown, attempt: number) => void;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Construit une fonction qui réessaie.
|
|
61
|
+
*
|
|
62
|
+
* Le délai double à chaque tentative, avec une part aléatoire : dix clients
|
|
63
|
+
* qui échouent en même temps ne doivent pas revenir tous ensemble, sinon le
|
|
64
|
+
* serveur qui se relève retombe aussitôt.
|
|
65
|
+
*/
|
|
66
|
+
declare function createRetry(options?: RetryOptions): <T>(task: (attempt: number) => Promise<T>) => Promise<T>;
|
|
67
|
+
interface ToAsyncStateInput<T> {
|
|
68
|
+
data?: T | null | undefined;
|
|
69
|
+
error?: unknown;
|
|
70
|
+
isLoading?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Traduit le trio `data / error / isLoading` en un état unique.
|
|
74
|
+
*
|
|
75
|
+
* C'est ce que rendent TanStack Query, SWR et RTK Query, et c'est ce que le
|
|
76
|
+
* composant `AsyncState` attend. Le cas qui justifie la fonction est `empty` :
|
|
77
|
+
* une liste chargée mais vide n'est pas un succès qu'on affiche tel quel.
|
|
78
|
+
*/
|
|
79
|
+
declare function toAsyncState<T>(input: ToAsyncStateInput<T>): AsyncState<T>;
|
|
80
|
+
|
|
81
|
+
export { type AsyncState, type AsyncStatus, type DebounceOptions, type Debounced, type RetryOptions, type ToAsyncStateInput, createRetry, debounce, sleep, throttle, toAsyncState };
|
package/dist/async.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Le temps et les promesses.
|
|
3
|
+
*
|
|
4
|
+
* `AsyncStatus` et `AsyncState` vivent ici plutôt que dans un paquet
|
|
5
|
+
* d'interface : un état de chargement n'a rien à voir avec le rendu, et le
|
|
6
|
+
* même trio `data / error / isLoading` sort de TanStack Query, de SWR ou
|
|
7
|
+
* d'un `useState` écrit à la main.
|
|
8
|
+
*/
|
|
9
|
+
type AsyncStatus = "idle" | "loading" | "success" | "empty" | "error";
|
|
10
|
+
interface AsyncState<T> {
|
|
11
|
+
status: AsyncStatus;
|
|
12
|
+
data?: T;
|
|
13
|
+
error?: unknown;
|
|
14
|
+
}
|
|
15
|
+
/** Attend, en respectant une annulation. */
|
|
16
|
+
declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
17
|
+
interface Debounced<A extends unknown[]> {
|
|
18
|
+
(...args: A): void;
|
|
19
|
+
/** Abandonne l'appel en attente. */
|
|
20
|
+
cancel: () => void;
|
|
21
|
+
/** Exécute tout de suite l'appel en attente, s'il y en a un. */
|
|
22
|
+
flush: () => void;
|
|
23
|
+
pending: () => boolean;
|
|
24
|
+
}
|
|
25
|
+
interface DebounceOptions {
|
|
26
|
+
/** Exécute au premier appel plutôt qu'au dernier. */
|
|
27
|
+
leading?: boolean;
|
|
28
|
+
/** Exécute aussi à la fin. Avec `leading`, les deux sont possibles. */
|
|
29
|
+
trailing?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Attend que ça se calme avant d'agir.
|
|
33
|
+
*
|
|
34
|
+
* Pour une recherche pendant la frappe : c'est le dernier appel qui compte,
|
|
35
|
+
* pas les douze précédents. `cancel` est indispensable au démontage — sans
|
|
36
|
+
* lui, la requête part alors que le composant n'existe plus.
|
|
37
|
+
*/
|
|
38
|
+
declare function debounce<A extends unknown[]>(fn: (...args: A) => void, wait?: number, options?: DebounceOptions): Debounced<A>;
|
|
39
|
+
/**
|
|
40
|
+
* Limite la cadence, sans rien perdre du dernier appel.
|
|
41
|
+
*
|
|
42
|
+
* Pour un défilement ou un redimensionnement : on veut une réaction
|
|
43
|
+
* régulière, pas une par image. Contrairement au `debounce`, le premier
|
|
44
|
+
* appel passe tout de suite.
|
|
45
|
+
*/
|
|
46
|
+
declare function throttle<A extends unknown[]>(fn: (...args: A) => void, wait?: number): Debounced<A>;
|
|
47
|
+
interface RetryOptions {
|
|
48
|
+
/** Nombre de tentatives supplémentaires après la première. */
|
|
49
|
+
retries?: number;
|
|
50
|
+
/** Délai de base, doublé à chaque tentative. */
|
|
51
|
+
delay?: number;
|
|
52
|
+
/** Plafond du délai, pour que l'attente reste supportable. */
|
|
53
|
+
maxDelay?: number;
|
|
54
|
+
/** Décide si l'erreur mérite une nouvelle tentative. */
|
|
55
|
+
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
56
|
+
onRetry?: (error: unknown, attempt: number) => void;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Construit une fonction qui réessaie.
|
|
61
|
+
*
|
|
62
|
+
* Le délai double à chaque tentative, avec une part aléatoire : dix clients
|
|
63
|
+
* qui échouent en même temps ne doivent pas revenir tous ensemble, sinon le
|
|
64
|
+
* serveur qui se relève retombe aussitôt.
|
|
65
|
+
*/
|
|
66
|
+
declare function createRetry(options?: RetryOptions): <T>(task: (attempt: number) => Promise<T>) => Promise<T>;
|
|
67
|
+
interface ToAsyncStateInput<T> {
|
|
68
|
+
data?: T | null | undefined;
|
|
69
|
+
error?: unknown;
|
|
70
|
+
isLoading?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Traduit le trio `data / error / isLoading` en un état unique.
|
|
74
|
+
*
|
|
75
|
+
* C'est ce que rendent TanStack Query, SWR et RTK Query, et c'est ce que le
|
|
76
|
+
* composant `AsyncState` attend. Le cas qui justifie la fonction est `empty` :
|
|
77
|
+
* une liste chargée mais vide n'est pas un succès qu'on affiche tel quel.
|
|
78
|
+
*/
|
|
79
|
+
declare function toAsyncState<T>(input: ToAsyncStateInput<T>): AsyncState<T>;
|
|
80
|
+
|
|
81
|
+
export { type AsyncState, type AsyncStatus, type DebounceOptions, type Debounced, type RetryOptions, type ToAsyncStateInput, createRetry, debounce, sleep, throttle, toAsyncState };
|
package/dist/async.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createRetry, debounce, sleep, throttle, toAsyncState } from './chunk-DKWI2AZG.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/number.ts
|
|
2
|
+
function formatter(locale, options) {
|
|
3
|
+
return new Intl.NumberFormat(locale, options);
|
|
4
|
+
}
|
|
5
|
+
function formatNumber(value, options = {}) {
|
|
6
|
+
const { locale = "fr-FR", ...formatOptions } = options;
|
|
7
|
+
return formatter(locale, { maximumFractionDigits: 2, ...formatOptions }).format(value);
|
|
8
|
+
}
|
|
9
|
+
function formatPercent(value, options = {}) {
|
|
10
|
+
const { locale = "fr-FR", ...formatOptions } = options;
|
|
11
|
+
return formatter(locale, {
|
|
12
|
+
style: "percent",
|
|
13
|
+
maximumFractionDigits: 2,
|
|
14
|
+
...formatOptions
|
|
15
|
+
}).format(value);
|
|
16
|
+
}
|
|
17
|
+
function clamp(value, min, max) {
|
|
18
|
+
return Math.min(Math.max(value, min), max);
|
|
19
|
+
}
|
|
20
|
+
function round(value, digits = 0) {
|
|
21
|
+
const factor = 10 ** digits;
|
|
22
|
+
return Math.round(value * factor) / factor;
|
|
23
|
+
}
|
|
24
|
+
function toNumber(value, fallback = 0) {
|
|
25
|
+
if (typeof value === "number") return value;
|
|
26
|
+
if (typeof value === "string") {
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
29
|
+
}
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
function parseDecimal(value) {
|
|
33
|
+
const parsed = Number(value);
|
|
34
|
+
if (!Number.isFinite(parsed)) {
|
|
35
|
+
throw new Error("The provided numeric value is invalid.");
|
|
36
|
+
}
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
function roundCurrency(value) {
|
|
40
|
+
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
41
|
+
}
|
|
42
|
+
function formatRate(value) {
|
|
43
|
+
return value.toFixed(4);
|
|
44
|
+
}
|
|
45
|
+
function safeDivide(a, b, fallback = 0) {
|
|
46
|
+
if (b === 0) return fallback;
|
|
47
|
+
const result = a / b;
|
|
48
|
+
return Number.isFinite(result) ? result : fallback;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { clamp, formatNumber, formatPercent, formatRate, parseDecimal, round, roundCurrency, safeDivide, toNumber };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/locale.ts
|
|
2
|
+
var DEFAULT_LOCALE = "fr-FR";
|
|
3
|
+
function resolveLocale(preferred) {
|
|
4
|
+
if (preferred) return preferred;
|
|
5
|
+
if (typeof navigator !== "undefined" && navigator.language) {
|
|
6
|
+
return navigator.language;
|
|
7
|
+
}
|
|
8
|
+
return DEFAULT_LOCALE;
|
|
9
|
+
}
|
|
10
|
+
function languageOf(locale = DEFAULT_LOCALE) {
|
|
11
|
+
return locale.split("-")[0] ?? locale;
|
|
12
|
+
}
|
|
13
|
+
function regionOf(locale = DEFAULT_LOCALE) {
|
|
14
|
+
return locale.split("-")[1];
|
|
15
|
+
}
|
|
16
|
+
var collators = /* @__PURE__ */ new Map();
|
|
17
|
+
function collator(locale = DEFAULT_LOCALE) {
|
|
18
|
+
let found = collators.get(locale);
|
|
19
|
+
if (!found) {
|
|
20
|
+
found = new Intl.Collator(locale, { sensitivity: "base", numeric: true });
|
|
21
|
+
collators.set(locale, found);
|
|
22
|
+
}
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
25
|
+
function compareStrings(a, b, locale = DEFAULT_LOCALE) {
|
|
26
|
+
return collator(locale).compare(a, b);
|
|
27
|
+
}
|
|
28
|
+
function looseIncludes(text, needle, locale = DEFAULT_LOCALE) {
|
|
29
|
+
if (!needle) return true;
|
|
30
|
+
const strip = (value) => value.toLocaleLowerCase(locale).normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
31
|
+
return strip(text).includes(strip(needle));
|
|
32
|
+
}
|
|
33
|
+
function formatList(items, options = {}) {
|
|
34
|
+
const { locale = DEFAULT_LOCALE, type = "conjunction" } = options;
|
|
35
|
+
if (items.length === 0) return "";
|
|
36
|
+
return new Intl.ListFormat(locale, { style: "long", type }).format(items);
|
|
37
|
+
}
|
|
38
|
+
function plural(count, singular, pluralForm) {
|
|
39
|
+
return Math.abs(count) < 2 ? singular : pluralForm ?? `${singular}s`;
|
|
40
|
+
}
|
|
41
|
+
function countLabel(count, singular, options = {}) {
|
|
42
|
+
const { pluralForm, zero, locale = DEFAULT_LOCALE } = options;
|
|
43
|
+
if (count === 0 && zero) return zero;
|
|
44
|
+
return `${count.toLocaleString(locale)} ${plural(count, singular, pluralForm)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { DEFAULT_LOCALE, collator, compareStrings, countLabel, formatList, languageOf, looseIncludes, plural, regionOf, resolveLocale };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/classname.ts
|
|
2
|
+
function append(tokens, value) {
|
|
3
|
+
if (!value) return;
|
|
4
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
5
|
+
tokens.push(String(value));
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
for (const item of value) append(tokens, item);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
for (const [key, enabled] of Object.entries(value)) {
|
|
13
|
+
if (enabled) tokens.push(key);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function cn(...inputs) {
|
|
17
|
+
const tokens = [];
|
|
18
|
+
for (const input of inputs) append(tokens, input);
|
|
19
|
+
return tokens.join(" ");
|
|
20
|
+
}
|
|
21
|
+
function toneClass(block, tone) {
|
|
22
|
+
return `${block}--${tone}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { cn, toneClass };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// src/async.ts
|
|
2
|
+
function sleep(ms, signal) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
if (signal?.aborted) {
|
|
5
|
+
reject(signal.reason ?? new Error("Annul\xE9"));
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const timer = setTimeout(() => {
|
|
9
|
+
signal?.removeEventListener("abort", onAbort);
|
|
10
|
+
resolve();
|
|
11
|
+
}, ms);
|
|
12
|
+
function onAbort() {
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
reject(signal?.reason ?? new Error("Annul\xE9"));
|
|
15
|
+
}
|
|
16
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function debounce(fn, wait = 250, options = {}) {
|
|
20
|
+
const { leading = false, trailing = true } = options;
|
|
21
|
+
let timer;
|
|
22
|
+
let lastArgs;
|
|
23
|
+
const run = () => {
|
|
24
|
+
timer = void 0;
|
|
25
|
+
if (trailing && lastArgs) {
|
|
26
|
+
const args = lastArgs;
|
|
27
|
+
lastArgs = void 0;
|
|
28
|
+
fn(...args);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const debounced = ((...args) => {
|
|
32
|
+
lastArgs = args;
|
|
33
|
+
if (timer === void 0 && leading) {
|
|
34
|
+
lastArgs = void 0;
|
|
35
|
+
fn(...args);
|
|
36
|
+
}
|
|
37
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
38
|
+
timer = setTimeout(run, wait);
|
|
39
|
+
});
|
|
40
|
+
debounced.cancel = () => {
|
|
41
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
42
|
+
timer = void 0;
|
|
43
|
+
lastArgs = void 0;
|
|
44
|
+
};
|
|
45
|
+
debounced.flush = () => {
|
|
46
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
47
|
+
run();
|
|
48
|
+
};
|
|
49
|
+
debounced.pending = () => timer !== void 0;
|
|
50
|
+
return debounced;
|
|
51
|
+
}
|
|
52
|
+
function throttle(fn, wait = 100) {
|
|
53
|
+
let last = 0;
|
|
54
|
+
let timer;
|
|
55
|
+
let lastArgs;
|
|
56
|
+
const invoke = (args) => {
|
|
57
|
+
last = Date.now();
|
|
58
|
+
lastArgs = void 0;
|
|
59
|
+
fn(...args);
|
|
60
|
+
};
|
|
61
|
+
const throttled = ((...args) => {
|
|
62
|
+
const elapsed = Date.now() - last;
|
|
63
|
+
lastArgs = args;
|
|
64
|
+
if (elapsed >= wait) {
|
|
65
|
+
if (timer !== void 0) {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
timer = void 0;
|
|
68
|
+
}
|
|
69
|
+
invoke(args);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (timer === void 0) {
|
|
73
|
+
timer = setTimeout(() => {
|
|
74
|
+
timer = void 0;
|
|
75
|
+
if (lastArgs) invoke(lastArgs);
|
|
76
|
+
}, wait - elapsed);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
throttled.cancel = () => {
|
|
80
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
81
|
+
timer = void 0;
|
|
82
|
+
lastArgs = void 0;
|
|
83
|
+
};
|
|
84
|
+
throttled.flush = () => {
|
|
85
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
86
|
+
timer = void 0;
|
|
87
|
+
if (lastArgs) invoke(lastArgs);
|
|
88
|
+
};
|
|
89
|
+
throttled.pending = () => timer !== void 0;
|
|
90
|
+
return throttled;
|
|
91
|
+
}
|
|
92
|
+
function createRetry(options = {}) {
|
|
93
|
+
const {
|
|
94
|
+
retries = 2,
|
|
95
|
+
delay = 300,
|
|
96
|
+
maxDelay = 8e3,
|
|
97
|
+
shouldRetry = () => true,
|
|
98
|
+
onRetry,
|
|
99
|
+
signal
|
|
100
|
+
} = options;
|
|
101
|
+
return async function run(task) {
|
|
102
|
+
let lastError;
|
|
103
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
104
|
+
try {
|
|
105
|
+
return await task(attempt);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
lastError = error;
|
|
108
|
+
if (attempt === retries || !shouldRetry(error, attempt)) throw error;
|
|
109
|
+
onRetry?.(error, attempt);
|
|
110
|
+
const base = Math.min(delay * 2 ** attempt, maxDelay);
|
|
111
|
+
await sleep(base + Math.random() * 0.1 * base, signal);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw lastError;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function toAsyncState(input) {
|
|
118
|
+
const { data, error, isLoading = false } = input;
|
|
119
|
+
if (error !== void 0 && error !== null) return { status: "error", error };
|
|
120
|
+
if (isLoading) return { status: "loading" };
|
|
121
|
+
if (data === void 0 || data === null) return { status: "idle" };
|
|
122
|
+
if (Array.isArray(data) && data.length === 0) {
|
|
123
|
+
return { status: "empty", data };
|
|
124
|
+
}
|
|
125
|
+
return { status: "success", data };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { createRetry, debounce, sleep, throttle, toAsyncState };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/query.ts
|
|
2
|
+
function serialize(value) {
|
|
3
|
+
if (value instanceof Date) return value.toISOString();
|
|
4
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
5
|
+
return String(value);
|
|
6
|
+
}
|
|
7
|
+
function toQueryString(params, options = {}) {
|
|
8
|
+
if (!params) return "";
|
|
9
|
+
const { prefix = true, skipEmpty = false } = options;
|
|
10
|
+
const entries = Object.entries(params).filter(([, value]) => value !== void 0 && value !== null).flatMap(([key, value]) => {
|
|
11
|
+
const values = Array.isArray(value) ? value : [value];
|
|
12
|
+
return values.filter((item) => item !== void 0 && item !== null).map((item) => serialize(item)).filter((item) => !skipEmpty || item !== "").map(
|
|
13
|
+
(item) => `${encodeURIComponent(key)}=${encodeURIComponent(item)}`
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
if (entries.length === 0) return "";
|
|
17
|
+
return `${prefix ? "?" : ""}${entries.join("&")}`;
|
|
18
|
+
}
|
|
19
|
+
function fromQueryString(search) {
|
|
20
|
+
const params = new URLSearchParams(
|
|
21
|
+
search.startsWith("?") ? search.slice(1) : search
|
|
22
|
+
);
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const [key, value] of params) {
|
|
25
|
+
const existing = out[key];
|
|
26
|
+
if (existing === void 0) out[key] = value;
|
|
27
|
+
else if (Array.isArray(existing)) existing.push(value);
|
|
28
|
+
else out[key] = [existing, value];
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
function removeEmptyParams(params) {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const [key, value] of Object.entries(params)) {
|
|
35
|
+
if (value === null || value === void 0) continue;
|
|
36
|
+
if (value === "") continue;
|
|
37
|
+
if (Array.isArray(value) && value.length === 0) continue;
|
|
38
|
+
out[key] = value;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function mergeSearchParams(base, patch, options = {}) {
|
|
43
|
+
const params = new URLSearchParams(
|
|
44
|
+
base.startsWith("?") ? base.slice(1) : base
|
|
45
|
+
);
|
|
46
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
47
|
+
params.delete(key);
|
|
48
|
+
if (value === null || value === void 0) continue;
|
|
49
|
+
const values = Array.isArray(value) ? value : [value];
|
|
50
|
+
for (const item of values) {
|
|
51
|
+
if (item === null || item === void 0) continue;
|
|
52
|
+
params.append(key, serialize(item));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const result = params.toString();
|
|
56
|
+
if (!result) return "";
|
|
57
|
+
return `${options.prefix === false ? "" : "?"}${result}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { fromQueryString, mergeSearchParams, removeEmptyParams, toQueryString };
|