@xmachines/play-vue 1.0.0-beta.9 → 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikael Karon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,280 +1,207 @@
1
- # @xmachines/play-vue
1
+ # `@xmachines/play-vue`
2
2
 
3
- **Vue renderer consuming signals and UI schema with provider pattern**
3
+ > Vue 3 renderer for the XMachines Play Architecture — passively observes actor signals and renders UI via `@xmachines/json-render-vue`.
4
4
 
5
- Signal-driven Vue rendering layer observing actor state with zero Vue state for business logic.
5
+ Part of the [XMachines Play monorepo](../../README.md).
6
6
 
7
- ## Overview
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ [![Version](https://img.shields.io/badge/version-1.1.0-blue)](https://www.npmjs.com/package/@xmachines/play-vue)
9
+
10
+ ---
8
11
 
9
- `@xmachines/play-vue` provides `PlayRenderer` for building Vue 3 UIs that passively observe actor signals. This package enables framework-swappable architecture where Vue is just a rendering target subscribing to signal changes — business logic lives entirely in the actor.
12
+ ## Overview
10
13
 
11
- Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md), this package implements:
14
+ `@xmachines/play-vue` is the Vue 3 rendering layer for XMachines Play. It bridges TC39 Signals (actor state) to Vue reactivity and drives component rendering through `@xmachines/json-render-vue`.
12
15
 
13
- - **Signal-Only Reactivity (INV-05):** No refs/reactive for business logic, TC39 signals only
14
- - **Passive Infrastructure (INV-04):** Components observe signals, send events to actor
16
+ **Architecture invariants this package upholds:**
15
17
 
16
- **Key Principle:** Vue state is never used for business logic. Signals are the source of truth.
18
+ - **Passive Infrastructure** Components observe actor signals; they never decide state transitions.
19
+ - **Signal-Only Reactivity** — TC39 Signals are the source of truth; Vue reactivity is used only to trigger re-renders.
20
+ - **Actor Authority** — The actor controls view selection; the renderer reflects it.
17
21
 
18
- Renderer receives actor via props (provider pattern), not children.
22
+ ---
19
23
 
20
24
  ## Installation
21
25
 
22
26
  ```bash
23
- npm install vue@^3.5.0
24
- npm install @xmachines/play-vue
27
+ pnpm add @xmachines/play-vue
25
28
  ```
26
29
 
27
- ## Current Exports
28
-
29
- - `PlayRenderer` (Vue component)
30
- - `PlayRendererProps` (TypeScript interface)
30
+ **Peer dependencies** (install alongside):
31
31
 
32
- **Peer dependencies:**
32
+ ```bash
33
+ pnpm add vue@^3.5.0 xstate@^5.31.0 @xstate/store@^3.17.0 @xmachines/json-render-vue@^0.18.0 @xmachines/json-render-core@^0.18.0 @xmachines/json-render-xstate@^0.18.0
34
+ ```
33
35
 
34
- - `vue` ^3.5.0 — Vue 3 runtime
36
+ ---
35
37
 
36
38
  ## Quick Start
37
39
 
38
40
  ```vue
39
41
  <!-- App.vue -->
42
+ <template>
43
+ <PlayUIProvider :actor="actor" :registryResult="registryResult">
44
+ <PlayRenderer />
45
+ </PlayUIProvider>
46
+ </template>
47
+
40
48
  <script setup lang="ts">
49
+ import { defineRegistry, PlayUIProvider, PlayRenderer } from "@xmachines/play-vue";
41
50
  import { definePlayer } from "@xmachines/play-xstate";
42
- import { defineCatalog } from "@xmachines/play-catalog";
43
- import { PlayRenderer } from "@xmachines/play-vue";
44
- import { z } from "zod";
45
- import LoginForm from "./components/LoginForm.vue";
46
- import Dashboard from "./components/Dashboard.vue";
47
-
48
- // 1. Define catalog (business logic layer)
49
- const catalog = defineCatalog({
50
- LoginForm: z.object({ error: z.string().optional() }),
51
- Dashboard: z.object({
52
- userId: z.string(),
53
- username: z.string(),
54
- }),
55
- });
51
+ import { myMachine } from "./machine.js";
52
+ import { myCatalog } from "./catalog.js";
53
+ import HomeSFC from "./views/Home.vue";
54
+ import LoginSFC from "./views/Login.vue";
56
55
 
57
- // 2. Define component map (view layer)
58
- const components = {
59
- LoginForm,
60
- Dashboard,
61
- };
62
-
63
- // 3. Create player actor (business logic runtime)
64
- const createPlayer = definePlayer({ machine: authMachine, catalog });
56
+ const createPlayer = definePlayer({ machine: myMachine });
65
57
  const actor = createPlayer();
66
58
  actor.start();
67
- </script>
68
59
 
69
- <template>
70
- <!-- 4. Render UI (actor via props) -->
71
- <PlayRenderer :actor="actor" :components="components">
72
- <template #fallback>
73
- <div>Loading...</div>
74
- </template>
75
- </PlayRenderer>
76
- </template>
60
+ const registryResult = defineRegistry(myCatalog, {
61
+ components: {
62
+ Home: HomeSFC, // .vue SFCs are auto-wrapped
63
+ Login: LoginSFC,
64
+ },
65
+ actions: {
66
+ login: async (args) => actor.send({ type: "auth.login", ...args }),
67
+ logout: async () => actor.send({ type: "auth.logout" }),
68
+ },
69
+ });
70
+ </script>
77
71
  ```
78
72
 
79
- ## API Reference
73
+ ---
80
74
 
81
- ### PlayRenderer
75
+ ## API Summary
82
76
 
83
- Main renderer component subscribing to actor signals and dynamically rendering catalog components:
77
+ ### Components
84
78
 
85
- ```typescript
86
- interface PlayRendererProps {
87
- actor: AbstractActor<any>;
88
- components: Record<string, Component>;
89
- }
90
- ```
79
+ #### `<PlayUIProvider>`
91
80
 
92
- **Props:**
81
+ Batteries-included composite provider. Wraps `<ActorProvider>` and `JSONUIProvider` in one component. **Recommended for most apps.**
93
82
 
94
- - `actor` - Actor instance with `currentView` signal
95
- - `components` - Map of component names to Vue components
83
+ | Prop | Type | Required | Description |
84
+ | --------------------- | -------------------------- | -------- | ------------------------------------------- |
85
+ | `actor` | `AbstractActor & Viewable` | ✅ | The XMachines actor instance |
86
+ | `registryResult` | `DefineRegistryResult` | ✅ | Result of `defineRegistry()` |
87
+ | `store` | `StateStore` | — | External controlled state store (optional) |
88
+ | `onRenderError` | `RenderErrorHandler` | — | Error handler for render failures |
89
+ | `navigate` | `(path: string) => void` | — | Link navigation function |
90
+ | `validationFunctions` | `Record<string, Function>` | — | Custom validation functions |
91
+ | `functions` | `Record<string, Function>` | — | Named functions for `$computed` expressions |
96
92
 
97
- **Slots:**
93
+ **Slots:** `default` (rendered content), `fallback` (shown while actor view is `null`)
98
94
 
99
- - `fallback` - Slot shown when `currentView` is null
95
+ #### `<PlayRenderer>`
100
96
 
101
- **Behavior:**
97
+ Zero-prop leaf component. Reads the current `spec` and `registry` from the nearest `<ActorProvider>` or `<PlayUIProvider>` context and renders via `<Renderer>`. Must be placed inside one of those providers.
102
98
 
103
- 1. Subscribes to `actor.currentView` signal using a `Signal.subtle.Watcher` inside a Vue component
104
- 2. Looks up component from `components` map using `view.component` string
105
- 3. Renders component with props from `view.props` + `send` function via Vue's dynamic `<component :is="..."/>`
99
+ ```vue
100
+ <PlayUIProvider :actor="actor" :registryResult="registryResult">
101
+ <PlayRenderer />
102
+ </PlayUIProvider>
103
+ ```
106
104
 
107
- **Example Component (LoginForm.vue):**
105
+ #### `<ActorProvider>`
108
106
 
109
- ```vue
110
- <script setup lang="ts">
111
- import type { AbstractActor } from "@xmachines/play-actor";
112
- import { ref } from "vue";
113
-
114
- const props = defineProps<{
115
- error?: string;
116
- send: AbstractActor<any>["send"];
117
- }>();
118
-
119
- const username = ref("");
120
-
121
- function handleSubmit() {
122
- props.send({
123
- type: "auth.login",
124
- username: username.value,
125
- });
126
- }
127
- </script>
107
+ Low-level escape hatch for custom provider composition. Owns the full actor lifecycle — signal subscription, per-view state store, handler resolution, and Vue context provision. Use `<PlayUIProvider>` unless you need fine-grained control.
128
108
 
129
- <template>
130
- <form @submit.prevent="handleSubmit">
131
- <p v-if="error" style="color: red">{{ error }}</p>
132
- <input v-model="username" required placeholder="Username" />
133
- <button type="submit">Log In</button>
134
- </form>
135
- </template>
136
- ```
109
+ | Prop | Type | Required | Description |
110
+ | ---------------- | -------------------------- | -------- | ------------------------------- |
111
+ | `actor` | `AbstractActor & Viewable` | ✅ | The XMachines actor instance |
112
+ | `registryResult` | `DefineRegistryResult` | ✅ | Result of `defineRegistry()` |
113
+ | `store` | `StateStore` | — | External controlled state store |
114
+ | `onRenderError` | `RenderErrorHandler` | — | Override render error handler |
137
115
 
138
- ## Examples
116
+ ---
139
117
 
140
- ### Provider Pattern
118
+ ### Functions
141
119
 
142
- ```vue
143
- <!-- App.vue -->
144
- <script setup lang="ts">
145
- import { PlayVueRouterProvider } from "@xmachines/play-vue-router";
146
- import { PlayRenderer } from "@xmachines/play-vue";
147
- import { provide } from "vue";
148
- import Header from "./components/Header.vue";
149
- import Footer from "./components/Footer.vue";
150
-
151
- // Provide actor to nested components like Header
152
- provide("actor", actor);
153
- </script>
120
+ #### `defineRegistry(catalog, options)`
154
121
 
155
- <template>
156
- <PlayVueRouterProvider :actor="actor" :router="router" :routeMap="routeMap">
157
- <template #default="{ currentActor, currentRouter }">
158
- <div>
159
- <Header />
160
- <PlayRenderer :actor="currentActor" :components="components" />
161
- <Footer />
162
- </div>
163
- </template>
164
- </PlayVueRouterProvider>
165
- </template>
166
- ```
122
+ Drop-in replacement for `defineRegistry` from `@xmachines/json-render-vue`. **Always import from `@xmachines/play-vue`** rather than `@xmachines/json-render-vue` when working with Vue SFCs — this wrapper automatically detects `.vue` SFCs in the `components` map and wraps them via `h()` so Vue composables (including `inject`-based ones) work correctly inside `<script setup>`.
167
123
 
168
- ```vue
169
- <!-- Header.vue -->
170
- <script setup lang="ts">
171
- import { inject, ref, onMounted, onUnmounted } from "vue";
172
- import type { AbstractActor } from "@xmachines/play-actor";
173
-
174
- const actor = inject<AbstractActor<any>>("actor")!;
175
- const route = ref<string | null>(null);
176
-
177
- let watcher: any;
178
-
179
- onMounted(() => {
180
- let pending = false;
181
- watcher = new Signal.subtle.Watcher(() => {
182
- if (!pending) {
183
- pending = true;
184
- queueMicrotask(() => {
185
- pending = false;
186
- for (const s of watcher.getPending()) s.get();
187
- route.value = actor.currentRoute.get();
188
- watcher.watch(actor.currentRoute);
189
- });
190
- }
191
- });
192
- route.value = actor.currentRoute.get();
193
- watcher.watch(actor.currentRoute);
124
+ ```typescript
125
+ import { defineRegistry } from "@xmachines/play-vue";
126
+ // NOT: import { defineRegistry } from "@xmachines/json-render-vue"
127
+
128
+ import LoginSFC from "./views/Login.vue";
129
+ import DashboardSFC from "./views/Dashboard.vue";
130
+
131
+ const registryResult = defineRegistry(catalog, {
132
+ components: {
133
+ Login: LoginSFC, // .vue SFC — auto-wrapped via h()
134
+ Dashboard: DashboardSFC,
135
+ },
136
+ actions: {
137
+ login: async (args, setState, getState) => {
138
+ /* ... */
139
+ },
140
+ },
194
141
  });
142
+ ```
195
143
 
196
- onUnmounted(() => {
197
- if (watcher) watcher.unwatch(actor.currentRoute);
198
- });
199
- </script>
144
+ Plain `ComponentFn` functions (non-SFC) also work and are passed through unchanged. Mixing SFCs and plain functions in the same registry is supported.
200
145
 
201
- <template>
202
- <header>
203
- <nav>Current: {{ route }}</nav>
204
- </header>
205
- </template>
146
+ #### `useActor()`
147
+
148
+ Vue composable for accessing the raw actor inside a `PlayRenderer` tree. Avoids prop drilling for deeply nested components.
149
+
150
+ ```typescript
151
+ import { useActor } from "@xmachines/play-vue";
152
+
153
+ // Inside a component rendered by PlayRenderer:
154
+ const actor = useActor();
155
+ actor.send({ type: "SUBMIT" });
206
156
  ```
207
157
 
208
- ## Architecture
158
+ Throws if called outside an `<ActorProvider>` or `<PlayUIProvider>` tree.
209
159
 
210
- This package implements **Signal-Only Reactivity (INV-05)** and **Passive Infrastructure (INV-04)**:
160
+ #### `getPlayViewContext()`
211
161
 
212
- 1. **No Business Logic in Vue:**
213
- - No ref/reactive for business state
214
- - No watch/watchEffect for business side effects
215
- - Vue only triggers renders, doesn't control state
162
+ Access the current `ViewContextValue` `{ spec, handlers, registry, store }` — from inside an `<ActorProvider>` tree.
216
163
 
217
- 2. **Signals as Source of Truth:**
218
- - `actor.currentView.get()` provides UI structure
219
- - `actor.currentRoute.get()` provides navigation state
220
- - Components observe signals via explicit watcher patterns
164
+ ```typescript
165
+ import { getPlayViewContext } from "@xmachines/play-vue";
221
166
 
222
- 3. **Event Forwarding:**
223
- - Components receive `send` function via props
224
- - User actions send events to actor (e.g., `{ type: "auth.login" }`)
225
- - Actor guards validate and process events
167
+ // Inside setup() of a component within an ActorProvider tree:
168
+ const view = getPlayViewContext();
169
+ // view.spec, view.handlers, view.registry, view.store
170
+ ```
226
171
 
227
- 4. **Microtask Batching:**
228
- - `Signal.subtle.Watcher` coalesces rapid signal changes
229
- - Prevents Vue thrashing from multiple signal updates
230
- - Single Vue render per microtask batch
172
+ ---
231
173
 
232
- 5. **Explicit Disposal Contract:**
233
- - Component teardown calls watcher `unwatch` in `onUnmounted`
234
- - Do not rely on GC-only cleanup
174
+ ### Re-exported from `@xmachines/json-render-vue`
235
175
 
236
- **Pattern:**
176
+ The following are re-exported so consumers import everything from `@xmachines/play-vue`:
237
177
 
238
- - Renderer receives actor via props (provider pattern)
239
- - Enables composition with navigation, headers, footers
240
- - Supports multiple renderers in same app
178
+ **Components:** `JSONUIProvider`, `StateProvider`, `ActionProvider`, `VisibilityProvider`, `ValidationProvider`, `Renderer`
241
179
 
242
- **Architectural Invariants:**
180
+ **Composables:** `useBoundProp`
243
181
 
244
- - **Signal-Only Reactivity (INV-05):** No Vue state for business logic
245
- - **Passive Infrastructure (INV-04):** Components reflect, never decide
182
+ **Types:** `JSONUIProviderProps`, `StateProviderProps`, `ActionProviderProps`, `ValidationProviderProps`, `RendererProps`, `ComponentFn`, `ComponentContext`, `DefineRegistryResult`
246
183
 
247
- ## Canonical Watcher Lifecycle
184
+ ---
248
185
 
249
- If you write your own custom integration, use the same watcher flow as `PlayRenderer`:
186
+ ## Testing
250
187
 
251
- 1. `notify` callback runs
252
- 2. Schedule work with `queueMicrotask`
253
- 3. Drain `watcher.getPending()`
254
- 4. Read actor signals and update Vue-local ref state
255
- 5. Re-arm with `watch(...)` or `watch()`
188
+ Run tests for this package in isolation:
256
189
 
257
- Watcher notify is one-shot. Re-arm is required for continuous observation.
190
+ ```bash
191
+ # From the monorepo root
192
+ pnpm --filter @xmachines/play-vue test
258
193
 
259
- ## Benefits
194
+ # Watch mode
195
+ pnpm --filter @xmachines/play-vue run test:watch
260
196
 
261
- - **Framework Swappable:** Business logic has zero Vue imports
262
- - **Type Safety:** Props validated against catalog schemas
263
- - **Simple Testing:** Test actors without Vue renderer
264
- - **Performance:** Microtask batching reduces unnecessary renders
265
- - **Composability:** Renderer prop enables complex layouts
197
+ # With coverage (80% threshold enforced on lines, functions, branches, statements)
198
+ pnpm exec vitest run --coverage --config packages/play-vue/vitest.config.ts
199
+ ```
266
200
 
267
- ## Related Packages
201
+ Tests use [Vitest](https://vitest.dev/) with `jsdom` environment and `@vue/test-utils` for component mounting.
268
202
 
269
- - **[@xmachines/play-xstate](../play-xstate)** - XState adapter providing actors
270
- - **[@xmachines/play-catalog](../play-catalog)** - UI schema validation
271
- - **[@xmachines/play-vue-router](../play-vue-router)** - Vue Router integration
272
- - **[@xmachines/play-actor](../play-actor)** - Actor base
273
- - **[@xmachines/play-signals](../play-signals)** - TC39 Signals primitives
203
+ ---
274
204
 
275
205
  ## License
276
206
 
277
- Copyright (c) 2016 [Mikael Karon](mailto:mikael@karon.se). All rights reserved.
278
-
279
- This work is licensed under the terms of the MIT license.
280
- For a copy, see <https://opensource.org/licenses/MIT>.
207
+ MIT see [LICENSE](LICENSE).
@@ -0,0 +1,40 @@
1
+ /**
2
+ * ViewContextValue and getPlayViewContext — shared context types for Vue provider architecture.
3
+ *
4
+ * Extracted from ActorProvider.vue so TypeScript can re-export these types from index.ts.
5
+ * (The vue-shim.d.ts only declares a default export from *.vue files, so named exports
6
+ * from .vue SFCs are not visible to TypeScript's re-export resolution.)
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import type { InjectionKey } from "vue";
11
+ import type { ComponentRegistry } from "@xmachines/json-render-vue";
12
+ import type { BaseViewContextValue } from "@xmachines/play-actor";
13
+ /**
14
+ * Context value provided by ActorProvider and consumed by PlayRenderer (zero-prop leaf).
15
+ * Accessible via getPlayViewContext() inside any ActorProvider/PlayUIProvider tree.
16
+ */
17
+ export interface ViewContextValue extends BaseViewContextValue<ComponentRegistry> {
18
+ }
19
+ /**
20
+ * Injection key for the ViewContextValue. Exported so ActorProvider.vue can use it
21
+ * as the canonical key without re-declaring.
22
+ *
23
+ * @internal Use getPlayViewContext() as the public API; do not inject ViewKey directly.
24
+ */
25
+ export declare const ViewKey: InjectionKey<ViewContextValue>;
26
+ /**
27
+ * Access the current ViewContextValue from inside an ActorProvider tree.
28
+ *
29
+ * @throws {Error} If called outside an <ActorProvider> (or <PlayUIProvider>) tree
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * import { getPlayViewContext } from "@xmachines/play-vue";
34
+ *
35
+ * const view = getPlayViewContext();
36
+ * // view.spec, view.handlers, view.registry
37
+ * ```
38
+ */
39
+ export declare function getPlayViewContext(): ViewContextValue;
40
+ //# sourceMappingURL=actor-provider-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actor-provider-context.d.ts","sourceRoot":"","sources":["../src/actor-provider-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,KAAK,CAAC;AAExC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAElE;;;GAGG;AACH,MAAM,WAAW,gBAAiB,SAAQ,oBAAoB,CAAC,iBAAiB,CAAC;CAAG;AAEpF;;;;;GAKG;AACH,eAAO,MAAM,OAAO,EAAE,YAAY,CAAC,gBAAgB,CAA4B,CAAC;AAEhF;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,IAAI,gBAAgB,CAErD"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `defineRegistry` wrapper for @xmachines/play-vue.
3
+ *
4
+ * Wraps `defineRegistry` from `@xmachines/json-render-vue` with automatic SFC support.
5
+ *
6
+ * ## Vue-specific — React and Solid do not need this
7
+ *
8
+ * In React, `useContext()` works anywhere inside the component call tree.
9
+ * In Solid, `useContext()` works inside reactive computations and component renders.
10
+ * Neither has the strict "synchronous setup() only" constraint that Vue's `inject()`
11
+ * imposes — so their `defineRegistry` implementations call `componentFn(ctx)` directly
12
+ * with no wrapping needed.
13
+ *
14
+ * For the DOM renderer, there is no component system at all — just render functions
15
+ * returning `HTMLElement` — so injection context is not applicable.
16
+ *
17
+ * Only Vue requires this adapter.
18
+ *
19
+ * ## Why this wrapper exists
20
+ *
21
+ * `@xmachines/json-render-vue`'s `defineRegistry` calls each registered component as a plain
22
+ * function: `componentFn(ctx)`. A `.vue` SFC (output of `defineComponent` or
23
+ * `<script setup>`) is an **object**, not a function — calling it throws.
24
+ *
25
+ * More fundamentally, `defineRegistry` calls components inside its own render
26
+ * function (the return value of `setup()`). Vue's `inject()` — and composables built
27
+ * on it: `useStateBinding`, `useStateStore` — only work during synchronous `setup()`
28
+ * execution, not inside render functions. Plain `.ts` `ComponentFn` files cannot
29
+ * call any Vue composable for this reason.
30
+ *
31
+ * This wrapper auto-detects Vue SFCs in the `components` map and wraps them via
32
+ * `h(SFC, ctx)`. The SFC renders as a child component with its own `setup()`,
33
+ * giving full access to composables inside `<script setup>`.
34
+ *
35
+ * ## Usage
36
+ *
37
+ * Import `defineRegistry` from `@xmachines/play-vue` instead of `@xmachines/json-render-vue`:
38
+ *
39
+ * ```ts
40
+ * import { defineRegistry } from "@xmachines/play-vue";
41
+ * // not: import { defineRegistry } from "@xmachines/json-render-vue";
42
+ *
43
+ * import LoginSFC from "./views/Login.vue";
44
+ * import DashboardSFC from "./views/Dashboard.vue";
45
+ *
46
+ * const { registry } = defineRegistry(catalog, {
47
+ * components: {
48
+ * Login: LoginSFC, // .vue SFC — auto-wrapped
49
+ * Dashboard: DashboardSFC, // .vue SFC — auto-wrapped
50
+ * },
51
+ * });
52
+ * ```
53
+ *
54
+ * Plain `ComponentFn` functions still work and are passed through unchanged.
55
+ * Mixing SFCs and plain functions in the same registry is supported.
56
+ */
57
+ import { type Component } from "vue";
58
+ import { defineRegistry as defineRegistryBase, type ComponentFn } from "@xmachines/json-render-vue";
59
+ import type { Catalog, InferCatalogComponents } from "@xmachines/json-render-core";
60
+ export type ComponentEntry<C extends Catalog, K extends keyof InferCatalogComponents<C>> = ComponentFn<C, K> | Component;
61
+ export type ComponentsMap<C extends Catalog> = {
62
+ [K in keyof InferCatalogComponents<C>]?: ComponentEntry<C, K>;
63
+ };
64
+ export type DefineRegistryOptions<C extends Catalog> = Omit<Parameters<typeof defineRegistryBase<C>>[1], "components"> & {
65
+ components?: ComponentsMap<C>;
66
+ };
67
+ /**
68
+ * Create a component registry, automatically wrapping `.vue` SFCs so they work
69
+ * correctly with `@xmachines/json-render-vue`'s rendering pipeline.
70
+ *
71
+ * Drop-in replacement for `defineRegistry` from `@xmachines/json-render-vue`. Import from
72
+ * `@xmachines/play-vue` to get SFC support for free.
73
+ *
74
+ * @param catalog - The json-render catalog defining component prop shapes.
75
+ * @param options - Registry options. `components` entries may be `.vue` SFCs
76
+ * (objects) or plain `ComponentFn` functions — both are handled automatically.
77
+ */
78
+ export declare function defineRegistry<C extends Catalog>(catalog: C, options: DefineRegistryOptions<C>): ReturnType<typeof defineRegistryBase<C>>;
79
+ //# sourceMappingURL=define-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-registry.d.ts","sourceRoot":"","sources":["../src/define-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,OAAO,EAAK,KAAK,SAAS,EAAE,MAAM,KAAK,CAAC;AACxC,OAAO,EACN,cAAc,IAAI,kBAAkB,EAEpC,KAAK,WAAW,EAEhB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAwCnF,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,MAAM,sBAAsB,CAAC,CAAC,CAAC,IACpF,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,GACjB,SAAS,CAAC;AAEb,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,OAAO,IAAI;KAC7C,CAAC,IAAI,MAAM,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC;CAC7D,CAAC;AAEF,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,OAAO,IAAI,IAAI,CAC1D,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAC3C,YAAY,CACZ,GAAG;IACH,UAAU,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;CAC9B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,OAAO,EAC/C,OAAO,EAAE,CAAC,EACV,OAAO,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAC/B,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAa1C"}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,27 @@
1
1
  /**
2
- * @xmachines/play-vue - Vue renderer for XMachines Play architecture
2
+ * @xmachines/play-vue - Vue 3 renderer for XMachines Play architecture
3
+ *
4
+ * Provides a thin Vue rendering layer that passively observes actor signals
5
+ * and renders UI components via @xmachines/json-render-vue. Vue reactivity is only used
6
+ * to trigger re-renders — signals are the source of truth.
7
+ *
8
+ * Re-exports `defineRegistry` (SFC-aware — auto-wraps `.vue` SFCs via `h()`),
9
+ * `useBoundProp`, `ComponentFn`, `ComponentContext`, and all json-render providers
10
+ * so consumers import everything from `@xmachines/play-vue` rather than
11
+ * `@xmachines/json-render-vue` directly.
3
12
  *
4
13
  * @packageDocumentation
5
14
  */
6
15
  export { default as PlayRenderer } from "./PlayRenderer.vue";
7
- export type { PlayRendererProps } from "./types.js";
16
+ export { default as ActorProvider } from "./ActorProvider.vue";
17
+ export { default as PlayUIProvider } from "./PlayUIProvider.vue";
18
+ export type { ActorProviderProps, PlayUIProviderProps, RenderErrorHandler, VisibilityProviderProps, } from "./types.js";
19
+ export { getPlayViewContext } from "./actor-provider-context.js";
20
+ export type { ViewContextValue } from "./actor-provider-context.js";
21
+ export { useActor } from "./useActor.js";
22
+ export type { AnyPlayActor } from "./useActor.js";
23
+ export { defineRegistry } from "./define-registry.js";
24
+ export type { DefineRegistryOptions, ComponentsMap, ComponentEntry } from "./define-registry.js";
25
+ export { JSONUIProvider, StateProvider, ActionProvider, VisibilityProvider, ValidationProvider, Renderer, useBoundProp, } from "@xmachines/json-render-vue";
26
+ export type { JSONUIProviderProps, StateProviderProps, ActionProviderProps, ValidationProviderProps, RendererProps, ComponentFn, ComponentContext, DefineRegistryResult, } from "@xmachines/json-render-vue";
8
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC7D,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGjE,YAAY,EACX,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,uBAAuB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAGpE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGlD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,qBAAqB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGjG,OAAO,EACN,cAAc,EACd,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,QAAQ,EACR,YAAY,GACZ,MAAM,4BAA4B,CAAC;AACpC,YAAY,EACX,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,oBAAoB,GACpB,MAAM,4BAA4B,CAAC"}
package/dist/types.d.ts CHANGED
@@ -3,19 +3,26 @@
3
3
  *
4
4
  * @packageDocumentation
5
5
  */
6
- import type { AbstractActor, Viewable } from "@xmachines/play-actor";
7
- import type { Component } from "vue";
8
- import type { AnyActorLogic } from "xstate";
6
+ import type { DefineRegistryResult } from "@xmachines/json-render-vue";
7
+ import type { BaseActorProviderProps } from "@xmachines/play-actor";
9
8
  /**
10
- * Props for PlayRenderer component
11
- *
12
- * @property actor - Actor instance with currentView signal (requires Viewable capability)
13
- * @property components - Map of component names to Vue components
9
+ * Props for the ActorProvider component.
10
+ * Extracted to types.ts so TypeScript can re-export without the vue-shim limitation.
14
11
  */
15
- export interface PlayRendererProps {
16
- /** Actor instance with currentView signal (requires Viewable capability) */
17
- actor: AbstractActor<AnyActorLogic> & Viewable;
18
- /** Map of component names to Vue components */
19
- components: Record<string, Component>;
12
+ export interface ActorProviderProps extends BaseActorProviderProps<DefineRegistryResult> {
13
+ }
14
+ /**
15
+ * Props for PlayUIProvider all ActorProvider props plus JSONUIProvider's own props.
16
+ */
17
+ export type { RenderErrorHandler } from "@xmachines/json-render-vue";
18
+ export interface VisibilityProviderProps {
19
+ }
20
+ export interface PlayUIProviderProps extends ActorProviderProps {
21
+ /** Navigate function forwarded to JSONUIProvider for link resolution */
22
+ navigate?: (path: string) => void;
23
+ /** Validation functions forwarded to JSONUIProvider */
24
+ validationFunctions?: Record<string, (value: unknown, args?: Record<string, unknown>) => boolean>;
25
+ /** Named functions for $computed expressions in props */
26
+ functions?: Record<string, (args?: Record<string, unknown>, state?: Record<string, unknown>) => unknown>;
20
27
  }
21
28
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AACrC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IACjC,4EAA4E;IAC5E,KAAK,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,QAAQ,CAAC;IAE/C,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACtC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAEpE;;;GAGG;AACH,MAAM,WAAW,kBAAmB,SAAQ,sBAAsB,CAAC,oBAAoB,CAAC;CAAG;AAE3F;;GAEG;AACH,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAIrE,MAAM,WAAW,uBAAuB;CAAG;AAE3C,MAAM,WAAW,mBAAoB,SAAQ,kBAAkB;IAC9D,wEAAwE;IACxE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,uDAAuD;IACvD,mBAAmB,CAAC,EAAE,MAAM,CAC3B,MAAM,EACN,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAC3D,CAAC;IACF,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CACjB,MAAM,EACN,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAC5E,CAAC;CACF"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * useActor — Vue composable for accessing the provided actor inside a PlayRenderer tree.
3
+ *
4
+ * Components rendered inside PlayRenderer can call useActor() to reach the actor
5
+ * instance without prop drilling.
6
+ *
7
+ * What is returned is a **swap-following proxy**, not the raw actor object:
8
+ * - It is a stable reference for the lifetime of the injection, yet forwards every
9
+ * operation (property reads/writes, `in`, `Object.keys` / spread, `instanceof`)
10
+ * to whichever actor is CURRENTLY provided — so it transparently follows a
11
+ * `props.actor` swap on `<ActorProvider>` without consumers re-injecting.
12
+ * - Method identity is stable per underlying method: `actor.send === actor.send`
13
+ * across repeated reads (allocation-free), and rebinds only when the underlying
14
+ * method changes or the actor is swapped.
15
+ * - It is NOT `===` the `actor` prop passed to `<ActorProvider>` (it is a distinct
16
+ * Proxy object). Any consumer keyed on actor identity — most notably a router
17
+ * bridge extending `RouterBridgeBase`, whose one-bridge-per-actor guard keys a
18
+ * WeakMap on the actor instance — must be given the actor prop itself, not the
19
+ * value returned by `useActor()`.
20
+ *
21
+ * @throws {Error} If called outside a PlayRenderer tree
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * import { useActor } from "@xmachines/play-vue";
26
+ *
27
+ * const actor = useActor();
28
+ * actor.send({ type: "SUBMIT" });
29
+ * ```
30
+ *
31
+ * @packageDocumentation
32
+ */
33
+ import type { InjectionKey } from "vue";
34
+ import type { AbstractActor } from "@xmachines/play-actor";
35
+ import type { AnyActorLogic } from "xstate";
36
+ /** Bare actor type accepted by Vue context providers. For the full routing + view shape, use `PlayActor` from `@xmachines/play-router`. */
37
+ export type AnyPlayActor = AbstractActor<AnyActorLogic>;
38
+ export declare const ActorKey: InjectionKey<AnyPlayActor>;
39
+ /**
40
+ * Provide the actor to all descendant components via Vue's inject/provide mechanism.
41
+ *
42
+ * Called inside `PlayRenderer.vue`'s `setup()` to make the actor available to any
43
+ * child component that calls `useActor()`. Not typically needed outside framework
44
+ * internals unless building a custom renderer wrapper.
45
+ *
46
+ * @param actor - The actor instance to inject into the component tree.
47
+ */
48
+ export declare function provideActor(actor: AnyPlayActor): void;
49
+ export declare function useActor(): AnyPlayActor;
50
+ //# sourceMappingURL=useActor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useActor.d.ts","sourceRoot":"","sources":["../src/useActor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,KAAK,CAAC;AAExC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAE5C,2IAA2I;AAC3I,MAAM,MAAM,YAAY,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC;AAExD,eAAO,MAAM,QAAQ,EAAE,YAAY,CAAC,YAAY,CAA6B,CAAC;AAE9E;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAEtD;AAED,wBAAgB,QAAQ,IAAI,YAAY,CAEvC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmachines/play-vue",
3
- "version": "1.0.0-beta.9",
3
+ "version": "1.1.0",
4
4
  "description": "Vue renderer for XMachines Play architecture",
5
5
  "keywords": [
6
6
  "reactive",
@@ -10,9 +10,11 @@
10
10
  "xmachines"
11
11
  ],
12
12
  "license": "MIT",
13
+ "author": "Mikael Karon <mikael@karon.se>",
13
14
  "repository": {
14
15
  "type": "git",
15
- "url": "git@gitlab.com:xmachin-es/xmachines-js.git"
16
+ "url": "git+ssh://git@gitlab.com/xmachin-es/xmachines-js.git",
17
+ "directory": "packages/play-vue"
16
18
  },
17
19
  "files": [
18
20
  "dist",
@@ -20,6 +22,7 @@
20
22
  "LICENSE"
21
23
  ],
22
24
  "type": "module",
25
+ "sideEffects": false,
23
26
  "main": "./dist/index.js",
24
27
  "types": "./dist/index.d.ts",
25
28
  "exports": {
@@ -33,27 +36,46 @@
33
36
  },
34
37
  "scripts": {
35
38
  "build": "vite build && tsc --build --force",
36
- "clean": "rm -rf dist node_modules/.vite node_modules/.vite-temp",
37
- "typecheck": "tsc --noEmit",
39
+ "clean": "rm -rf dist *.tsbuildinfo coverage .vitest-attachments test/browser/__screenshots__ node_modules/.svelte2tsx-* node_modules/.vite*",
40
+ "lint": "oxlint .",
41
+ "format": "oxfmt .",
38
42
  "test": "vitest",
39
- "test:watch": "vitest",
40
- "prepublishOnly": "npm run build"
43
+ "test:watch": "vitest"
41
44
  },
42
45
  "dependencies": {
43
- "@xmachines/play-actor": "1.0.0-beta.9",
44
- "@xmachines/play-catalog": "1.0.0-beta.9",
45
- "@xmachines/play-signals": "1.0.0-beta.9"
46
+ "@xmachines/play": "1.1.0",
47
+ "@xmachines/play-actor": "1.1.0",
48
+ "@xmachines/play-signals": "1.1.0"
46
49
  },
47
50
  "devDependencies": {
48
- "@types/node": "^25.5.0",
51
+ "@testing-library/jest-dom": "^6.9.1",
52
+ "@types/node": "^26.2.0",
49
53
  "@vitejs/plugin-vue": "^6.0.5",
50
- "@vue/test-utils": "^2.4.6",
51
- "@xmachines/shared": "1.0.0-beta.9",
52
- "typescript": "^5.9.3",
53
- "vitest": "^4.1.0",
54
- "vue": "^3.5.0"
54
+ "@vitest/browser-playwright": "^4.1.10",
55
+ "@vue/test-utils": "^2.4.9",
56
+ "@xmachines/json-render-core": "^0.19.0-xm.2",
57
+ "@xmachines/json-render-vue": "^0.19.0-xm.2",
58
+ "@xmachines/json-render-xstate": "^0.19.0-xm.2",
59
+ "@xstate/store": "^3.17.0",
60
+ "oxfmt": "^0.64.0",
61
+ "oxlint": "^1.79.0",
62
+ "typescript": "^5.9.3 || ^6.0.3",
63
+ "vite": "^8.0.10",
64
+ "vite-plugin-dts": "^4.5.4",
65
+ "vitest": "^4.1.11",
66
+ "vue": "^3.5.33",
67
+ "xstate": "^5.31.0",
68
+ "zod": "^4.4.1"
55
69
  },
56
70
  "peerDependencies": {
57
- "vue": "^3.5.0"
71
+ "@xmachines/json-render-core": "^0.19.0-xm.2",
72
+ "@xmachines/json-render-vue": "^0.19.0-xm.2",
73
+ "@xmachines/json-render-xstate": "^0.19.0-xm.2",
74
+ "@xstate/store": "^3.17.0",
75
+ "vue": "^3.5.0",
76
+ "xstate": "^5.31.0"
77
+ },
78
+ "engines": {
79
+ "node": ">=22.0.0"
58
80
  }
59
81
  }
@@ -1,9 +0,0 @@
1
- import e from "./PlayRenderer.vue_vue_type_script_lang.js";
2
- /* empty css */
3
- import t from "./_virtual/_plugin-vue_export-helper.js";
4
- //#region src/PlayRenderer.vue
5
- var n = /* @__PURE__ */ t(e, [["__scopeId", "data-v-275eda6e"]]);
6
- //#endregion
7
- export { n as default };
8
-
9
- //# sourceMappingURL=PlayRenderer.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PlayRenderer.js","names":[],"sources":["../src/PlayRenderer.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * PlayRenderer - Main Vue renderer component for XMachines Play architecture\n *\n * Architecture (per RESEARCH.md Pattern 1):\n * - Subscribes to actor.currentView signal via Signal.subtle.Watcher\n * - Dynamically renders catalog components based on view.component string\n * - Forwards user events to actor via actor.send()\n * - Vue ref only for triggering renders, NOT business logic\n *\n * Signal bridge uses one-shot re-watch pattern:\n * TC39 Signal watchers stop watching after notification, so watcher.watch()\n * must be called inside a microtask after getPending() to re-arm for the\n * next notification.\n *\n * CRITICAL: Never call signal.get() or signal.set() inside the Watcher's\n * notify callback. The callback runs synchronously during the signal graph's\n * dirty-propagation phase. All reads must be deferred to a queueMicrotask.\n *\n * @invariant Actor Authority - Actor decides all state transitions via guards\n * @invariant Passive Infrastructure - Component observes signals, sends events\n * @invariant Signal-Only Reactivity - Business logic state lives in actor signals\n */\n\nimport { defineComponent, ref, computed, toRaw, onUnmounted, h, type PropType } from \"vue\";\nimport { Signal } from \"@xmachines/play-signals\";\nimport type { PlayRendererProps } from \"./types.js\";\nimport type { AbstractActor, Viewable } from \"@xmachines/play-actor\";\nimport type { AnyActorLogic } from \"xstate\";\nimport type { Component } from \"vue\";\n\nexport default defineComponent({\n\tname: \"PlayRenderer\",\n\tprops: {\n\t\tactor: {\n\t\t\ttype: Object as PropType<AbstractActor<AnyActorLogic> & Viewable>,\n\t\t\trequired: true,\n\t\t},\n\t\tcomponents: {\n\t\t\ttype: Object as PropType<Record<string, Component>>,\n\t\t\trequired: true,\n\t\t},\n\t},\n\tsetup(props, { slots }) {\n\t\t// Unwrap actor from Vue's reactive proxy to access raw Signal objects\n\t\t// CRITICAL: toRaw() preserves Signal's 'this' binding for .get() and watcher operations\n\t\tconst actor = toRaw(props.actor);\n\n\t\t// Get initial value from unwrapped signal\n\t\tconst initialView = actor.currentView.get();\n\n\t\t// Vue ref for triggering re-renders (NOT business logic state)\n\t\t// Signal is source of truth, ref is just Vue's render trigger\n\t\tconst view = ref<{ component: string; props: Record<string, unknown> } | null>(initialView);\n\n\t\t// Signal watcher for bridging TC39 Signals to Vue reactivity\n\t\t// Created immediately (not in onMounted) so signal changes during the\n\t\t// synchronous mount phase are captured.\n\t\t//\n\t\t// The notify callback runs synchronously during the signal graph's\n\t\t// dirty-propagation phase. NEVER read (.get()) or write (.set()) signals\n\t\t// inside it. Only schedule a microtask to do the actual work.\n\t\tconst watcher = new Signal.subtle.Watcher(() => {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\t// Step 1: Acknowledge notification (clears watcher's dirty flags)\n\t\t\t\twatcher.getPending();\n\n\t\t\t\t// Step 2: Read signal value (safe — notification phase is over)\n\t\t\t\tview.value = actor.currentView.get();\n\n\t\t\t\t// Step 3: Re-watch for next notification (one-shot pattern)\n\t\t\t\t// TC39 watchers stop notifying after first notification until re-armed\n\t\t\t\twatcher.watch(actor.currentView);\n\t\t\t});\n\t\t});\n\n\t\t// Start watching the signal\n\t\twatcher.watch(actor.currentView);\n\n\t\tonUnmounted(() => {\n\t\t\t// Unwatch to stop receiving notifications\n\t\t\twatcher.unwatch(actor.currentView);\n\t\t});\n\n\t\t// Bind send function to actor for correct 'this' context\n\t\tconst sendBound = actor.send.bind(actor);\n\n\t\t// Compute Component from view.component lookup\n\t\tconst ResolvedComponent = computed(() => {\n\t\t\tif (!view.value) return null;\n\n\t\t\t// Handle null/undefined components catalog gracefully\n\t\t\tif (!props.components) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`Components catalog is ${props.components === null ? \"null\" : \"undefined\"}. ` +\n\t\t\t\t\t\t`Cannot render component \"${view.value.component}\".`,\n\t\t\t\t);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Look up component from catalog\n\t\t\tconst comp = toRaw(props.components[view.value.component]);\n\n\t\t\tif (!comp) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`Component \"${view.value.component}\" not found in catalog. ` +\n\t\t\t\t\t\t`Available components: ${Object.keys(props.components).join(\", \")}`,\n\t\t\t\t);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\treturn comp;\n\t\t});\n\n\t\t// Use render function to avoid Vue 3.5 SFC template <slot> + jsdom renderSlot crash\n\t\treturn () => {\n\t\t\t// No view — show fallback slot\n\t\t\tif (!view.value) {\n\t\t\t\treturn slots.fallback ? slots.fallback() : null;\n\t\t\t}\n\n\t\t\t// View exists but component not found\n\t\t\tif (!ResolvedComponent.value) {\n\t\t\t\treturn h(\n\t\t\t\t\t\"div\",\n\t\t\t\t\t{ class: \"play-renderer-error\" },\n\t\t\t\t\t`Component \"${view.value.component}\" not found in catalog`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Render matched component dynamically\n\t\t\treturn h(ResolvedComponent.value, {\n\t\t\t\t...view.value.props,\n\t\t\t\tsend: sendBound,\n\t\t\t});\n\t\t};\n\t},\n});\n</script>\n\n<style scoped>\n.play-renderer-error {\n\tpadding: 1rem;\n\tbackground-color: #fee;\n\tborder: 1px solid #fcc;\n\tborder-radius: 4px;\n\tcolor: #c00;\n\tfont-family: monospace;\n}\n</style>\n"],"mappings":""}
@@ -1,35 +0,0 @@
1
- import { computed as e, defineComponent as t, h as n, onUnmounted as r, ref as i, toRaw as a } from "vue";
2
- import { Signal as o } from "@xmachines/play-signals";
3
- //#region src/PlayRenderer.vue?vue&type=script&lang.ts
4
- var s = t({
5
- name: "PlayRenderer",
6
- props: {
7
- actor: {
8
- type: Object,
9
- required: !0
10
- },
11
- components: {
12
- type: Object,
13
- required: !0
14
- }
15
- },
16
- setup(t, { slots: s }) {
17
- let c = a(t.actor), l = i(c.currentView.get()), u = new o.subtle.Watcher(() => {
18
- queueMicrotask(() => {
19
- u.getPending(), l.value = c.currentView.get(), u.watch(c.currentView);
20
- });
21
- });
22
- u.watch(c.currentView), r(() => {
23
- u.unwatch(c.currentView);
24
- });
25
- let d = c.send.bind(c), f = e(() => l.value ? t.components ? a(t.components[l.value.component]) || (console.error(`Component "${l.value.component}" not found in catalog. Available components: ${Object.keys(t.components).join(", ")}`), null) : (console.error(`Components catalog is ${t.components === null ? "null" : "undefined"}. Cannot render component "${l.value.component}".`), null) : null);
26
- return () => l.value ? f.value ? n(f.value, {
27
- ...l.value.props,
28
- send: d
29
- }) : n("div", { class: "play-renderer-error" }, `Component "${l.value.component}" not found in catalog`) : s.fallback ? s.fallback() : null;
30
- }
31
- });
32
- //#endregion
33
- export { s as default };
34
-
35
- //# sourceMappingURL=PlayRenderer.vue_vue_type_script_lang.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PlayRenderer.vue_vue_type_script_lang.js","names":[],"sources":["../src/PlayRenderer.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * PlayRenderer - Main Vue renderer component for XMachines Play architecture\n *\n * Architecture (per RESEARCH.md Pattern 1):\n * - Subscribes to actor.currentView signal via Signal.subtle.Watcher\n * - Dynamically renders catalog components based on view.component string\n * - Forwards user events to actor via actor.send()\n * - Vue ref only for triggering renders, NOT business logic\n *\n * Signal bridge uses one-shot re-watch pattern:\n * TC39 Signal watchers stop watching after notification, so watcher.watch()\n * must be called inside a microtask after getPending() to re-arm for the\n * next notification.\n *\n * CRITICAL: Never call signal.get() or signal.set() inside the Watcher's\n * notify callback. The callback runs synchronously during the signal graph's\n * dirty-propagation phase. All reads must be deferred to a queueMicrotask.\n *\n * @invariant Actor Authority - Actor decides all state transitions via guards\n * @invariant Passive Infrastructure - Component observes signals, sends events\n * @invariant Signal-Only Reactivity - Business logic state lives in actor signals\n */\n\nimport { defineComponent, ref, computed, toRaw, onUnmounted, h, type PropType } from \"vue\";\nimport { Signal } from \"@xmachines/play-signals\";\nimport type { PlayRendererProps } from \"./types.js\";\nimport type { AbstractActor, Viewable } from \"@xmachines/play-actor\";\nimport type { AnyActorLogic } from \"xstate\";\nimport type { Component } from \"vue\";\n\nexport default defineComponent({\n\tname: \"PlayRenderer\",\n\tprops: {\n\t\tactor: {\n\t\t\ttype: Object as PropType<AbstractActor<AnyActorLogic> & Viewable>,\n\t\t\trequired: true,\n\t\t},\n\t\tcomponents: {\n\t\t\ttype: Object as PropType<Record<string, Component>>,\n\t\t\trequired: true,\n\t\t},\n\t},\n\tsetup(props, { slots }) {\n\t\t// Unwrap actor from Vue's reactive proxy to access raw Signal objects\n\t\t// CRITICAL: toRaw() preserves Signal's 'this' binding for .get() and watcher operations\n\t\tconst actor = toRaw(props.actor);\n\n\t\t// Get initial value from unwrapped signal\n\t\tconst initialView = actor.currentView.get();\n\n\t\t// Vue ref for triggering re-renders (NOT business logic state)\n\t\t// Signal is source of truth, ref is just Vue's render trigger\n\t\tconst view = ref<{ component: string; props: Record<string, unknown> } | null>(initialView);\n\n\t\t// Signal watcher for bridging TC39 Signals to Vue reactivity\n\t\t// Created immediately (not in onMounted) so signal changes during the\n\t\t// synchronous mount phase are captured.\n\t\t//\n\t\t// The notify callback runs synchronously during the signal graph's\n\t\t// dirty-propagation phase. NEVER read (.get()) or write (.set()) signals\n\t\t// inside it. Only schedule a microtask to do the actual work.\n\t\tconst watcher = new Signal.subtle.Watcher(() => {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\t// Step 1: Acknowledge notification (clears watcher's dirty flags)\n\t\t\t\twatcher.getPending();\n\n\t\t\t\t// Step 2: Read signal value (safe — notification phase is over)\n\t\t\t\tview.value = actor.currentView.get();\n\n\t\t\t\t// Step 3: Re-watch for next notification (one-shot pattern)\n\t\t\t\t// TC39 watchers stop notifying after first notification until re-armed\n\t\t\t\twatcher.watch(actor.currentView);\n\t\t\t});\n\t\t});\n\n\t\t// Start watching the signal\n\t\twatcher.watch(actor.currentView);\n\n\t\tonUnmounted(() => {\n\t\t\t// Unwatch to stop receiving notifications\n\t\t\twatcher.unwatch(actor.currentView);\n\t\t});\n\n\t\t// Bind send function to actor for correct 'this' context\n\t\tconst sendBound = actor.send.bind(actor);\n\n\t\t// Compute Component from view.component lookup\n\t\tconst ResolvedComponent = computed(() => {\n\t\t\tif (!view.value) return null;\n\n\t\t\t// Handle null/undefined components catalog gracefully\n\t\t\tif (!props.components) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`Components catalog is ${props.components === null ? \"null\" : \"undefined\"}. ` +\n\t\t\t\t\t\t`Cannot render component \"${view.value.component}\".`,\n\t\t\t\t);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Look up component from catalog\n\t\t\tconst comp = toRaw(props.components[view.value.component]);\n\n\t\t\tif (!comp) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`Component \"${view.value.component}\" not found in catalog. ` +\n\t\t\t\t\t\t`Available components: ${Object.keys(props.components).join(\", \")}`,\n\t\t\t\t);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\treturn comp;\n\t\t});\n\n\t\t// Use render function to avoid Vue 3.5 SFC template <slot> + jsdom renderSlot crash\n\t\treturn () => {\n\t\t\t// No view — show fallback slot\n\t\t\tif (!view.value) {\n\t\t\t\treturn slots.fallback ? slots.fallback() : null;\n\t\t\t}\n\n\t\t\t// View exists but component not found\n\t\t\tif (!ResolvedComponent.value) {\n\t\t\t\treturn h(\n\t\t\t\t\t\"div\",\n\t\t\t\t\t{ class: \"play-renderer-error\" },\n\t\t\t\t\t`Component \"${view.value.component}\" not found in catalog`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Render matched component dynamically\n\t\t\treturn h(ResolvedComponent.value, {\n\t\t\t\t...view.value.props,\n\t\t\t\tsend: sendBound,\n\t\t\t});\n\t\t};\n\t},\n});\n</script>\n\n<style scoped>\n.play-renderer-error {\n\tpadding: 1rem;\n\tbackground-color: #fee;\n\tborder: 1px solid #fcc;\n\tborder-radius: 4px;\n\tcolor: #c00;\n\tfont-family: monospace;\n}\n</style>\n"],"mappings":";;;AA+BA,IAAA,IAAe,EAAgB;CAC9B,MAAM;CACN,OAAO;EACN,OAAO;GACN,MAAM;GACN,UAAU;GACV;EACD,YAAY;GACX,MAAM;GACN,UAAU;GACV;EACD;CACD,MAAM,GAAO,EAAE,YAAS;EAGvB,IAAM,IAAQ,EAAM,EAAM,MAAM,EAO1B,IAAO,EAJO,EAAM,YAAY,KAAK,CAIgD,EASrF,IAAU,IAAI,EAAO,OAAO,cAAc;AAC/C,wBAAqB;AASpB,IAPA,EAAQ,YAAY,EAGpB,EAAK,QAAQ,EAAM,YAAY,KAAK,EAIpC,EAAQ,MAAM,EAAM,YAAY;KAC/B;IACD;AAKF,EAFA,EAAQ,MAAM,EAAM,YAAY,EAEhC,QAAkB;AAEjB,KAAQ,QAAQ,EAAM,YAAY;IACjC;EAGF,IAAM,IAAY,EAAM,KAAK,KAAK,EAAM,EAGlC,IAAoB,QACpB,EAAK,QAGL,EAAM,aASE,EAAM,EAAM,WAAW,EAAK,MAAM,WAAW,KAGzD,QAAQ,MACP,cAAc,EAAK,MAAM,UAAU,gDACT,OAAO,KAAK,EAAM,WAAW,CAAC,KAAK,KAAK,GAClE,EACM,SAfP,QAAQ,MACP,yBAAyB,EAAM,eAAe,OAAO,SAAS,YAAY,6BAC7C,EAAK,MAAM,UAAU,IAClD,EACM,QARgB,KAuBvB;AAGF,eAEM,EAAK,QAKL,EAAkB,QAShB,EAAE,EAAkB,OAAO;GACjC,GAAG,EAAK,MAAM;GACd,MAAM;GACN,CAAC,GAXM,EACN,OACA,EAAE,OAAO,uBAAuB,EAChC,cAAc,EAAK,MAAM,UAAU,wBACnC,GATM,EAAM,WAAW,EAAM,UAAS,GAAI;;CAmB9C,CAAC"}
@@ -1,8 +0,0 @@
1
- //#region \0plugin-vue:export-helper
2
- var e = (e, t) => {
3
- let n = e.__vccOpts || e;
4
- for (let [e, r] of t) n[e] = r;
5
- return n;
6
- };
7
- //#endregion
8
- export { e as default };
package/dist/index.css DELETED
@@ -1,2 +0,0 @@
1
- .play-renderer-error[data-v-275eda6e]{color:#c00;background-color:#fee;border:1px solid #fcc;border-radius:4px;padding:1rem;font-family:monospace}
2
- /*$vite$:1*/
package/dist/index.js DELETED
@@ -1,2 +0,0 @@
1
- import e from "./PlayRenderer.js";
2
- export { e as PlayRenderer };