@liquiddom/vue 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 +31 -65
- package/dist/index.d.ts +35 -12
- package/dist/index.js +93 -42
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @liquiddom/vue
|
|
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,6 +49,7 @@
|
|
|
7
49
|
Initial public release. Vue 3.4+ bindings for [`liquiddom`](https://www.npmjs.com/package/liquiddom).
|
|
8
50
|
|
|
9
51
|
**Exports:**
|
|
52
|
+
|
|
10
53
|
- `<LiquidProvider :config>` — owns a `LiquidDOMInstance` via provide/inject
|
|
11
54
|
- `LiquidPlugin` — alternative install via `app.use(LiquidPlugin, config?)`
|
|
12
55
|
- `useLiquid()` — returns `Ref<LiquidDOMInstance | null>` (reactive)
|
|
@@ -14,6 +57,7 @@ Initial public release. Vue 3.4+ bindings for [`liquiddom`](https://www.npmjs.co
|
|
|
14
57
|
- `<LiquidElement :as :liquid-type ...attrs>` — drop-in tag wrapping `useLiquidRef`
|
|
15
58
|
|
|
16
59
|
**Behavior:**
|
|
60
|
+
|
|
17
61
|
- SSR-safe (provider effect gated on `typeof window`)
|
|
18
62
|
- Pure TypeScript with `h()` render functions — no `.vue` SFC dependency in the published package
|
|
19
63
|
- `liquidType` is captured once on first attach (not reactive)
|
package/README.md
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# @liquiddom/vue
|
|
2
2
|
|
|
3
|
-
Vue 3.4+ bindings for [`liquiddom`](https://www.npmjs.com/package/liquiddom)
|
|
3
|
+
Vue 3.4+ 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/vue
|
|
8
|
+
npm install liquiddom@alpha @liquiddom/vue@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
|
```vue
|
|
@@ -18,98 +16,66 @@ import { LiquidProvider, LiquidElement } from "@liquiddom/vue";
|
|
|
18
16
|
</script>
|
|
19
17
|
|
|
20
18
|
<template>
|
|
21
|
-
<LiquidProvider :config="{
|
|
22
|
-
<LiquidElement as="button">
|
|
23
|
-
<LiquidElement as="a" href="#">Also liquid</LiquidElement>
|
|
19
|
+
<LiquidProvider :config="{ material: { viscosity: 0.6, cohesion: 0.85, recovery: 0.4 } }">
|
|
20
|
+
<LiquidElement as="button" :viscosity="0.3">Splash</LiquidElement>
|
|
24
21
|
</LiquidProvider>
|
|
25
22
|
</template>
|
|
26
23
|
```
|
|
27
24
|
|
|
28
|
-
##
|
|
29
|
-
|
|
30
|
-
### 1. `<LiquidElement>` — drop-in tag
|
|
25
|
+
## API
|
|
31
26
|
|
|
32
|
-
|
|
27
|
+
| Export | Purpose |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `<LiquidProvider :config>` | Owns one `LiquidDOMInstance` via provide/inject; `config` is read once on mount |
|
|
30
|
+
| `LiquidPlugin` | Alternative install: `app.use(LiquidPlugin, config?)` |
|
|
31
|
+
| `LiquidKey`, `LiquidErrorKey` | The injection keys (instance, error) |
|
|
32
|
+
| `useLiquid()` | `Ref<LiquidDOMInstance \| null>`; `null` before init and after the instance stopped |
|
|
33
|
+
| `useLiquidError()` | `Ref<unknown>`: the stop's error or the `create()` rejection; `null` otherwise |
|
|
34
|
+
| `useLiquidRef<T>({ viscosity?, recovery?, drag? }?)` | Template ref that observes once mounted; options are read once at setup |
|
|
35
|
+
| `<LiquidElement :as :viscosity :recovery :drag>` | Convenience tag; attrs, class, style and events are forwarded |
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
<LiquidElement as="button" :liquid-type="4 /* Shake */">
|
|
36
|
-
Shaky
|
|
37
|
-
</LiquidElement>
|
|
38
|
-
```
|
|
37
|
+
SSR-safe: nothing runs on the server.
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
## When the liquid stops
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
If the instance stops for good (a frame that throws, for example), the provider (or the plugin) destroys it: the elements are restored, `useLiquid().value` is `null` and `useLiquidError().value` is the error. A `create()` that rejects sets the same error. There is no automatic retry; remount the provider (`v-if` or a new `key`) to try again. Your own `config.onError` is still called.
|
|
43
42
|
|
|
44
43
|
```vue
|
|
45
44
|
<script setup lang="ts">
|
|
46
|
-
import {
|
|
47
|
-
|
|
48
|
-
const cardRef = useLiquidRef<HTMLDivElement>({ liquidType: 3 /* Dragged */ });
|
|
45
|
+
import { useLiquidError } from "@liquiddom/vue";
|
|
46
|
+
const error = useLiquidError();
|
|
49
47
|
</script>
|
|
50
48
|
|
|
51
49
|
<template>
|
|
52
|
-
<
|
|
50
|
+
<p v-if="error" role="alert">Liquid stopped: {{ String(error) }}</p>
|
|
53
51
|
</template>
|
|
54
52
|
```
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
`<LiquidElement :drag="false">` (or `useLiquidRef({ drag: false })`) opts an element out of every drag; like `viscosity`, it is read once.
|
|
55
|
+
|
|
56
|
+
## Material
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
`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`). `useLiquid()` returns a `Ref`, so read `.value`:
|
|
59
59
|
|
|
60
60
|
```vue
|
|
61
61
|
<script setup lang="ts">
|
|
62
|
-
import {
|
|
62
|
+
import { presets } from "liquiddom";
|
|
63
|
+
import { useLiquid } from "@liquiddom/vue";
|
|
63
64
|
|
|
64
65
|
const liquid = useLiquid();
|
|
65
|
-
const buttonRef = useLiquidRef<HTMLButtonElement>();
|
|
66
|
-
|
|
67
|
-
function splash() {
|
|
68
|
-
if (liquid.value && buttonRef.value) {
|
|
69
|
-
liquid.value.impulse(buttonRef.value, { magnitude: 20 });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
66
|
</script>
|
|
73
67
|
|
|
74
68
|
<template>
|
|
75
|
-
<button
|
|
69
|
+
<button @click="liquid?.setMaterial(presets.honey)">Honey</button>
|
|
76
70
|
</template>
|
|
77
71
|
```
|
|
78
72
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
import { createApp } from "vue";
|
|
83
|
-
import { LiquidPlugin } from "@liquiddom/vue";
|
|
84
|
-
import App from "./App.vue";
|
|
85
|
-
|
|
86
|
-
createApp(App)
|
|
87
|
-
.use(LiquidPlugin, { capacity: 16 })
|
|
88
|
-
.mount("#app");
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
The plugin attaches the instance app-wide; `useLiquid()` works without a surrounding `<LiquidProvider>`. The plugin monkey-patches `app.unmount` for cleanup.
|
|
92
|
-
|
|
93
|
-
## API
|
|
94
|
-
|
|
95
|
-
| Export | Description |
|
|
96
|
-
|--------|-------------|
|
|
97
|
-
| `<LiquidProvider :config>` | Owns a `LiquidDOMInstance` via provide/inject. `config` captured once on mount. |
|
|
98
|
-
| `LiquidPlugin` | Alternative install via `app.use(LiquidPlugin, config?)`. |
|
|
99
|
-
| `useLiquid()` | Returns `Ref<LiquidDOMInstance \| null>`. Reactive — updates when async create resolves. |
|
|
100
|
-
| `useLiquidRef<T>(opts?)` | Template ref that auto-observes / unobserves across lifecycle. Internal watch uses `flush: "post"` so the element is mounted before observe fires. |
|
|
101
|
-
| `<LiquidElement :as :liquid-type ...attrs>` | Convenience tag. Uses `inheritAttrs: false` + manual spread so class/style/events/data-* forward correctly. |
|
|
102
|
-
|
|
103
|
-
## Notes
|
|
104
|
-
|
|
105
|
-
- **SSR-safe** — provider effect gated on `typeof window`; `useLiquidRef`'s watch doesn't fire on the server.
|
|
106
|
-
- **No `.vue` SFC dependency in the package** — adapter is pure TypeScript using `h()` render functions, so it can be consumed without `vite-plugin-vue` configured for `node_modules`.
|
|
107
|
-
- **`liquidType` is captured at first attach** — `:liquid-type="reactive"` looks reactive but is read once. Change requires re-mount.
|
|
73
|
+
In a template the ref is unwrapped; in script use `useLiquid().value?.setMaterial(...)`. `getMaterial()` returns the resolved material. A preset also works at create time: `<LiquidProvider :config="{ material: presets.jelly }">`.
|
|
108
74
|
|
|
109
|
-
##
|
|
75
|
+
## Migrating from 0.2
|
|
110
76
|
|
|
111
|
-
|
|
77
|
+
`useLiquidRef({ liquidType })` and `<LiquidElement :liquidType>` are gone; use `{ viscosity?, recovery? }`.
|
|
112
78
|
|
|
113
79
|
## License
|
|
114
80
|
|
|
115
|
-
|
|
81
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* @liquiddom/vue — Vue 3.4+ bindings.
|
|
3
3
|
*/
|
|
4
4
|
import { type InjectionKey, type Plugin, type PropType, type Ref } from "vue";
|
|
5
|
-
import { type LiquidDOMInstance, type LiquidOptions } from "liquiddom";
|
|
5
|
+
import { type ElementOptions, type LiquidDOMInstance, type LiquidOptions } from "liquiddom";
|
|
6
6
|
/** Injection key used by both `<LiquidProvider>` and `LiquidPlugin`. */
|
|
7
7
|
export declare const LiquidKey: InjectionKey<Ref<LiquidDOMInstance | null>>;
|
|
8
|
+
/** W85 (D85-2): injection key of the error that left the provider (or the plugin) without an instance. */
|
|
9
|
+
export declare const LiquidErrorKey: InjectionKey<Ref<unknown>>;
|
|
8
10
|
export declare const LiquidProvider: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
9
11
|
config: {
|
|
10
12
|
type: PropType<LiquidOptions>;
|
|
@@ -17,17 +19,20 @@ export declare const LiquidProvider: import("vue").DefineComponent<import("vue")
|
|
|
17
19
|
};
|
|
18
20
|
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
19
21
|
export declare const LiquidPlugin: Plugin<[LiquidOptions?]>;
|
|
20
|
-
/** Read the live instance from injection. Null ref outside a provider/plugin. */
|
|
22
|
+
/** Read the live instance from injection. Null ref outside a provider/plugin and once the instance stopped. */
|
|
21
23
|
export declare function useLiquid(): Ref<LiquidDOMInstance | null>;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* W85 (D85-2): `Ref` of the error that left the provider (or the plugin) without an instance: a
|
|
26
|
+
* stop's error (the instance is destroyed) or the create() rejection; null otherwise and outside a
|
|
27
|
+
* provider. No retry: remount the provider (`v-if`, `key`) to create a new instance.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useLiquidError(): Ref<unknown>;
|
|
30
|
+
/** Element options (viscosity, recovery, drag), captured once at setup (D66-4). */
|
|
31
|
+
export type UseLiquidRefOptions = ElementOptions;
|
|
25
32
|
/**
|
|
26
33
|
* Template-ref composable: auto-observes when both element and instance are ready.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* earlier would miss the mounted node. `liquidType` is captured once at setup;
|
|
30
|
-
* changes to a `:liquidType="..."` binding after mount are NOT reactive (v1 limitation).
|
|
34
|
+
* `flush: "post"` so the watch fires after Vue's DOM commit. Options are read
|
|
35
|
+
* once at setup; changing a bound prop later has no effect (v1 limitation).
|
|
31
36
|
*/
|
|
32
37
|
export declare function useLiquidRef<T extends HTMLElement = HTMLElement>(opts?: UseLiquidRefOptions): Ref<T | null>;
|
|
33
38
|
export declare const LiquidElement: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
@@ -35,10 +40,18 @@ export declare const LiquidElement: import("vue").DefineComponent<import("vue").
|
|
|
35
40
|
type: StringConstructor;
|
|
36
41
|
default: string;
|
|
37
42
|
};
|
|
38
|
-
|
|
43
|
+
viscosity: {
|
|
44
|
+
type: NumberConstructor;
|
|
45
|
+
default: undefined;
|
|
46
|
+
};
|
|
47
|
+
recovery: {
|
|
39
48
|
type: NumberConstructor;
|
|
40
49
|
default: undefined;
|
|
41
50
|
};
|
|
51
|
+
drag: {
|
|
52
|
+
type: BooleanConstructor;
|
|
53
|
+
default: undefined;
|
|
54
|
+
};
|
|
42
55
|
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
43
56
|
[key: string]: any;
|
|
44
57
|
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
@@ -46,11 +59,21 @@ export declare const LiquidElement: import("vue").DefineComponent<import("vue").
|
|
|
46
59
|
type: StringConstructor;
|
|
47
60
|
default: string;
|
|
48
61
|
};
|
|
49
|
-
|
|
62
|
+
viscosity: {
|
|
63
|
+
type: NumberConstructor;
|
|
64
|
+
default: undefined;
|
|
65
|
+
};
|
|
66
|
+
recovery: {
|
|
50
67
|
type: NumberConstructor;
|
|
51
68
|
default: undefined;
|
|
52
69
|
};
|
|
70
|
+
drag: {
|
|
71
|
+
type: BooleanConstructor;
|
|
72
|
+
default: undefined;
|
|
73
|
+
};
|
|
53
74
|
}>> & Readonly<{}>, {
|
|
75
|
+
viscosity: number;
|
|
76
|
+
recovery: number;
|
|
77
|
+
drag: boolean;
|
|
54
78
|
as: string;
|
|
55
|
-
liquidType: number;
|
|
56
79
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
package/dist/index.js
CHANGED
|
@@ -1,33 +1,78 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @liquiddom/vue — Vue 3.4+ bindings.
|
|
3
3
|
*/
|
|
4
|
-
import { defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, watch, } from "vue";
|
|
4
|
+
import { defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, shallowRef, watch, } from "vue";
|
|
5
5
|
import { LiquidDOM } from "liquiddom";
|
|
6
6
|
/** Injection key used by both `<LiquidProvider>` and `LiquidPlugin`. */
|
|
7
7
|
export const LiquidKey = Symbol("LiquidDOM");
|
|
8
|
+
/** W85 (D85-2): injection key of the error that left the provider (or the plugin) without an instance. */
|
|
9
|
+
export const LiquidErrorKey = Symbol("LiquidDOMError");
|
|
10
|
+
/**
|
|
11
|
+
* Kept identical to withOnError in packages/react/src/index.tsx.
|
|
12
|
+
* W85 (D85-2): `config` with the adapter's handler chained in front of `config.onError`, which is
|
|
13
|
+
* still called, after it. An `onError` that is not a function is passed on unchanged, so create()
|
|
14
|
+
* rejects with core's TypeError (design resolution 9).
|
|
15
|
+
*/
|
|
16
|
+
function withOnError(config, own) {
|
|
17
|
+
const theirs = config?.onError;
|
|
18
|
+
if (theirs !== undefined && typeof theirs !== "function")
|
|
19
|
+
return { ...config };
|
|
20
|
+
const chained = theirs;
|
|
21
|
+
return {
|
|
22
|
+
...config,
|
|
23
|
+
onError: (err) => {
|
|
24
|
+
own(err);
|
|
25
|
+
chained?.(err);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* W85 (D85-2): creates the instance of a provider or the plugin into its refs. On a stop (core's
|
|
31
|
+
* onError; synchronous, design resolution 2) the instance is destroyed, `instance` becomes null and
|
|
32
|
+
* `error` the stop's error; a create() rejection sets `error` too (its console.error stays). No
|
|
33
|
+
* retry. Once `gone()` (unmounted) a resolving instance is destroyed on arrival, as is one that
|
|
34
|
+
* stopped while create() still ran (design resolution 11).
|
|
35
|
+
*/
|
|
36
|
+
function createInto(config, instance, error, gone, owner) {
|
|
37
|
+
let stopped = false;
|
|
38
|
+
const onStop = (err) => {
|
|
39
|
+
if (gone() || stopped)
|
|
40
|
+
return;
|
|
41
|
+
stopped = true;
|
|
42
|
+
const inst = instance.value;
|
|
43
|
+
instance.value = null;
|
|
44
|
+
inst?.destroy();
|
|
45
|
+
error.value = err;
|
|
46
|
+
};
|
|
47
|
+
// A then pair, not then/catch: a throw inside the resolve body (a user's flush:"sync" watcher)
|
|
48
|
+
// must not be reported as a create failure while the instance stays live.
|
|
49
|
+
LiquidDOM.create(withOnError(config, onStop)).then((inst) => {
|
|
50
|
+
if (gone() || stopped) {
|
|
51
|
+
// Race path: the owner unmounted before create() resolved — instance.value was never
|
|
52
|
+
// assigned, so this branch owns the destroy (also for an instance that stopped meanwhile).
|
|
53
|
+
inst.destroy();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
instance.value = inst;
|
|
57
|
+
}, (err) => {
|
|
58
|
+
console.error(`[liquiddom/vue] ${owner} failed to create instance:`, err);
|
|
59
|
+
if (!gone())
|
|
60
|
+
error.value = err;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
8
63
|
export const LiquidProvider = defineComponent({
|
|
9
64
|
name: "LiquidProvider",
|
|
10
65
|
props: { config: { type: Object } },
|
|
11
66
|
setup(props, { slots }) {
|
|
12
67
|
const instance = ref(null);
|
|
68
|
+
const error = shallowRef(null);
|
|
13
69
|
provide(LiquidKey, instance);
|
|
70
|
+
provide(LiquidErrorKey, error);
|
|
14
71
|
let cancelled = false;
|
|
15
|
-
onMounted(
|
|
72
|
+
onMounted(() => {
|
|
16
73
|
if (typeof window === "undefined")
|
|
17
74
|
return;
|
|
18
|
-
|
|
19
|
-
const inst = await LiquidDOM.create(props.config);
|
|
20
|
-
if (cancelled) {
|
|
21
|
-
// Race path: unmount fired before create resolved — instance.value
|
|
22
|
-
// was never assigned, so this branch owns the destroy.
|
|
23
|
-
inst.destroy();
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
instance.value = inst;
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
console.error("[liquiddom/vue] LiquidProvider failed to create instance:", err);
|
|
30
|
-
}
|
|
75
|
+
createInto(props.config, instance, error, () => cancelled, "LiquidProvider");
|
|
31
76
|
});
|
|
32
77
|
onBeforeUnmount(() => {
|
|
33
78
|
cancelled = true;
|
|
@@ -40,29 +85,19 @@ export const LiquidProvider = defineComponent({
|
|
|
40
85
|
export const LiquidPlugin = {
|
|
41
86
|
install(app, config) {
|
|
42
87
|
const instance = ref(null);
|
|
88
|
+
const error = shallowRef(null);
|
|
43
89
|
app.provide(LiquidKey, instance);
|
|
90
|
+
app.provide(LiquidErrorKey, error);
|
|
44
91
|
if (typeof window === "undefined")
|
|
45
92
|
return;
|
|
46
93
|
let unmounted = false;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (unmounted) {
|
|
50
|
-
// Race path: app.unmount() fired before create() resolved.
|
|
51
|
-
// The unmount wrapper below could not destroy because instance.value was still null,
|
|
52
|
-
// so this branch owns the destroy. Do NOT delete without restoring the symmetric guard.
|
|
53
|
-
inst.destroy();
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
instance.value = inst;
|
|
57
|
-
})
|
|
58
|
-
.catch((err) => {
|
|
59
|
-
console.error("[liquiddom/vue] LiquidPlugin failed to create instance:", err);
|
|
60
|
-
});
|
|
94
|
+
// The "app.unmount() BEFORE create() resolved" path is handled in createInto (destroy on arrival).
|
|
95
|
+
createInto(config, instance, error, () => unmounted, "LiquidPlugin");
|
|
61
96
|
const origUnmount = app.unmount.bind(app);
|
|
62
97
|
app.unmount = () => {
|
|
63
98
|
unmounted = true;
|
|
64
99
|
// Race-symmetric destroy: handles "app.unmount() AFTER create() resolved".
|
|
65
|
-
// The "BEFORE create() resolved" path is handled in
|
|
100
|
+
// The "BEFORE create() resolved" path is handled in createInto. Both
|
|
66
101
|
// branches are required for full cleanup coverage.
|
|
67
102
|
instance.value?.destroy();
|
|
68
103
|
instance.value = null;
|
|
@@ -70,28 +105,41 @@ export const LiquidPlugin = {
|
|
|
70
105
|
};
|
|
71
106
|
},
|
|
72
107
|
};
|
|
73
|
-
/** Read the live instance from injection. Null ref outside a provider/plugin. */
|
|
108
|
+
/** Read the live instance from injection. Null ref outside a provider/plugin and once the instance stopped. */
|
|
74
109
|
export function useLiquid() {
|
|
75
110
|
return inject(LiquidKey, () => ref(null), true);
|
|
76
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* W85 (D85-2): `Ref` of the error that left the provider (or the plugin) without an instance: a
|
|
114
|
+
* stop's error (the instance is destroyed) or the create() rejection; null otherwise and outside a
|
|
115
|
+
* provider. No retry: remount the provider (`v-if`, `key`) to create a new instance.
|
|
116
|
+
*/
|
|
117
|
+
export function useLiquidError() {
|
|
118
|
+
return inject(LiquidErrorKey, () => shallowRef(null), true);
|
|
119
|
+
}
|
|
120
|
+
/** Kept identical to pickElementOptions in packages/react/src/index.tsx. W85 (D85-3, design resolution 8): every own key whose value is defined; none → undefined. */
|
|
121
|
+
function pickElementOptions(opts) {
|
|
122
|
+
if (!opts)
|
|
123
|
+
return undefined;
|
|
124
|
+
const out = {};
|
|
125
|
+
for (const [key, value] of Object.entries(opts))
|
|
126
|
+
if (value !== undefined)
|
|
127
|
+
out[key] = value;
|
|
128
|
+
return Object.keys(out).length === 0 ? undefined : out;
|
|
129
|
+
}
|
|
77
130
|
/**
|
|
78
131
|
* Template-ref composable: auto-observes when both element and instance are ready.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* earlier would miss the mounted node. `liquidType` is captured once at setup;
|
|
82
|
-
* changes to a `:liquidType="..."` binding after mount are NOT reactive (v1 limitation).
|
|
132
|
+
* `flush: "post"` so the watch fires after Vue's DOM commit. Options are read
|
|
133
|
+
* once at setup; changing a bound prop later has no effect (v1 limitation).
|
|
83
134
|
*/
|
|
84
135
|
export function useLiquidRef(opts) {
|
|
85
136
|
const instance = useLiquid();
|
|
86
|
-
// Vue's generic Ref<T> erases to Ref<UnwrapRef<T> | null> through `ref()`,
|
|
87
|
-
// which doesn't structurally match the function signature `Ref<T | null>`.
|
|
88
|
-
// Casting at the type-construction site keeps the public API contract clean.
|
|
89
137
|
const elRef = ref(null);
|
|
90
|
-
const
|
|
138
|
+
const elementOptions = pickElementOptions(opts);
|
|
91
139
|
watch([elRef, instance], ([el, inst], _prev, onCleanup) => {
|
|
92
140
|
if (!el || !inst)
|
|
93
141
|
return;
|
|
94
|
-
inst.observe(el,
|
|
142
|
+
inst.observe(el, elementOptions);
|
|
95
143
|
onCleanup(() => {
|
|
96
144
|
inst.unobserve(el);
|
|
97
145
|
});
|
|
@@ -103,10 +151,13 @@ export const LiquidElement = defineComponent({
|
|
|
103
151
|
inheritAttrs: false,
|
|
104
152
|
props: {
|
|
105
153
|
as: { type: String, default: "div" },
|
|
106
|
-
|
|
154
|
+
viscosity: { type: Number, default: undefined },
|
|
155
|
+
recovery: { type: Number, default: undefined },
|
|
156
|
+
// W85 (D85-3): `default: undefined`, since Vue casts an absent Boolean prop to false.
|
|
157
|
+
drag: { type: Boolean, default: undefined },
|
|
107
158
|
},
|
|
108
159
|
setup(props, { slots, attrs }) {
|
|
109
|
-
const elRef = useLiquidRef({
|
|
160
|
+
const elRef = useLiquidRef({ viscosity: props.viscosity, recovery: props.recovery, drag: props.drag });
|
|
110
161
|
return () => h(props.as, { ...attrs, ref: elRef }, slots.default?.());
|
|
111
162
|
},
|
|
112
163
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liquiddom/vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-alpha.1",
|
|
4
4
|
"description": "Vue 3 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
|
"vue": "^3.4.0"
|
|
29
29
|
},
|
|
30
30
|
"author": "Dennis Schmock",
|