@journ/lift 0.1.1 → 0.2.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.
Files changed (76) hide show
  1. package/README.md +39 -9
  2. package/dist/env-C0PNnbwt.js +4 -0
  3. package/dist/logger-Dse1660l.js +33 -0
  4. package/dist/prefers-reduced-motion.d.ts +3 -2
  5. package/dist/prefers-reduced-motion.js +1 -3
  6. package/dist/required-context.d.ts +3 -2
  7. package/dist/required-context.js +2 -2
  8. package/dist/try-catch-D4d3N9_W.js +29 -0
  9. package/dist/types-p0KNRcj3.d.ts +4 -0
  10. package/dist/use-battery.d.ts +12 -0
  11. package/dist/use-battery.js +64 -0
  12. package/dist/use-browser.d.ts +6 -0
  13. package/dist/use-browser.js +51 -0
  14. package/dist/use-callback-ref.d.ts +5 -0
  15. package/dist/use-callback-ref.js +11 -0
  16. package/dist/use-controllable-state.d.ts +3 -2
  17. package/dist/use-controllable-state.js +6 -4
  18. package/dist/use-debounce-state.d.ts +9 -0
  19. package/dist/use-debounce-state.js +28 -0
  20. package/dist/use-debounced-callback.d.ts +16 -0
  21. package/dist/use-debounced-callback.js +68 -0
  22. package/dist/use-debounced-value.d.ts +3 -2
  23. package/dist/use-debounced-value.js +3 -3
  24. package/dist/use-did-update.d.ts +5 -0
  25. package/dist/use-did-update.js +24 -0
  26. package/dist/use-disclosure.d.ts +15 -0
  27. package/dist/use-disclosure.js +16 -0
  28. package/dist/use-draft.d.ts +7 -6
  29. package/dist/use-draft.js +2 -2
  30. package/dist/use-haptic.d.ts +8 -0
  31. package/dist/use-haptic.js +84 -0
  32. package/dist/use-headroom.d.ts +18 -0
  33. package/dist/use-headroom.js +65 -0
  34. package/dist/use-idle-queue.d.ts +28 -0
  35. package/dist/use-idle-queue.js +221 -0
  36. package/dist/use-intersection-observer.d.ts +17 -0
  37. package/dist/use-intersection-observer.js +50 -0
  38. package/dist/use-isomorphic-effect.d.ts +5 -0
  39. package/dist/use-isomorphic-effect.js +5 -0
  40. package/dist/use-keybindings.d.ts +14 -0
  41. package/dist/use-keybindings.js +76 -0
  42. package/dist/use-local-storage.d.ts +13 -0
  43. package/dist/use-local-storage.js +76 -0
  44. package/dist/use-logger.d.ts +4 -0
  45. package/dist/use-logger.js +17 -0
  46. package/dist/use-long-press.d.ts +12 -0
  47. package/dist/use-long-press.js +43 -0
  48. package/dist/use-map.d.ts +4 -0
  49. package/dist/use-map.js +30 -0
  50. package/dist/use-measure.d.ts +8 -0
  51. package/dist/use-measure.js +27 -0
  52. package/dist/use-media-query.d.ts +14 -0
  53. package/dist/use-media-query.js +24 -0
  54. package/dist/use-mouse.d.ts +18 -0
  55. package/dist/use-mouse.js +40 -0
  56. package/dist/use-network.d.ts +17 -0
  57. package/dist/use-network.js +38 -0
  58. package/dist/use-page-leave.d.ts +4 -0
  59. package/dist/use-page-leave.js +11 -0
  60. package/dist/use-pagination.d.ts +19 -0
  61. package/dist/use-pagination.js +55 -0
  62. package/dist/use-reduced-motion.d.ts +4 -0
  63. package/dist/use-reduced-motion.js +5 -0
  64. package/dist/use-scroll-direction.d.ts +5 -0
  65. package/dist/use-scroll-direction.js +33 -0
  66. package/dist/use-uncontrolled.d.ts +11 -0
  67. package/dist/use-uncontrolled.js +21 -0
  68. package/dist/use-validated-state.d.ts +10 -0
  69. package/dist/use-validated-state.js +22 -0
  70. package/dist/use-window-event.d.ts +5 -0
  71. package/dist/use-window-event.js +11 -0
  72. package/dist/use-window-scroll.d.ts +10 -0
  73. package/dist/use-window-scroll.js +33 -0
  74. package/package.json +149 -9
  75. package/dist/index.d.ts +0 -6
  76. package/dist/index.js +0 -6
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @journ/lift
2
2
 
