@retronew/call-vue 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,26 @@ Call & await Vue components like async functions. A Vue 3 port of
10
10
  (`createCallable`, `call`/`upsert`/`end`/`update`) built on native Vue
11
11
  reactivity — no context providers, no global store to wire up.
12
12
 
13
+ [Documentation with live demos](https://call-vue.retronew.dev) ·
14
+ [Examples](https://call-vue.retronew.dev/examples) ·
15
+ [Concepts](https://call-vue.retronew.dev/concepts) ·
16
+ [Full API reference](https://call-vue.retronew.dev/api)
17
+
18
+ ## Contents
19
+
20
+ - [Install](#install)
21
+ - [Quick start](#quick-start)
22
+ - [API](#api)
23
+ - [Exit transitions](#exit-transitions)
24
+ - [Root props and TypeScript](#root-props-and-typescript)
25
+ - [Mutation flow](#mutation-flow)
26
+ - [Async components](#async-components)
27
+ - [SSR](#ssr)
28
+ - [Stacking](#stacking)
29
+ - [Errors and troubleshooting](#errors-and-troubleshooting)
30
+ - [Capability matrix](#capability-matrix)
31
+ - [FAQ](#faq)
32
+
13
33
  ## Why
14
34
 
15
35
  Confirmation dialogs, prompts, and toasts are usually one-off components you
@@ -30,6 +50,9 @@ No global state, no extra store — `<Confirm />` mounted once *is* the stack.
30
50
  pnpm add @retronew/call-vue
31
51
  ```
32
52
 
53
+ Use `npm install @retronew/call-vue`, `yarn add @retronew/call-vue`, or
54
+ `bun add @retronew/call-vue` with another package manager.
55
+
33
56
  ## Quick start
34
57
 
35
58
  1. Define the component. It receives your own props **plus** an injected
@@ -127,6 +150,19 @@ See the [Claude Code skill](skills/call-vue/SKILL.md) for the stacking model,
127
150
  `unmountingDelay` exit-transition pattern, and the single-`<Root>` constraint
128
151
  in depth.
129
152
 
153
+ ### Public types
154
+
155
+ All public types are flat named exports:
156
+
157
+ | Type | Purpose |
158
+ | --- | --- |
159
+ | `CallFunction<Props, Response>` | The typed `call()` method. |
160
+ | `UpsertFunction<Props, Response>` | The typed singleton `upsert()` method. |
161
+ | `CallContext<Props, Response, RootProps>` | The injected `call` prop. |
162
+ | `PropsWithCall<Props, Response, RootProps>` | Your props merged with `call`. |
163
+ | `UserComponent<Props, Response, RootProps>` | A component accepted by `createCallable`. |
164
+ | `Callable<Props, Response, RootProps>` | The Root component plus its imperative methods. |
165
+
130
166
  ## Exit transitions
131
167
 
132
168
  Pass a second argument to `createCallable` to keep an ended call mounted
@@ -166,6 +202,39 @@ separate `.Root` alias. Its public type is Vue's general `Component` shape, so
166
202
  it remains valid whether the internal Root is represented as an options object
167
203
  or a functional component.
168
204
 
205
+ ## Mutation flow
206
+
207
+ For the common “submit → await a side effect → close only on success” flow,
208
+ import the opt-in composable from its own subpath:
209
+
210
+ ```vue
211
+ <script setup lang="ts">
212
+ import { toRef } from 'vue'
213
+ import type { PropsWithCall } from '@retronew/call-vue'
214
+ import { useMutationFlow, type MutationFn } from '@retronew/call-vue/mutation-flow'
215
+
216
+ type Props = { mutationFn: MutationFn<boolean> }
217
+ const props = defineProps<PropsWithCall<Props, boolean, {}>>()
218
+ const submit = useMutationFlow(props.call, toRef(props, 'mutationFn'))
219
+ </script>
220
+
221
+ <template>
222
+ <button :disabled="submit.pending" @click="submit()">Save</button>
223
+ <button :disabled="submit.pending" @click="props.call.end(false)">Cancel</button>
224
+ </template>
225
+ ```
226
+
227
+ `MutationFn<Response, Payload>` receives only `{ end }` and decides when to
228
+ close the Call. If it returns or rejects without calling `end`, the Call stays
229
+ open and `pending` clears, so the user can retry. Errors are not swallowed.
230
+
231
+ When `mutationFn` is optional, `submit(payload).orEnd(value)` supplies a
232
+ per-button fallback response only when no handler was provided. Omitting the
233
+ chain intentionally leaves the Call open for another explicit close path.
234
+
235
+ Pass a `Ref` such as `toRef(props, 'mutationFn')` when a live Call can receive
236
+ an updated handler through `Callable.update()`.
237
+
169
238
  ## Async components
170
239
 
171
240
  `defineAsyncComponent()` can be passed directly to `createCallable`. The loader
@@ -222,6 +291,73 @@ stack, or rendering all of them with a depth-based transform).
222
291
  - Unmounting `<Confirm />` resets its stack — a fresh mount always starts
223
292
  empty.
224
293
 
294
+ ## Errors and troubleshooting
295
+
296
+ | Error or symptom | Cause and solution |
297
+ | --- | --- |
298
+ | `No <Root> found!` | Mount the returned Callable once and wait for client `onMounted` before calling it. During SSR, move the call to a client interaction. |
299
+ | `Multiple instances of <Root> found!` | The same Callable is mounted in more than one live location. Keep exactly one Root for that Callable. |
300
+ | The Promise never resolves | Every success, cancel, Escape, and backdrop path must explicitly run `call.end(response)` or an external `Callable.end(...)`. Hiding the UI is not enough. |
301
+ | Exit animation is cut off | Match `createCallable(component, unmountingDelay)` to the CSS leave duration and style against `call.ended`. |
302
+ | A targeted `void` end fails to type-check | Use `Toast.end(promise, undefined)`. `Toast.end()` is the broadcast form. |
303
+ | Root data appears missing | Read mounted Root props from `call.root`; normal call props remain top-level component props. |
304
+
305
+ The documentation site has the expanded
306
+ [troubleshooting guide](https://call-vue.retronew.dev/troubleshooting).
307
+
308
+ ## Capability matrix
309
+
310
+ This package targets `react-call`'s framework-neutral core semantics, while
311
+ using Vue-native components and lifecycle primitives.
312
+
313
+ | Capability | `call-vue` | Notes |
314
+ | --- | --- | --- |
315
+ | `createCallable`, `call`, `end` | Supported | Promise and broadcast/targeted semantics match the upstream core. |
316
+ | Concurrent Stack | Supported | Every normal call remains independently active. |
317
+ | `upsert`, `update` | Supported | Singleton Promise identity and targeted/broadcast updates are covered. |
318
+ | Root props | Supported | Available reactively through `call.root`. |
319
+ | Exit lifecycle | Supported | `call.ended` plus `unmountingDelay`. |
320
+ | Vue async components | Supported | Use `defineAsyncComponent`; empty stacks stay lazy. |
321
+ | SSR-safe Root creation | Supported | Calling remains client-only. |
322
+ | `<Callable.Root />` alias | Not provided | The direct `<Callable />` Root is the only API; the legacy alias was removed rather than soft-deprecated. |
323
+ | Mutation-flow helper subpath | Supported | Import `useMutationFlow` and its types from `@retronew/call-vue/mutation-flow`. |
324
+ | Vite HMR transform | Not published | Normal Vue HMR applies, but open-call preservation is not promised yet. |
325
+ | Multi-preview host helper | Not published | Mount one Callable Root outside repeated Storybook/Histoire previews manually. |
326
+
327
+ Unsupported entries are deliberate capability boundaries, not hidden aliases.
328
+ Do not import `react-call`-specific subpaths from this package.
329
+
330
+ ## FAQ
331
+
332
+ ### What if more than one call is active?
333
+
334
+ The Root renders all Calls as a Stack in insertion order. Your component may
335
+ show all of them, position them by `call.index`, or visually prioritize the
336
+ latest using `call.stackSize`.
337
+
338
+ ### Can I place more than one Root?
339
+
340
+ Not for the same Callable. You may mount one `Confirm`, one `Toast`, and one
341
+ `Picker` together because those are three independent Callable values.
342
+
343
+ ### Does `upsert()` replace normal calls?
344
+
345
+ No. The singleton upsert instance coexists with normal `call()` instances.
346
+ Repeated upserts update only the singleton and return its original Promise.
347
+
348
+ ### Is mutation flow just an async function?
349
+
350
+ The domain work is an async function. `useMutationFlow` additionally
351
+ standardizes pending state, duplicate-submit behavior, payload typing, retry
352
+ after failure, and the rule that only an explicit `call.end()` closes the Call.
353
+ Those semantics are why it ships as an optional subpath rather than in the
354
+ core entry.
355
+
356
+ ### Can I use Teleport?
357
+
358
+ Yes. Teleport is presentation owned by your user component. Mount the Callable
359
+ Root once, then Teleport each rendered dialog or toast to the desired target.
360
+
225
361
  ## Claude Code skill
226
362
 
227
363
  This package ships a [Claude Code skill](skills/call-vue/SKILL.md) covering
@@ -0,0 +1,29 @@
1
+ import { Ref } from "vue";
2
+ //#region src/mutation-flow/index.d.ts
3
+ /** The narrow call view exposed to a mutation handler. */
4
+ type MutationCall<Response> = {
5
+ end: (response: Response) => void;
6
+ };
7
+ /** An async side-effect that decides when its Call should close. */
8
+ type MutationFn<Response, Payload = void> = (call: MutationCall<Response>, payload: Payload) => Promise<void>;
9
+ /** Runs a required mutation and exposes its in-flight state. */
10
+ type Trigger<Payload> = ((payload: Payload) => void) & {
11
+ pending: boolean;
12
+ };
13
+ /** Adds a per-callsite fallback for an optional mutation. */
14
+ type ChainTrigger<Payload, Response> = ((payload: Payload) => {
15
+ orEnd: (value: Response) => void;
16
+ }) & {
17
+ pending: boolean;
18
+ };
19
+ type MutationSource<Response, Payload> = MutationFn<Response, Payload> | Readonly<Ref<MutationFn<Response, Payload> | undefined>> | undefined;
20
+ /**
21
+ * Coordinates an async submission with a Call's lifecycle.
22
+ *
23
+ * Pass a `Ref` when the handler can change while the Call is mounted, such as
24
+ * when `Callable.update()` replaces a `mutationFn` prop.
25
+ */
26
+ declare function useMutationFlow<Response, Payload = void>(call: MutationCall<Response>, mutationFn: MutationFn<Response, Payload> | Readonly<Ref<MutationFn<Response, Payload>>>): Trigger<Payload>;
27
+ declare function useMutationFlow<Response, Payload = void>(call: MutationCall<Response>, mutationFn: MutationSource<Response, Payload>): ChainTrigger<Payload, Response>;
28
+ //#endregion
29
+ export { ChainTrigger, MutationCall, MutationFn, Trigger, useMutationFlow };
@@ -0,0 +1,29 @@
1
+ import { isRef, ref } from "vue";
2
+ //#region src/mutation-flow/index.ts
3
+ const noopChain = { orEnd: () => {} };
4
+ function resolveMutation(source) {
5
+ return isRef(source) ? source.value : source;
6
+ }
7
+ function useMutationFlow(call, mutationSource) {
8
+ const pending = ref(false);
9
+ let inFlight = false;
10
+ const trigger = ((payload) => {
11
+ if (inFlight) return noopChain;
12
+ const mutationFn = resolveMutation(mutationSource);
13
+ if (!mutationFn) return { orEnd: (value) => call.end(value) };
14
+ inFlight = true;
15
+ pending.value = true;
16
+ mutationFn(call, payload).finally(() => {
17
+ inFlight = false;
18
+ pending.value = false;
19
+ });
20
+ return noopChain;
21
+ });
22
+ Object.defineProperty(trigger, "pending", {
23
+ enumerable: true,
24
+ get: () => pending.value
25
+ });
26
+ return trigger;
27
+ }
28
+ //#endregion
29
+ export { useMutationFlow };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retronew/call-vue",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Call & await Vue components like async functions — a Vue 3 port of react-call.",
5
5
  "keywords": [
6
6
  "async",
@@ -32,6 +32,10 @@
32
32
  "types": "./dist/index.d.mts",
33
33
  "default": "./dist/index.mjs"
34
34
  },
35
+ "./mutation-flow": {
36
+ "types": "./dist/mutation-flow/index.d.mts",
37
+ "default": "./dist/mutation-flow/index.mjs"
38
+ },
35
39
  "./package.json": "./package.json"
36
40
  },
37
41
  "publishConfig": {