@retronew/call-vue 0.2.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
@@ -43,7 +66,7 @@ import type { PropsWithCall } from '@retronew/call-vue'
43
66
  type Props = { message: string }
44
67
  type Response = boolean
45
68
 
46
- defineProps<PropsWithCall<Props, Response, Record<string, never>>>()
69
+ defineProps<PropsWithCall<Props, Response, {}>>()
47
70
  </script>
48
71
 
49
72
  <template>
@@ -109,10 +132,37 @@ async function handleDelete() {
109
132
  instance's position in the stack, how many are open, and whatever props
110
133
  were passed to `<Confirm rootProp="…" />`.
111
134
 
135
+ For a `void` response, the two external forms are intentionally distinct:
136
+
137
+ ```ts
138
+ const promise = Toast.upsert({ text: 'Uploading…' })
139
+
140
+ Toast.end(promise, undefined) // end only this call
141
+ Toast.end() // end every open call
142
+ ```
143
+
144
+ Do not write `Toast.end(promise)`: JavaScript would otherwise interpret that
145
+ single argument as the response for the broadcast overload. The public type
146
+ rejects this form. Inside the user component, `call.end()` remains the natural
147
+ way to finish its own `void` call.
148
+
112
149
  See the [Claude Code skill](skills/call-vue/SKILL.md) for the stacking model,
113
150
  `unmountingDelay` exit-transition pattern, and the single-`<Root>` constraint
114
151
  in depth.
115
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
+
116
166
  ## Exit transitions
117
167
 
118
168
  Pass a second argument to `createCallable` to keep an ended call mounted
@@ -125,6 +175,106 @@ export const Toast = createCallable<Props, Response>(ToastCard, 200 /* ms */)
125
175
 
126
176
  Inside `ToastCard`, branch on `call.ended` to trigger the leave state.
127
177
 
178
+ ## Root props and TypeScript
179
+
180
+ The third generic is the type of props mounted on the Callable Root. A normal
181
+ interface works directly; it does not need to extend `Record<string, unknown>`:
182
+
183
+ ```ts
184
+ interface RootProps {
185
+ accent: string
186
+ }
187
+
188
+ export const Notice = createCallable<NoticeProps, void, RootProps>(NoticeCard)
189
+ ```
190
+
191
+ ```vue
192
+ <Notice accent="#6366f1" />
193
+ ```
194
+
195
+ Every active card reads the current value through `call.root.accent`. Updating
196
+ the Root prop re-renders active calls. For cross-file `.vue` prop validation,
197
+ run `vue-tsc --noEmit` in addition to a plain TypeScript check; plain `tsc`
198
+ cannot inspect generated SFC props by itself.
199
+
200
+ The Callable itself is the Root component. Mount `<Notice />`; there is no
201
+ separate `.Root` alias. Its public type is Vue's general `Component` shape, so
202
+ it remains valid whether the internal Root is represented as an options object
203
+ or a functional component.
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
+
238
+ ## Async components
239
+
240
+ `defineAsyncComponent()` can be passed directly to `createCallable`. The loader
241
+ is not invoked until the first call creates a component instance:
242
+
243
+ ```ts
244
+ import { defineAsyncComponent } from 'vue'
245
+ import PickerLoadError from './PickerLoadError.vue'
246
+ import PickerLoading from './PickerLoading.vue'
247
+
248
+ const AsyncPicker = defineAsyncComponent({
249
+ loader: () => import('./PickerDialog.vue'),
250
+ loadingComponent: PickerLoading,
251
+ errorComponent: PickerLoadError,
252
+ delay: 0,
253
+ })
254
+
255
+ export const Picker = createCallable<PickerProps, PickedItem>(AsyncPicker)
256
+ ```
257
+
258
+ Use Vue's `loadingComponent`/`errorComponent` options when a call may be added
259
+ after its surrounding `<Suspense>` has already resolved. Ending a call while
260
+ its component is still loading does not resurrect it when the loader finishes.
261
+
262
+ ## SSR
263
+
264
+ Callable Roots are safe to render repeatedly with Vue SSR: server-side setup
265
+ does not register a live Root or leak the Root count into later requests. The
266
+ user component is not rendered on the server while the stack is empty.
267
+ `call()` and `upsert()` remain client-imperative APIs and throw
268
+ `No <Root> found!` until the Root's client `onMounted` hook has run.
269
+
270
+ ## Accessibility responsibility
271
+
272
+ `call-vue` controls stack and Promise lifecycles; it is deliberately headless.
273
+ Dialog semantics remain the user component's responsibility. A production
274
+ dialog should at least provide an accessible name/description, move focus
275
+ inside on open, trap Tab, close with Escape when appropriate, restore focus on
276
+ unmount, and respect reduced-motion preferences.
277
+
128
278
  ## Stacking
129
279
 
130
280
  Every `call()` while a previous one is still open stacks on top of it —
@@ -141,6 +291,73 @@ stack, or rendering all of them with a depth-based transform).
141
291
  - Unmounting `<Confirm />` resets its stack — a fresh mount always starts