3
- React hooks for state and timing, built on fitter-happier. No DOM access, so they work in PWAs, React Native and Wails desktop apps alike.
3
+ React hooks for state, timing and browser APIs, built on fitter-happier. Hooks marked * read `window`, `document` or `navigator` and need a browser; the others run anywhere React does.
4
4
 
5
5
  ## Install
6
6
 
@@ -11,7 +11,8 @@ pnpm add @journ/lift react
11
11
  ## Usage
12
12
 
13
13
  ```tsx
14
- import { useControllableState, useDebouncedValue, useDraft } from '@journ/lift'
14
+ import { useControllableState } from '@journ/lift/use-controllable-state'
15
+ import { useDebouncedValue } from '@journ/lift/use-debounced-value'
15
16
 
16
17
  function Toggle(props: { pressed?: boolean; defaultPressed?: boolean; onPressedChange?: (pressed: boolean) => void }) {
17
18
  const [pressed, setPressed] = useControllableState(
@@ -32,10 +33,39 @@ function Search({ query }: { query: string }) {
32
33
  }
33
34
  ```
34
35
 
35
- | Entry | Exports |
36
- | ------------------------------------ | ---------------------------------------------------------------------- |
37
- | `@journ/lift/use-controllable-state` | `useControllableState(value, defaultValue, onChange)` |
38
- | `@journ/lift/use-debounced-value` | `useDebouncedValue(value, delay)` |
39
- | `@journ/lift/use-draft` | `useDraft`: text typed freely that becomes a value on commit |
40
- | `@journ/lift/required-context` | `createRequiredContext(owner)`: a context whose hook throws outside it |
41
- | `@journ/lift/prefers-reduced-motion` | `prefersReducedMotion()` |
36
+ | Entry | Exports | Notes |
37
+ | --------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------- |
38
+ | `@journ/lift/use-battery` | `useBattery()` | Battery level and charging state * |
39
+ | `@journ/lift/use-browser` | `useBrowser()`, `getBrowserVersion()` | The detected browser and its major version * |
40
+ | `@journ/lift/use-callback-ref` | `useCallbackRef(fn)` | A stable callback that always calls the latest `fn` |
41
+ | `@journ/lift/use-controllable-state` | `useControllableState(value, defaultValue, onChange)` | |
42
+ | `@journ/lift/use-debounce-state` | `useDebouncedState(initial, wait, options)` | State whose setter debounces updates * |
43
+ | `@journ/lift/use-debounced-callback` | `useDebouncedCallback(fn, delayOrOptions)` | A debounced `fn` with `flush`, `cancel` and `isPending` * |
44
+ | `@journ/lift/use-debounced-value` | `useDebouncedValue(value, delay)` | |
45
+ | `@journ/lift/use-did-update` | `useDidUpdate(effect, deps)` | An effect that skips the first run |
46
+ | `@journ/lift/use-disclosure` | `useDisclosure(initial, options)` | Open, close and toggle state with `onOpen` and `onClose` |
47
+ | `@journ/lift/use-draft` | `useDraft` | Text typed freely that becomes a value on commit |
48
+ | `@journ/lift/use-haptic` | `useHaptic`, `useHapticsConfirm`, `useHapticsError`, `supportsHaptics` | Vibration and iOS haptic feedback * |
49
+ | `@journ/lift/use-headroom` | `useHeadroom(options)` | Pinned state and scroll progress for a header that hides on scroll down * |
50
+ | `@journ/lift/use-idle-queue` | `useIdleQueue(options)` | A queue that runs tasks while the browser is idle * |
51
+ | `@journ/lift/use-intersection-observer` | `useIntersectionObserver(options)` | Whether an element intersects the viewport * |
52
+ | `@journ/lift/use-isomorphic-effect` | `useIsomorphicEffect` | `useLayoutEffect` in the browser, `useEffect` on the server * |
53
+ | `@journ/lift/use-keybindings` | `useKeybindings(map, target, options)` | Keyboard shortcuts and key sequences * |
54
+ | `@journ/lift/use-local-storage` | `useLocalStorage`, `useSessionStorage` | State synced with web storage * |
55
+ | `@journ/lift/use-logger` | `useLogger(name, props)` | Logs mount, update and unmount |
56
+ | `@journ/lift/use-long-press` | `useLongPress(onLongPress, disabled)` | Pointer handlers that fire after a 500 ms hold |
57
+ | `@journ/lift/use-map` | `useMap(entries)` | A `Map` that re-renders on change |
58
+ | `@journ/lift/use-measure` | `useMeasure()` | Element width and height * |
59
+ | `@journ/lift/use-media-query` | `useMediaQuery(query)`, `useBreakpoint(name)`, `breakpoints` | A media query match * |
60
+ | `@journ/lift/use-mouse` | `useMouse(options)`, `useMousePosition()` | Pointer position, relative to an element or the window * |
61
+ | `@journ/lift/use-network` | `useNetwork()` | Online state and connection details * |
62
+ | `@journ/lift/use-page-leave` | `usePageLeave(fn)` | Calls `fn` when the pointer leaves the page * |
63
+ | `@journ/lift/use-pagination` | `usePagination(options)`, `DOTS` | Page ranges with siblings and boundaries |
64
+ | `@journ/lift/use-reduced-motion` | `useReducedMotion()` | The `prefers-reduced-motion` media query * |
65
+ | `@journ/lift/use-scroll-direction` | `useScrollDirection()` | `up`, `down` or `unknown` * |
66
+ | `@journ/lift/use-uncontrolled` | `useUncontrolled(options)` | Controlled or uncontrolled value with a change handler |
67
+ | `@journ/lift/use-validated-state` | `useValidatedState(initial, validate)` | A value with its validity and last valid value |
68
+ | `@journ/lift/use-window-event` | `useWindowEvent(type, listener, options)` | A window event listener that never goes stale * |
69
+ | `@journ/lift/use-window-scroll` | `useWindowScroll()` | Window scroll position and a smooth `scrollTo` * |
70
+ | `@journ/lift/required-context` | `createRequiredContext(owner)` | a context whose hook throws outside it |
71
+ | `@journ/lift/prefers-reduced-motion` | `prefersReducedMotion()` | |
@@ -0,0 +1,4 @@
1
+ //#region src/lib/env.ts
2
+ const isServerSide = () => typeof window === "undefined";
3
+ //#endregion
4
+ export { isServerSide as t };
@@ -0,0 +1,33 @@
1
+ //#region src/lib/logger.ts
2
+ const isProduction = () => {
3
+ const env = import.meta.env;
4
+ if (typeof env?.PROD === "boolean") return env.PROD;
5
+ return globalThis.process?.env?.NODE_ENV === "production";
6
+ };
7
+ const createLogger = () => {
8
+ const shouldLog = (level) => {
9
+ if (isProduction()) return level === "error";
10
+ return true;
11
+ };
12
+ const error = (...args) => {
13
+ if (shouldLog("error")) console.error(...args);
14
+ };
15
+ const warn = (...args) => {
16
+ if (shouldLog("warn")) console.warn(...args);
17
+ };
18
+ const info = (...args) => {
19
+ if (shouldLog("info")) console.info(...args);
20
+ };
21
+ const log = (...args) => {
22
+ if (shouldLog("log")) console.log(...args);
23
+ };
24
+ return {
25
+ error,
26
+ warn,
27
+ info,
28
+ log
29
+ };
30
+ };
31
+ const logger = createLogger();
32
+ //#endregion
33
+ export { logger as t };
@@ -1,3 +1,4 @@
1
1
  //#region src/prefers-reduced-motion.d.ts
