@venizia/ardor-react 0.4.8-1 → 0.4.8-11
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 +8 -5
- package/dist/contexts/application.d.ts +4 -0
- package/dist/contexts/application.js +1 -0
- package/dist/hooks/redux/use-app-dispatch.d.ts +5 -1
- package/dist/hooks/redux/use-app-dispatch.js +5 -1
- package/dist/hooks/redux/use-app-selector.d.ts +1 -0
- package/dist/hooks/redux/use-app-selector.js +1 -0
- package/dist/hooks/resize-observation.d.ts +4 -2
- package/dist/hooks/resize-observation.js +28 -12
- package/dist/hooks/use-application-context.d.ts +2 -0
- package/dist/hooks/use-application-context.js +2 -0
- package/dist/hooks/use-artifact.d.ts +26 -0
- package/dist/hooks/use-artifact.js +22 -0
- package/dist/hooks/use-autosave.d.ts +8 -0
- package/dist/hooks/use-autosave.js +4 -0
- package/dist/hooks/use-before-unload.d.ts +3 -0
- package/dist/hooks/use-before-unload.js +1 -0
- package/dist/hooks/use-confirm.d.ts +7 -0
- package/dist/hooks/use-confirm.js +1 -0
- package/dist/hooks/use-copy-to-clipboard.d.ts +15 -0
- package/dist/hooks/use-copy-to-clipboard.js +13 -0
- package/dist/hooks/use-debounce.d.ts +6 -0
- package/dist/hooks/use-debounce.js +1 -0
- package/dist/hooks/use-element-size.d.ts +14 -0
- package/dist/hooks/use-injectable.d.ts +31 -0
- package/dist/hooks/use-injectable.js +14 -0
- package/dist/hooks/use-intersection.d.ts +37 -8
- package/dist/hooks/use-intersection.js +64 -28
- package/dist/hooks/use-resize-effect.d.ts +25 -3
- package/dist/hooks/use-resize-effect.js +19 -5
- package/dist/hooks/use-stored-state.d.ts +21 -3
- package/dist/hooks/use-stored-state.js +53 -7
- package/dist/hooks/use-window-dimensions.d.ts +1 -0
- package/dist/hooks/use-window-dimensions.js +1 -0
- package/package.json +13 -14
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
|
|
|
@@ -15,15 +17,14 @@ which re-exports this package.
|
|
|
15
17
|
| Area | Exports |
|
|
16
18
|
|---|---|
|
|
17
19
|
| Context | `ApplicationContext`, `useApplicationContext`, `useApplicationLogger` |
|
|
18
|
-
| Injection | `useInjectable` - resolve a binding by key or by class |
|
|
20
|
+
| Injection | `useInjectable` - resolve a binding by key or by class; the artifact hooks `useApplicationContainer`, `useService`, `useProvider`, `useComponent`, `useConfiguration`, `useRepository` |
|
|
19
21
|
| Redux | `DispatchHooks.create`, `SelectorHooks.create` - factories that bind your `RootState` and `AppDispatch` (the old `createAppDispatch` and `createAppSelectors` are removed; `bun run ardor codemod namespace` moves your code) |
|
|
20
|
-
| UI hooks | `useDebounce`, `useAutosave`, `useConfirm`, `useBeforeUnload`, `useCopyToClipboard`, `useWindowDimensions` |
|
|
22
|
+
| UI hooks | `useDebounce`, `useAutosave`, `useConfirm`, `useBeforeUnload`, `useCopyToClipboard`, `useWindowDimensions`, `useElementSize`, `useResizeEffect`, `useIntersection`, `useStoredState`, `useMediaQuery`, `useDocumentVisibility`, `useOnlineStatus` |
|
|
21
23
|
| Types | `TUseInjectableKeys` |
|
|
22
24
|
|
|
23
25
|
## Typing your own binding keys
|
|
24
26
|
|
|
25
|
-
`IUseInjectableKeysOverrides` is declared here, so augment this package
|
|
26
|
-
into the module that declares it, never through a re-export:
|
|
27
|
+
`IUseInjectableKeysOverrides` is declared here, so augment this package when it is the one you depend on:
|
|
27
28
|
|
|
28
29
|
```typescript
|
|
29
30
|
declare module '@venizia/ardor-react' {
|
|
@@ -33,6 +34,8 @@ declare module '@venizia/ardor-react' {
|
|
|
33
34
|
}
|
|
34
35
|
```
|
|
35
36
|
|
|
37
|
+
With `@venizia/ardor`, write `declare module '@venizia/ardor'` instead.
|
|
38
|
+
|
|
36
39
|
## License
|
|
37
40
|
|
|
38
41
|
MIT - see [LICENSE.md](./LICENSE.md).
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { Container } from '@venizia/ignis-inversion';
|
|
3
3
|
import type { ILogger } from '@venizia/ignis-helpers/core';
|
|
4
|
+
/** What `ApplicationContext` carries; both are `null` outside a provider. */
|
|
4
5
|
export interface IApplicationContextValue {
|
|
6
|
+
/** The application's container. */
|
|
5
7
|
container: Container | null;
|
|
8
|
+
/** The application's logger. */
|
|
6
9
|
logger: ILogger | null;
|
|
7
10
|
}
|
|
11
|
+
/** Read through `useApplicationContext` and `useApplicationLogger`; the default value holds `null`s. */
|
|
8
12
|
export declare const ApplicationContext: React.Context<IApplicationContextValue>;
|
|
9
13
|
//# sourceMappingURL=application.d.ts.map
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Dispatch, UnknownAction } from '@reduxjs/toolkit';
|
|
2
|
+
/** Builds the typed dispatch hooks of a store. */
|
|
2
3
|
export declare class DispatchHooks {
|
|
3
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* The typed dispatch hooks of a store: bind your `AppDispatch` once, and export what comes back.
|
|
6
|
+
* `useMultipleAppDispatch` returns a function that dispatches each action in order and skips falsy ones.
|
|
7
|
+
*/
|
|
4
8
|
static create<TDispatch extends Dispatch<UnknownAction> = Dispatch<UnknownAction>>(): {
|
|
5
9
|
useAppDispatch: import("react-redux").UseDispatch<TDispatch>;
|
|
6
10
|
useMultipleAppDispatch: () => (...actions: Parameters<TDispatch>[0][]) => void;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useDispatch } from 'react-redux';
|
|
3
|
+
/** Builds the typed dispatch hooks of a store. */
|
|
3
4
|
export class DispatchHooks {
|
|
4
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* The typed dispatch hooks of a store: bind your `AppDispatch` once, and export what comes back.
|
|
7
|
+
* `useMultipleAppDispatch` returns a function that dispatches each action in order and skips falsy ones.
|
|
8
|
+
*/
|
|
5
9
|
static create() {
|
|
6
10
|
const useAppDispatch = useDispatch.withTypes();
|
|
7
11
|
const useMultipleAppDispatch = () => {
|
|
@@ -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() {
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
export type TResizeBox = Exclude<ResizeObserverBoxOptions, 'device-pixel-content-box'>;
|
|
3
3
|
export interface IResizeObservationOptions {
|
|
4
4
|
box: TResizeBox;
|
|
5
|
-
/** Also observe the direct children of each target, and again whenever they change. */
|
|
5
|
+
/** Also observe the direct children of each target, and again whenever they change. @default false */
|
|
6
6
|
children?: boolean;
|
|
7
|
-
/**
|
|
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,24 +9,37 @@ export class ResizeObservation {
|
|
|
9
9
|
let watched = new Set();
|
|
10
10
|
let current = [];
|
|
11
11
|
let frame;
|
|
12
|
-
const
|
|
12
|
+
const flush = () => {
|
|
13
|
+
const batch = [...latest.values()];
|
|
14
|
+
latest.clear();
|
|
15
|
+
if (batch.length > 0) {
|
|
16
|
+
onFrame(batch);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
let observer;
|
|
20
|
+
const onEntries = (entries) => {
|
|
13
21
|
for (const entry of entries) {
|
|
14
22
|
latest.set(entry.target, entry);
|
|
15
23
|
}
|
|
24
|
+
if (beforePaint) {
|
|
25
|
+
flush();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
16
28
|
if (frame !== undefined) {
|
|
17
29
|
return;
|
|
18
30
|
}
|
|
19
31
|
frame = requestAnimationFrame(() => {
|
|
20
32
|
frame = undefined;
|
|
21
|
-
|
|
22
|
-
latest.clear();
|
|
23
|
-
if (batch.length > 0) {
|
|
24
|
-
onFrame(batch);
|
|
25
|
-
}
|
|
33
|
+
flush();
|
|
26
34
|
});
|
|
27
|
-
}
|
|
35
|
+
};
|
|
28
36
|
const update = (targets) => {
|
|
29
37
|
const wanted = new Set(children ? targets.flatMap((target) => [target, ...target.children]) : targets);
|
|
38
|
+
// No target and no observer yet: there is nothing to watch, so none is made.
|
|
39
|
+
if (!observer && wanted.size === 0) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
observer ??= new ResizeObserver(onEntries);
|
|
30
43
|
for (const element of watched) {
|
|
31
44
|
if (!wanted.has(element)) {
|
|
32
45
|
observer.unobserve(element);
|
|
@@ -40,7 +53,7 @@ export class ResizeObservation {
|
|
|
40
53
|
}
|
|
41
54
|
watched = wanted;
|
|
42
55
|
};
|
|
43
|
-
|
|
56
|
+
let mutations;
|
|
44
57
|
return {
|
|
45
58
|
sync: (targets) => {
|
|
46
59
|
if (targets.length === current.length &&
|
|
@@ -50,13 +63,16 @@ export class ResizeObservation {
|
|
|
50
63
|
current = targets;
|
|
51
64
|
update(targets);
|
|
52
65
|
// A removed target stays registered on `mutations` until `stop`; its callback only updates the current targets.
|
|
53
|
-
|
|
54
|
-
mutations
|
|
66
|
+
if (children && targets.length > 0) {
|
|
67
|
+
mutations ??= new MutationObserver(() => update(current));
|
|
68
|
+
for (const target of targets) {
|
|
69
|
+
mutations.observe(target, { childList: true });
|
|
70
|
+
}
|
|
55
71
|
}
|
|
56
72
|
},
|
|
57
73
|
stop: () => {
|
|
58
74
|
mutations?.disconnect();
|
|
59
|
-
observer
|
|
75
|
+
observer?.disconnect();
|
|
60
76
|
if (frame !== undefined) {
|
|
61
77
|
cancelAnimationFrame(frame);
|
|
62
78
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** The application's `Container`. Throws when there is no `ApplicationContext` provider above. */
|
|
1
2
|
export declare const useApplicationContext: () => import("@venizia/ignis-inversion").Container;
|
|
3
|
+
/** The application's logger. Throws when there is no `ApplicationContext` provider above. */
|
|
2
4
|
export declare const useApplicationLogger: () => import("@venizia/ignis-helpers/core").ILogger;
|
|
3
5
|
//# sourceMappingURL=use-application-context.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { getError } from '@venizia/ignis-inversion';
|
|
3
3
|
import { ApplicationContext } from '../contexts/application.js';
|
|
4
|
+
/** The application's `Container`. Throws when there is no `ApplicationContext` provider above. */
|
|
4
5
|
export const useApplicationContext = () => {
|
|
5
6
|
const context = React.useContext(ApplicationContext);
|
|
6
7
|
if (!context?.container) {
|
|
@@ -10,6 +11,7 @@ export const useApplicationContext = () => {
|
|
|
10
11
|
}
|
|
11
12
|
return context.container;
|
|
12
13
|
};
|
|
14
|
+
/** The application's logger. Throws when there is no `ApplicationContext` provider above. */
|
|
13
15
|
export const useApplicationLogger = () => {
|
|
14
16
|
const context = React.useContext(ApplicationContext);
|
|
15
17
|
if (!context?.logger) {
|
|
@@ -2,19 +2,45 @@ import { type Container, type TClass } from '@venizia/ignis-inversion';
|
|
|
2
2
|
import { type TUseInjectableKeys } from './use-injectable.js';
|
|
3
3
|
/** By class: the return type is inferred from the target. */
|
|
4
4
|
export interface IUseArtifactByTarget<TInstance> {
|
|
5
|
+
/** The container to resolve from. @default The application's container (`ApplicationContext`) */
|
|
5
6
|
container?: Container;
|
|
7
|
+
/** The class to resolve; its recorded binding key is used. */
|
|
6
8
|
target: TClass<TInstance>;
|
|
7
9
|
key?: never;
|
|
8
10
|
}
|
|
9
11
|
/** By key: the caller supplies the type. */
|
|
10
12
|
export interface IUseArtifactByKey {
|
|
13
|
+
/** The container to resolve from. @default The application's container (`ApplicationContext`) */
|
|
11
14
|
container?: Container;
|
|
15
|
+
/** The binding key to resolve. */
|
|
12
16
|
key: TUseInjectableKeys;
|
|
13
17
|
target?: never;
|
|
14
18
|
}
|
|
19
|
+
/** Resolves the instance bound under the `service` namespace by `target` or `key`. A `target` bound under another namespace throws. */
|
|
15
20
|
export declare const useService: <TInstance = unknown>(opts: IUseArtifactByTarget<TInstance> | IUseArtifactByKey) => TInstance;
|
|
21
|
+
/** Resolves the instance bound under the `provider` namespace by `target` or `key`. A `target` bound under another namespace throws. */
|
|
16
22
|
export declare const useProvider: <TInstance = unknown>(opts: IUseArtifactByTarget<TInstance> | IUseArtifactByKey) => TInstance;
|
|
23
|
+
/** Resolves the instance bound under the `component` namespace by `target` or `key`. A `target` bound under another namespace throws. */
|
|
17
24
|
export declare const useComponent: <TInstance = unknown>(opts: IUseArtifactByTarget<TInstance> | IUseArtifactByKey) => TInstance;
|
|
25
|
+
/** Resolves the instance bound under the `configuration` namespace by `target` or `key`. A `target` bound under another namespace throws. */
|
|
18
26
|
export declare const useConfiguration: <TInstance = unknown>(opts: IUseArtifactByTarget<TInstance> | IUseArtifactByKey) => TInstance;
|
|
27
|
+
/**
|
|
28
|
+
* Resolves the instance bound under the `repository` namespace by `target` or `key`. A `target` bound under another namespace throws.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* import { repository, useRepository } from '@venizia/ardor';
|
|
33
|
+
*
|
|
34
|
+
* @repository()
|
|
35
|
+
* class ProductRepository {
|
|
36
|
+
* findStock(productId: string) { return productId ? 42 : 0; }
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* export function StockDisplay() {
|
|
40
|
+
* const productRepository = useRepository({ target: ProductRepository });
|
|
41
|
+
* return <span>Stock: {productRepository.findStock('p1')}</span>;
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
19
45
|
export declare const useRepository: <TInstance = unknown>(opts: IUseArtifactByTarget<TInstance> | IUseArtifactByKey) => TInstance;
|
|
20
46
|
//# sourceMappingURL=use-artifact.d.ts.map
|
|
@@ -22,26 +22,48 @@ const useArtifactInNamespace = (opts) => {
|
|
|
22
22
|
}
|
|
23
23
|
return useInjectable(options);
|
|
24
24
|
};
|
|
25
|
+
/** Resolves the instance bound under the `service` namespace by `target` or `key`. A `target` bound under another namespace throws. */
|
|
25
26
|
export const useService = (opts) => useArtifactInNamespace({
|
|
26
27
|
options: opts,
|
|
27
28
|
namespace: BindingNamespaces.SERVICE,
|
|
28
29
|
hook: 'useService',
|
|
29
30
|
});
|
|
31
|
+
/** Resolves the instance bound under the `provider` namespace by `target` or `key`. A `target` bound under another namespace throws. */
|
|
30
32
|
export const useProvider = (opts) => useArtifactInNamespace({
|
|
31
33
|
options: opts,
|
|
32
34
|
namespace: BindingNamespaces.PROVIDER,
|
|
33
35
|
hook: 'useProvider',
|
|
34
36
|
});
|
|
37
|
+
/** Resolves the instance bound under the `component` namespace by `target` or `key`. A `target` bound under another namespace throws. */
|
|
35
38
|
export const useComponent = (opts) => useArtifactInNamespace({
|
|
36
39
|
options: opts,
|
|
37
40
|
namespace: BindingNamespaces.COMPONENT,
|
|
38
41
|
hook: 'useComponent',
|
|
39
42
|
});
|
|
43
|
+
/** Resolves the instance bound under the `configuration` namespace by `target` or `key`. A `target` bound under another namespace throws. */
|
|
40
44
|
export const useConfiguration = (opts) => useArtifactInNamespace({
|
|
41
45
|
options: opts,
|
|
42
46
|
namespace: BindingNamespaces.CONFIGURATION,
|
|
43
47
|
hook: 'useConfiguration',
|
|
44
48
|
});
|
|
49
|
+
/**
|
|
50
|
+
* Resolves the instance bound under the `repository` namespace by `target` or `key`. A `target` bound under another namespace throws.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* import { repository, useRepository } from '@venizia/ardor';
|
|
55
|
+
*
|
|
56
|
+
* @repository()
|
|
57
|
+
* class ProductRepository {
|
|
58
|
+
* findStock(productId: string) { return productId ? 42 : 0; }
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* export function StockDisplay() {
|
|
62
|
+
* const productRepository = useRepository({ target: ProductRepository });
|
|
63
|
+
* return <span>Stock: {productRepository.findStock('p1')}</span>;
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
45
67
|
export const useRepository = (opts) => useArtifactInNamespace({
|
|
46
68
|
options: opts,
|
|
47
69
|
namespace: BindingNamespaces.REPOSITORY,
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
/** What `useAutosave` saves, through which callback, and when. */
|
|
1
2
|
export interface IUseAutosaveOptions<TData, TReturn> {
|
|
3
|
+
/** The value to save; a change starts the `interval` wait. */
|
|
2
4
|
data: TData;
|
|
5
|
+
/** Called with the latest `data` once it has been still for `interval`. A rejection is logged, not thrown. */
|
|
3
6
|
onSave: (data: TData) => Promise<TReturn> | TReturn | void;
|
|
4
7
|
/** @default 2000 */
|
|
5
8
|
interval?: number;
|
|
9
|
+
/** Save the latest `data` on unmount. */
|
|
6
10
|
enableSaveOnUnmount?: boolean;
|
|
7
11
|
/** @default true */
|
|
8
12
|
enabled?: boolean;
|
|
9
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Calls `onSave` with `data` after it has stopped changing for `interval` milliseconds. The first
|
|
16
|
+
* render does not save. The latest `onSave` is always the one called.
|
|
17
|
+
*/
|
|
10
18
|
export declare const useAutosave: <TData, TReturn>(opts: IUseAutosaveOptions<TData, TReturn>) => void;
|
|
11
19
|
//# sourceMappingURL=use-autosave.d.ts.map
|
|
@@ -2,6 +2,10 @@ import React from 'react';
|
|
|
2
2
|
import { LoggerFactory } from '@venizia/ignis-helpers/core';
|
|
3
3
|
import { useDebounce } from './use-debounce.js';
|
|
4
4
|
const logger = LoggerFactory.getLogger(['useAutosave']);
|
|
5
|
+
/**
|
|
6
|
+
* Calls `onSave` with `data` after it has stopped changing for `interval` milliseconds. The first
|
|
7
|
+
* render does not save. The latest `onSave` is always the one called.
|
|
8
|
+
*/
|
|
5
9
|
export const useAutosave = (opts) => {
|
|
6
10
|
const { data, onSave, interval = 2000, enableSaveOnUnmount, enabled = true } = opts;
|
|
7
11
|
const valueOnCleanup = React.useRef(data);
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
/** When `useBeforeUnload` asks, and the text it sets. */
|
|
1
2
|
export interface IUseBeforeUnloadOptions {
|
|
2
3
|
/** @default true */
|
|
3
4
|
enabled?: boolean;
|
|
5
|
+
/** Set as the `returnValue` of the event; browsers may show their own text instead. */
|
|
4
6
|
message?: string;
|
|
5
7
|
}
|
|
8
|
+
/** While `enabled`, asks the browser to confirm before the page is left or reloaded. */
|
|
6
9
|
export declare const useBeforeUnload: (opts: IUseBeforeUnloadOptions) => void;
|
|
7
10
|
//# sourceMappingURL=use-before-unload.d.ts.map
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
/** The state of one confirmation prompt, and the handlers that settle it. */
|
|
1
2
|
export interface IUseConfirmReturn {
|
|
3
|
+
/** The pending prompt text; `undefined` when none is open. */
|
|
2
4
|
message?: string;
|
|
5
|
+
/** Opens a prompt. Resolves `true` on `handleConfirm`, `false` on `handleAbort` or when another `confirm` replaces it. */
|
|
3
6
|
confirm: (opts: {
|
|
4
7
|
message: string;
|
|
5
8
|
}) => Promise<boolean>;
|
|
9
|
+
/** Closes the prompt without settling it. */
|
|
6
10
|
handleClose: () => void;
|
|
11
|
+
/** Resolves the pending `confirm` with `true` and closes. */
|
|
7
12
|
handleConfirm: () => void;
|
|
13
|
+
/** Resolves the pending `confirm` with `false` and closes. */
|
|
8
14
|
handleAbort: () => void;
|
|
9
15
|
}
|
|
16
|
+
/** State for a confirmation prompt: `confirm` returns a promise that the prompt's buttons settle. */
|
|
10
17
|
export declare const useConfirm: () => IUseConfirmReturn;
|
|
11
18
|
//# sourceMappingURL=use-confirm.d.ts.map
|
|
@@ -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: {
|
|
@@ -10,5 +12,18 @@ export interface IUseCopyToClipboardReturn {
|
|
|
10
12
|
/** The result of the last copy, until `resetMs` has passed. */
|
|
11
13
|
isCopied: boolean;
|
|
12
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Copies text with `BrowserClipboard`; `isCopied` is the result of the last copy.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* import { useCopyToClipboard } from '@venizia/ardor';
|
|
21
|
+
*
|
|
22
|
+
* export function CopyButton() {
|
|
23
|
+
* const { copy, isCopied } = useCopyToClipboard({ resetMs: 2000 });
|
|
24
|
+
* return <button onClick={() => copy({ value: 'hello' })}>{isCopied ? 'Copied' : 'Copy'}</button>;
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
13
28
|
export declare const useCopyToClipboard: (opts?: IUseCopyToClipboardOptions) => IUseCopyToClipboardReturn;
|
|
14
29
|
//# sourceMappingURL=use-copy-to-clipboard.d.ts.map
|
|
@@ -10,6 +10,19 @@ const copyToClipboard = async (opts) => {
|
|
|
10
10
|
}
|
|
11
11
|
return isCopied;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Copies text with `BrowserClipboard`; `isCopied` is the result of the last copy.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* import { useCopyToClipboard } from '@venizia/ardor';
|
|
19
|
+
*
|
|
20
|
+
* export function CopyButton() {
|
|
21
|
+
* const { copy, isCopied } = useCopyToClipboard({ resetMs: 2000 });
|
|
22
|
+
* return <button onClick={() => copy({ value: 'hello' })}>{isCopied ? 'Copied' : 'Copy'}</button>;
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
13
26
|
export const useCopyToClipboard = (opts = {}) => {
|
|
14
27
|
const { resetMs = 0 } = opts;
|
|
15
28
|
const [isCopied, setCopied] = React.useState(false);
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
/** The value `useDebounce` follows, how long it waits, and when it runs. */
|
|
1
2
|
export interface IUseDebounceOptions<TValue> {
|
|
3
|
+
/** The value to follow; the output changes once it has been still for `delay`. */
|
|
2
4
|
value: TValue;
|
|
5
|
+
/** Milliseconds to wait after the last change. @default 500 */
|
|
3
6
|
delay?: number;
|
|
4
7
|
/** @default true */
|
|
5
8
|
enabled?: boolean;
|
|
6
9
|
}
|
|
10
|
+
/** The settled value of `useDebounce`. */
|
|
7
11
|
export interface IUseDebounceReturn<TValue> {
|
|
12
|
+
/** The last `value` that stayed unchanged for `delay`; the first `value` until then. */
|
|
8
13
|
debouncedValue: TValue;
|
|
9
14
|
}
|
|
15
|
+
/** Follows `value` after it has stopped changing for `delay`. Outside a browser, or when not `enabled`, it never updates. */
|
|
10
16
|
export declare const useDebounce: <TValue>(opts: IUseDebounceOptions<TValue>) => IUseDebounceReturn<TValue>;
|
|
11
17
|
//# sourceMappingURL=use-debounce.d.ts.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { App, BrowserUtility } from '@venizia/ardor-kernel';
|
|
3
|
+
/** Follows `value` after it has stopped changing for `delay`. Outside a browser, or when not `enabled`, it never updates. */
|
|
3
4
|
export const useDebounce = (opts) => {
|
|
4
5
|
const { value, delay, enabled = true } = opts;
|
|
5
6
|
const [debouncedValue, setDebouncedValue] = React.useState(value);
|
|
@@ -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,13 +9,16 @@ 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 {
|
|
21
|
+
/** Give it to the element to measure. */
|
|
18
22
|
ref: React.RefCallback<Element>;
|
|
19
23
|
}
|
|
20
24
|
/**
|
|
@@ -24,6 +28,16 @@ export interface IUseElementSizeRefReturn extends IUseElementSizeReturn {
|
|
|
24
28
|
* is read when the effect runs, so it follows a new `ref`, `box` or `round`, not a `ref.current`
|
|
25
29
|
* that is replaced later; pass a stable `ref`. A new element, `box` or `round` starts again from
|
|
26
30
|
* 0 by 0 with `measured` false.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* import { useElementSize } from '@venizia/ardor';
|
|
35
|
+
*
|
|
36
|
+
* export function Panel() {
|
|
37
|
+
* const { ref, width, height, measured } = useElementSize();
|
|
38
|
+
* return <div ref={ref}>{measured ? `${width} x ${height}` : '...'}</div>;
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
27
41
|
*/
|
|
28
42
|
export declare function useElementSize(opts: IUseElementSizeOptions & {
|
|
29
43
|
ref: React.RefObject<Element | null>;
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import { type Container, type TClass } from '@venizia/ignis-inversion';
|
|
2
2
|
import type { CoreBindings, ValueOf } from '@venizia/ardor-kernel';
|
|
3
|
+
/** Declaration-merge string keys into this interface to make them valid `key` values of the injectable hooks. */
|
|
3
4
|
export interface IUseInjectableKeysOverrides {
|
|
4
5
|
}
|
|
6
|
+
/** The binding keys the kernel defines (the string values of `CoreBindings`). */
|
|
5
7
|
export type TUseInjectableKeysDefault = Extract<ValueOf<typeof CoreBindings>, string>;
|
|
8
|
+
/** A key the injectable hooks accept: a kernel binding key or one added to `IUseInjectableKeysOverrides`. */
|
|
6
9
|
export type TUseInjectableKeys = TUseInjectableKeysDefault | keyof IUseInjectableKeysOverrides;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve by `key` or by `target` class, never both.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { CoreBindings, type TUseInjectableOptions } from '@venizia/ardor';
|
|
16
|
+
*
|
|
17
|
+
* const byKey: TUseInjectableOptions = { key: CoreBindings.DEFAULT_REST_DATA_PROVIDER };
|
|
18
|
+
* class Greeter {}
|
|
19
|
+
* const byTarget: TUseInjectableOptions = { target: Greeter };
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
7
22
|
export type TUseInjectableOptions = {
|
|
23
|
+
/** The container to resolve from. @default The application's container (`ApplicationContext`) */
|
|
8
24
|
container?: Container;
|
|
9
25
|
key: TUseInjectableKeys;
|
|
10
26
|
target?: never;
|
|
11
27
|
} | {
|
|
28
|
+
/** The container to resolve from. @default The application's container (`ApplicationContext`) */
|
|
12
29
|
container?: Container;
|
|
13
30
|
key?: never;
|
|
14
31
|
target: TClass<unknown>;
|
|
@@ -16,9 +33,23 @@ export type TUseInjectableOptions = {
|
|
|
16
33
|
/**
|
|
17
34
|
* The container an injectable hook resolves from: the one passed in, else the application's. Shared,
|
|
18
35
|
* so a hook never checks one container and resolves from another.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* import { CoreBindings, useApplicationContainer } from '@venizia/ardor';
|
|
40
|
+
*
|
|
41
|
+
* export function BindingCheck() {
|
|
42
|
+
* const container = useApplicationContainer();
|
|
43
|
+
* return <span>{container.isBound({ key: CoreBindings.DEFAULT_REST_DATA_PROVIDER }) ? 'bound' : 'unbound'}</span>;
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
19
46
|
*/
|
|
20
47
|
export declare const useApplicationContainer: (opts?: {
|
|
21
48
|
container?: Container;
|
|
22
49
|
}) => Container;
|
|
50
|
+
/**
|
|
51
|
+
* Resolves an instance from the container by `key` or by `target` class. Throws when the key is not
|
|
52
|
+
* bound, when the class has no recorded binding key, or when neither `key` nor `target` is given.
|
|
53
|
+
*/
|
|
23
54
|
export declare const useInjectable: <TInstance>(opts: TUseInjectableOptions) => TInstance;
|
|
24
55
|
//# sourceMappingURL=use-injectable.d.ts.map
|
|
@@ -4,6 +4,16 @@ import { ApplicationContext } from '../contexts/application.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* The container an injectable hook resolves from: the one passed in, else the application's. Shared,
|
|
6
6
|
* so a hook never checks one container and resolves from another.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* import { CoreBindings, useApplicationContainer } from '@venizia/ardor';
|
|
11
|
+
*
|
|
12
|
+
* export function BindingCheck() {
|
|
13
|
+
* const container = useApplicationContainer();
|
|
14
|
+
* return <span>{container.isBound({ key: CoreBindings.DEFAULT_REST_DATA_PROVIDER }) ? 'bound' : 'unbound'}</span>;
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
7
17
|
*/
|
|
8
18
|
export const useApplicationContainer = (opts) => {
|
|
9
19
|
const applicationContext = React.useContext(ApplicationContext);
|
|
@@ -15,6 +25,10 @@ export const useApplicationContainer = (opts) => {
|
|
|
15
25
|
}
|
|
16
26
|
return container;
|
|
17
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* Resolves an instance from the container by `key` or by `target` class. Throws when the key is not
|
|
30
|
+
* bound, when the class has no recorded binding key, or when neither `key` nor `target` is given.
|
|
31
|
+
*/
|
|
18
32
|
export const useInjectable = (opts) => {
|
|
19
33
|
const container = useApplicationContainer({ container: opts?.container });
|
|
20
34
|
const { key, target } = opts;
|
|
@@ -1,16 +1,45 @@
|
|
|
1
1
|
import { type TElementTarget } from './element-target.js';
|
|
2
|
+
/** The element or elements `useIntersection` watches, against which root, and when it delivers an entry. */
|
|
2
3
|
export interface IUseIntersectionOptions {
|
|
3
|
-
/** Read after every render: an element, a ref to one, or `null
|
|
4
|
-
target: TElementTarget;
|
|
4
|
+
/** Read after every render: an element, a ref to one, or `null`; or a list of them to watch several. */
|
|
5
|
+
target: TElementTarget | TElementTarget[];
|
|
6
|
+
/**
|
|
7
|
+
* The scrolling container the target is measured against. A ref is read after every render, and nothing is
|
|
8
|
+
* observed while its `current` is `null`. A literal `null`, or leaving it out, is the viewport.
|
|
9
|
+
*/
|
|
10
|
+
root?: TElementTarget;
|
|
5
11
|
/** @default '0px' */
|
|
6
12
|
rootMargin?: string;
|
|
7
|
-
/** The ratio of the element in view at which an entry is delivered. @default 0 */
|
|
8
|
-
threshold?: number;
|
|
13
|
+
/** The ratio of the element in view at which an entry is delivered; a number or a list of them. @default 0 */
|
|
14
|
+
threshold?: number | number[];
|
|
9
15
|
}
|
|
16
|
+
type TEntry = IntersectionObserverEntry | undefined;
|
|
10
17
|
/**
|
|
11
|
-
* The latest `IntersectionObserverEntry` of the target, or `undefined` before the first one and
|
|
12
|
-
*
|
|
13
|
-
*
|
|
18
|
+
* The latest `IntersectionObserverEntry` of the target, or `undefined` before the first one and where there is
|
|
19
|
+
* no `IntersectionObserver`. The state changes only when `isIntersecting` or the ratio changed.
|
|
20
|
+
*
|
|
21
|
+
* With a list as `target` it returns an array in target order: the latest entry of each target, `undefined` for
|
|
22
|
+
* one with no element or not reported yet. A new element, `root`, `rootMargin` or `threshold` starts again from
|
|
23
|
+
* `undefined` for every target; the browser reports each target again at once.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* import React from 'react';
|
|
28
|
+
* import { useIntersection } from '@venizia/ardor';
|
|
29
|
+
*
|
|
30
|
+
* export function Lazy() {
|
|
31
|
+
* const ref = React.useRef<HTMLDivElement>(null);
|
|
32
|
+
* const entry = useIntersection({ target: ref, threshold: 0.5 });
|
|
33
|
+
* return <div ref={ref}>{entry?.isIntersecting ? 'visible' : 'hidden'}</div>;
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
14
36
|
*/
|
|
15
|
-
export declare
|
|
37
|
+
export declare function useIntersection(opts: IUseIntersectionOptions & {
|
|
38
|
+
target: TElementTarget[];
|
|
39
|
+
}): TEntry[];
|
|
40
|
+
export declare function useIntersection(opts: IUseIntersectionOptions & {
|
|
41
|
+
target: TElementTarget;
|
|
42
|
+
}): TEntry;
|
|
43
|
+
export declare function useIntersection(opts: IUseIntersectionOptions): TEntry | TEntry[];
|
|
44
|
+
export {};
|
|
16
45
|
//# sourceMappingURL=use-intersection.d.ts.map
|
|
@@ -1,38 +1,74 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ElementTargets } from './element-target.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
const EMPTY_SCOPE = { elements: [], root: null, isRootPending: false };
|
|
4
|
+
const NO_ENTRIES = new Map();
|
|
5
|
+
const resolveScope = (opts) => {
|
|
6
|
+
const { target, root } = opts;
|
|
7
|
+
const targets = Array.isArray(target) ? target : [target];
|
|
8
|
+
const rootElement = root === undefined ? null : ElementTargets.resolve({ target: root });
|
|
9
|
+
return {
|
|
10
|
+
elements: targets.map((item) => ElementTargets.resolve({ target: item })),
|
|
11
|
+
root: rootElement,
|
|
12
|
+
isRootPending: rootElement === null && root !== undefined && root !== null,
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
const sameScope = (a, b) => {
|
|
16
|
+
return (a.root === b.root &&
|
|
17
|
+
a.isRootPending === b.isRootPending &&
|
|
18
|
+
a.elements.length === b.elements.length &&
|
|
19
|
+
a.elements.every((element, index) => element === b.elements[index]));
|
|
20
|
+
};
|
|
21
|
+
export function useIntersection(opts) {
|
|
22
|
+
const { target, root, rootMargin, threshold } = opts;
|
|
23
|
+
const isList = Array.isArray(target);
|
|
24
|
+
const count = isList ? target.length : 1;
|
|
25
|
+
// A number list is compared by value, so a list made anew in each render observes nothing again.
|
|
26
|
+
const thresholdKey = threshold === undefined ? undefined : JSON.stringify(threshold);
|
|
27
|
+
const [scope, setScope] = React.useState(EMPTY_SCOPE);
|
|
28
|
+
const [entries, setEntries] = React.useState(NO_ENTRIES);
|
|
29
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps -- the target and root are read after every render, and a state set to an equal scope renders nothing
|
|
13
30
|
React.useEffect(() => {
|
|
14
|
-
|
|
31
|
+
const next = resolveScope({ target, root });
|
|
32
|
+
setScope((current) => (sameScope(current, next) ? current : next));
|
|
15
33
|
});
|
|
16
34
|
React.useEffect(() => {
|
|
17
|
-
|
|
18
|
-
|
|
35
|
+
setEntries((current) => (current.size === 0 ? current : NO_ENTRIES));
|
|
36
|
+
const watched = new Set(scope.elements.filter((element) => element !== null));
|
|
37
|
+
if (watched.size === 0 || scope.isRootPending || typeof IntersectionObserver === 'undefined') {
|
|
19
38
|
return () => { };
|
|
20
39
|
}
|
|
21
|
-
const observer = new IntersectionObserver((
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
const observer = new IntersectionObserver((delivered) => {
|
|
41
|
+
setEntries((current) => {
|
|
42
|
+
let next = current;
|
|
43
|
+
for (const entry of delivered) {
|
|
44
|
+
const known = next.get(entry.target);
|
|
45
|
+
if (known?.isIntersecting === entry.isIntersecting &&
|
|
46
|
+
known.intersectionRatio === entry.intersectionRatio) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (next === current) {
|
|
50
|
+
next = new Map(current);
|
|
51
|
+
}
|
|
52
|
+
next.set(entry.target, entry);
|
|
53
|
+
}
|
|
54
|
+
return next;
|
|
31
55
|
});
|
|
32
|
-
}, {
|
|
33
|
-
|
|
56
|
+
}, {
|
|
57
|
+
root: scope.root ?? undefined,
|
|
58
|
+
rootMargin,
|
|
59
|
+
threshold: thresholdKey === undefined ? undefined : JSON.parse(thresholdKey),
|
|
60
|
+
});
|
|
61
|
+
for (const element of watched) {
|
|
62
|
+
observer.observe(element);
|
|
63
|
+
}
|
|
34
64
|
return () => observer.disconnect();
|
|
35
|
-
}, [
|
|
36
|
-
return
|
|
37
|
-
}
|
|
65
|
+
}, [scope, rootMargin, thresholdKey]);
|
|
66
|
+
return React.useMemo(() => {
|
|
67
|
+
const read = Array.from({ length: count }, (_, index) => {
|
|
68
|
+
const element = scope.elements[index];
|
|
69
|
+
return element ? entries.get(element) : undefined;
|
|
70
|
+
});
|
|
71
|
+
return isList ? read : read[0];
|
|
72
|
+
}, [count, isList, scope, entries]);
|
|
73
|
+
}
|
|
38
74
|
//# sourceMappingURL=use-intersection.js.map
|
|
@@ -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,32 @@ 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
|
|
15
|
-
* `onResize` is used without observing again, and the
|
|
16
|
-
* element held in state, a ref whose element changed and
|
|
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.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* import React from 'react';
|
|
31
|
+
* import { useResizeEffect } from '@venizia/ardor';
|
|
32
|
+
*
|
|
33
|
+
* export function Panel() {
|
|
34
|
+
* const ref = React.useRef<HTMLDivElement>(null);
|
|
35
|
+
* useResizeEffect({ targets: [ref], onResize: (entries) => console.log(entries.length) });
|
|
36
|
+
* return <div ref={ref} />;
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
17
39
|
*/
|
|
18
40
|
export declare const useResizeEffect: (opts: IUseResizeEffectOptions) => void;
|
|
19
41
|
//# sourceMappingURL=use-resize-effect.d.ts.map
|
|
@@ -2,12 +2,25 @@ 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
|
|
6
|
-
* `onResize` is used without observing again, and the
|
|
7
|
-
* element held in state, a ref whose element changed and
|
|
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.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import React from 'react';
|
|
13
|
+
* import { useResizeEffect } from '@venizia/ardor';
|
|
14
|
+
*
|
|
15
|
+
* export function Panel() {
|
|
16
|
+
* const ref = React.useRef<HTMLDivElement>(null);
|
|
17
|
+
* useResizeEffect({ targets: [ref], onResize: (entries) => console.log(entries.length) });
|
|
18
|
+
* return <div ref={ref} />;
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
8
21
|
*/
|
|
9
22
|
export const useResizeEffect = (opts) => {
|
|
10
|
-
const { targets, onResize, box = 'content-box', children = false } = opts;
|
|
23
|
+
const { targets, onResize, box = 'content-box', children = false, beforePaint = false } = opts;
|
|
11
24
|
const handleResize = React.useRef(onResize);
|
|
12
25
|
const observation = React.useRef(undefined);
|
|
13
26
|
React.useEffect(() => {
|
|
@@ -18,11 +31,12 @@ export const useResizeEffect = (opts) => {
|
|
|
18
31
|
const created = ResizeObservation.observe({
|
|
19
32
|
box,
|
|
20
33
|
children,
|
|
34
|
+
beforePaint,
|
|
21
35
|
onFrame: (entries) => handleResize.current(entries),
|
|
22
36
|
});
|
|
23
37
|
observation.current = created;
|
|
24
38
|
return created.stop;
|
|
25
|
-
}, [box, children]);
|
|
39
|
+
}, [box, children, beforePaint]);
|
|
26
40
|
React.useEffect(() => {
|
|
27
41
|
observation.current?.sync(targets.flatMap((target) => ElementTargets.resolve({ target }) ?? []));
|
|
28
42
|
});
|
|
@@ -1,19 +1,27 @@
|
|
|
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
|
-
key:
|
|
4
|
+
/** The storage key. `null` turns the storage off: nothing is read, written or watched, `value` starts from `defaultValue` and `setValue` keeps it in memory only, as `useState` does. */
|
|
5
|
+
key: string | null;
|
|
6
|
+
/** The value while the key holds nothing. */
|
|
4
7
|
defaultValue: TValue;
|
|
5
8
|
/** @default 'local' */
|
|
6
9
|
storage?: TStorageArea;
|
|
7
10
|
/** @default JSON */
|
|
8
11
|
codec?: IStorageCodec<TValue>;
|
|
9
|
-
/** Write storage once, this many milliseconds after the last `setValue`; `value` changes at once. @default 0 */
|
|
12
|
+
/** Write storage once, this many milliseconds after the last `setValue`; `value` changes at once. Above 0, also pass a stable `codec`. @default 0 */
|
|
10
13
|
writeDelayMs?: number;
|
|
11
14
|
}
|
|
15
|
+
/** The value `useStoredState` holds, and how to change or remove it. */
|
|
12
16
|
export interface IUseStoredStateReturn<TValue> {
|
|
17
|
+
/** The stored value, or `defaultValue` when the key holds nothing. */
|
|
13
18
|
value: TValue;
|
|
19
|
+
/** Stores a value, or the result of an updater called with the current one. */
|
|
14
20
|
setValue: (next: TValue | ((current: TValue) => TValue)) => void;
|
|
21
|
+
/** Removes the key, so `value` is `defaultValue` again. */
|
|
15
22
|
remove: () => void;
|
|
16
23
|
}
|
|
24
|
+
/** The call signatures of `useStoredState`. */
|
|
17
25
|
export interface IUseStoredState {
|
|
18
26
|
/** With a codec, the value type is the codec's: a literal `defaultValue` is checked against it, not widened past it. */
|
|
19
27
|
<TValue>(opts: Omit<IUseStoredStateOptions<TValue>, 'defaultValue' | 'codec'> & {
|
|
@@ -27,7 +35,17 @@ export interface IUseStoredState {
|
|
|
27
35
|
* that writes through `BrowserStorage`, and with other tabs. Pass a stable `codec` and `defaultValue`.
|
|
28
36
|
* A write or a removal that the storage refuses is kept in this hook until the storage next changes.
|
|
29
37
|
* With `writeDelayMs` the value waits in the same place for the delay: a change of the storage from
|
|
30
|
-
* outside drops it, and an unmount
|
|
38
|
+
* outside drops it, and an unmount, a new key or storage, a `pagehide` event or a hidden page writes it at once.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* import { useStoredState } from '@venizia/ardor';
|
|
43
|
+
*
|
|
44
|
+
* export function ThemeToggle() {
|
|
45
|
+
* const { value, setValue } = useStoredState({ key: '@app/prefs/theme', defaultValue: 'light' });
|
|
46
|
+
* return <button onClick={() => setValue(value === 'light' ? 'dark' : 'light')}>{value}</button>;
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
31
49
|
*/
|
|
32
50
|
export declare const useStoredState: IUseStoredState;
|
|
33
51
|
//# sourceMappingURL=use-stored-state.d.ts.map
|
|
@@ -7,7 +7,17 @@ const isUpdater = (next) => typeof next === 'function';
|
|
|
7
7
|
* that writes through `BrowserStorage`, and with other tabs. Pass a stable `codec` and `defaultValue`.
|
|
8
8
|
* A write or a removal that the storage refuses is kept in this hook until the storage next changes.
|
|
9
9
|
* With `writeDelayMs` the value waits in the same place for the delay: a change of the storage from
|
|
10
|
-
* outside drops it, and an unmount
|
|
10
|
+
* outside drops it, and an unmount, a new key or storage, a `pagehide` event or a hidden page writes it at once.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import { useStoredState } from '@venizia/ardor';
|
|
15
|
+
*
|
|
16
|
+
* export function ThemeToggle() {
|
|
17
|
+
* const { value, setValue } = useStoredState({ key: '@app/prefs/theme', defaultValue: 'light' });
|
|
18
|
+
* return <button onClick={() => setValue(value === 'light' ? 'dark' : 'light')}>{value}</button>;
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
11
21
|
*/
|
|
12
22
|
export const useStoredState = (opts) => {
|
|
13
23
|
const { key, defaultValue, storage, codec, writeDelayMs = 0 } = opts;
|
|
@@ -25,15 +35,39 @@ export const useStoredState = (opts) => {
|
|
|
25
35
|
const write = React.useCallback((entry) => {
|
|
26
36
|
keep(BrowserStorage.set(entry) ? undefined : entry);
|
|
27
37
|
}, [keep]);
|
|
28
|
-
|
|
38
|
+
const isDelayed = writeDelayMs > 0;
|
|
39
|
+
const flush = React.useCallback(() => {
|
|
40
|
+
const pending = keptRef.current;
|
|
41
|
+
if (timerRef.current !== undefined && pending?.key != null) {
|
|
42
|
+
write({ ...pending, key: pending.key });
|
|
43
|
+
}
|
|
44
|
+
}, [write]);
|
|
45
|
+
// A new key or storage, an unmount, and a page that closes or hides write the pending value to the
|
|
46
|
+
// key it was set for: closing a tab does not unmount.
|
|
29
47
|
React.useEffect(() => {
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
48
|
+
const onHide = () => {
|
|
49
|
+
if (document.visibilityState === 'hidden') {
|
|
50
|
+
flush();
|
|
33
51
|
}
|
|
34
52
|
};
|
|
35
|
-
|
|
53
|
+
// The value kept in memory under a `null` key does not follow the hook to a real key, or back to a later `null`.
|
|
54
|
+
if (key !== null && keptRef.current?.key === null) {
|
|
55
|
+
keep(undefined);
|
|
56
|
+
}
|
|
57
|
+
if (isDelayed) {
|
|
58
|
+
window.addEventListener('pagehide', flush);
|
|
59
|
+
document.addEventListener('visibilitychange', onHide);
|
|
60
|
+
}
|
|
61
|
+
return () => {
|
|
62
|
+
window.removeEventListener('pagehide', flush);
|
|
63
|
+
document.removeEventListener('visibilitychange', onHide);
|
|
64
|
+
flush();
|
|
65
|
+
};
|
|
66
|
+
}, [key, storage, isDelayed, flush, keep]);
|
|
36
67
|
const subscribe = React.useCallback((listener) => {
|
|
68
|
+
if (key === null) {
|
|
69
|
+
return () => { };
|
|
70
|
+
}
|
|
37
71
|
return BrowserStorage.subscribe({
|
|
38
72
|
key,
|
|
39
73
|
storage,
|
|
@@ -44,6 +78,9 @@ export const useStoredState = (opts) => {
|
|
|
44
78
|
});
|
|
45
79
|
}, [key, storage, keep]);
|
|
46
80
|
const getSnapshot = React.useCallback(() => {
|
|
81
|
+
if (key === null) {
|
|
82
|
+
return defaultValue;
|
|
83
|
+
}
|
|
47
84
|
const text = BrowserStorage.get({ key, storage, fallback: null, codec: RAW_TEXT });
|
|
48
85
|
if (text === null) {
|
|
49
86
|
return defaultValue;
|
|
@@ -61,7 +98,12 @@ export const useStoredState = (opts) => {
|
|
|
61
98
|
const setValue = React.useCallback((next) => {
|
|
62
99
|
const hit = keptRef.current;
|
|
63
100
|
const current = hit?.key === key && hit.storage === storage ? hit.value : getSnapshot();
|
|
64
|
-
const
|
|
101
|
+
const nextValue = isUpdater(next) ? next(current) : next;
|
|
102
|
+
if (key === null) {
|
|
103
|
+
keep({ key, storage, codec, value: nextValue });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const entry = { key, storage, codec, value: nextValue };
|
|
65
107
|
if (!(writeDelayMs > 0)) {
|
|
66
108
|
write(entry);
|
|
67
109
|
return;
|
|
@@ -70,6 +112,10 @@ export const useStoredState = (opts) => {
|
|
|
70
112
|
timerRef.current = setTimeout(() => write(entry), writeDelayMs);
|
|
71
113
|
}, [key, storage, codec, writeDelayMs, getSnapshot, keep, write]);
|
|
72
114
|
const remove = React.useCallback(() => {
|
|
115
|
+
if (key === null) {
|
|
116
|
+
keep(undefined);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
73
119
|
const isRemoved = BrowserStorage.remove({ key, storage });
|
|
74
120
|
keep(isRemoved ? undefined : { key, storage, value: defaultValue });
|
|
75
121
|
}, [key, storage, defaultValue, keep]);
|
|
@@ -6,6 +6,7 @@ const readWindowDimensions = () => {
|
|
|
6
6
|
? { width: window.innerWidth, height: window.innerHeight }
|
|
7
7
|
: { width: 0, height: 0 };
|
|
8
8
|
};
|
|
9
|
+
/** The window's `innerWidth` and `innerHeight`, updated on resize; 0 x 0 where there is no window. */
|
|
9
10
|
export const useWindowDimensions = () => {
|
|
10
11
|
const [windowDimensions, setWindowDimensions] = React.useState(readWindowDimensions);
|
|
11
12
|
React.useEffect(() => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@venizia/ardor-react",
|
|
3
|
-
"version": "0.4.8-
|
|
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,
|
|
3
|
+
"version": "0.4.8-11",
|
|
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",
|
|
7
7
|
"ardor-react",
|
|
@@ -55,11 +55,10 @@
|
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
|
-
"force-update": "sh ../../scripts/build/force-update.sh",
|
|
59
58
|
"prepublishOnly": "make rebuild"
|
|
60
59
|
},
|
|
61
60
|
"dependencies": {
|
|
62
|
-
"@venizia/ardor-kernel": "0.4.8-
|
|
61
|
+
"@venizia/ardor-kernel": "0.4.8-11"
|
|
63
62
|
},
|
|
64
63
|
"devDependencies": {
|
|
65
64
|
"@happy-dom/global-registrator": "^21.0.4",
|
|
@@ -69,11 +68,11 @@
|
|
|
69
68
|
"@types/bun": "^1.4.3",
|
|
70
69
|
"@types/react": "^19",
|
|
71
70
|
"@types/react-dom": "^19",
|
|
72
|
-
"@venizia/dev-configs": "^0.4.8-
|
|
73
|
-
"@venizia/ignis-filter": "^0.4.7-
|
|
74
|
-
"@venizia/ignis-helpers": "^0.4.8-
|
|
75
|
-
"@venizia/ignis-inversion": "^0.4.7-
|
|
76
|
-
"@venizia/ignis-kernel": "^0.4.8-
|
|
71
|
+
"@venizia/dev-configs": "^0.4.8-7",
|
|
72
|
+
"@venizia/ignis-filter": "^0.4.7-4",
|
|
73
|
+
"@venizia/ignis-helpers": "^0.4.8-9",
|
|
74
|
+
"@venizia/ignis-inversion": "^0.4.7-3",
|
|
75
|
+
"@venizia/ignis-kernel": "^0.4.8-11",
|
|
77
76
|
"eslint": "^10.13.0",
|
|
78
77
|
"oxlint": "1.89.0",
|
|
79
78
|
"oxlint-tsgolint": "7.0.2004",
|
|
@@ -87,10 +86,10 @@
|
|
|
87
86
|
},
|
|
88
87
|
"peerDependencies": {
|
|
89
88
|
"@reduxjs/toolkit": ">=2.13.0",
|
|
90
|
-
"@venizia/ignis-filter": "^0.4.7-
|
|
91
|
-
"@venizia/ignis-helpers": "^0.4.8-
|
|
92
|
-
"@venizia/ignis-inversion": "^0.4.7-
|
|
93
|
-
"@venizia/ignis-kernel": "^0.4.8-
|
|
89
|
+
"@venizia/ignis-filter": "^0.4.7-4",
|
|
90
|
+
"@venizia/ignis-helpers": "^0.4.8-9",
|
|
91
|
+
"@venizia/ignis-inversion": "^0.4.7-3",
|
|
92
|
+
"@venizia/ignis-kernel": "^0.4.8-11",
|
|
94
93
|
"react": ">=19.3.1",
|
|
95
94
|
"react-redux": ">=9.3.0",
|
|
96
95
|
"reflect-metadata": ">=0.2.2"
|
|
@@ -111,7 +110,7 @@
|
|
|
111
110
|
"react",
|
|
112
111
|
"react-redux"
|
|
113
112
|
],
|
|
114
|
-
"limit": "
|
|
113
|
+
"limit": "3875 B"
|
|
115
114
|
}
|
|
116
115
|
],
|
|
117
116
|
"engines": {
|