142
292
  empty.
143
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
+
144
361
  ## Claude Code skill
145
362
 
146
363
  This package ships a [Claude Code skill](skills/call-vue/SKILL.md) covering
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { Component, DefineComponent } from "vue";
1
+ import { Component } from "vue";
2
2
  //#region src/createCallable/types.d.ts
3
3
  /**
4
4
  * Properties every `call.*` context carries, regardless of stack position.
@@ -40,19 +40,18 @@ type PropsWithCall<Props, Response, RootProps> = Props & {
40
40
  * or functional) whose props satisfy `PropsWithCall<Props, Response, RootProps>`.
41
41
  */
42
42
  type UserComponent<Props, Response, RootProps> = Component<PropsWithCall<Props, Response, RootProps>>;
43
+ type EndFunction<Response> = ((promise: Promise<Response>, response: [Response] extends [void] ? undefined : Response) => void) & ([Response] extends [void] ? (response?: undefined) => void : (response: Response) => void);
43
44
  /**
44
45
  * What `createCallable` returns.
45
46
  *
46
47
  * The callable is the Root component itself — mount it with `<Confirm />`
47
- * (or `<Confirm.Root />`) and use the imperative methods (`call`, `upsert`,
48
- * `end`, `update`) as properties on the very same object.
48
+ * and use the imperative methods (`call`, `upsert`, `end`, `update`) as
49
+ * properties on the very same object.
49
50
  */
50
- type Callable<Props, Response, RootProps> = DefineComponent<RootProps> & {
51
- /** Alias for the callable itself — `Confirm.Root === Confirm`. */
52
- Root: DefineComponent<RootProps>;
51
+ type Callable<Props, Response, RootProps> = Component<RootProps> & {
53
52
  call: CallFunction<Props, Response>;
54
53
  upsert: UpsertFunction<Props, Response>;
55
- end: ((promise: Promise<Response>, response: Response) => void) & ((response: Response) => void);
54
+ end: EndFunction<Response>;
56
55
  update: ((promise: Promise<Response>, props: Partial<Props>) => void) & ((props: Partial<Props>) => void);
57
56
  };
58
57
  //#endregion
@@ -65,8 +64,8 @@ type Callable<Props, Response, RootProps> = DefineComponent<RootProps> & {
65
64
  *
66
65
  * This is a Vue-native port of react-call's `createCallable` — see
67
66
  * `skills/call-vue/SKILL.md` for the full mental model (stack, upsert,
68
- * mutation flow).
67
+ * lifecycle constraints).
69
68
  */
70
- declare function createCallable<Props = void, Response = void, RootProps extends Record<string, unknown> = Record<string, never>>(UserComponent: UserComponent<Props, Response, RootProps>, unmountingDelay?: number): Callable<Props, Response, RootProps>;
69
+ declare function createCallable<Props = void, Response = void, RootProps extends object = {}>(UserComponent: UserComponent<Props, Response, RootProps>, unmountingDelay?: number): Callable<Props, Response, RootProps>;
71
70
  //#endregion
72
71
  export { type CallContext, type CallFunction, type Callable, type PropsWithCall, type UpsertFunction, type UserComponent, createCallable };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineComponent, h, onUnmounted, shallowRef } from "vue";