2
- export declare function prefersReducedMotion(): boolean;
3
- //#endregion
2
+ declare const prefersReducedMotion: () => boolean;
3
+ //#endregion
4
+ export { prefersReducedMotion };
@@ -1,6 +1,4 @@
1
1
  //#region src/prefers-reduced-motion.ts
2
- function prefersReducedMotion() {
3
- return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
4
- }
2
+ const prefersReducedMotion = () => typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
5
3
  //#endregion
6
4
  export { prefersReducedMotion };
@@ -4,5 +4,6 @@ import { Context } from "react";
4
4
  * A context that parts must be rendered inside of. The hook throws a clear error
5
5
  * naming the owner, so a part used on its own fails loudly instead of rendering wrong.
6
6
  */
7
- export declare function createRequiredContext<T>(owner: string): readonly [Context<T | null>, () => T];
8
- //#endregion
7
+ declare const createRequiredContext: <T>(owner: string) => readonly [Context<T | null>, () => T];
8
+ //#endregion
9
+ export { createRequiredContext };
@@ -4,7 +4,7 @@ import { createContext, use } from "react";
4
4
  * A context that parts must be rendered inside of. The hook throws a clear error
5
5
  * naming the owner, so a part used on its own fails loudly instead of rendering wrong.
6
6
  */
