@retronew/call-vue 0.2.0 → 0.3.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
@@ -43,7 +43,7 @@ import type { PropsWithCall } from '@retronew/call-vue'
43
43
  type Props = { message: string }
44
44
  type Response = boolean
45
45
 
46
- defineProps<PropsWithCall<Props, Response, Record<string, never>>>()
46
+ defineProps<PropsWithCall<Props, Response, {}>>()
47
47
  </script>
48
48
 
49
49
  <template>
@@ -109,6 +109,20 @@ async function handleDelete() {
109
109
  instance's position in the stack, how many are open, and whatever props
110
110
  were passed to `<Confirm rootProp="…" />`.
111
111
 
112
+ For a `void` response, the two external forms are intentionally distinct:
113
+
114
+ ```ts
115
+ const promise = Toast.upsert({ text: 'Uploading…' })
116
+
117
+ Toast.end(promise, undefined) // end only this call
118
+ Toast.end() // end every open call
119
+ ```
120
+
121
+ Do not write `Toast.end(promise)`: JavaScript would otherwise interpret that
122
+ single argument as the response for the broadcast overload. The public type
123
+ rejects this form. Inside the user component, `call.end()` remains the natural
124
+ way to finish its own `void` call.
125
+
112
126
  See the [Claude Code skill](skills/call-vue/SKILL.md) for the stacking model,
113
127
  `unmountingDelay` exit-transition pattern, and the single-`<Root>` constraint
114
128
  in depth.
@@ -125,6 +139,73 @@ export const Toast = createCallable<Props, Response>(ToastCard, 200 /* ms */)
125
139
 
126
140
  Inside `ToastCard`, branch on `call.ended` to trigger the leave state.
127
141
 
142
+ ## Root props and TypeScript
143
+
144
+ The third generic is the type of props mounted on the Callable Root. A normal
145
+ interface works directly; it does not need to extend `Record<string, unknown>`:
146
+
147
+ ```ts
148
+ interface RootProps {
149
+ accent: string
150
+ }
151
+
152
+ export const Notice = createCallable<NoticeProps, void, RootProps>(NoticeCard)
153
+ ```
154
+
155
+ ```vue
156
+ <Notice accent="#6366f1" />
157
+ ```
158
+
159
+ Every active card reads the current value through `call.root.accent`. Updating
160
+ the Root prop re-renders active calls. For cross-file `.vue` prop validation,
161
+ run `vue-tsc --noEmit` in addition to a plain TypeScript check; plain `tsc`
162
+ cannot inspect generated SFC props by itself.
163
+
164
+ The Callable itself is the Root component. Mount `<Notice />`; there is no
165
+ separate `.Root` alias. Its public type is Vue's general `Component` shape, so
166
+ it remains valid whether the internal Root is represented as an options object
167
+ or a functional component.
168
+
169
+ ## Async components
170
+
171
+ `defineAsyncComponent()` can be passed directly to `createCallable`. The loader
172
+ is not invoked until the first call creates a component instance:
173
+
174
+ ```ts
175
+ import { defineAsyncComponent } from 'vue'
176
+ import PickerLoadError from './PickerLoadError.vue'
177
+ import PickerLoading from './PickerLoading.vue'
178
+
179
+ const AsyncPicker = defineAsyncComponent({
180
+ loader: () => import('./PickerDialog.vue'),
181
+ loadingComponent: PickerLoading,
182
+ errorComponent: PickerLoadError,
183
+ delay: 0,
184
+ })
185
+
186
+ export const Picker = createCallable<PickerProps, PickedItem>(AsyncPicker)
187
+ ```
188
+
189
+ Use Vue's `loadingComponent`/`errorComponent` options when a call may be added
190
+ after its surrounding `<Suspense>` has already resolved. Ending a call while
191
+ its component is still loading does not resurrect it when the loader finishes.
192
+
193
+ ## SSR
194
+
195
+ Callable Roots are safe to render repeatedly with Vue SSR: server-side setup
196
+ does not register a live Root or leak the Root count into later requests. The
197
+ user component is not rendered on the server while the stack is empty.
198
+ `call()` and `upsert()` remain client-imperative APIs and throw
199
+ `No <Root> found!` until the Root's client `onMounted` hook has run.
200
+
201
+ ## Accessibility responsibility
202
+
203
+ `call-vue` controls stack and Promise lifecycles; it is deliberately headless.
204
+ Dialog semantics remain the user component's responsibility. A production
205
+ dialog should at least provide an accessible name/description, move focus
206
+ inside on open, trap Tab, close with Escape when appropriate, restore focus on
207
+ unmount, and respect reduced-motion preferences.
208
+
128
209
  ## Stacking
129
210
 
130
211
  Every `call()` while a previous one is still open stacks on top of it —
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retronew/call-vue",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Call & await Vue components like async functions — a Vue 3 port of react-call.",
5
5
  "keywords": [
6
6
  "async",
@@ -38,13 +38,13 @@
38
38
  "access": "public"
39
39
  },
40
40
  "devDependencies": {
41
- "@types/node": "^26.2.0",
41
+ "@types/node": "^26.3.0",
42
42
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
43
43
  "@vue/test-utils": "^2.4.11",
44
44
  "jsdom": "^30.0.1",
45
45
  "typescript": "^7.0.2",
46
- "vite-plus": "^0.2.9",
47
- "vitest": "4.1.10",
46
+ "vite-plus": "^0.3.0",
47
+ "vitest": "4.1.11",
48
48
  "vue": "^3.5.41"
49
49
  },
50
50
  "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