@sia-ui/react 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 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,38 @@
1
+ # @sia-ui/react
2
+
3
+ Les primitives de runtime dont un composant DOM a besoin.
4
+
5
+ ```bash
6
+ pnpm add @sia-ui/react
7
+ ```
8
+
9
+ ```tsx
10
+ import { SiaProvider, Portal, usePortal, useOutsideClick } from "@sia-ui/react";
11
+ ```
12
+
13
+ - `SiaProvider` — thème clair/sombre, variables CSS;
14
+ - `Portal` et `usePortal` — rendre hors du flux, sans casser le rendu serveur;
15
+ - `useOutsideClick` — écoute `pointerdown`, pas `click` : une sélection de
16
+ texte qui déborde ne ferme pas l'overlay;
17
+ - `useControllableState`, `useDisclosure`, `mergeRefs`.
18
+
19
+ `react` et `react-dom` sont des `peerDependencies`.
20
+
21
+ ## Place dans SIA UI
22
+
23
+ ```
24
+ @sia-ui/tokens valeurs : couleurs, espacements, mouvement, vocabulaire
25
+ @sia-ui/utils fonctions sans dépendance
26
+ @sia-ui/api client HTTP — indépendant de l'interface
27
+ @sia-ui/headless comportement, avec React, sans plateforme
28
+ @sia-ui/react primitives de runtime DOM
29
+ @sia-ui/react-web composants DOM
30
+ @sia-ui/react-native composants natifs, à venir
31
+ ```
32
+
33
+ Une chose s'écrit au niveau le plus bas où elle a du sens, et une seule fois.
34
+ Voir [la documentation](https://github.com/BeatJo/sia-ui/tree/main/docs).
35
+
36
+ ## Licence
37
+
38
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var tokens = require('@sia-ui/tokens');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var reactDom = require('react-dom');
7
+ var clipboard = require('@sia-ui/utils/clipboard');
8
+
9
+ // src/theme-provider.tsx
10
+ function read(key, fallback, area, deserialize) {
11
+ if (!area) return fallback;
12
+ try {
13
+ const raw = area.getItem(key);
14
+ return raw === null ? fallback : deserialize(raw);
15
+ } catch {
16
+ return fallback;
17
+ }
18
+ }
19
+ function areaFor(kind) {
20
+ if (typeof window === "undefined") return null;
21
+ try {
22
+ return kind === "session" ? window.sessionStorage : window.localStorage;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ function useLocalStorage(key, initialValue, options = {}) {
28
+ const {
29
+ storage = "local",
30
+ serialize = JSON.stringify,
31
+ deserialize = JSON.parse,
32
+ syncTabs = false
33
+ } = options;
34
+ const area = areaFor(storage);
35
+ const [value, setValue] = react.useState(initialValue);
36
+ const hydrated = react.useRef(false);
37
+ react.useEffect(() => {
38
+ if (hydrated.current) return;
39
+ hydrated.current = true;
40
+ const stored = read(key, initialValue, area, deserialize);
41
+ if (stored !== initialValue) setValue(stored);
42
+ }, []);
43
+ const write = react.useCallback(
44
+ (next) => {
45
+ setValue((current) => {
46
+ const resolved = typeof next === "function" ? next(current) : next;
47
+ try {
48
+ area?.setItem(key, serialize(resolved));
49
+ } catch {
50
+ }
51
+ return resolved;
52
+ });
53
+ },
54
+ [area, key, serialize]
55
+ );
56
+ const remove = react.useCallback(() => {
57
+ try {
58
+ area?.removeItem(key);
59
+ } catch {
60
+ }
61
+ setValue(initialValue);
62
+ }, [area, key]);
63
+ react.useEffect(() => {
64
+ if (!syncTabs || typeof window === "undefined") return void 0;
65
+ const onStorage = (event) => {
66
+ if (event.key !== key || event.storageArea !== area) return;
67
+ setValue(
68
+ event.newValue === null ? initialValue : deserialize(event.newValue)
69
+ );
70
+ };
71
+ window.addEventListener("storage", onStorage);
72
+ return () => window.removeEventListener("storage", onStorage);
73
+ }, [area, key, syncTabs]);
74
+ return [value, write, remove];
75
+ }
76
+ function useMediaQuery(query, serverValue = false) {
77
+ const subscribe = react.useCallback(
78
+ (onChange) => {
79
+ if (typeof window === "undefined" || !window.matchMedia) return () => {
80
+ };
81
+ const list = window.matchMedia(query);
82
+ list.addEventListener("change", onChange);
83
+ return () => list.removeEventListener("change", onChange);
84
+ },
85
+ [query]
86
+ );
87
+ const getSnapshot = react.useCallback(() => {
88
+ if (typeof window === "undefined" || !window.matchMedia) return serverValue;
89
+ return window.matchMedia(query).matches;
90
+ }, [query, serverValue]);
91
+ const getServerSnapshot = react.useCallback(() => serverValue, [serverValue]);
92
+ return react.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
93
+ }
94
+ var breakpoints = {
95
+ sm: 480,
96
+ md: 640,
97
+ lg: 768,
98
+ xl: 1024,
99
+ "2xl": 1280
100
+ };
101
+ function useBreakpoint(name) {
102
+ return useMediaQuery(`(min-width: ${breakpoints[name]}px)`);
103
+ }
104
+ function useIsMobile() {
105
+ return !useMediaQuery(`(min-width: ${breakpoints.lg}px)`);
106
+ }
107
+ function usePrefersDark() {
108
+ return useMediaQuery("(prefers-color-scheme: dark)");
109
+ }
110
+ function usePrefersReducedMotion() {
111
+ return useMediaQuery("(prefers-reduced-motion: reduce)");
112
+ }
113
+ var COLOR_MODE_KEY = "sia-ui.color-mode";
114
+ var ThemeContext = react.createContext({
115
+ colorMode: "light",
116
+ resolvedColorMode: "light",
117
+ setColorMode: () => {
118
+ },
119
+ toggleColorMode: () => {
120
+ }
121
+ });
122
+ function SiaProvider({
123
+ children,
124
+ colorMode: controlled,
125
+ defaultColorMode = "system",
126
+ onColorModeChange,
127
+ storageKey = COLOR_MODE_KEY,
128
+ theme,
129
+ className
130
+ }) {
131
+ const [stored, setStored] = useLocalStorage(
132
+ storageKey === false ? COLOR_MODE_KEY : storageKey,
133
+ defaultColorMode
134
+ );
135
+ const mode = controlled ?? (storageKey === false ? defaultColorMode : stored);
136
+ const prefersDark = usePrefersDark();
137
+ const resolved = mode === "system" ? prefersDark ? "dark" : "light" : mode;
138
+ const setColorMode = react.useCallback(
139
+ (next) => {
140
+ if (storageKey !== false && controlled === void 0) setStored(next);
141
+ onColorModeChange?.(next);
142
+ },
143
+ [controlled, onColorModeChange, setStored, storageKey]
144
+ );
145
+ const variables = react.useMemo(() => {
146
+ const semanticTheme = resolved === "dark" ? tokens.darkTheme : tokens.lightTheme;
147
+ return {
148
+ ...tokens.toCssVariables(tokens.primitives),
149
+ ...tokens.toCssVariables(semanticTheme),
150
+ ...tokens.toCssVariables(theme ?? {})
151
+ };
152
+ }, [resolved, theme]);
153
+ react.useEffect(() => {
154
+ if (typeof document === "undefined") return;
155
+ const root = document.documentElement;
156
+ root.dataset.siaColorMode = resolved;
157
+ root.style.colorScheme = resolved;
158
+ }, [resolved]);
159
+ const value = react.useMemo(
160
+ () => ({
161
+ colorMode: mode,
162
+ resolvedColorMode: resolved,
163
+ setColorMode,
164
+ // Bascule depuis ce qui est affiché, pas depuis ce qui est demandé :
165
+ // en mode `system` la personne voit du sombre et attend du clair.
166
+ toggleColorMode: () => setColorMode(resolved === "dark" ? "light" : "dark")
167
+ }),
168
+ [mode, resolved, setColorMode]
169
+ );
170
+ return /* @__PURE__ */ jsxRuntime.jsx(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx(
171
+ "div",
172
+ {
173
+ className,
174
+ "data-sia-root": "",
175
+ "data-sia-color-mode": resolved,
176
+ style: variables,
177
+ children
178
+ }
179
+ ) });
180
+ }
181
+ function useSiaTheme() {
182
+ return react.useContext(ThemeContext);
183
+ }
184
+ function useColorMode() {
185
+ const { colorMode, resolvedColorMode, setColorMode, toggleColorMode } = react.useContext(ThemeContext);
186
+ return { colorMode, resolvedColorMode, setColorMode, toggleColorMode };
187
+ }
188
+ function colorModeScript(storageKey = COLOR_MODE_KEY, defaultMode = "system") {
189
+ return `(function(){try{var m=localStorage.getItem(${JSON.stringify(storageKey)});m=m?JSON.parse(m):${JSON.stringify(defaultMode)};if(m==="system"){m=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";}document.documentElement.dataset.siaColorMode=m;document.documentElement.style.colorScheme=m;}catch(e){}})();`;
190
+ }
191
+ function useControllableState({
192
+ value,
193
+ defaultValue,
194
+ onChange
195
+ }) {
196
+ const [internalValue, setInternalValue] = react.useState(defaultValue);
197
+ const controlled = value !== void 0;
198
+ const currentValue = controlled ? value : internalValue;
199
+ const setValue = react.useCallback(
200
+ (nextValue) => {
201
+ if (!controlled) setInternalValue(nextValue);
202
+ onChange?.(nextValue);
203
+ },
204
+ [controlled, onChange]
205
+ );
206
+ return [currentValue, setValue];
207
+ }
208
+ function useDisclosure(options = {}) {
209
+ const [open, setOpen] = useControllableState({
210
+ value: options.open,
211
+ defaultValue: options.defaultOpen ?? false,
212
+ onChange: options.onOpenChange
213
+ });
214
+ return {
215
+ open,
216
+ setOpen,
217
+ onOpen: react.useCallback(() => setOpen(true), [setOpen]),
218
+ onClose: react.useCallback(() => setOpen(false), [setOpen]),
219
+ onToggle: react.useCallback(() => setOpen(!open), [open, setOpen])
220
+ };
221
+ }
222
+
223
+ // src/merge-refs.ts
224
+ function mergeRefs(...refs) {
225
+ return (value) => {
226
+ for (const ref of refs) {
227
+ if (typeof ref === "function") ref(value);
228
+ else if (ref) ref.current = value;
229
+ }
230
+ };
231
+ }
232
+ function usePortal(options = {}) {
233
+ const { container, id } = options;
234
+ const [node, setNode] = react.useState(null);
235
+ react.useEffect(() => {
236
+ if (typeof document === "undefined") return void 0;
237
+ if (container) {
238
+ setNode(container);
239
+ return void 0;
240
+ }
241
+ const element = document.createElement("div");
242
+ if (id) element.id = id;
243
+ element.setAttribute("data-sia-portal", "");
244
+ document.body.appendChild(element);
245
+ setNode(element);
246
+ return () => {
247
+ element.remove();
248
+ setNode(null);
249
+ };
250
+ }, [container, id]);
251
+ return node;
252
+ }
253
+
254
+ // src/portal.tsx
255
+ function Portal({ children, container }) {
256
+ const node = usePortal(container ? { container } : {});
257
+ return node ? reactDom.createPortal(children, node) : null;
258
+ }
259
+ function useOutsideClick(targets, onOutside, options = {}) {
260
+ const { enabled = true } = options;
261
+ const handler = react.useRef(onOutside);
262
+ handler.current = onOutside;
263
+ const refs = react.useRef(targets);
264
+ refs.current = targets;
265
+ react.useEffect(() => {
266
+ if (!enabled || typeof document === "undefined") return void 0;
267
+ const listener = (event) => {
268
+ const target = event.target;
269
+ if (!target || !target.isConnected) return;
270
+ const zones = Array.isArray(refs.current) ? refs.current : [refs.current];
271
+ if (zones.some((zone) => zone.current?.contains(target))) return;
272
+ handler.current(event);
273
+ };
274
+ document.addEventListener("pointerdown", listener, true);
275
+ return () => document.removeEventListener("pointerdown", listener, true);
276
+ }, [enabled]);
277
+ }
278
+ function useCopyToClipboard(options = {}) {
279
+ const { resetAfter = 2e3 } = options;
280
+ const [copied, setCopied] = react.useState(false);
281
+ const [failed, setFailed] = react.useState(false);
282
+ const timer = react.useRef(void 0);
283
+ react.useEffect(() => () => clearTimeout(timer.current), []);
284
+ const copy = react.useCallback(
285
+ async (text) => {
286
+ const ok = await clipboard.copyText(text);
287
+ setCopied(ok);
288
+ setFailed(!ok);
289
+ clearTimeout(timer.current);
290
+ timer.current = setTimeout(() => {
291
+ setCopied(false);
292
+ setFailed(false);
293
+ }, resetAfter);
294
+ return ok;
295
+ },
296
+ [resetAfter]
297
+ );
298
+ return { copy, copied, failed };
299
+ }
300
+
301
+ exports.COLOR_MODE_KEY = COLOR_MODE_KEY;
302
+ exports.Portal = Portal;
303
+ exports.SiaProvider = SiaProvider;
304
+ exports.breakpoints = breakpoints;
305
+ exports.colorModeScript = colorModeScript;
306
+ exports.mergeRefs = mergeRefs;
307
+ exports.useBreakpoint = useBreakpoint;
308
+ exports.useColorMode = useColorMode;
309
+ exports.useControllableState = useControllableState;
310
+ exports.useCopyToClipboard = useCopyToClipboard;
311
+ exports.useDisclosure = useDisclosure;
312
+ exports.useIsMobile = useIsMobile;
313
+ exports.useLocalStorage = useLocalStorage;
314
+ exports.useMediaQuery = useMediaQuery;
315
+ exports.useOutsideClick = useOutsideClick;
316
+ exports.usePortal = usePortal;
317
+ exports.usePrefersDark = usePrefersDark;
318
+ exports.usePrefersReducedMotion = usePrefersReducedMotion;
319
+ exports.useSiaTheme = useSiaTheme;
@@ -0,0 +1,208 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as react from 'react';
3
+ import { PropsWithChildren, Ref, RefCallback, RefObject } from 'react';
4
+ import { SiaTheme } from '@sia-ui/tokens';
5
+
6
+ /** Ce que l'application demande. */
7
+ type ColorMode = "light" | "dark" | "system";
8
+ /** Ce qui est réellement appliqué — `system` a été résolu. */
9
+ type ResolvedColorMode = "light" | "dark";
10
+ declare const COLOR_MODE_KEY = "sia-ui.color-mode";
11
+ interface SiaProviderProps extends PropsWithChildren {
12
+ /** Le mode imposé. Sans lui, le mode est piloté par le fournisseur. */
13
+ colorMode?: ColorMode;
14
+ /** Le mode de départ quand rien n'est encore enregistré. */
15
+ defaultColorMode?: ColorMode;
16
+ onColorModeChange?: (mode: ColorMode) => void;
17
+ /** Clé de persistance, ou `false` pour ne rien enregistrer. */
18
+ storageKey?: string | false;
19
+ theme?: SiaTheme;
20
+ className?: string;
21
+ }
22
+ interface ThemeValue {
23
+ /** Ce qui est demandé : `light`, `dark` ou `system`. */
24
+ colorMode: ColorMode;
25
+ /** Ce qui est appliqué, `system` résolu. */
26
+ resolvedColorMode: ResolvedColorMode;
27
+ setColorMode: (mode: ColorMode) => void;
28
+ /** Bascule entre clair et sombre, en partant de ce qui est affiché. */
29
+ toggleColorMode: () => void;
30
+ }
31
+ /**
32
+ * Le thème de l'application.
33
+ *
34
+ * `next-themes` apparaît dans cinq des projets du disque D, pour cette seule
35
+ * fonction. Il tient ici en un fournisseur, sans dépendance et sans être lié
36
+ * à un cadre : `system` suit la préférence du poste, le choix est enregistré,
37
+ * et `colorMode` reste disponible pour piloter le mode depuis l'extérieur.
38
+ */
39
+ declare function SiaProvider({ children, colorMode: controlled, defaultColorMode, onColorModeChange, storageKey, theme, className, }: SiaProviderProps): react_jsx_runtime.JSX.Element;
40
+ /** Le thème en vigueur, et de quoi en changer. */
41
+ declare function useSiaTheme(): ThemeValue;
42
+ /**
43
+ * Le mode et son sélecteur, sans le reste.
44
+ *
45
+ * ```tsx
46
+ * const { resolvedColorMode, toggleColorMode } = useColorMode();
47
+ * <Toggle pressed={resolvedColorMode === "dark"} onPressedChange={toggleColorMode} />
48
+ * ```
49
+ */
50
+ declare function useColorMode(): {
51
+ colorMode: ColorMode;
52
+ resolvedColorMode: ResolvedColorMode;
53
+ setColorMode: (mode: ColorMode) => void;
54
+ toggleColorMode: () => void;
55
+ };
56
+ /**
57
+ * Le script à poser avant le premier rendu, pour éviter le flash.
58
+ *
59
+ * Entre le chargement du document et le premier rendu de React, la page
60
+ * s'affiche dans le mode par défaut — un éclair blanc sur un thème sombre.
61
+ * Ce script lit le choix enregistré et pose l'attribut avant la peinture.
62
+ *
63
+ * ```tsx
64
+ * <head>
65
+ * <script dangerouslySetInnerHTML={{ __html: colorModeScript() }} />
66
+ * </head>
67
+ * ```
68
+ */
69
+ declare function colorModeScript(storageKey?: string, defaultMode?: ColorMode): string;
70
+
71
+ interface ControllableStateOptions<T> {
72
+ value?: T | undefined;
73
+ defaultValue: T;
74
+ onChange?: ((value: T) => void) | undefined;
75
+ }
76
+ declare function useControllableState<T>({ value, defaultValue, onChange }: ControllableStateOptions<T>): readonly [T, (nextValue: T) => void];
77
+
78
+ declare function useDisclosure(options?: {
79
+ open?: boolean;
80
+ defaultOpen?: boolean;
81
+ onOpenChange?: (open: boolean) => void;
82
+ }): {
83
+ open: boolean;
84
+ setOpen: (nextValue: boolean) => void;
85
+ onOpen: () => void;
86
+ onClose: () => void;
87
+ onToggle: () => void;
88
+ };
89
+
90
+ declare function mergeRefs<T>(...refs: Array<Ref<T> | undefined>): RefCallback<T>;
91
+
92
+ interface PortalProps extends PropsWithChildren {
93
+ /** Conteneur existant à réutiliser. Par défaut, un nœud créé sous `document.body`. */
94
+ container?: HTMLElement | null | undefined;
95
+ }
96
+ /**
97
+ * Rend ses enfants hors du flux, dans un nœud attaché au document.
98
+ *
99
+ * Le rendu est différé jusqu'à ce que `usePortal` ait un nœud : sans cela le
100
+ * rendu serveur tenterait d'atteindre `document`, et la première passe cliente
101
+ * rendrait dans le vide.
102
+ */
103
+ declare function Portal({ children, container }: PortalProps): react.ReactPortal | null;
104
+
105
+ interface UsePortalOptions {
106
+ /** Conteneur existant à réutiliser. Par défaut, un nœud créé sous `document.body`. */
107
+ container?: HTMLElement | null | undefined;
108
+ /** Identifiant du nœud créé, utile pour le retrouver dans le DOM. */
109
+ id?: string | undefined;
110
+ }
111
+ /**
112
+ * Renvoie le nœud dans lequel une couche flottante doit être rendue, ou `null`
113
+ * tant que le document n'est pas disponible — le rendu serveur passe par ce cas.
114
+ *
115
+ * Le nœud créé par le hook est retiré au démontage; un conteneur fourni par
116
+ * l'appelant ne l'est jamais, puisqu'il ne nous appartient pas.
117
+ */
118
+ declare function usePortal(options?: UsePortalOptions): HTMLElement | null;
119
+
120
+ type OutsideClickTarget = RefObject<HTMLElement | null>;
121
+ interface UseOutsideClickOptions {
122
+ /** Désactive l'écoute sans démonter le composant. */
123
+ enabled?: boolean;
124
+ }
125
+ /**
126
+ * Appelle `onOutside` lorsqu'un pointeur est pressé hors de toutes les zones
127
+ * passées — un panneau et son déclencheur, par exemple.
128
+ *
129
+ * L'écoute se fait sur `pointerdown` et non sur `click` : un clic dont la
130
+ * pression a commencé à l'intérieur et s'est terminée à l'extérieur — une
131
+ * sélection de texte qui déborde — ne doit pas fermer l'overlay.
132
+ */
133
+ declare function useOutsideClick(targets: OutsideClickTarget | readonly OutsideClickTarget[], onOutside: (event: PointerEvent) => void, options?: UseOutsideClickOptions): void;
134
+
135
+ /**
136
+ * Une requête média, suivie en direct.
137
+ *
138
+ * `useSyncExternalStore` plutôt qu'un `useState` + `useEffect` : le rendu
139
+ * serveur rend `false`, la première passe cliente lit la vraie valeur, et
140
+ * React sait que la source est extérieure. Un `useEffect` produirait un
141
+ * clignotement au premier affichage — la mise en page saute une fois montée.
142
+ */
143
+ declare function useMediaQuery(query: string, serverValue?: boolean): boolean;
144
+ /** Les points de rupture du système, alignés sur les feuilles de style. */
145
+ declare const breakpoints: {
146
+ readonly sm: 480;
147
+ readonly md: 640;
148
+ readonly lg: 768;
149
+ readonly xl: 1024;
150
+ readonly "2xl": 1280;
151
+ };
152
+ type Breakpoint = keyof typeof breakpoints;
153
+ /** Vrai au-dessus du point de rupture demandé. */
154
+ declare function useBreakpoint(name: Breakpoint): boolean;
155
+ /** Vrai sous 768 px — le seuil où les mises en page du dépôt basculent. */
156
+ declare function useIsMobile(): boolean;
157
+ /** La préférence système de thème sombre. */
158
+ declare function usePrefersDark(): boolean;
159
+ /**
160
+ * La préférence système de mouvement réduit.
161
+ *
162
+ * Les feuilles de style la respectent déjà. Ce hook sert pour ce que le CSS
163
+ * ne peut pas couper : une animation pilotée en JavaScript, un défilement
164
+ * automatique, un carrousel.
165
+ */
166
+ declare function usePrefersReducedMotion(): boolean;
167
+
168
+ interface UseLocalStorageOptions<T> {
169
+ /** `sessionStorage` pour ce qui ne doit pas survivre à l'onglet. */
170
+ storage?: "local" | "session";
171
+ serialize?: (value: T) => string;
172
+ deserialize?: (raw: string) => T;
173
+ /**
174
+ * Suit les changements venus d'un autre onglet. Utile pour une déconnexion
175
+ * ou un thème; inutile — et coûteux — pour un brouillon local.
176
+ */
177
+ syncTabs?: boolean;
178
+ }
179
+ /**
180
+ * Une valeur qui survit au rechargement.
181
+ *
182
+ * La lecture initiale est paresseuse : elle se fait au premier rendu client,
183
+ * jamais pendant le rendu serveur, ce qui évite l'erreur d'hydratation quand
184
+ * la valeur stockée diffère du repli.
185
+ */
186
+ declare function useLocalStorage<T>(key: string, initialValue: T, options?: UseLocalStorageOptions<T>): readonly [T, (next: T | ((current: T) => T)) => void, () => void];
187
+
188
+ interface UseCopyToClipboardOptions {
189
+ /** Durée pendant laquelle `copied` reste vrai. */
190
+ resetAfter?: number;
191
+ }
192
+ interface CopyToClipboard {
193
+ copy: (text: string) => Promise<boolean>;
194
+ /** Vrai juste après une copie réussie — pour changer l'icône du bouton. */
195
+ copied: boolean;
196
+ /** Vrai si la dernière tentative a échoué. */
197
+ failed: boolean;
198
+ }
199
+ /**
200
+ * Copier, et le montrer.
201
+ *
202
+ * La copie elle-même est dans `@sia-ui/utils/clipboard`, avec son repli pour
203
+ * les contextes non sécurisés. Ce qui est ici est le seul ajout de React :
204
+ * l'état éphémère qui permet au bouton de dire « copié » deux secondes.
205
+ */
206
+ declare function useCopyToClipboard(options?: UseCopyToClipboardOptions): CopyToClipboard;
207
+
208
+ export { type Breakpoint, COLOR_MODE_KEY, type ColorMode, type ControllableStateOptions, type CopyToClipboard, type OutsideClickTarget, Portal, type PortalProps, type ResolvedColorMode, SiaProvider, type SiaProviderProps, type UseCopyToClipboardOptions, type UseLocalStorageOptions, type UseOutsideClickOptions, type UsePortalOptions, breakpoints, colorModeScript, mergeRefs, useBreakpoint, useColorMode, useControllableState, useCopyToClipboard, useDisclosure, useIsMobile, useLocalStorage, useMediaQuery, useOutsideClick, usePortal, usePrefersDark, usePrefersReducedMotion, useSiaTheme };
@@ -0,0 +1,208 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as react from 'react';
3
+ import { PropsWithChildren, Ref, RefCallback, RefObject } from 'react';
4
+ import { SiaTheme } from '@sia-ui/tokens';
5
+
6
+ /** Ce que l'application demande. */
7
+ type ColorMode = "light" | "dark" | "system";
8
+ /** Ce qui est réellement appliqué — `system` a été résolu. */
9
+ type ResolvedColorMode = "light" | "dark";
10
+ declare const COLOR_MODE_KEY = "sia-ui.color-mode";
11
+ interface SiaProviderProps extends PropsWithChildren {
12
+ /** Le mode imposé. Sans lui, le mode est piloté par le fournisseur. */
13
+ colorMode?: ColorMode;
14
+ /** Le mode de départ quand rien n'est encore enregistré. */
15
+ defaultColorMode?: ColorMode;
16
+ onColorModeChange?: (mode: ColorMode) => void;
17
+ /** Clé de persistance, ou `false` pour ne rien enregistrer. */
18
+ storageKey?: string | false;
19
+ theme?: SiaTheme;
20
+ className?: string;
21
+ }
22
+ interface ThemeValue {
23
+ /** Ce qui est demandé : `light`, `dark` ou `system`. */
24
+ colorMode: ColorMode;
25
+ /** Ce qui est appliqué, `system` résolu. */
26
+ resolvedColorMode: ResolvedColorMode;
27
+ setColorMode: (mode: ColorMode) => void;
28
+ /** Bascule entre clair et sombre, en partant de ce qui est affiché. */
29
+ toggleColorMode: () => void;
30
+ }
31
+ /**
32
+ * Le thème de l'application.
33
+ *
34
+ * `next-themes` apparaît dans cinq des projets du disque D, pour cette seule
35
+ * fonction. Il tient ici en un fournisseur, sans dépendance et sans être lié
36
+ * à un cadre : `system` suit la préférence du poste, le choix est enregistré,
37
+ * et `colorMode` reste disponible pour piloter le mode depuis l'extérieur.
38
+ */
39
+ declare function SiaProvider({ children, colorMode: controlled, defaultColorMode, onColorModeChange, storageKey, theme, className, }: SiaProviderProps): react_jsx_runtime.JSX.Element;
40
+ /** Le thème en vigueur, et de quoi en changer. */
41
+ declare function useSiaTheme(): ThemeValue;
42
+ /**
43
+ * Le mode et son sélecteur, sans le reste.
44
+ *
45
+ * ```tsx
46
+ * const { resolvedColorMode, toggleColorMode } = useColorMode();
47
+ * <Toggle pressed={resolvedColorMode === "dark"} onPressedChange={toggleColorMode} />
48
+ * ```
49
+ */
50
+ declare function useColorMode(): {
51
+ colorMode: ColorMode;
52
+ resolvedColorMode: ResolvedColorMode;
53
+ setColorMode: (mode: ColorMode) => void;
54
+ toggleColorMode: () => void;
55
+ };
56
+ /**
57
+ * Le script à poser avant le premier rendu, pour éviter le flash.
58
+ *
59
+ * Entre le chargement du document et le premier rendu de React, la page
60
+ * s'affiche dans le mode par défaut — un éclair blanc sur un thème sombre.
61
+ * Ce script lit le choix enregistré et pose l'attribut avant la peinture.
62
+ *
63
+ * ```tsx
64
+ * <head>
65
+ * <script dangerouslySetInnerHTML={{ __html: colorModeScript() }} />
66
+ * </head>
67
+ * ```
68
+ */
69
+ declare function colorModeScript(storageKey?: string, defaultMode?: ColorMode): string;
70
+
71
+ interface ControllableStateOptions<T> {
72
+ value?: T | undefined;
73
+ defaultValue: T;
74
+ onChange?: ((value: T) => void) | undefined;
75
+ }
76
+ declare function useControllableState<T>({ value, defaultValue, onChange }: ControllableStateOptions<T>): readonly [T, (nextValue: T) => void];
77
+
78
+ declare function useDisclosure(options?: {
79
+ open?: boolean;
80
+ defaultOpen?: boolean;
81
+ onOpenChange?: (open: boolean) => void;
82
+ }): {
83
+ open: boolean;
84
+ setOpen: (nextValue: boolean) => void;
85
+ onOpen: () => void;
86
+ onClose: () => void;
87
+ onToggle: () => void;
88
+ };
89
+
90
+ declare function mergeRefs<T>(...refs: Array<Ref<T> | undefined>): RefCallback<T>;
91
+
92
+ interface PortalProps extends PropsWithChildren {
93
+ /** Conteneur existant à réutiliser. Par défaut, un nœud créé sous `document.body`. */
94
+ container?: HTMLElement | null | undefined;
95
+ }
96
+ /**
97
+ * Rend ses enfants hors du flux, dans un nœud attaché au document.
98
+ *
99
+ * Le rendu est différé jusqu'à ce que `usePortal` ait un nœud : sans cela le
100
+ * rendu serveur tenterait d'atteindre `document`, et la première passe cliente
101
+ * rendrait dans le vide.
102
+ */
103
+ declare function Portal({ children, container }: PortalProps): react.ReactPortal | null;
104
+
105
+ interface UsePortalOptions {
106
+ /** Conteneur existant à réutiliser. Par défaut, un nœud créé sous `document.body`. */
107
+ container?: HTMLElement | null | undefined;
108
+ /** Identifiant du nœud créé, utile pour le retrouver dans le DOM. */
109
+ id?: string | undefined;
110
+ }
111
+ /**
112
+ * Renvoie le nœud dans lequel une couche flottante doit être rendue, ou `null`
113
+ * tant que le document n'est pas disponible — le rendu serveur passe par ce cas.
114
+ *
115
+ * Le nœud créé par le hook est retiré au démontage; un conteneur fourni par
116
+ * l'appelant ne l'est jamais, puisqu'il ne nous appartient pas.
117
+ */
118
+ declare function usePortal(options?: UsePortalOptions): HTMLElement | null;
119
+
120
+ type OutsideClickTarget = RefObject<HTMLElement | null>;
121
+ interface UseOutsideClickOptions {
122
+ /** Désactive l'écoute sans démonter le composant. */
123
+ enabled?: boolean;
124
+ }
125
+ /**
126
+ * Appelle `onOutside` lorsqu'un pointeur est pressé hors de toutes les zones
127
+ * passées — un panneau et son déclencheur, par exemple.
128
+ *
129
+ * L'écoute se fait sur `pointerdown` et non sur `click` : un clic dont la
130
+ * pression a commencé à l'intérieur et s'est terminée à l'extérieur — une
131
+ * sélection de texte qui déborde — ne doit pas fermer l'overlay.
132
+ */
133
+ declare function useOutsideClick(targets: OutsideClickTarget | readonly OutsideClickTarget[], onOutside: (event: PointerEvent) => void, options?: UseOutsideClickOptions): void;
134
+
135
+ /**
136
+ * Une requête média, suivie en direct.
137
+ *
138
+ * `useSyncExternalStore` plutôt qu'un `useState` + `useEffect` : le rendu
139
+ * serveur rend `false`, la première passe cliente lit la vraie valeur, et
140
+ * React sait que la source est extérieure. Un `useEffect` produirait un
141
+ * clignotement au premier affichage — la mise en page saute une fois montée.
142
+ */
143
+ declare function useMediaQuery(query: string, serverValue?: boolean): boolean;
144
+ /** Les points de rupture du système, alignés sur les feuilles de style. */
145
+ declare const breakpoints: {
146
+ readonly sm: 480;
147
+ readonly md: 640;
148
+ readonly lg: 768;
149
+ readonly xl: 1024;
150
+ readonly "2xl": 1280;
151
+ };
152
+ type Breakpoint = keyof typeof breakpoints;
153
+ /** Vrai au-dessus du point de rupture demandé. */
154
+ declare function useBreakpoint(name: Breakpoint): boolean;
155
+ /** Vrai sous 768 px — le seuil où les mises en page du dépôt basculent. */
156
+ declare function useIsMobile(): boolean;
157
+ /** La préférence système de thème sombre. */
158
+ declare function usePrefersDark(): boolean;
159
+ /**
160
+ * La préférence système de mouvement réduit.
161
+ *
162
+ * Les feuilles de style la respectent déjà. Ce hook sert pour ce que le CSS
163
+ * ne peut pas couper : une animation pilotée en JavaScript, un défilement
164
+ * automatique, un carrousel.
165
+ */
166
+ declare function usePrefersReducedMotion(): boolean;
167
+
168
+ interface UseLocalStorageOptions<T> {
169
+ /** `sessionStorage` pour ce qui ne doit pas survivre à l'onglet. */
170
+ storage?: "local" | "session";
171
+ serialize?: (value: T) => string;
172
+ deserialize?: (raw: string) => T;
173
+ /**
174
+ * Suit les changements venus d'un autre onglet. Utile pour une déconnexion
175
+ * ou un thème; inutile — et coûteux — pour un brouillon local.
176
+ */
177
+ syncTabs?: boolean;
178
+ }
179
+ /**
180
+ * Une valeur qui survit au rechargement.
181
+ *
182
+ * La lecture initiale est paresseuse : elle se fait au premier rendu client,
183
+ * jamais pendant le rendu serveur, ce qui évite l'erreur d'hydratation quand
184
+ * la valeur stockée diffère du repli.
185
+ */
186
+ declare function useLocalStorage<T>(key: string, initialValue: T, options?: UseLocalStorageOptions<T>): readonly [T, (next: T | ((current: T) => T)) => void, () => void];
187
+
188
+ interface UseCopyToClipboardOptions {
189
+ /** Durée pendant laquelle `copied` reste vrai. */
190
+ resetAfter?: number;
191
+ }
192
+ interface CopyToClipboard {
193
+ copy: (text: string) => Promise<boolean>;
194
+ /** Vrai juste après une copie réussie — pour changer l'icône du bouton. */
195
+ copied: boolean;
196
+ /** Vrai si la dernière tentative a échoué. */
197
+ failed: boolean;
198
+ }
199
+ /**
200
+ * Copier, et le montrer.
201
+ *
202
+ * La copie elle-même est dans `@sia-ui/utils/clipboard`, avec son repli pour
203
+ * les contextes non sécurisés. Ce qui est ici est le seul ajout de React :
204
+ * l'état éphémère qui permet au bouton de dire « copié » deux secondes.
205
+ */
206
+ declare function useCopyToClipboard(options?: UseCopyToClipboardOptions): CopyToClipboard;
207
+
208
+ export { type Breakpoint, COLOR_MODE_KEY, type ColorMode, type ControllableStateOptions, type CopyToClipboard, type OutsideClickTarget, Portal, type PortalProps, type ResolvedColorMode, SiaProvider, type SiaProviderProps, type UseCopyToClipboardOptions, type UseLocalStorageOptions, type UseOutsideClickOptions, type UsePortalOptions, breakpoints, colorModeScript, mergeRefs, useBreakpoint, useColorMode, useControllableState, useCopyToClipboard, useDisclosure, useIsMobile, useLocalStorage, useMediaQuery, useOutsideClick, usePortal, usePrefersDark, usePrefersReducedMotion, useSiaTheme };
package/dist/index.js ADDED
@@ -0,0 +1,299 @@
1
+ import { createContext, useState, useRef, useEffect, useCallback, useSyncExternalStore, useMemo, useContext } from 'react';
2
+ import { toCssVariables, darkTheme, lightTheme, primitives } from '@sia-ui/tokens';
3
+ import { jsx } from 'react/jsx-runtime';
4
+ import { createPortal } from 'react-dom';
5
+ import { copyText } from '@sia-ui/utils/clipboard';
6
+
7
+ // src/theme-provider.tsx
8
+ function read(key, fallback, area, deserialize) {
9
+ if (!area) return fallback;
10
+ try {
11
+ const raw = area.getItem(key);
12
+ return raw === null ? fallback : deserialize(raw);
13
+ } catch {
14
+ return fallback;
15
+ }
16
+ }
17
+ function areaFor(kind) {
18
+ if (typeof window === "undefined") return null;
19
+ try {
20
+ return kind === "session" ? window.sessionStorage : window.localStorage;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ function useLocalStorage(key, initialValue, options = {}) {
26
+ const {
27
+ storage = "local",
28
+ serialize = JSON.stringify,
29
+ deserialize = JSON.parse,
30
+ syncTabs = false
31
+ } = options;
32
+ const area = areaFor(storage);
33
+ const [value, setValue] = useState(initialValue);
34
+ const hydrated = useRef(false);
35
+ useEffect(() => {
36
+ if (hydrated.current) return;
37
+ hydrated.current = true;
38
+ const stored = read(key, initialValue, area, deserialize);
39
+ if (stored !== initialValue) setValue(stored);
40
+ }, []);
41
+ const write = useCallback(
42
+ (next) => {
43
+ setValue((current) => {
44
+ const resolved = typeof next === "function" ? next(current) : next;
45
+ try {
46
+ area?.setItem(key, serialize(resolved));
47
+ } catch {
48
+ }
49
+ return resolved;
50
+ });
51
+ },
52
+ [area, key, serialize]
53
+ );
54
+ const remove = useCallback(() => {
55
+ try {
56
+ area?.removeItem(key);
57
+ } catch {
58
+ }
59
+ setValue(initialValue);
60
+ }, [area, key]);
61
+ useEffect(() => {
62
+ if (!syncTabs || typeof window === "undefined") return void 0;
63
+ const onStorage = (event) => {
64
+ if (event.key !== key || event.storageArea !== area) return;
65
+ setValue(
66
+ event.newValue === null ? initialValue : deserialize(event.newValue)
67
+ );
68
+ };
69
+ window.addEventListener("storage", onStorage);
70
+ return () => window.removeEventListener("storage", onStorage);
71
+ }, [area, key, syncTabs]);
72
+ return [value, write, remove];
73
+ }
74
+ function useMediaQuery(query, serverValue = false) {
75
+ const subscribe = useCallback(
76
+ (onChange) => {
77
+ if (typeof window === "undefined" || !window.matchMedia) return () => {
78
+ };
79
+ const list = window.matchMedia(query);
80
+ list.addEventListener("change", onChange);
81
+ return () => list.removeEventListener("change", onChange);
82
+ },
83
+ [query]
84
+ );
85
+ const getSnapshot = useCallback(() => {
86
+ if (typeof window === "undefined" || !window.matchMedia) return serverValue;
87
+ return window.matchMedia(query).matches;
88
+ }, [query, serverValue]);
89
+ const getServerSnapshot = useCallback(() => serverValue, [serverValue]);
90
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
91
+ }
92
+ var breakpoints = {
93
+ sm: 480,
94
+ md: 640,
95
+ lg: 768,
96
+ xl: 1024,
97
+ "2xl": 1280
98
+ };
99
+ function useBreakpoint(name) {
100
+ return useMediaQuery(`(min-width: ${breakpoints[name]}px)`);
101
+ }
102
+ function useIsMobile() {
103
+ return !useMediaQuery(`(min-width: ${breakpoints.lg}px)`);
104
+ }
105
+ function usePrefersDark() {
106
+ return useMediaQuery("(prefers-color-scheme: dark)");
107
+ }
108
+ function usePrefersReducedMotion() {
109
+ return useMediaQuery("(prefers-reduced-motion: reduce)");
110
+ }
111
+ var COLOR_MODE_KEY = "sia-ui.color-mode";
112
+ var ThemeContext = createContext({
113
+ colorMode: "light",
114
+ resolvedColorMode: "light",
115
+ setColorMode: () => {
116
+ },
117
+ toggleColorMode: () => {
118
+ }
119
+ });
120
+ function SiaProvider({
121
+ children,
122
+ colorMode: controlled,
123
+ defaultColorMode = "system",
124
+ onColorModeChange,
125
+ storageKey = COLOR_MODE_KEY,
126
+ theme,
127
+ className
128
+ }) {
129
+ const [stored, setStored] = useLocalStorage(
130
+ storageKey === false ? COLOR_MODE_KEY : storageKey,
131
+ defaultColorMode
132
+ );
133
+ const mode = controlled ?? (storageKey === false ? defaultColorMode : stored);
134
+ const prefersDark = usePrefersDark();
135
+ const resolved = mode === "system" ? prefersDark ? "dark" : "light" : mode;
136
+ const setColorMode = useCallback(
137
+ (next) => {
138
+ if (storageKey !== false && controlled === void 0) setStored(next);
139
+ onColorModeChange?.(next);
140
+ },
141
+ [controlled, onColorModeChange, setStored, storageKey]
142
+ );
143
+ const variables = useMemo(() => {
144
+ const semanticTheme = resolved === "dark" ? darkTheme : lightTheme;
145
+ return {
146
+ ...toCssVariables(primitives),
147
+ ...toCssVariables(semanticTheme),
148
+ ...toCssVariables(theme ?? {})
149
+ };
150
+ }, [resolved, theme]);
151
+ useEffect(() => {
152
+ if (typeof document === "undefined") return;
153
+ const root = document.documentElement;
154
+ root.dataset.siaColorMode = resolved;
155
+ root.style.colorScheme = resolved;
156
+ }, [resolved]);
157
+ const value = useMemo(
158
+ () => ({
159
+ colorMode: mode,
160
+ resolvedColorMode: resolved,
161
+ setColorMode,
162
+ // Bascule depuis ce qui est affiché, pas depuis ce qui est demandé :
163
+ // en mode `system` la personne voit du sombre et attend du clair.
164
+ toggleColorMode: () => setColorMode(resolved === "dark" ? "light" : "dark")
165
+ }),
166
+ [mode, resolved, setColorMode]
167
+ );
168
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, { value, children: /* @__PURE__ */ jsx(
169
+ "div",
170
+ {
171
+ className,
172
+ "data-sia-root": "",
173
+ "data-sia-color-mode": resolved,
174
+ style: variables,
175
+ children
176
+ }
177
+ ) });
178
+ }
179
+ function useSiaTheme() {
180
+ return useContext(ThemeContext);
181
+ }
182
+ function useColorMode() {
183
+ const { colorMode, resolvedColorMode, setColorMode, toggleColorMode } = useContext(ThemeContext);
184
+ return { colorMode, resolvedColorMode, setColorMode, toggleColorMode };
185
+ }
186
+ function colorModeScript(storageKey = COLOR_MODE_KEY, defaultMode = "system") {
187
+ return `(function(){try{var m=localStorage.getItem(${JSON.stringify(storageKey)});m=m?JSON.parse(m):${JSON.stringify(defaultMode)};if(m==="system"){m=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";}document.documentElement.dataset.siaColorMode=m;document.documentElement.style.colorScheme=m;}catch(e){}})();`;
188
+ }
189
+ function useControllableState({
190
+ value,
191
+ defaultValue,
192
+ onChange
193
+ }) {
194
+ const [internalValue, setInternalValue] = useState(defaultValue);
195
+ const controlled = value !== void 0;
196
+ const currentValue = controlled ? value : internalValue;
197
+ const setValue = useCallback(
198
+ (nextValue) => {
199
+ if (!controlled) setInternalValue(nextValue);
200
+ onChange?.(nextValue);
201
+ },
202
+ [controlled, onChange]
203
+ );
204
+ return [currentValue, setValue];
205
+ }
206
+ function useDisclosure(options = {}) {
207
+ const [open, setOpen] = useControllableState({
208
+ value: options.open,
209
+ defaultValue: options.defaultOpen ?? false,
210
+ onChange: options.onOpenChange
211
+ });
212
+ return {
213
+ open,
214
+ setOpen,
215
+ onOpen: useCallback(() => setOpen(true), [setOpen]),
216
+ onClose: useCallback(() => setOpen(false), [setOpen]),
217
+ onToggle: useCallback(() => setOpen(!open), [open, setOpen])
218
+ };
219
+ }
220
+
221
+ // src/merge-refs.ts
222
+ function mergeRefs(...refs) {
223
+ return (value) => {
224
+ for (const ref of refs) {
225
+ if (typeof ref === "function") ref(value);
226
+ else if (ref) ref.current = value;
227
+ }
228
+ };
229
+ }
230
+ function usePortal(options = {}) {
231
+ const { container, id } = options;
232
+ const [node, setNode] = useState(null);
233
+ useEffect(() => {
234
+ if (typeof document === "undefined") return void 0;
235
+ if (container) {
236
+ setNode(container);
237
+ return void 0;
238
+ }
239
+ const element = document.createElement("div");
240
+ if (id) element.id = id;
241
+ element.setAttribute("data-sia-portal", "");
242
+ document.body.appendChild(element);
243
+ setNode(element);
244
+ return () => {
245
+ element.remove();
246
+ setNode(null);
247
+ };
248
+ }, [container, id]);
249
+ return node;
250
+ }
251
+
252
+ // src/portal.tsx
253
+ function Portal({ children, container }) {
254
+ const node = usePortal(container ? { container } : {});
255
+ return node ? createPortal(children, node) : null;
256
+ }
257
+ function useOutsideClick(targets, onOutside, options = {}) {
258
+ const { enabled = true } = options;
259
+ const handler = useRef(onOutside);
260
+ handler.current = onOutside;
261
+ const refs = useRef(targets);
262
+ refs.current = targets;
263
+ useEffect(() => {
264
+ if (!enabled || typeof document === "undefined") return void 0;
265
+ const listener = (event) => {
266
+ const target = event.target;
267
+ if (!target || !target.isConnected) return;
268
+ const zones = Array.isArray(refs.current) ? refs.current : [refs.current];
269
+ if (zones.some((zone) => zone.current?.contains(target))) return;
270
+ handler.current(event);
271
+ };
272
+ document.addEventListener("pointerdown", listener, true);
273
+ return () => document.removeEventListener("pointerdown", listener, true);
274
+ }, [enabled]);
275
+ }
276
+ function useCopyToClipboard(options = {}) {
277
+ const { resetAfter = 2e3 } = options;
278
+ const [copied, setCopied] = useState(false);
279
+ const [failed, setFailed] = useState(false);
280
+ const timer = useRef(void 0);
281
+ useEffect(() => () => clearTimeout(timer.current), []);
282
+ const copy = useCallback(
283
+ async (text) => {
284
+ const ok = await copyText(text);
285
+ setCopied(ok);
286
+ setFailed(!ok);
287
+ clearTimeout(timer.current);
288
+ timer.current = setTimeout(() => {
289
+ setCopied(false);
290
+ setFailed(false);
291
+ }, resetAfter);
292
+ return ok;
293
+ },
294
+ [resetAfter]
295
+ );
296
+ return { copy, copied, failed };
297
+ }
298
+
299
+ export { COLOR_MODE_KEY, Portal, SiaProvider, breakpoints, colorModeScript, mergeRefs, useBreakpoint, useColorMode, useControllableState, useCopyToClipboard, useDisclosure, useIsMobile, useLocalStorage, useMediaQuery, useOutsideClick, usePortal, usePrefersDark, usePrefersReducedMotion, useSiaTheme };
@@ -0,0 +1,12 @@
1
+ /* src/styles.css */
2
+ [data-sia-root] {
3
+ box-sizing: border-box;
4
+ color: var(--sia-foreground);
5
+ background: var(--sia-background);
6
+ font-family: var(--sia-font-sans);
7
+ }
8
+ [data-sia-root] *,
9
+ [data-sia-root] *::before,
10
+ [data-sia-root] *::after {
11
+ box-sizing: inherit;
12
+ }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@sia-ui/react",
3
+ "version": "0.1.0",
4
+ "description": "React runtime foundations for SIA UI.",
5
+ "keywords": [
6
+ "sia-ui",
7
+ "react",
8
+ "provider",
9
+ "hooks",
10
+ "portal"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Ivan Mbella",
14
+ "homepage": "https://github.com/BeatJo/sia-ui#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/BeatJo/sia-ui/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/BeatJo/sia-ui.git",
21
+ "directory": "packages/react"
22
+ },
23
+ "type": "module",
24
+ "sideEffects": [
25
+ "**/*.css"
26
+ ],
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "main": "./dist/index.cjs",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js",
39
+ "require": "./dist/index.cjs"
40
+ },
41
+ "./styles.css": "./dist/styles.css"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "@sia-ui/utils": "0.1.0",
48
+ "@sia-ui/tokens": "0.1.0"
49
+ },
50
+ "peerDependencies": {
51
+ "react": ">=18.2.0",
52
+ "react-dom": ">=18.2.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/react": "^19.2.7",
56
+ "@types/react-dom": "^19.2.3",
57
+ "react": "^19.2.3",
58
+ "react-dom": "^19.2.3",
59
+ "tsup": "^8.5.1",
60
+ "typescript": "^5.9.3",
61
+ "vitest": "^4.0.16"
62
+ },
63
+ "scripts": {
64
+ "build": "tsup",
65
+ "typecheck": "tsc --noEmit",
66
+ "test": "vitest run --passWithNoTests",
67
+ "lint": "eslint src",
68
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
69
+ }
70
+ }