7
- function createRequiredContext(owner) {
7
+ const createRequiredContext = (owner) => {
8
8
  const RequiredContext = createContext(null);
9
9
  const useRequired = () => {
10
10
  const value = use(RequiredContext);
@@ -12,6 +12,6 @@ function createRequiredContext(owner) {
12
12
  return value;
13
13
  };
14
14
  return [RequiredContext, useRequired];
15
- }
15
+ };
16
16
  //#endregion
17
17
  export { createRequiredContext };
@@ -0,0 +1,29 @@
1
+ //#region src/lib/try-catch.ts
2
+ const tryCatch = async (promise) => {
3
+ try {
4
+ return {
5
+ success: true,
6
+ data: await promise
7
+ };
8
+ } catch (error) {
9
+ return {
10
+ success: false,
11
+ error
12
+ };
13
+ }
14
+ };
15
+ const tryCatchSync = (fn) => {
16
+ try {
17
+ return {
18
+ success: true,
19
+ data: fn()
20
+ };
21
+ } catch (error) {
22
+ return {
23
+ success: false,
24
+ error
25
+ };
26
+ }
27
+ };
28
+ //#endregion
29
+ export { tryCatchSync as n, tryCatch as t };
@@ -0,0 +1,4 @@
1
+ //#region src/lib/types.d.ts
2
+ type ANY = any;
3
+ //#endregion
4
+ export { ANY as t };
@@ -0,0 +1,12 @@
1
+ //#region src/use-battery.d.ts
2
+ type BatteryState = {
3
+ supported: boolean;
4
+ loading: boolean;
5
+ level: number | null;
6
+ charging: boolean | null;
7
+ chargingTime: number | null;
8
+ dischargingTime: number | null;
9
+ };
10
+ declare const useBattery: () => BatteryState;
11
+ //#endregion
12
+ export { type BatteryState, useBattery };
@@ -0,0 +1,64 @@
1
+ import { t as tryCatch } from "./try-catch-D4d3N9_W.js";
2
+ import { useEffect, useState, useSyncExternalStore } from "react";
3
+ //#region src/use-battery.ts
4
+ const BATTERY_EVENTS = [
5
+ "levelchange",
6
+ "chargingchange",
7
+ "chargingtimechange",
8
+ "dischargingtimechange"
9
+ ];
10
+ const INITIAL_STATE = {
11
+ supported: true,
12
+ loading: true,
13
+ level: null,
14
+ charging: null,
15
+ chargingTime: null,
16
+ dischargingTime: null
17
+ };
18
+ const UNSUPPORTED_STATE = {
19
+ ...INITIAL_STATE,
20
+ supported: false,
21
+ loading: false
22
+ };
23
+ const readBattery = (battery) => ({
24
+ supported: true,
25
+ loading: false,
26
+ level: battery.level,
27
+ charging: battery.charging,
28
+ chargingTime: battery.chargingTime,
29
+ dischargingTime: battery.dischargingTime
30
+ });
31
+ const subscribeNever = () => () => {};
32
+ const hasBatteryApi = () => "getBattery" in navigator;
33
+ const useBattery = () => {
34
+ const supported = useSyncExternalStore(subscribeNever, hasBatteryApi, () => true);
35
+ const [state, setState] = useState(INITIAL_STATE);
36
+ useEffect(() => {
37
+ const nav = navigator;
38
+ if (!nav.getBattery) return;
39
+ const getBattery = nav.getBattery.bind(nav);
40
+ let cancelled = false;
41
+ let detach = () => {};
42
+ tryCatch(new Promise((resolve) => resolve(getBattery()))).then((result) => {
43
+ if (cancelled) return;
44
+ if (!result.success) {
45
+ setState(UNSUPPORTED_STATE);
46
+ return;
47
+ }
48
+ const battery = result.data;
49
+ const update = () => setState(readBattery(battery));
50
+ update();
51
+ for (const event of BATTERY_EVENTS) battery.addEventListener(event, update);
52
+ detach = () => {
53
+ for (const event of BATTERY_EVENTS) battery.removeEventListener(event, update);
54
+ };
55
+ });
56
+ return () => {
57
+ cancelled = true;
58
+ detach();
59
+ };
60
+ }, []);
61
+ return supported ? state : UNSUPPORTED_STATE;
62
+ };
63
+ //#endregion
64
+ export { useBattery };
@@ -0,0 +1,6 @@
1
+ //#region src/use-browser.d.ts
2
+ type Browser = "undetermined" | "chrome" | "firefox" | "safari" | "edge" | "opera" | "ie" | "other";
3
+ declare const getBrowserVersion: () => string | null;
4
+ declare const useBrowser: () => Browser;
5
+ //#endregion
6
+ export { type Browser, getBrowserVersion, useBrowser };
@@ -0,0 +1,51 @@
1
+ import { t as isServerSide } from "./env-C0PNnbwt.js";
2
+ import { useSyncExternalStore } from "react";
3
+ //#region src/use-browser.ts
4
+ const matchers = [
5
+ {
6
+ browser: "edge",
7
+ detect: /Edge\/([0-9]+)/i,
8
+ version: /Edge\/([0-9]+)/i
9
+ },
10
+ {
11
+ browser: "chrome",
12
+ detect: /(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9]+)/i,
13
+ version: /Chrom(?:e|ium)\/([0-9]+)/i
14
+ },
15
+ {
16
+ browser: "firefox",
17
+ detect: /Firefox\/([0-9]+)/i,
18
+ version: /Firefox\/([0-9]+)/i
19
+ },
20
+ {
21
+ browser: "safari",
22
+ detect: /^((?!chrome|android).)*safari/i,
23
+ version: /Version\/([0-9]+)/i
24
+ },
25
+ {
26
+ browser: "opera",
27
+ detect: /(OPR|Opera)\/([0-9]+)/i,
28
+ version: /(?:OPR|Opera)\/([0-9]+)/i
29
+ },
30
+ {
31
+ browser: "ie",
32
+ detect: /MSIE|Trident/i,
33
+ version: /(?:MSIE |rv:)([0-9]+)/i
34
+ }
35
+ ];
36
+ const detectBrowser = (userAgent) => matchers.find(({ detect }) => detect.test(userAgent));
37
+ const getBrowser = () => {
38
+ if (isServerSide()) return "undetermined";
39
+ return detectBrowser(window.navigator.userAgent)?.browser ?? "other";
40
+ };
41
+ const getBrowserVersion = () => {
42
+ if (isServerSide()) return null;
43
+ const { userAgent } = window.navigator;
44
+ const matcher = detectBrowser(userAgent);
45
+ return matcher ? userAgent.match(matcher.version)?.[1] ?? null : null;
46
+ };
47
+ const subscribe = () => () => {};
48
+ const getServerSnapshot = () => "undetermined";
49
+ const useBrowser = () => useSyncExternalStore(subscribe, getBrowser, getServerSnapshot);
50
+ //#endregion
51
+ export { getBrowserVersion, useBrowser };
@@ -0,0 +1,5 @@
1
+ import { t as ANY } from "./types-p0KNRcj3.js";
2
+ //#region src/use-callback-ref.d.ts
3
+ declare const useCallbackRef: <T extends (...args: ANY[]) => ANY>(callback: T | undefined) => T;
4
+ //#endregion
5
+ export { useCallbackRef };
@@ -0,0 +1,11 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ //#region src/use-callback-ref.ts
3
+ const useCallbackRef = (callback) => {
4
+ const callbackRef = useRef(callback);
5
+ useEffect(() => {
6
+ callbackRef.current = callback;
7
+ });
8
+ return useMemo(() => ((...args) => callbackRef.current?.(...args)), []);
9
+ };
10
+ //#endregion
11
+ export { useCallbackRef };
@@ -3,5 +3,6 @@
3
3
  * State that the caller may own. `value` undefined means uncontrolled: the hook
