@uniflowed/hooks 0.0.0-alpha.4 → 0.0.0-alpha.41

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/events.js ADDED
@@ -0,0 +1,300 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/hooks/events`: a stream the server is pushing.
4
+ //
5
+ // One hook, and it is here rather than in `./channels.js` for a reason that
6
+ // module's header states from the other side: what belongs there is a value
7
+ // crossing the page's boundary that is *not* the server, because a request is
8
+ // `@uniflowed/query`'s and `@uniflowed/fetch`'s and comes with caching, retry
9
+ // and revalidation that a broadcast does not want. An event stream is the
10
+ // server and has none of that either — there is no cache entry, nothing to
11
+ // revalidate, and no request to repeat. It is a connection, which is a third
12
+ // thing, so it is a third file.
13
+ //
14
+ // `@uniflowed/server/events` is the other end of it.
15
+ //
16
+ // # What the platform already does, and the one thing it does not
17
+ //
18
+ // `EventSource` reconnects. It backs off, it sends the last `id:` it saw back
19
+ // as `Last-Event-ID`, and it needs no help — for as long as the server keeps
20
+ // answering. There is exactly one case where it gives up permanently: a
21
+ // response that is not a `200` with `content-type: text/event-stream`. Then it
22
+ // fires `error`, moves to `CLOSED`, and never tries again. A deployment
23
+ // restarting behind a load balancer answers `502` for a second or two, and
24
+ // every open stream in every tab is dead until somebody reloads the page.
25
+ //
26
+ // That is the case this hook exists for. When the browser gives up, it opens a
27
+ // new `EventSource` on a doubling delay; when the browser is retrying on its
28
+ // own it stays out of the way, because two reconnection schedules on one
29
+ // connection is worse than either.
30
+ //
31
+ // The other half is that none of the platform's reconnecting is visible. An
32
+ // application that wants to draw "reconnecting…" cannot ask `EventSource`
33
+ // anything but `readyState`, and only from an event handler. [`status`] is that
34
+ // as state.
35
+ //
36
+ // # What a reconnection this hook makes cannot carry
37
+ //
38
+ // `Last-Event-ID`. The browser sends it on its *own* retry and there is no way
39
+ // to set it on a new `EventSource` — the constructor takes a URL and one flag.
40
+ // So [`lastEventId`] is handed back, and a stream that needs to resume puts it
41
+ // in the URL itself:
42
+ //
43
+ // const stream = useEventSource(`/api/feed?after=${cursor ?? ""}`);
44
+ //
45
+ // Written down rather than worked around: a hook that silently appended a query
46
+ // parameter would be inventing a protocol on the application's behalf.
47
+ //
48
+ // # Before hydration
49
+ //
50
+ // `status` is `"idle"` on the server and on the client's first render, through
51
+ // `useSupported` — see the reason there. Nothing is opened during a render; the
52
+ // connection is made in an effect, so a prerender neither connects nor pretends
53
+ // it did. `"idle"` rather than `"connecting"` because they are different facts
54
+ // and only one of them is true: nothing has been attempted here.
55
+
56
+ import { useCallback, useEffect, useMemo, useRef, useState } from "@uniflowed/react";
57
+
58
+ import { useSupported } from "./browser.js";
59
+ import { useStableCallback } from "./lifecycle.js";
60
+
61
+ /**
62
+ * The part of `EventSource` this module uses.
63
+ *
64
+ * Declared here rather than taken from Flow's library definition, for the
65
+ * reason `./channels.js` gives about `BroadcastChannel`: the constructor is
66
+ * looked up at runtime and may be absent, so it has to be a value with a known
67
+ * constructor signature, and the listener is typed with the two fields that are
68
+ * read — which removes an `instanceof MessageEvent` whose name is not defined
69
+ * in every host these tests run in.
70
+ */
71
+ type StreamEvent = { data?: mixed, lastEventId?: mixed, ... };
72
+
73
+ declare class Stream {
74
+ constructor(url: string, options?: { withCredentials?: boolean, ... }): void;
75
+ readonly readyState: number;
76
+ close(): void;
77
+ addEventListener(type: string, listener: (event: StreamEvent) => mixed): void;
78
+ removeEventListener(type: string, listener: (event: StreamEvent) => mixed): void;
79
+ }
80
+
81
+ /**
82
+ * `EventSource`, which is the host's rather than the document's.
83
+ *
84
+ * The same split `./channels.js` describes: in a browser the two are one
85
+ * object, and in a process where a document has been installed onto another
86
+ * runtime's global they are not — an event stream is a facility of the runtime,
87
+ * and a hosted `Window` is not required to carry one.
88
+ */
89
+ declare var EventSource: Class<Stream> | void;
90
+
91
+ function streamConstructor(): Class<Stream> | null {
92
+ return typeof EventSource === "undefined" ? null : (EventSource ?? null);
93
+ }
94
+
95
+ /** `EventSource.CLOSED`, spelled as the number so no global is read for it. */
96
+ const CLOSED = 2;
97
+
98
+ /** Where the connection is. */
99
+ export type EventStreamStatus =
100
+ /** Nothing has been attempted: a prerender, no URL, or `enabled: false`. */
101
+ | "idle"
102
+ /** Opening, or reopening — by the browser or by this hook. */
103
+ | "connecting"
104
+ /** Receiving. */
105
+ | "open"
106
+ /** Closed on purpose, by `close()`. It will not reopen on its own. */
107
+ | "closed";
108
+
109
+ /** One message, as the three fields the format carries. */
110
+ export type ServerEvent = {|
111
+ /** The `event:` name, or `message` where the stream sent none. */
112
+ readonly name: string,
113
+ /** The `data:` payload, as text. */
114
+ readonly data: string,
115
+ /** The `id:`, or `null`. */
116
+ readonly id: string | null,
117
+ |};
118
+
119
+ /** How the connection behaves. */
120
+ export type EventSourceOptions = {|
121
+ /**
122
+ * Named events to listen for, besides the unnamed `message`.
123
+ *
124
+ * A stream that sends `event: progress` delivers nothing to a `message`
125
+ * listener, which is the single most common way an event stream appears to
126
+ * be connected and silent.
127
+ */
128
+ readonly events?: $ReadOnlyArray<string>,
129
+ /** Send cookies to another origin. Same-origin streams do not need it. */
130
+ readonly withCredentials?: boolean,
131
+ /** `false` closes the connection and opens none; `null` for the URL does too. */
132
+ readonly enabled?: boolean,
133
+ /** Called for every event, for a stream whose messages are events. */
134
+ readonly onEvent?: (event: ServerEvent) => mixed,
135
+ /** Milliseconds before this hook's first reopen; doubles. Default 1000. */
136
+ readonly retryDelay?: number,
137
+ /** The ceiling the doubling stops at. Default 30000. */
138
+ readonly maxRetryDelay?: number,
139
+ |};
140
+
141
+ /** What `useEventSource` hands back. */
142
+ export type UseEventSourceReturn = {|
143
+ readonly status: EventStreamStatus,
144
+ /**
145
+ * The most recent event, or `null` before one arrives.
146
+ *
147
+ * `./channels.js` deliberately does not hand back the last broadcast, and
148
+ * the argument there is that a message is an event: a tab that just opened
149
+ * has never received one and cannot ask. A stream is not that. It is a
150
+ * connection with a lifetime, and for as long as it is open "the latest
151
+ * message" is defined — which is what a progress bar, a price and a presence
152
+ * indicator all are. `onEvent` is there for the streams that really are
153
+ * events.
154
+ */
155
+ readonly last: ServerEvent | null,
156
+ /**
157
+ * The last `id:` seen, for a stream that resumes.
158
+ *
159
+ * The browser sends this itself when *it* reconnects. It is here for the
160
+ * reconnection this hook makes, which cannot; see the module header.
161
+ */
162
+ readonly lastEventId: string | null,
163
+ /** Close it. Nothing reopens until `enabled` or the URL changes. */
164
+ readonly close: () => void,
165
+ /** Whether this runtime has `EventSource` at all. */
166
+ readonly supported: boolean,
167
+ |};
168
+
169
+ const DEFAULT_RETRY = 1_000;
170
+ const DEFAULT_MAX_RETRY = 30_000;
171
+
172
+ /**
173
+ * Subscribe to a server-sent event stream.
174
+ *
175
+ * `url` is re-read when it changes, and a changed URL is a new connection —
176
+ * which is what makes the resume in the module header work at all. Pass `null`
177
+ * to connect to nothing, which is how a stream that depends on something not
178
+ * loaded yet waits without a second hook.
179
+ */
180
+ export hook useEventSource(url: string | null, options?: EventSourceOptions): UseEventSourceReturn {
181
+ const supported = useSupported(() => streamConstructor() != null);
182
+ const [status, setStatus] = useState<EventStreamStatus>("idle");
183
+ const [last, setLast] = useState<ServerEvent | null>(null);
184
+ const [lastEventId, setLastEventId] = useState<string | null>(null);
185
+ // Bumped to ask for a new connection, which is the only thing that can
186
+ // reopen one the browser has given up on: `EventSource` in `CLOSED` cannot
187
+ // be restarted, so the effect has to run again with a new object.
188
+ const [attempt, setAttempt] = useState(0);
189
+ const [stopped, setStopped] = useState(false);
190
+
191
+ const onEvent = options?.onEvent;
192
+ const stable = useStableCallback((event: ServerEvent) => {
193
+ onEvent?.(event);
194
+ });
195
+
196
+ // The names are carried as one string rather than as the array the caller
197
+ // passed, and rebuilt inside the effect from it. An array literal in a
198
+ // render is a new array every time, so depending on one would tear the
199
+ // connection down on every keystroke anywhere in the component — and a ref
200
+ // written during a render is the other way to get this wrong, which this
201
+ // package's header rules out.
202
+ const key = (options?.events ?? []).join(",");
203
+ const withCredentials = options?.withCredentials === true;
204
+ const retryDelay = options?.retryDelay ?? DEFAULT_RETRY;
205
+ const maxRetryDelay = options?.maxRetryDelay ?? DEFAULT_MAX_RETRY;
206
+ // How many times *this hook* has reopened since the last success. Not
207
+ // `attempt`, which also moves for a URL change, and not state, because
208
+ // resetting it on `open` must not be a render.
209
+ const backoff = useRef(0);
210
+
211
+ const enabled = (options?.enabled ?? true) && !stopped;
212
+
213
+ useEffect(() => {
214
+ const Constructor = streamConstructor();
215
+ if (Constructor == null || url == null || !enabled) {
216
+ // The connection state mirrors whether the external stream exists.
217
+ // uf-lint-disable-next-line react-compiler/set-state-in-effect
218
+ setStatus(stopped ? "closed" : "idle");
219
+ return;
220
+ }
221
+
222
+ const source = new Constructor(url, { withCredentials });
223
+ setStatus("connecting");
224
+ let reopen: TimeoutID | null = null;
225
+
226
+ const onOpen = () => {
227
+ // The delay is reset here rather than on the first message: a stream
228
+ // that connects and sends nothing for an hour has still connected, and
229
+ // treating it as a failure would put the next real outage at the top of
230
+ // the backoff.
231
+ backoff.current = 0;
232
+ setStatus("open");
233
+ };
234
+
235
+ const onMessage = (name: string) => (event: StreamEvent) => {
236
+ const sent = event.lastEventId;
237
+ const id = typeof sent === "string" && sent !== "" ? sent : null;
238
+ const received: ServerEvent = { name, data: String(event.data ?? ""), id };
239
+ setLast(received);
240
+ if (id != null) {
241
+ setLastEventId(id);
242
+ }
243
+ stable(received);
244
+ };
245
+
246
+ const onError = () => {
247
+ if (source.readyState !== CLOSED) {
248
+ // The browser is retrying on its own, with `Last-Event-ID`, which is
249
+ // strictly better than anything this hook can do. Say so and wait.
250
+ setStatus("connecting");
251
+ return;
252
+ }
253
+ // It has given up — a non-200, or a body that was not an event stream.
254
+ // Nothing reopens this object, so a new one is asked for on a delay.
255
+ setStatus("connecting");
256
+ const wait = Math.min(retryDelay * 2 ** backoff.current, maxRetryDelay);
257
+ backoff.current += 1;
258
+ reopen = setTimeout(() => setAttempt((count) => count + 1), wait);
259
+ };
260
+
261
+ const listeners: Array<[string, (event: StreamEvent) => mixed]> = [
262
+ ["message", onMessage("message")],
263
+ ];
264
+ for (const name of key === "" ? [] : key.split(",")) {
265
+ listeners.push([name, onMessage(name)]);
266
+ }
267
+
268
+ source.addEventListener("open", onOpen);
269
+ source.addEventListener("error", onError);
270
+ for (const [name, listener] of listeners) {
271
+ source.addEventListener(name, listener);
272
+ }
273
+
274
+ return () => {
275
+ if (reopen != null) {
276
+ clearTimeout(reopen);
277
+ }
278
+ source.removeEventListener("open", onOpen);
279
+ source.removeEventListener("error", onError);
280
+ for (const [name, listener] of listeners) {
281
+ source.removeEventListener(name, listener);
282
+ }
283
+ // Closed as well as unsubscribed: an `EventSource` nobody is listening
284
+ // to still holds a connection and still reconnects, so leaving it open
285
+ // is a socket per navigation for the life of the tab.
286
+ source.close();
287
+ };
288
+ // `key` stands in for `options.events`, and `attempt` is what a reopen
289
+ // moves. `backoff` is a ref, and `stable` never changes identity.
290
+ }, [url, enabled, stopped, key, attempt, retryDelay, maxRetryDelay, withCredentials, stable]);
291
+
292
+ const close = useCallback(() => {
293
+ setStopped(true);
294
+ }, []);
295
+
296
+ return useMemo(
297
+ () => ({ status, last, lastEventId, close, supported }),
298
+ [status, last, lastEventId, close, supported],
299
+ );
300
+ }
package/index.js CHANGED
@@ -2,11 +2,22 @@
2
2
  //
3
3
  // `@uniflowed/hooks`: the hooks a React application writes anyway.
4
4
  //
5
- // Not a large collection. Every hook here is one that people write by hand in
6
- // every project and get subtly wrong in the same way each time — a timer that
7
- // calls a stale closure, a subscription re-established on every keystroke, a
8
- // slow request overwriting a fast one, persisted state that differs between the
9
- // server render and the first paint.
5
+ // Every hook here is one that people write by hand in every project and get
6
+ // subtly wrong in the same way each time — a timer that calls a stale closure,
7
+ // a subscription re-established on every keystroke, a slow request overwriting
8
+ // a fast one, persisted state that differs between the server render and the
9
+ // first paint, a shortcut that fires while somebody is typing, a dialog that
10
+ // unlocks the page while a second dialog is still open.
11
+ //
12
+ // VueUse is the benchmark, and most of what it offers is not here on purpose.
13
+ // Its `Reactivity`, `Watch` and `Array` categories exist because Vue's `ref`
14
+ // is a mutable box that has to be wrapped, unwrapped, synced and derived;
15
+ // React has no box, and `useMemo` over a plain array is the whole of
16
+ // `useArrayFilter`. Its `Component` category is Vue's template machinery —
17
+ // `templateRef`, `unrefElement`, `useVModel` — which is `ref` and props here.
18
+ // Porting either would have produced hooks whose only purpose was to look
19
+ // familiar. The "Readiness" section below says what is here, what is
20
+ // deliberately elsewhere in uf, and what is simply not built.
10
21
  //
11
22
  // # Prerendering is the constraint that shapes the surface
12
23
  //
@@ -21,23 +32,61 @@
21
32
  // its sidebar under 48rem wants `false` on the server and one that renders a
22
33
  // mobile menu wants `true`, and a library cannot know which.
23
34
  //
35
+ // Where there is exactly one honest answer, the caller is not asked. `useHash`
36
+ // takes no server value because the browser strips the fragment before the
37
+ // request goes out, so `""` is not a default standing in for something better —
38
+ // it is what the server knows, and offering an override would have invited a
39
+ // caller to state a value that cannot be true.
40
+ //
41
+ // The hooks built on effects rather than stores — everything in `dom.js`, and
42
+ // the three in `browser.js` whose first value only arrives in a callback — do
43
+ // nothing at all before hydration, and report the same starting value on both
44
+ // sides for that reason. Each module's header says which it is and why.
45
+ //
46
+ // # React's rules are the design, not a constraint on it
47
+ //
48
+ // Nothing here writes during a render, reads a ref during a render, or depends
49
+ // on a render having happened exactly once. Callbacks that cross into effects
50
+ // go through `useStableCallback`, whose ref is written in an insertion effect
51
+ // rather than in the body, so Strict Mode's double render and a render the
52
+ // React Compiler skips are both correct. Every effect's cleanup removes
53
+ // exactly what its body added, which is what makes Strict Mode's
54
+ // mount-unmount-mount balanced — including the module-level counters behind
55
+ // `useScrollLock` and `useStorage`.
56
+ //
24
57
  // # How the package is laid out
25
58
  //
26
- // Six modules beside this one, split by what a hook's subject is — because
59
+ // Nine modules beside this one, split by what a hook's subject is — because
27
60
  // that is the question a reader looking for one actually asks:
28
61
  //
29
62
  // - `lifecycle.js` — the component itself: mounted, previous, run once.
30
- // - `state.js` — a value the component owns, with the operations that suit it.
63
+ // - `state.js` — a value the component owns, with the operations that suit it:
64
+ // toggle, counter, list, set, cycle, undo/redo, storage.
31
65
  // - `timing.js` — when something runs: intervals, timeouts, debounce,
32
- // throttle.
33
- // - `async.js` — one promise, and the stale-response bug.
34
- // - `browser.js` — the ambient environment: viewport, connection, preferences.
66
+ // throttle, frames, idleness, and the clock behind "3 minutes ago".
67
+ // - `render.js` — what a render has to fix rather than derive: the instant it
68
+ // was made at, and the seed anything random on the page is drawn from.
69
+ // - `async.js` — one promise: its states, its abort signal, its retries.
70
+ // - `browser.js` — the ambient environment: viewport, scroll, connection,
71
+ // position, permissions, preferences, the fragment in the address bar. Its
72
+ // header carries the table of what every one of them renders before
73
+ // hydration.
35
74
  // - `dom.js` — one element the caller holds a ref to: listen, measure,
36
- // observe.
75
+ // observe, press.
76
+ // - `keyboard.js` — what is being pressed: a chord, and a held key.
77
+ // - `channels.js` — a value that came from outside the page: another tab, the
78
+ // system clipboard.
79
+ // - `events.js` — a stream the server is pushing, and where its reconnection
80
+ // is. Not `channels.js`, whose boundary is deliberately everything except
81
+ // the server, and not `@uniflowed/query`, because there is no cache entry
82
+ // and nothing to revalidate: a connection is a third thing.
37
83
  //
38
- // The two that are easiest to confuse are the last two, so each says so in its
39
- // own header: `browser.js` needs no ref because there is one browser, and
40
- // `dom.js` needs one because there are as many answers as there are elements.
84
+ // The two that are easiest to confuse are `browser.js` and `dom.js`, so each
85
+ // says so in its own header: `browser.js` needs no ref because there is one
86
+ // browser, and `dom.js` needs one because there are as many answers as there
87
+ // are elements. `keyboard.js` is neither, which is why it is a third file
88
+ // rather than a corner of one of them, and its header says what the four hard
89
+ // parts of a shortcut are.
41
90
  //
42
91
  // They sit here rather than under an `internal/`, and each has its own
43
92
  // subpath. Every name in them is exported from this file, so calling them
@@ -45,12 +94,82 @@
45
94
  // hop to reach the first line of code. `internal/` is for a module consumers
46
95
  // must not reach; this package has none.
47
96
  //
48
- // `lifecycle.js` is the one the others import — `timing.js`, `state.js` and
49
- // `dom.js` all want `useStableCallback` — and it is still a subject rather
50
- // than a bag of shared helpers. A hook goes there because it is about the
51
- // component's life, not because more than one file wanted it.
97
+ // `lifecycle.js` and `browser.js` are the two the others import —
98
+ // `useStableCallback` and `browserWindow` — and both are still subjects rather
99
+ // than bags of shared helpers. A hook goes in `lifecycle.js` because it is
100
+ // about the component's life, never because more than one file wanted it.
101
+ //
102
+ // # Readiness
103
+ //
104
+ // **Implemented and tested.** The component's life; the state shapes; every
105
+ // timer, including the adaptive schedule behind `useTimeAgo`; `useAsync` with
106
+ // abort and retry; media queries, colour scheme, reduced motion, online,
107
+ // document visibility, window size and scroll, the address bar's fragment,
108
+ // scroll lock; element size,
109
+ // intersection, mutations, hover, focus-within, click-outside, long press,
110
+ // element scroll, the element as state; key chords and held keys; storage with
111
+ // cross-tab sync;
112
+ // broadcast channels; the clipboard; a server-sent event stream, including the
113
+ // one case the platform's own reconnection gives up on; the render anchor and
114
+ // the seeded stream in `render.js`. `packages/hooks/hooks.test.js` covers
115
+ // behaviour and cleanup, and `packages/hooks/hooks-ssr.test.js` renders the
116
+ // whole surface in a process that has no DOM at all.
117
+ //
118
+ // **Experimental.** `useGeolocation`, `useNetwork` and `usePermission`. The
119
+ // shapes are settled and the cleanup is right, but the browsers disagree about
120
+ // them more than the rest of this package does: Network Information is
121
+ // Chromium's alone, the Permissions API rejects rather than answers for names
122
+ // it does not know, and geolocation cannot be exercised end to end in a
123
+ // headless document — the tests cover the unsupported path, the subscription
124
+ // and its teardown, not a real fix. Treat the fields as advisory.
125
+ //
126
+ // **Not implemented, and not planned here.** Everything whose subject is
127
+ // somewhere else in uf: a request with a cache is `@uniflowed/query`, a form
128
+ // field is `@uniflowed/form`, an atom two routes read is `@uniflowed/state`, a
129
+ // rendered instant is `@uniflowed/web`'s `Time`, styling and dark mode are
130
+ // `@uniflowed/stylex`, and a virtual list or an infinite scroller is
131
+ // `@uniflowed/ui`. Beyond those: the device APIs VueUse wraps that a general
132
+ // application does not reach for — Bluetooth, gamepads, USB, speech, wake
133
+ // lock, screen capture, web workers, battery, vibration — are absent rather
134
+ // than shallow. A wrapper over one of those is three lines and a `supported`
135
+ // flag; what makes it worth shipping is knowing the failure modes, and this
136
+ // package does not yet.
52
137
 
53
- export type { Async } from "./async.js";
138
+ export type { Async, AsyncOptions } from "./async.js";
139
+ export type {
140
+ BrowserHistory,
141
+ BrowserLocation,
142
+ BrowserNavigator,
143
+ BrowserWindow,
144
+ EffectiveConnectionType,
145
+ GeolocationReading,
146
+ Geoposition,
147
+ Network,
148
+ NetworkConnection,
149
+ NetworkMeasurement,
150
+ PermissionAnswer,
151
+ PermissionName,
152
+ ScrollOffset,
153
+ Size,
154
+ } from "./browser.js";
155
+ export type { UseBroadcastReturn, UseClipboardReturn } from "./channels.js";
156
+ export type { ListenerOptions, ListenerTarget, MutationOptions, Ref } from "./dom.js";
157
+ export type {
158
+ EventSourceOptions,
159
+ EventStreamStatus,
160
+ ServerEvent,
161
+ UseEventSourceReturn,
162
+ } from "./events.js";
163
+ export type { KeyComboOptions } from "./keyboard.js";
164
+ export type { RenderEnvelope } from "./render.js";
165
+ export type {
166
+ UseCounterReturn,
167
+ UseCycleReturn,
168
+ UseListReturn,
169
+ UseSetReturn,
170
+ UseToggleReturn,
171
+ UseUndoableReturn,
172
+ } from "./state.js";
54
173
 
55
174
  export { useAsync } from "./async.js";
56
175
  export {
@@ -63,27 +182,63 @@ export {
63
182
  useUnmount,
64
183
  } from "./lifecycle.js";
65
184
  export {
185
+ useAnimationFrame,
66
186
  useDebouncedCallback,
67
187
  useDebouncedValue,
188
+ useIdle,
68
189
  useInterval,
190
+ useNow,
69
191
  useThrottledCallback,
192
+ useTimeAgo,
70
193
  useTimeout,
71
194
  } from "./timing.js";
72
195
  export {
196
+ browserWindow,
73
197
  useDocumentVisible,
198
+ useGeolocation,
199
+ useHash,
74
200
  useMediaQuery,
201
+ useNetwork,
75
202
  useOnline,
76
- usePrefersReducedMotion,
203
+ usePermission,
77
204
  usePreferredColorScheme,
205
+ usePrefersReducedMotion,
206
+ useScrollLock,
207
+ useSupported,
208
+ useWindowScroll,
78
209
  useWindowSize,
79
210
  } from "./browser.js";
80
211
  export {
81
212
  useClickOutside,
82
213
  useElementRef,
83
214
  useElementSize,
215
+ useElementState,
84
216
  useEventListener,
85
217
  useFocusWithin,
86
218
  useHover,
87
219
  useIntersecting,
220
+ useLongPress,
221
+ useMutationObserver,
222
+ useScroll,
88
223
  } from "./dom.js";
89
- export { useCounter, useStorage, useToggle } from "./state.js";
224
+ export { useKeyCombo, useKeyHeld } from "./keyboard.js";
225
+ export {
226
+ RENDER_META,
227
+ RenderProvider,
228
+ useRandom,
229
+ useRenderEnvelope,
230
+ useRenderTimeZone,
231
+ useRenderedAt,
232
+ useShuffled,
233
+ } from "./render.js";
234
+ export { useBroadcast, useClipboard } from "./channels.js";
235
+ export { useEventSource } from "./events.js";
236
+ export {
237
+ useCounter,
238
+ useCycle,
239
+ useList,
240
+ useSet,
241
+ useStorage,
242
+ useToggle,
243
+ useUndoable,
244
+ } from "./state.js";