@liquiddom/react 0.2.0-rc.0 → 0.3.0-alpha.1
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/CHANGELOG.md +44 -0
- package/README.md +32 -55
- package/dist/index.d.ts +24 -23
- package/dist/index.js +85 -29
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @liquiddom/react
|
|
2
2
|
|
|
3
|
+
## 0.3.0-alpha.1
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3bf4a9e: A stopped instance is reported. The new option `onError(err)` is called once when an instance stops for good (a frame that throws, a failed grid allocation for a growth or the first area hint, a failed Canvas2D rebuild after a lost WebGPU device), after its `console.error`; it is never called for a `create()` rejection, a fallback or after `destroy()`. The React and Vue providers (and the Vue plugin) now destroy a stopped instance and expose the reason through the new `useLiquidError()` (Vue also exports `LiquidErrorKey`); a `create()` rejection sets it too, and `config.onError` is still called. `<LiquidElement drag={false}>` and `useLiquidRef({ drag: false })` pass the drag opt-out to `observe()`. An app that observes its elements after `create()` (every adapter app, or `autoObserve: false`) now gets the same grid resolution as `autoObserve`: the first observed elements set the area hint once. A Vue example joins the React one, and both are smoke-tested from the published packages.
|
|
8
|
+
- 1a68179: **liquiddom 0.3 (alpha): the soft-body engine is retired and replaced by an MLS-MPM fluid engine.** This first alpha ships the liquid _at rest_: each observed element is drawn as crisp liquid behind its real DOM text, with reduced motion and print handled. Splash, shake and the pointer field arrive in the next alpha. This is a breaking release; 0.2 code does not run unchanged. The 0.2 engine is preserved at the git tag `softbody-final`.
|
|
9
|
+
|
|
10
|
+
### Old → new
|
|
11
|
+
|
|
12
|
+
| 0.2 | 0.3 |
|
|
13
|
+
| -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| option `capacity` | `maxElements` (fixed; `observe()` beyond it throws `RangeError`) |
|
|
15
|
+
| option `physics` (`LiquidPhysicsConfig`) | `material: { viscosity, cohesion, recovery }` |
|
|
16
|
+
| export `presets` (goo/jelly/firm) | removed; material presets `water`/`honey`/`jelly` arrive in the next alpha |
|
|
17
|
+
| options `colorDefault`, `colorHover`, `colorSource`, `theme`, `refraction`, `preserveBackgrounds`, `snapDurationMs`, `canvasZIndex`, `maxDt` | removed; `create()` throws a `TypeError` naming the replacement |
|
|
18
|
+
| `observe(el, liquidType)` | `observe(el, { viscosity?, recovery? })` |
|
|
19
|
+
| `grow()`, `tween()`, `getBuffer()`, `spawnDroplet()`, `despawnDroplet()`, `setBackgroundTexture()` | removed |
|
|
20
|
+
| `impulse()` | removed; `splash(el, opts)` arrives in the next alpha |
|
|
21
|
+
| `setPhysicsConfig()`, `getPhysicsConfig()`, `validatePhysicsConfig` | removed; `setMaterial()` / `getMaterial()` arrive in the next alpha; `validateMaterial` (@internal) |
|
|
22
|
+
| `refreshTheme()`, `refreshShadow()` | `refresh(el)` |
|
|
23
|
+
| instance property `capacity` | `elementCapacity`, plus the new `particleCapacity` |
|
|
24
|
+
| `isReducedMotion`, `isScrolling`, `pointerActive`, `pointerX`, `pointerY`, `preserveBackgrounds`, `isScrollSnapping` | removed |
|
|
25
|
+
| `LiquidInstancePanicDetail`, the `liquiddom:instance-panic` event, `SpawnDropletOptions` | removed (WASM init is single-flight; there is nothing to recover from) |
|
|
26
|
+
| React/Vue `useLiquidRef({ liquidType })`, `<LiquidElement liquidType>` | `useLiquidRef({ viscosity?, recovery? })`, `<LiquidElement viscosity recovery>` (type names `UseLiquidRefOptions` / `LiquidElementProps` are kept, their shape changed) |
|
|
27
|
+
|
|
28
|
+
### `SplashOptions` changed shape
|
|
29
|
+
|
|
30
|
+
In 0.2, `SplashOptions` was `{ threshold, count, jitter?, speedScale?, lifetimeMs?, radius? }` and configured droplet spawning inside `impulse()`. That type is gone in this alpha. The next alpha adds a **different** `SplashOptions`, `{ strength?: number /* 0–2, default 1 */, at?: { x: number; y: number } /* client px */ }`, for `instance.splash(el, opts)`. Code that imports `SplashOptions` must be rewritten, not just recompiled.
|
|
31
|
+
|
|
32
|
+
### Known regression: colours no longer auto-refresh
|
|
33
|
+
|
|
34
|
+
0.2 watched every observed element with a MutationObserver and re-read its background colour and box-shadow on `style`/`class` changes. 0.3 snapshots `background-color` and `color` once at `observe()`. Until the extended MutationObserver lands (slice 4), call `instance.refresh(el)` after you change an element's colours (theme switch, class toggle, inline style).
|
|
35
|
+
|
|
36
|
+
### Other behaviour changes
|
|
37
|
+
|
|
38
|
+
- `LiquidDOM.create()` rejects with `LiquidWasmLoadError` when the WASM fails to load. The silent mock mode is gone.
|
|
39
|
+
- Observed elements get `class="liquid-element"` (and `data-liquid-stack` when they need stacking). One `<style id="liquiddom-styles">` is injected per document. `unobserve()` restores the element exactly. Borders and box-shadows of observed elements are not drawn.
|
|
40
|
+
- The canvas is `aria-hidden="true"`, sits below the observed elements (focus rings stay visible), and is hidden in print and forced-colors.
|
|
41
|
+
- `renderer: 'auto'` uses Canvas2D until the WebGPU fluid renderer lands. `renderer: 'webgpu'` initialises WebGPU but draws nothing yet (one `console.warn`).
|
|
42
|
+
- `gravity` is accepted and validated but has no effect yet.
|
|
43
|
+
- `pause()` is no longer undone when a hidden tab becomes visible again.
|
|
44
|
+
|
|
3
45
|
## 0.2.0-rc.0
|
|
4
46
|
|
|
5
47
|
### Minor Changes
|
|
@@ -7,12 +49,14 @@
|
|
|
7
49
|
Initial public release. React 18+ bindings for [`liquiddom`](https://www.npmjs.com/package/liquiddom).
|
|
8
50
|
|
|
9
51
|
**Exports:**
|
|
52
|
+
|
|
10
53
|
- `<LiquidProvider config?>` — owns a `LiquidDOMInstance` via React Context
|
|
11
54
|
- `useLiquid()` — read the instance (returns `null` before init / outside provider)
|
|
12
55
|
- `useLiquidRef<T>(opts?)` — callback ref that auto-observes/unobserves
|
|
13
56
|
- `<LiquidElement as? liquidType? ...rest>` — drop-in tag wrapping `useLiquidRef`
|
|
14
57
|
|
|
15
58
|
**Behavior:**
|
|
59
|
+
|
|
16
60
|
- Strict-mode safe (idempotent observe — React 18 double-mount in dev does not double-register)
|
|
17
61
|
- SSR-safe (provider effect gated on `typeof window`)
|
|
18
62
|
- `liquiddom` is a peer dependency (single WASM instance shared across the app)
|
package/README.md
CHANGED
|
@@ -1,96 +1,73 @@
|
|
|
1
1
|
# @liquiddom/react
|
|
2
2
|
|
|
3
|
-
React 18+ bindings for [`liquiddom`](https://www.npmjs.com/package/liquiddom)
|
|
3
|
+
React 18+ bindings for [`liquiddom`](https://www.npmjs.com/package/liquiddom): WASM-driven fluid dynamics on real DOM elements.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install liquiddom @liquiddom/react
|
|
8
|
+
npm install liquiddom@alpha @liquiddom/react@alpha
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
`liquiddom` is a peer dependency so a single WASM instance is shared across your app.
|
|
12
|
-
|
|
13
11
|
## Quickstart
|
|
14
12
|
|
|
15
13
|
```tsx
|
|
16
14
|
import { LiquidProvider, LiquidElement } from "@liquiddom/react";
|
|
17
15
|
|
|
18
|
-
export
|
|
16
|
+
export function App() {
|
|
19
17
|
return (
|
|
20
|
-
<LiquidProvider config={{
|
|
21
|
-
<LiquidElement as="button">
|
|
22
|
-
<LiquidElement as="a" href="#">Also liquid</LiquidElement>
|
|
18
|
+
<LiquidProvider config={{ material: { viscosity: 0.6, cohesion: 0.85, recovery: 0.4 } }}>
|
|
19
|
+
<LiquidElement as="button" viscosity={0.3}>Splash</LiquidElement>
|
|
23
20
|
</LiquidProvider>
|
|
24
21
|
);
|
|
25
22
|
}
|
|
26
23
|
```
|
|
27
24
|
|
|
28
|
-
##
|
|
25
|
+
## API
|
|
29
26
|
|
|
30
|
-
|
|
27
|
+
| Export | Purpose |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `<LiquidProvider config?>` | Owns one `LiquidDOMInstance`; `config` is read once on mount |
|
|
30
|
+
| `LiquidContext` | The raw context (instance or `null`) |
|
|
31
|
+
| `useLiquid()` | The instance, or `null` before init, outside a provider and after it stopped |
|
|
32
|
+
| `useLiquidError()` | Why there is no instance: the stop's error or the `create()` rejection; `null` otherwise |
|
|
33
|
+
| `useLiquidRef<T>({ viscosity?, recovery?, drag? }?)` | Callback ref that observes on attach and unobserves on detach; options are captured on first render |
|
|
34
|
+
| `<LiquidElement as? viscosity? recovery? drag? …rest>` | Convenience tag around `useLiquidRef`; every other prop is forwarded |
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
StrictMode-safe (single-flight WASM init plus idempotent `observe`). SSR-safe (the provider effect only runs in the browser).
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
<LiquidElement as="button" liquidType={4 /* Shake */}>
|
|
36
|
-
Shaky
|
|
37
|
-
</LiquidElement>
|
|
38
|
-
```
|
|
38
|
+
## When the liquid stops
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
When you need full control over the element (custom components, third-party libs).
|
|
40
|
+
If the instance stops for good (a frame that throws, for example), the provider destroys it: the elements are restored, `useLiquid()` returns `null` and `useLiquidError()` returns the error. A `create()` that rejects (no WebGPU with `renderer: 'webgpu'`, a WASM load failure) sets the same error. There is no automatic retry; remount the provider (a new `key`) to try again. Your own `config.onError` is still called. Under `createRoot` (React 18's default, React 19's only root) a stop is one render; under the legacy `ReactDOM.render` root `useLiquid()` and `useLiquidError()` can both read `null` for one render.
|
|
43
41
|
|
|
44
42
|
```tsx
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const ref = useLiquidRef<HTMLDivElement>({ liquidType: 3 /* Dragged */ });
|
|
49
|
-
return <div ref={ref} className="my-card">Drag me</div>;
|
|
43
|
+
function Status() {
|
|
44
|
+
const error = useLiquidError();
|
|
45
|
+
return error ? <p role="alert">Liquid stopped: {String(error)}</p> : null;
|
|
50
46
|
}
|
|
51
47
|
```
|
|
52
48
|
|
|
53
|
-
|
|
49
|
+
`<LiquidElement drag={false}>` (or `useLiquidRef({ drag: false })`) opts an element out of every drag; like `viscosity`, it is read once.
|
|
50
|
+
|
|
51
|
+
## Material
|
|
54
52
|
|
|
55
|
-
|
|
53
|
+
`config` is read once on mount, so changing `config.material` later does nothing. Change the material live through the instance, for example with one of the core's `presets` (`water`, `honey`, `jelly`):
|
|
56
54
|
|
|
57
55
|
```tsx
|
|
58
|
-
import {
|
|
56
|
+
import { presets } from "liquiddom";
|
|
57
|
+
import { useLiquid } from "@liquiddom/react";
|
|
59
58
|
|
|
60
|
-
function
|
|
59
|
+
function HoneyButton() {
|
|
61
60
|
const liquid = useLiquid();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<button
|
|
66
|
-
ref={ref}
|
|
67
|
-
onClick={() => liquid?.impulse(ref.current!, { magnitude: 20 })}
|
|
68
|
-
>
|
|
69
|
-
Splash me
|
|
70
|
-
</button>
|
|
71
|
-
);
|
|
61
|
+
return <button onClick={() => liquid?.setMaterial(presets.honey)}>Honey</button>;
|
|
72
62
|
}
|
|
73
63
|
```
|
|
74
64
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| Export | Description |
|
|
78
|
-
|--------|-------------|
|
|
79
|
-
| `<LiquidProvider config?>` | Owns a `LiquidDOMInstance` via React Context. `config` is captured once on mount — re-renders don't re-create the instance. |
|
|
80
|
-
| `useLiquid()` | Returns the active instance, or `null` before init / outside a provider. |
|
|
81
|
-
| `useLiquidRef<T>(opts?)` | Callback ref that auto-observes on mount, unobserves on unmount. `opts.liquidType` captured at first attach. |
|
|
82
|
-
| `<LiquidElement as? liquidType? ...rest>` | Convenience tag — internally uses `useLiquidRef`. Forwards all other props to the underlying element. |
|
|
83
|
-
|
|
84
|
-
## Notes
|
|
85
|
-
|
|
86
|
-
- **Strict-mode safe** — `useLiquidRef` observes idempotently, so React 18's double-mount in dev doesn't double-register.
|
|
87
|
-
- **SSR-safe** — the provider effect is gated on `typeof window`, so server rendering is a no-op.
|
|
88
|
-
- **Single instance per provider** — nested providers each spin up their own WASM instance. For most apps, one provider at the root is correct.
|
|
65
|
+
`getMaterial()` returns the resolved material. A preset also works at create time: `<LiquidProvider config={{ material: presets.jelly }}>`.
|
|
89
66
|
|
|
90
|
-
##
|
|
67
|
+
## Migrating from 0.2
|
|
91
68
|
|
|
92
|
-
|
|
69
|
+
`useLiquidRef({ liquidType })` and `<LiquidElement liquidType>` are gone; use `{ viscosity?, recovery? }`. See the core package's changelog for the full old → new table.
|
|
93
70
|
|
|
94
71
|
## License
|
|
95
72
|
|
|
96
|
-
|
|
73
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @liquiddom/react — React 18+ bindings.
|
|
3
3
|
*/
|
|
4
4
|
import { type ReactNode, type RefCallback, type HTMLAttributes } from "react";
|
|
5
|
-
import { type LiquidDOMInstance, type LiquidOptions } from "liquiddom";
|
|
6
|
-
/** Context value: the live instance, or null before init
|
|
5
|
+
import { type ElementOptions, type LiquidDOMInstance, type LiquidOptions } from "liquiddom";
|
|
6
|
+
/** Context value: the live instance, or null before init, after destroy and once it stopped (W85). */
|
|
7
7
|
export declare const LiquidContext: import("react").Context<LiquidDOMInstance | null>;
|
|
8
8
|
/** Provider props. `config` is read ONCE on mount; subsequent changes are ignored. */
|
|
9
9
|
export interface LiquidProviderProps {
|
|
@@ -11,36 +11,37 @@ export interface LiquidProviderProps {
|
|
|
11
11
|
children: ReactNode;
|
|
12
12
|
}
|
|
13
13
|
export declare function LiquidProvider({ config, children }: LiquidProviderProps): JSX.Element;
|
|
14
|
-
/** Read the live instance from context. Null before init
|
|
14
|
+
/** Read the live instance from context. Null before init, outside a provider and once it stopped. */
|
|
15
15
|
export declare function useLiquid(): LiquidDOMInstance | null;
|
|
16
|
-
export interface UseLiquidRefOptions {
|
|
17
|
-
liquidType?: number;
|
|
18
|
-
}
|
|
19
16
|
/**
|
|
20
|
-
*
|
|
21
|
-
* the provider
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
* W85 (D85-2): the error that left the provider without an instance: a stop's error (a frame that
|
|
18
|
+
* threw, a failed grid growth or rebuild; the provider has destroyed the instance) or the create()
|
|
19
|
+
* rejection. Null otherwise and outside a provider. No retry: remount the provider (a new `key`)
|
|
20
|
+
* to create a new instance.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useLiquidError(): unknown;
|
|
23
|
+
/** Element options (viscosity, recovery, drag), captured at first attach (D66-4). */
|
|
24
|
+
export type UseLiquidRefOptions = ElementOptions;
|
|
25
|
+
/**
|
|
26
|
+
* Callback ref that auto-observes the attached element. Safe before the
|
|
27
|
+
* provider's instance is ready (the effect re-runs when it arrives). Options
|
|
28
|
+
* are captured once, on first render (D66-4).
|
|
26
29
|
*
|
|
27
|
-
*
|
|
28
|
-
* time the effect ran. A later `setEl(null)` does NOT change which element
|
|
29
|
-
* the captured cleanup unobserves.
|
|
30
|
+
* Strict-mode safe: cleanup unobserves, remount re-observes; observe is idempotent.
|
|
30
31
|
*/
|
|
31
32
|
export declare function useLiquidRef<T extends HTMLElement>(opts?: UseLiquidRefOptions): RefCallback<T>;
|
|
32
33
|
export interface LiquidElementProps {
|
|
33
34
|
as?: keyof JSX.IntrinsicElements;
|
|
34
|
-
|
|
35
|
+
/** [0, 1]; captured at first attach. */
|
|
36
|
+
viscosity?: number;
|
|
37
|
+
/** Seconds in [0.2, 3]; captured at first attach. */
|
|
38
|
+
recovery?: number;
|
|
39
|
+
/** W85 (D85-3): `false` opts the element out of every drag (mouse, pen, touch); captured at first attach. */
|
|
40
|
+
drag?: boolean;
|
|
35
41
|
children?: ReactNode;
|
|
36
42
|
}
|
|
37
43
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* Polymorphic typing is intentionally loose for v1 — props are typed as
|
|
42
|
-
* `HTMLAttributes<HTMLElement>` so most common attributes typecheck across
|
|
43
|
-
* tags. Tag-specific attributes (`type` on input, `disabled` on button) may
|
|
44
|
-
* require a cast. Full polymorphic typing deferred to a follow-up ward.
|
|
44
|
+
* Renders the chosen tag (default `div`) and auto-observes it. `viscosity`,
|
|
45
|
+
* `recovery` and `drag` are consumed (not forwarded to the DOM); every other prop is spread.
|
|
45
46
|
*/
|
|
46
47
|
export declare function LiquidElement(props: LiquidElementProps & HTMLAttributes<HTMLElement>): JSX.Element;
|
package/dist/index.js
CHANGED
|
@@ -4,20 +4,59 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
4
4
|
*/
|
|
5
5
|
import { createContext, useContext, useEffect, useRef, useState, } from "react";
|
|
6
6
|
import { LiquidDOM, } from "liquiddom";
|
|
7
|
-
/** Context value: the live instance, or null before init
|
|
7
|
+
/** Context value: the live instance, or null before init, after destroy and once it stopped (W85). */
|
|
8
8
|
export const LiquidContext = createContext(null);
|
|
9
|
+
/** W85 (D85-2, design resolution 10): why the provider has no instance; null otherwise. Private: read it with useLiquidError(). */
|
|
10
|
+
const LiquidErrorContext = createContext(null);
|
|
11
|
+
/**
|
|
12
|
+
* Kept identical to withOnError in packages/vue/src/index.ts.
|
|
13
|
+
* W85 (D85-2): `config` with the provider's handler chained in front of `config.onError`, which is
|
|
14
|
+
* still called, after it. An `onError` that is not a function is passed on unchanged, so create()
|
|
15
|
+
* rejects with core's TypeError (design resolution 9).
|
|
16
|
+
*/
|
|
17
|
+
function withOnError(config, own) {
|
|
18
|
+
const theirs = config?.onError;
|
|
19
|
+
if (theirs !== undefined && typeof theirs !== "function")
|
|
20
|
+
return { ...config };
|
|
21
|
+
const chained = theirs;
|
|
22
|
+
return {
|
|
23
|
+
...config,
|
|
24
|
+
onError: (err) => {
|
|
25
|
+
own(err);
|
|
26
|
+
chained?.(err);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
9
30
|
export function LiquidProvider({ config, children }) {
|
|
10
31
|
const [instance, setInstance] = useState(null);
|
|
32
|
+
const [error, setError] = useState(null);
|
|
11
33
|
const configRef = useRef(config);
|
|
12
34
|
useEffect(() => {
|
|
13
35
|
if (typeof window === "undefined")
|
|
14
36
|
return; // SSR guard — never runs server-side
|
|
15
37
|
let cancelled = false;
|
|
16
38
|
let created = null;
|
|
17
|
-
|
|
39
|
+
// W85 (D85-2): set by the instance's onError. A stop can arrive before create() resolves (design
|
|
40
|
+
// resolution 11); the instance is then destroyed on arrival.
|
|
41
|
+
let stopped = false;
|
|
42
|
+
// W85 (design resolution 2): synchronous, from inside core's onError, which runs last in the stop
|
|
43
|
+
// (design resolution 1). useLiquid() never hands out a stopped instance; React 18 batches a
|
|
44
|
+
// setState from a RAF or event callback.
|
|
45
|
+
const onStop = (err) => {
|
|
46
|
+
if (cancelled || stopped)
|
|
47
|
+
return;
|
|
48
|
+
stopped = true;
|
|
49
|
+
const inst = created;
|
|
50
|
+
created = null;
|
|
51
|
+
inst?.destroy();
|
|
52
|
+
setInstance(null);
|
|
53
|
+
setError(() => err); // an updater: the error itself may be a function
|
|
54
|
+
};
|
|
55
|
+
LiquidDOM.create(withOnError(configRef.current, onStop))
|
|
18
56
|
.then((inst) => {
|
|
19
|
-
if (cancelled) {
|
|
20
|
-
// Provider unmounted (or strict-mode cleanup) before create resolved
|
|
57
|
+
if (cancelled || stopped) {
|
|
58
|
+
// Provider unmounted (or strict-mode cleanup) before create resolved, or the instance
|
|
59
|
+
// stopped while create() still ran.
|
|
21
60
|
inst.destroy();
|
|
22
61
|
return;
|
|
23
62
|
}
|
|
@@ -26,59 +65,76 @@ export function LiquidProvider({ config, children }) {
|
|
|
26
65
|
})
|
|
27
66
|
.catch((err) => {
|
|
28
67
|
console.error("[liquiddom/react] LiquidProvider failed to create instance:", err);
|
|
68
|
+
// W85 (D85-2): no retry; a remount creates a new instance.
|
|
69
|
+
if (!cancelled)
|
|
70
|
+
setError(() => err);
|
|
29
71
|
});
|
|
30
72
|
return () => {
|
|
31
73
|
cancelled = true;
|
|
32
74
|
if (created)
|
|
33
75
|
created.destroy();
|
|
34
|
-
|
|
35
|
-
//
|
|
76
|
+
created = null;
|
|
77
|
+
// Ward-fix M1: the context must never hand out a destroyed instance
|
|
78
|
+
// ("null after destroy"), e.g. when React re-runs this effect on a kept
|
|
79
|
+
// component (Fast Refresh, strict-mode reconnects). React 18 ignores the
|
|
80
|
+
// update after a real unmount.
|
|
81
|
+
setInstance(null);
|
|
82
|
+
setError(null); // W85: a re-run effect starts without an error
|
|
36
83
|
};
|
|
37
84
|
}, []); // empty deps — config captured by ref, ignored after mount
|
|
38
|
-
return _jsx(LiquidContext.Provider, { value: instance, children: children });
|
|
85
|
+
return (_jsx(LiquidErrorContext.Provider, { value: error, children: _jsx(LiquidContext.Provider, { value: instance, children: children }) }));
|
|
39
86
|
}
|
|
40
|
-
/** Read the live instance from context. Null before init
|
|
87
|
+
/** Read the live instance from context. Null before init, outside a provider and once it stopped. */
|
|
41
88
|
export function useLiquid() {
|
|
42
89
|
return useContext(LiquidContext);
|
|
43
90
|
}
|
|
44
91
|
/**
|
|
45
|
-
*
|
|
46
|
-
* the provider
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
|
|
50
|
-
|
|
92
|
+
* W85 (D85-2): the error that left the provider without an instance: a stop's error (a frame that
|
|
93
|
+
* threw, a failed grid growth or rebuild; the provider has destroyed the instance) or the create()
|
|
94
|
+
* rejection. Null otherwise and outside a provider. No retry: remount the provider (a new `key`)
|
|
95
|
+
* to create a new instance.
|
|
96
|
+
*/
|
|
97
|
+
export function useLiquidError() {
|
|
98
|
+
return useContext(LiquidErrorContext);
|
|
99
|
+
}
|
|
100
|
+
/** Kept identical to pickElementOptions in packages/vue/src/index.ts. W85 (D85-3, design resolution 8): every own key whose value is defined; none → undefined. */
|
|
101
|
+
function pickElementOptions(opts) {
|
|
102
|
+
if (!opts)
|
|
103
|
+
return undefined;
|
|
104
|
+
const out = {};
|
|
105
|
+
for (const [key, value] of Object.entries(opts))
|
|
106
|
+
if (value !== undefined)
|
|
107
|
+
out[key] = value;
|
|
108
|
+
return Object.keys(out).length === 0 ? undefined : out;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Callback ref that auto-observes the attached element. Safe before the
|
|
112
|
+
* provider's instance is ready (the effect re-runs when it arrives). Options
|
|
113
|
+
* are captured once, on first render (D66-4).
|
|
51
114
|
*
|
|
52
|
-
*
|
|
53
|
-
* time the effect ran. A later `setEl(null)` does NOT change which element
|
|
54
|
-
* the captured cleanup unobserves.
|
|
115
|
+
* Strict-mode safe: cleanup unobserves, remount re-observes; observe is idempotent.
|
|
55
116
|
*/
|
|
56
117
|
export function useLiquidRef(opts) {
|
|
57
118
|
const instance = useLiquid();
|
|
58
119
|
const [el, setEl] = useState(null);
|
|
59
|
-
const
|
|
120
|
+
const optsRef = useRef(pickElementOptions(opts));
|
|
60
121
|
useEffect(() => {
|
|
61
122
|
if (!el || !instance)
|
|
62
123
|
return;
|
|
63
|
-
instance.observe(el,
|
|
124
|
+
instance.observe(el, optsRef.current);
|
|
64
125
|
return () => {
|
|
65
126
|
instance.unobserve(el);
|
|
66
127
|
};
|
|
67
|
-
}, [el, instance
|
|
128
|
+
}, [el, instance]);
|
|
68
129
|
return setEl;
|
|
69
130
|
}
|
|
70
131
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* Polymorphic typing is intentionally loose for v1 — props are typed as
|
|
75
|
-
* `HTMLAttributes<HTMLElement>` so most common attributes typecheck across
|
|
76
|
-
* tags. Tag-specific attributes (`type` on input, `disabled` on button) may
|
|
77
|
-
* require a cast. Full polymorphic typing deferred to a follow-up ward.
|
|
132
|
+
* Renders the chosen tag (default `div`) and auto-observes it. `viscosity`,
|
|
133
|
+
* `recovery` and `drag` are consumed (not forwarded to the DOM); every other prop is spread.
|
|
78
134
|
*/
|
|
79
135
|
export function LiquidElement(props) {
|
|
80
|
-
const { as,
|
|
81
|
-
const ref = useLiquidRef({
|
|
136
|
+
const { as, viscosity, recovery, drag, children, ...rest } = props;
|
|
137
|
+
const ref = useLiquidRef({ viscosity, recovery, drag });
|
|
82
138
|
const Tag = (as ?? "div");
|
|
83
139
|
return (_jsx(Tag, { ref: ref, ...rest, children: children }));
|
|
84
140
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liquiddom/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-alpha.1",
|
|
4
4
|
"description": "React bindings for liquiddom",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"test": "vitest run"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"liquiddom": "^0.
|
|
27
|
+
"liquiddom": "^0.3.0-alpha.0",
|
|
28
28
|
"react": "^18.0.0 || ^19.0.0"
|
|
29
29
|
},
|
|
30
30
|
"author": "Dennis Schmock",
|