4
4
  * keeps the state and still reports every change through `onChange`.
5
5
  */
6
- export declare function useControllableState<T>(value: T | null | undefined, defaultValue: T | null, onChange?: (value: T) => void): readonly [T | null, (next: T) => void];
7
- //#endregion
6
+ declare const useControllableState: <T>(value: T | null | undefined, defaultValue: T | null, onChange?: (value: T) => void) => readonly [T | null, (next: T) => void];
7
+ //#endregion
8
+ export { useControllableState };
@@ -1,17 +1,19 @@
1
+ import { useCallbackRef } from "./use-callback-ref.js";
1
2
  import { useCallback, useState } from "react";
2
3
  //#region src/use-controllable-state.ts
3
4
  /**
4
5
  * State that the caller may own. `value` undefined means uncontrolled: the hook
5
6
  * keeps the state and still reports every change through `onChange`.
6
7
  */
7
- function useControllableState(value, defaultValue, onChange) {
8
+ const useControllableState = (value, defaultValue, onChange) => {
8
9
  const [uncontrolled, setUncontrolled] = useState(defaultValue);
9
10
  const controlled = value !== void 0;
11
+ const reportChange = useCallbackRef(onChange);
10
12
  const setValue = useCallback((next) => {
11
13
  if (!controlled) setUncontrolled(next);
12
- onChange?.(next);
13
- }, [controlled, onChange]);
14
+ reportChange(next);
15
+ }, [controlled, reportChange]);
14
16
  return [controlled ? value : uncontrolled, setValue];
