@venizia/ardor-react 0.4.8-2 → 0.4.8-4

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/README.md CHANGED
@@ -4,9 +4,11 @@ The React bindings of [ARDOR](https://github.com/VENIZIA-AI/ardor): the applicat
4
4
  hooks that resolve out of it, and the UI hooks. No react-admin.
5
5
 
6
6
  ```bash
7
- bun add @venizia/ardor-react @venizia/ardor-kernel @venizia/ignis-inversion @venizia/ignis-filter @venizia/ignis-kernel @venizia/ignis-helpers reflect-metadata
7
+ bun add @venizia/ardor-react @venizia/ardor-kernel @venizia/ignis-inversion @venizia/ignis-filter @venizia/ignis-kernel @venizia/ignis-helpers reflect-metadata react react-redux
8
8
  ```
9
9
 
10
+ `@reduxjs/toolkit` is an optional peer: only the types of `DispatchHooks` use it.
11
+
10
12
  Most applications install [`@venizia/ardor`](https://www.npmjs.com/package/@venizia/ardor) instead,
11
13
  which re-exports this package.
12
14
 
@@ -1,3 +1,4 @@
1
+ /** Builds the typed selector hooks of a Redux store. */
1
2
  export declare class SelectorHooks {
2
3
  /** The three typed selector hooks of a store: bind your `RootState` once, and export what comes back. */
3
4
  static create<TState>(): {
@@ -25,6 +25,7 @@ const deepEqual = (a, b) => {
25
25
  return Object.hasOwn(b, key) && deepEqual(Reflect.get(a, key), Reflect.get(b, key));
26
26
  }));
27
27
  };
28
+ /** Builds the typed selector hooks of a Redux store. */
28
29
  export class SelectorHooks {
29
30
  /** The three typed selector hooks of a store: bind your `RootState` once, and export what comes back. */
30
31
  static create() {
@@ -4,7 +4,9 @@ export interface IResizeObservationOptions {
4
4
  box: TResizeBox;
5
5
  /** Also observe the direct children of each target, and again whenever they change. @default false */
6
6
  children?: boolean;
7
- /** Called at most once per animation frame, with the latest entry of each element that resized. */
7
+ /** Call `onFrame` inside the `ResizeObserver` callback, after layout and before paint, instead of on the next animation frame. */
8
+ beforePaint?: boolean;
9
+ /** Called with the latest entry of each element that resized: at most once per animation frame, or once per observer callback with `beforePaint`. */
8
10
  onFrame: (entries: ResizeObserverEntry[]) => void;
9
11
  }
10
12
  export interface IResizeObservation {
@@ -1,7 +1,7 @@
1
1
  export class ResizeObservation {
2
2
  /** Does nothing without `ResizeObserver`. */
3
3
  static observe(opts) {
4
- const { box, children = false, onFrame } = opts;
4
+ const { box, children = false, beforePaint = false, onFrame } = opts;
5
5
  if (typeof ResizeObserver === 'undefined') {
6
6
  return { sync: () => { }, stop: () => { } };
7
7
  }
@@ -9,20 +9,27 @@ export class ResizeObservation {
9
9
  let watched = new Set();
10
10
  let current = [];
11
11
  let frame;
12
+ const flush = () => {
13
+ const batch = [...latest.values()];
14
+ latest.clear();
15
+ if (batch.length > 0) {
16
+ onFrame(batch);
17
+ }
18
+ };
12
19
  const observer = new ResizeObserver((entries) => {
13
20
  for (const entry of entries) {
14
21
  latest.set(entry.target, entry);
15
22
  }
23
+ if (beforePaint) {
24
+ flush();
25
+ return;
26
+ }
16
27
  if (frame !== undefined) {
17
28
  return;
18
29
  }
19
30
  frame = requestAnimationFrame(() => {
20
31
  frame = undefined;
21
- const batch = [...latest.values()];
22
- latest.clear();
23
- if (batch.length > 0) {
24
- onFrame(batch);
25
- }
32
+ flush();
26
33
  });
27
34
  });
28
35
  const update = (targets) => {
@@ -1,3 +1,4 @@
1
+ /** What `useAutosave` saves, through which callback, and when. */
1
2
  export interface IUseAutosaveOptions<TData, TReturn> {
2
3
  /** The value to save; a change starts the `interval` wait. */
3
4
  data: TData;
@@ -1,3 +1,4 @@
1
+ /** When `useBeforeUnload` asks, and the text it sets. */
1
2
  export interface IUseBeforeUnloadOptions {
2
3
  /** @default true */
3
4
  enabled?: boolean;
@@ -1,3 +1,4 @@
1
+ /** The state of one confirmation prompt, and the handlers that settle it. */
1
2
  export interface IUseConfirmReturn {
2
3
  /** The pending prompt text; `undefined` when none is open. */
3
4
  message?: string;
@@ -1,7 +1,9 @@
1
+ /** How long `useCopyToClipboard` keeps `isCopied` set. */
1
2
  export interface IUseCopyToClipboardOptions {
2
3
  /** Set `isCopied` back to `false` this many milliseconds after a copy; 0 or less never resets. @default 0 */
3
4
  resetMs?: number;
4
5
  }
6
+ /** The copy function of `useCopyToClipboard` and the result of its last call. */
5
7
  export interface IUseCopyToClipboardReturn {
6
8
  /** Resolves `true` when the text is on the clipboard and `false` otherwise, and never throws. */
7
9
  copy: (opts: {
@@ -1,3 +1,4 @@
1
+ /** The value `useDebounce` follows, how long it waits, and when it runs. */
1
2
  export interface IUseDebounceOptions<TValue> {
2
3
  /** The value to follow; the output changes once it has been still for `delay`. */
3
4
  value: TValue;
@@ -6,6 +7,7 @@ export interface IUseDebounceOptions<TValue> {
6
7
  /** @default true */
7
8
  enabled?: boolean;
8
9
  }
10
+ /** The settled value of `useDebounce`. */
9
11
  export interface IUseDebounceReturn<TValue> {
10
12
  /** The last `value` that stayed unchanged for `delay`; the first `value` until then. */
11
13
  debouncedValue: TValue;
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { type TResizeBox } from './resize-observation.js';
3
+ /** Which element `useElementSize` measures, which box it reads, and how it rounds. */
3
4
  export interface IUseElementSizeOptions {
4
5
  /** The element to measure. Leave it out to measure the element the returned `ref` is given to. */
5
6
  ref?: React.RefObject<Element | null>;
@@ -8,12 +9,14 @@ export interface IUseElementSizeOptions {
8
9
  /** Round width and height to whole pixels, so a fractional size does not render again. @default false */
9
10
  round?: boolean;
10
11
  }
12
+ /** The size `useElementSize` read, in CSS pixels. */
11
13
  export interface IUseElementSizeReturn {
12
14
  width: number;
13
15
  height: number;
14
16
  /** `false` until the first size is read, so 0 by 0 can be told from a measured empty element. */
15
17
  measured: boolean;
16
18
  }
19
+ /** The size of `useElementSize` called without a `ref` option, with the ref to give an element. */
17
20
  export interface IUseElementSizeRefReturn extends IUseElementSizeReturn {
18
21
  /** Give it to the element to measure. */
19
22
  ref: React.RefCallback<Element>;
@@ -1,4 +1,5 @@
1
1
  import { type TElementTarget } from './element-target.js';
2
+ /** The element `useIntersection` watches and when it delivers an entry. */
2
3
  export interface IUseIntersectionOptions {
3
4
  /** Read after every render: an element, a ref to one, or `null`. */
4
5
  target: TElementTarget;
@@ -1,5 +1,6 @@
1
1
  import { type TElementTarget } from './element-target.js';
2
2
  import { type TResizeBox } from './resize-observation.js';
3
+ /** What `useResizeEffect` watches and the callback it calls. */
3
4
  export interface IUseResizeEffectOptions {
4
5
  /** Read after every render: an element, a ref to one, or `null`, which is skipped. */
5
6
  targets: TElementTarget[];
@@ -9,11 +10,20 @@ export interface IUseResizeEffectOptions {
9
10
  box?: TResizeBox;
10
11
  /** Also observe the direct children of each target, and again whenever they change. @default false */
11
12
  children?: boolean;
13
+ /**
14
+ * Call `onResize` inside the `ResizeObserver` callback, after layout and before paint, instead of on
15
+ * the next animation frame, once per observer callback. What `onResize` draws or styles directly
16
+ * shows in the frame that resized; a state update renders later. Resizing an observed element from
17
+ * it can report "ResizeObserver loop completed with undelivered notifications".
18
+ * @default false
19
+ */
20
+ beforePaint?: boolean;
12
21
  }
13
22
  /**
14
- * Calls `onResize` at most once per animation frame when any of the targets resizes. A new
15
- * `onResize` is used without observing again, and the targets are read after every render, so an
16
- * element held in state, a ref whose element changed and a list that grows or shrinks are followed.
23
+ * Calls `onResize` at most once per animation frame when any of the targets resizes, or inside the
24
+ * observer callback with `beforePaint`. A new `onResize` is used without observing again, and the
25
+ * targets are read after every render, so an element held in state, a ref whose element changed and
26
+ * a list that grows or shrinks are followed.
17
27
  *
18
28
  * @example
19
29
  * ```tsx
@@ -2,9 +2,10 @@ import React from 'react';
2
2
  import { ElementTargets } from './element-target.js';
3
3
  import { ResizeObservation } from './resize-observation.js';
4
4
  /**
5
- * Calls `onResize` at most once per animation frame when any of the targets resizes. A new
6
- * `onResize` is used without observing again, and the targets are read after every render, so an
7
- * element held in state, a ref whose element changed and a list that grows or shrinks are followed.
5
+ * Calls `onResize` at most once per animation frame when any of the targets resizes, or inside the
6
+ * observer callback with `beforePaint`. A new `onResize` is used without observing again, and the
7
+ * targets are read after every render, so an element held in state, a ref whose element changed and
8
+ * a list that grows or shrinks are followed.
8
9
  *
9
10
  * @example
10
11
  * ```tsx
@@ -19,7 +20,7 @@ import { ResizeObservation } from './resize-observation.js';
19
20
  * ```
20
21
  */
21
22
  export const useResizeEffect = (opts) => {
22
- const { targets, onResize, box = 'content-box', children = false } = opts;
23
+ const { targets, onResize, box = 'content-box', children = false, beforePaint = false } = opts;
23
24
  const handleResize = React.useRef(onResize);
24
25
  const observation = React.useRef(undefined);
25
26
  React.useEffect(() => {
@@ -30,11 +31,12 @@ export const useResizeEffect = (opts) => {
30
31
  const created = ResizeObservation.observe({
31
32
  box,
32
33
  children,
34
+ beforePaint,
33
35
  onFrame: (entries) => handleResize.current(entries),
34
36
  });
35
37
  observation.current = created;
36
38
  return created.stop;
37
- }, [box, children]);
39
+ }, [box, children, beforePaint]);
38
40
  React.useEffect(() => {
39
41
  observation.current?.sync(targets.flatMap((target) => ElementTargets.resolve({ target }) ?? []));
40
42
  });
@@ -1,4 +1,5 @@
1
1
  import { type IStorageCodec, type TStorageArea } from '@venizia/ardor-kernel';
2
+ /** What `useStoredState` stores, where and how. */
2
3
  export interface IUseStoredStateOptions<TValue> {
3
4
  /** The storage key. */
4
5
  key: string;
@@ -11,6 +12,7 @@ export interface IUseStoredStateOptions<TValue> {
11
12
  /** Write storage once, this many milliseconds after the last `setValue`; `value` changes at once. Above 0, also pass a stable `codec`. @default 0 */
12
13
  writeDelayMs?: number;
13
14
  }
15
+ /** The value `useStoredState` holds, and how to change or remove it. */
14
16
  export interface IUseStoredStateReturn<TValue> {
15
17
  /** The stored value, or `defaultValue` when the key holds nothing. */
16
18
  value: TValue;
@@ -19,6 +21,7 @@ export interface IUseStoredStateReturn<TValue> {
19
21
  /** Removes the key, so `value` is `defaultValue` again. */
20
22
  remove: () => void;
21
23
  }
24
+ /** The call signatures of `useStoredState`. */
22
25
  export interface IUseStoredState {
23
26
  /** With a codec, the value type is the codec's: a literal `defaultValue` is checked against it, not widened past it. */
24
27
  <TValue>(opts: Omit<IUseStoredStateOptions<TValue>, 'defaultValue' | 'codec'> & {
@@ -39,7 +42,7 @@ export interface IUseStoredState {
39
42
  * import { useStoredState } from '@venizia/ardor';
40
43
  *
41
44
  * export function ThemeToggle() {
42
- * const { value, setValue } = useStoredState({ key: 'theme', defaultValue: 'light' });
45
+ * const { value, setValue } = useStoredState({ key: '@app/store/theme', defaultValue: 'light' });
43
46
  * return <button onClick={() => setValue(value === 'light' ? 'dark' : 'light')}>{value}</button>;
44
47
  * }
45
48
  * ```
@@ -14,7 +14,7 @@ const isUpdater = (next) => typeof next === 'function';
14
14
  * import { useStoredState } from '@venizia/ardor';
15
15
  *
16
16
  * export function ThemeToggle() {
17
- * const { value, setValue } = useStoredState({ key: 'theme', defaultValue: 'light' });
17
+ * const { value, setValue } = useStoredState({ key: '@app/store/theme', defaultValue: 'light' });
18
18
  * return <button onClick={() => setValue(value === 'light' ? 'dark' : 'light')}>{value}</button>;
19
19
  * }
20
20
  * ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@venizia/ardor-react",
3
- "version": "0.4.8-2",
3
+ "version": "0.4.8-4",
4
4
  "description": "ARDOR React bindings - the application context, the injectable and logger hooks that read it, typed Redux hook factories, and the framework-agnostic UI hooks (debounce, autosave, confirm, clipboard, element size, window dimensions). No react-admin.",
5
5
  "keywords": [
6
6
  "ardor",
@@ -59,7 +59,7 @@
59
59
  "prepublishOnly": "make rebuild"
60
60
  },
61
61
  "dependencies": {
62
- "@venizia/ardor-kernel": "0.4.8-2"
62
+ "@venizia/ardor-kernel": "0.4.8-4"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@happy-dom/global-registrator": "^21.0.4",
@@ -69,11 +69,11 @@
69
69
  "@types/bun": "^1.4.3",
70
70
  "@types/react": "^19",
71
71
  "@types/react-dom": "^19",
72
- "@venizia/dev-configs": "^0.4.8-1",
73
- "@venizia/ignis-filter": "^0.4.7-1",
74
- "@venizia/ignis-helpers": "^0.4.8-1",
75
- "@venizia/ignis-inversion": "^0.4.7-1",
76
- "@venizia/ignis-kernel": "^0.4.8-1",
72
+ "@venizia/dev-configs": "^0.4.8-2",
73
+ "@venizia/ignis-filter": "^0.4.7-2",
74
+ "@venizia/ignis-helpers": "^0.4.8-3",
75
+ "@venizia/ignis-inversion": "^0.4.7-2",
76
+ "@venizia/ignis-kernel": "^0.4.8-3",
77
77
  "eslint": "^10.13.0",
78
78
  "oxlint": "1.89.0",
79
79
  "oxlint-tsgolint": "7.0.2004",
@@ -87,10 +87,10 @@
87
87
  },
88
88
  "peerDependencies": {
89
89
  "@reduxjs/toolkit": ">=2.13.0",
90
- "@venizia/ignis-filter": "^0.4.7-1",
91
- "@venizia/ignis-helpers": "^0.4.8-1",
92
- "@venizia/ignis-inversion": "^0.4.7-1",
93
- "@venizia/ignis-kernel": "^0.4.8-1",
90
+ "@venizia/ignis-filter": "^0.4.7-2",
91
+ "@venizia/ignis-helpers": "^0.4.8-3",
92
+ "@venizia/ignis-inversion": "^0.4.7-2",
93
+ "@venizia/ignis-kernel": "^0.4.8-3",
94
94
  "react": ">=19.3.1",
95
95
  "react-redux": ">=9.3.0",
96
96
  "reflect-metadata": ">=0.2.2"