1
+ import { defineComponent, h, onMounted, onUnmounted, shallowRef } from "vue";
2
2
  //#region src/createCallable/store.ts
3
3
  /**
4
4
  * Vue-native replacement for react-call's `useSyncExternalStore`-backed
@@ -58,7 +58,7 @@ function createStackStore() {
58
58
  *
59
59
  * This is a Vue-native port of react-call's `createCallable` — see
60
60
  * `skills/call-vue/SKILL.md` for the full mental model (stack, upsert,
61
- * mutation flow).
61
+ * lifecycle constraints).
62
62
  */
63
63
  function createCallable(UserComponent, unmountingDelay = 0) {
64
64
  const store = createStackStore();
@@ -121,13 +121,13 @@ function createCallable(UserComponent, unmountingDelay = 0) {
121
121
  });
122
122
  return promise;
123
123
  };
124
- const end = (...args) => {
124
+ const end = ((...args) => {
125
125
  const targeted = args.length === 2;
126
126
  const promise = targeted ? args[0] : null;
127
127
  const response = targeted ? args[1] : args[0];
128
128
  if (!targeted || promise === store.getUpsertPromise()) store.setUpsertPromise(null);
129
129
  return createEnd(promise)(response);
130
- };
130
+ });
131
131
  const update = (...args) => {
132
132
  const targeted = args.length === 2;
133
133
  store.set(targeted ? args[0] : null, (c) => ({
@@ -142,8 +142,11 @@ function createCallable(UserComponent, unmountingDelay = 0) {
142
142
  name: "CallableRoot",
143
143
  inheritAttrs: false,
144
144
  setup(_, { attrs }) {
145
- const unmountRoot = store.mountRoot();
146
- onUnmounted(unmountRoot);
145
+ let unmountRoot;
146
+ onMounted(() => {
147
+ unmountRoot = store.mountRoot();
148
+ });
149
+ onUnmounted(() => unmountRoot?.());
147
150
  return () => store.stack.value.map((item, index, stack) => h(UserComponent, {
148
151
  ...item.props,
149
152
  key: item.key,
@@ -159,7 +162,6 @@ function createCallable(UserComponent, unmountingDelay = 0) {
159
162
  }
160
163
  });
161
164
  return Object.assign(Root, {
162
- Root,
163
165
  call,
164
166
  upsert,
165
167
  end,
@@ -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.2.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,19 +32,23 @@
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": {
38
42
  "access": "public"
39
43
  },
40
44
  "devDependencies": {
41
- "@types/node": "^26.2.0",
45
+ "@types/node": "^26.3.0",
42
46
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
43
47
  "@vue/test-utils": "^2.4.11",
44
48
  "jsdom": "^30.0.1",
45
49
  "typescript": "^7.0.2",
46
- "vite-plus": "^0.2.9",
47
- "vitest": "4.1.10",
50
+ "vite-plus": "^0.3.0",
51
+ "vitest": "4.1.11",
48
52
  "vue": "^3.5.41"
49
53
  },
50
54
  "peerDependencies": {
@@ -33,7 +33,7 @@ served by routing.
33
33
  `upsert`, `end`, `update`) attached to the same object. Don't call it a
34
34
  "modal/dialog/component".
35
35
  - **Root** — the mounting form of the Callable: the bare `<Confirm />`
36
- (`Confirm.Root === Confirm`, kept as an alias). Not a "provider/portal".
36
+ itself. There is no separate `.Root` alias. Not a "provider/portal".
37
37
  - **Call** — one imperative invocation (`Confirm.call({...})`), resolves to a
38
38
  **Response**.
39
39
  - **Stack** — the ordered list of active Calls a Root renders (not a
@@ -53,7 +53,7 @@ type Props = { message: string }
53
53
  type Response = boolean
54
54
 
55
55
  // `call` is the special injected prop (the CallContext)
56
- defineProps<PropsWithCall<Props, Response, Record<string, never>>>()
56
+ defineProps<PropsWithCall<Props, Response, {}>>()
57
57
  </script>
58
58
 
59
59
  <template>
@@ -86,7 +86,8 @@ const accepted = await Confirm.call({ message: 'Continue?' })
86
86
  ```
87
87
 
88
88
  Generics are `createCallable<Props, Response, RootProps>` (all optional,
89
- default to `void`/`void`/`Record<string, never>`).
89
+ default to `void`/`void`/`{}`). `RootProps` accepts a normal interface without
90
+ extending `Record<string, unknown>`.
90
91
 
91
92
  Plain functional components work too — `createCallable` accepts anything
92
93
  matching Vue's `Component<Props>` type, including a bare
@@ -109,6 +110,10 @@ tiny callable.
109
110
  - **End / update from the caller** — `Confirm.end(promise, value)` /
110
111
  `Confirm.update(promise, partialProps)` target one Call; omit the promise
111
112
  argument to affect every currently active Call instead.
113
+ - **Void responses** — externally, use `Toast.end(promise, undefined)` to
114
+ target one Call and `Toast.end()` to end all Calls. Never write
115
+ `Toast.end(promise)`: a single argument is the broadcast response position.
116
+ Inside the component, `call.end()` is valid and ends only that instance.
112
117
 
113
118
  ## Hard rules (the common failures)
114
119
 
@@ -136,6 +141,16 @@ tiny callable.
136
141
  swap into an exit-animation state; the instance stays mounted for 200ms
137
142
  after `end()` so a CSS transition (or Vue `<Transition>` wrapping the
138
143
  Callable's stack render) has time to play before removal.
144
+ - **SSR only registers on the client.** Repeated server renders do not count as
145
+ mounted Roots. `call()`/`upsert()` before client `onMounted` throw
146
+ *"No `<Root>` found!"*.
147
+ - **Async components load on demand.** Pass `defineAsyncComponent()` directly
148
+ to `createCallable`; an empty stack does not invoke the loader. Prefer its
149
+ `loadingComponent` and `errorComponent` options for Calls inserted after an
150
+ already-resolved `<Suspense>` boundary.
151
+ - **SFC types need Vue tooling.** Use `vue-tsc --noEmit` to validate cross-file
152
+ `.vue` props; a plain TypeScript shim cannot prove that the business props
153
+ and injected `call` prop match.
139
154
 
140
155
  ## Anti-patterns
141
156
 
@@ -146,9 +161,12 @@ tiny callable.
146
161
  - Treating the Callable as a plain component to render with data props — the
147
162
  props you pass to `<Confirm />` itself are **RootProps** (shared context),
148
163
  never the per-instance data; that always goes through `.call(props)`.
149
- - Reaching into `call.root` for data that changes per-Call — it's constant
150
- for the Root's whole lifetime (whatever `<Confirm />` was mounted with);
151
- put per-Call data in the component's own props instead.
164
+ - Reaching into `call.root` for data that changes per-Call — Root props are
165
+ shared by every active Call (and react to Root prop updates); put data that
166
+ differs per Call in the component's own props instead.
167
+ - Treating a dialog Callable as accessible by default — the library is
168
+ headless. The user component owns naming, initial focus, Tab trapping,
169
+ Escape behavior, focus restoration, and reduced motion.
152
170
 
153
171
  ## Quick reference
154
172