15
- }
17
+ };
16
18
  //#endregion
17
19
  export { useControllableState };
@@ -0,0 +1,9 @@
1
+ import { SetStateAction } from "react";
2
+ //#region src/use-debounce-state.d.ts
3
+ type UseDebouncedStateOptions = {
4
+ leading?: boolean;
5
+ };
6
+ type UseDebouncedStateReturnValue<T> = [T, (newValue: SetStateAction<T>) => void];
7
+ declare const useDebouncedState: <T = unknown>(defaultValue: T, wait: number, { leading }?: UseDebouncedStateOptions) => UseDebouncedStateReturnValue<T>;
8
+ //#endregion
9
+ export { useDebouncedState };
@@ -0,0 +1,28 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ //#region src/use-debounce-state.ts
3
+ const useDebouncedState = (defaultValue, wait, { leading = false } = {}) => {
4
+ const [value, setValue] = useState(defaultValue);
5
+ const timeoutRef = useRef(void 0);
6
+ const leadingRef = useRef(true);
7
+ const cancel = useCallback(() => window.clearTimeout(timeoutRef.current), []);
8
+ useEffect(() => cancel, [cancel]);
9
+ return [value, useCallback((newValue) => {
10
+ cancel();
11
+ if (leadingRef.current && leading) {
12
+ setValue(newValue);
13
+ timeoutRef.current = window.setTimeout(() => {
14
+ leadingRef.current = true;
15
+ }, wait);
16
+ } else timeoutRef.current = window.setTimeout(() => {
17
+ leadingRef.current = true;
18
+ setValue(newValue);
19
+ }, wait);
20
+ leadingRef.current = false;
21
+ }, [
22
+ leading,
23
+ cancel,
24
+ wait
25
+ ])];
26
+ };
27
+ //#endregion
28
+ export { useDebouncedState };
@@ -0,0 +1,16 @@
1
+ import { t as ANY } from "./types-p0KNRcj3.js";
2
+ //#region src/use-debounced-callback.d.ts
3
+ type UseDebouncedCallbackOptions = {
4
+ delay: number;
5
+ flushOnUnmount?: boolean;
6
+ leading?: boolean;
7
+ maxWait?: number;
8
+ };
9
+ type UseDebouncedCallbackReturnValue<T extends (...args: ANY[]) => ANY> = ((...args: Parameters<T>) => void) & {
10
+ flush: () => void;
11
+ cancel: () => void;
12
+ isPending: () => boolean;
13
+ };
14
+ declare const useDebouncedCallback: <T extends (...args: ANY[]) => ANY>(callback: T, options: number | UseDebouncedCallbackOptions) => UseDebouncedCallbackReturnValue<T>;
15
+ //#endregion
16
+ export { type UseDebouncedCallbackOptions, type UseDebouncedCallbackReturnValue, useDebouncedCallback };
@@ -0,0 +1,68 @@
1
+ import { useCallbackRef } from "./use-callback-ref.js";
2
+ import { useEffect, useMemo } from "react";
3
+ //#region src/use-debounced-callback.ts
4
+ const createDebounced = (run, { delay, leading, maxWait }) => {
5
+ let debounceTimer;
6
+ let maxWaitTimer;
7
+ let latestArgs;
8
+ let isFirstCall = true;
9
+ let pending = false;
10
+ const cancel = () => {
11
+ window.clearTimeout(debounceTimer);
12
+ window.clearTimeout(maxWaitTimer);
13
+ debounceTimer = void 0;
14
+ maxWaitTimer = void 0;
15
+ isFirstCall = true;
16
+ pending = false;
17
+ };
18
+ const flush = () => {
19
+ if (pending && debounceTimer !== void 0 && latestArgs) {
20
+ const args = latestArgs;
21
+ cancel();
22
+ run(...args);
23
+ }
24
+ };
25
+ const startMaxWait = () => {
26
+ if (maxWait !== void 0 && maxWaitTimer === void 0) maxWaitTimer = window.setTimeout(flush, maxWait);
27
+ };
28
+ const debounced = (...args) => {
29
+ window.clearTimeout(debounceTimer);
30
+ latestArgs = args;
31
+ const isLeadingCall = leading && isFirstCall;
32
+ isFirstCall = false;
33
+ if (isLeadingCall) {
34
+ run(...args);
35
+ debounceTimer = window.setTimeout(cancel, delay);
36
+ } else {
37
+ pending = true;
38
+ debounceTimer = window.setTimeout(leading ? cancel : flush, delay);
39
+ }
40
+ startMaxWait();
41
+ };
42
+ return Object.assign(debounced, {
43
+ flush,
44
+ cancel,
45
+ isPending: () => pending
46
+ });
47
+ };
48
+ const useDebouncedCallback = (callback, options) => {
49
+ const { delay, flushOnUnmount = false, leading = false, maxWait } = typeof options === "number" ? { delay: options } : options;
50
+ const handleCallback = useCallbackRef(callback);
51
+ const debounced = useMemo(() => createDebounced(handleCallback, {
52
+ delay,
53
+ leading,
54
+ maxWait
55
+ }), [
56
+ handleCallback,
57
+ delay,
58
+ leading,
59
+ maxWait
60
+ ]);
61
+ useEffect(() => () => {
62
+ if (flushOnUnmount) debounced.flush();
63
+ else debounced.cancel();
64
+ }, [debounced, flushOnUnmount]);
65
+ return debounced;
66
+ };
67
+ //#endregion
68
+ export { useDebouncedCallback };
@@ -1,3 +1,4 @@
1
1
  //#region src/use-debounced-value.d.ts
2
- export declare function useDebouncedValue<T>(value: T, delay: number): T;
3
- //#endregion
2
+ declare const useDebouncedValue: <T>(value: T, delay: number) => T;
3
+ //#endregion
4
+ export { useDebouncedValue };
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from "react";
2
- import { debounce } from "@journ/fitter-happier";
2
+ import { debounce } from "@journ/fitter-happier/debounce";
3
3
  //#region src/use-debounced-value.ts
4
- function useDebouncedValue(value, delay) {
4
+ const useDebouncedValue = (value, delay) => {
5
5
  const [debounced, setDebounced] = useState(value);
6
6
  useEffect(() => {
7
7
  const update = debounce(setDebounced, delay);
@@ -9,6 +9,6 @@ function useDebouncedValue(value, delay) {
9
9
  return update.cancel;
10
10
  }, [value, delay]);
11
11
  return debounced;
12
- }
12
+ };
13
13
  //#endregion
14
14
  export { useDebouncedValue };
@@ -0,0 +1,5 @@
1
+ import { DependencyList, EffectCallback } from "react";
2
+ //#region src/use-did-update.d.ts
3
+ declare const useDidUpdate: (fn: EffectCallback, dependencies?: DependencyList) => void;
4
+ //#endregion
5
+ export { useDidUpdate };
@@ -0,0 +1,24 @@
1
+ import { useEffect, useRef } from "react";
2
+ //#region src/use-did-update.ts
3
+ const dependenciesChanged = (previous, next) => previous.length !== next.length || next.some((dep, index) => !Object.is(dep, previous[index]));
4
+ const useDidUpdate = (fn, dependencies) => {
5
+ const mounted = useRef(false);
6
+ const previousRender = useRef(null);
7
+ const previousDependencies = useRef(void 0);
8
+ const render = {};
9
+ useEffect(() => {
10
+ const isSameRender = previousRender.current === render;
11
+ const previous = previousDependencies.current;
12
+ previousRender.current = render;
13
+ previousDependencies.current = dependencies;
14
+ if (!mounted.current) {
15
+ mounted.current = true;
16
+ return;
17
+ }
18
+ if (isSameRender) return;
19
+ if (dependencies && previous && !dependenciesChanged(previous, dependencies)) return;
20
+ return fn();
21
+ }, dependencies);
22
+ };
23
+ //#endregion
24
+ export { useDidUpdate };
@@ -0,0 +1,15 @@
1
+ //#region src/use-disclosure.d.ts
2
+ type UseDisclosureOptions = {
3
+ onOpen?: () => void;
4
+ onClose?: () => void;
5
+ };
6
+ type UseDisclosureHandlers = {
7
+ set: (value: boolean) => void;
8
+ open: () => void;
9
+ close: () => void;
10
+ toggle: () => void;
11
+ };
12
+ type UseDisclosureReturnValue = [boolean, UseDisclosureHandlers];
13
+ declare const useDisclosure: (initialState?: boolean, { onOpen, onClose }?: UseDisclosureOptions) => UseDisclosureReturnValue;
14
+ //#endregion
15
+ export { type UseDisclosureHandlers, type UseDisclosureOptions, type UseDisclosureReturnValue, useDisclosure };
@@ -0,0 +1,16 @@
1
+ import { useDidUpdate } from "./use-did-update.js";
2
+ import { useCallback, useEffectEvent, useState } from "react";
3
+ //#region src/use-disclosure.ts
4
+ const useDisclosure = (initialState = false, { onOpen, onClose } = {}) => {
5
+ const [opened, setOpened] = useState(initialState);
6
+ const notify = useEffectEvent((isOpen) => isOpen ? onOpen?.() : onClose?.());
7
+ useDidUpdate(() => notify(opened), [opened]);
8
+ return [opened, {
9
+ open: useCallback(() => setOpened(true), []),
10
+ close: useCallback(() => setOpened(false), []),
11
+ toggle: useCallback(() => setOpened((current) => !current), []),
12
+ set: setOpened
13
+ }];
14
+ };
15
+ //#endregion
16
+ export { useDisclosure };
@@ -1,20 +1,21 @@
1
1
  //#region src/use-draft.d.ts
2
- export interface UseDraftOptions<T> {
2
+ type UseDraftOptions<T> = {
3
3
  value: T;
4
4
  format: (value: T) => string;
5
5
  /** Return undefined to reject the text, which keeps the old value. */
6
6
  parse: (text: string) => T | undefined;
7
7
  onCommit: (value: T) => void;
8
- }
9
- export interface Draft {
8
+ };
9
+ type Draft = {
10
10
  text: string;
11
11
  edit: (text: string) => void;
12
12
  discard: () => void;
13
13
  commit: () => void;
14
- }
14
+ };
15
15
  /**
16
16
  * Text that is typed freely and only turns into a value on commit. While not
17
17
  * editing it shows `format(value)`.
18
18
  */
19
- export declare function useDraft<T>({ value, format, parse, onCommit }: UseDraftOptions<T>): Draft;
20
- //#endregion
19
+ declare const useDraft: <T>({ value, format, parse, onCommit }: UseDraftOptions<T>) => Draft;
20
+ //#endregion
21
+ export { type Draft, type UseDraftOptions, useDraft };
package/dist/use-draft.js CHANGED
@@ -4,7 +4,7 @@ import { useState } from "react";
4
4
  * Text that is typed freely and only turns into a value on commit. While not
5
5
  * editing it shows `format(value)`.
6
6
  */
7
- function useDraft({ value, format, parse, onCommit }) {
7
+ const useDraft = ({ value, format, parse, onCommit }) => {
8
8
  const [draft, setDraft] = useState(null);
9
9
  return {
10
10
  text: draft ?? format(value),
@@ -16,6 +16,6 @@ function useDraft({ value, format, parse, onCommit }) {
16
16
  setDraft(null);
17
17
  }
18
18
  };
19
- }
19
+ };
20
20
  //#endregion
21
21
  export { useDraft };