foundry-vtt-react 0.1.0 → 0.1.4

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 Tim Sandberg
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,26 +1,49 @@
1
1
  # Foundry VTT React
2
2
 
3
- This package provides extensions of various Foundry VTT classes to support React applications development within Foundry.
3
+ This package provides extensions of various Foundry VTT classes to support building React applications inside Foundry VTT.
4
+
5
+ It is a **library**, consumed by module developers — not a Foundry module itself.
6
+
7
+ This is a bit experimental, but enables a modern JS workflow and if you know and enjoy React workflows with HMR/fast-refresh, this should have you flying in no time!
8
+
9
+ **Disclaimers:**
10
+ - ⚠️ Only tested against Foundry V13 at this point, v14 compatability testing coming soon (wanna try it out for me?)
11
+ - ℹ️ Would love input if you're a Vite pro -- I have a suspicion devSetup could be a plugin, haven't had time to look into it.
4
12
 
5
13
  ## Installation
6
14
 
7
- Currently supported FoundryVTT classes:
15
+ ```bash
16
+ npm install foundry-vtt-react
17
+ # or: pnpm add foundry-vtt-react
18
+ ```
19
+
20
+ `react` and `react-dom` (v19) are peer dependencies — install them in your module if you haven't already:
21
+
22
+ ```bash
23
+ npm install react react-dom
24
+ ```
25
+
26
+ ## Supported Foundry classes
27
+
28
+ - `ReactApplicationV2` — a base `ApplicationV2` that renders a React component instead of a Handlebars template.
29
+ - `ReactActorSheetV2` — an `ActorSheetV2` that renders with React, letting your component react to document changes through the native sheet lifecycle.
8
30
 
9
- - `ReactApplicationV2`: A base application class for creating React-based applications in Foundry VTT.
10
- - `ReactActorSheetV2`: An actor sheet class that uses React for rendering the UI, allowing the React app to listen for changes to the document using the native ActoSheetV2 lifecycle events.
31
+ Both are produced by the same `ReactApplicationMixin`, so they share the options and lifecycle described below.
11
32
 
12
33
  ## Usage
13
34
 
14
- There are two important pieces to developing React applications in Foundry.
35
+ There are two pieces to developing React applications in Foundry:
15
36
 
16
- 1. Creating your ReactApplicationV2 instance and passing in your React component.
17
- 2. Configuring ViteJS development server to work with your local FoundryVTT instance.
37
+ 1. Creating a React-powered application instance and passing in your React component.
38
+ 2. Configuring a Vite dev server to work with your local Foundry instance (see [Development setup](#development-setup-with-vite)).
18
39
 
19
- ### Creating a ReactApplicationV2 Instance
40
+ ### Creating a ReactApplicationV2 instance
20
41
 
21
- To create a new React-powered Foundry application, you can instantiate the `ReactApplicationV2` class and provide your React component along with any initial properties and window options.
42
+ Instantiate `ReactApplicationV2` with your React component plus any initial props and window options:
43
+
44
+ ```jsx
45
+ import { ReactApplicationV2 } from "foundry-vtt-react";
22
46
 
23
- ```javascript
24
47
  // Basic component
25
48
  function MyReactComponent(props) {
26
49
  return <div>Hello, {props.data}!</div>;
@@ -36,4 +59,252 @@ const app = new ReactApplicationV2({
36
59
  app.render(true);
37
60
  ```
38
61
 
39
- ![alt text](assets/image.png)
62
+ A React component rendered inside a Foundry application window
63
+
64
+ **Constructor options**
65
+
66
+
67
+ | Option | Type | Description |
68
+ | -------------- | --------------------- | ---------------------------------------------------------------------------------------- |
69
+ | `reactApp` | `React.ComponentType` | The component mounted into the application window. |
70
+ | `initialProps` | `object` (optional) | Props passed to `reactApp` on mount. Also reachable via `_prepareContext` (see below). |
71
+ | ...options | `ApplicationV2` | Any standard `ApplicationV2` options (`window`, `position`, `classes`, `actions`, etc.). |
72
+
73
+
74
+ ### Building a React actor sheet
75
+
76
+ Subclass `ReactActorSheetV2`, set `reactApp`, and register it as the sheet for your actor type. Override `_prepareContext` to choose exactly which props your component receives — this is also where you hand your component the `[ContextConnector](#reacting-to-foundry-updates-with-contextconnector)` so it can subscribe to live document updates:
77
+
78
+ ```jsx
79
+ import { ReactActorSheetV2 } from "foundry-vtt-react";
80
+ import MySheetApp from "./MySheetApp";
81
+
82
+ class MyActorSheet extends ReactActorSheetV2 {
83
+ reactApp = MySheetApp;
84
+
85
+ static DEFAULT_OPTIONS = {
86
+ window: { title: "My Sheet", resizable: true },
87
+ position: { width: 625, height: 750 },
88
+ classes: ["my-sheet"],
89
+ };
90
+
91
+ async _prepareContext(options) {
92
+ const context = await super._prepareContext(options);
93
+ // Pick the props your React app receives:
94
+ context.initialProps = {
95
+ actor: context.document,
96
+ source: context.source,
97
+ contextConnector: this.contextConnector, // for live updates
98
+ };
99
+ return context;
100
+ }
101
+ }
102
+ ```
103
+
104
+ Register it like any other sheet, e.g.:
105
+
106
+ ```js
107
+ foundry.documents.collections.Actors.registerSheet("my-module", MyActorSheet, {
108
+ types: ["character"],
109
+ makeDefault: true,
110
+ });
111
+ ```
112
+
113
+ ### Reacting to Foundry updates with ContextConnector
114
+
115
+ Every React application instance owns a `ContextConnector` at `this.contextConnector`. On **every** render, the mixin calls `contextConnector.publishContext(context)` with the prepared context (which, for a sheet, includes the updated `document`). Pass the connector into your component (via `initialProps`, as shown above) and subscribe to those updates so React re-renders when the Foundry document changes.
116
+
117
+ `onUpdate(callback)` returns a **disposer** function, ideal for `useEffect` cleanup:
118
+
119
+ ```jsx
120
+ import { useEffect, useState } from "react";
121
+
122
+ function MySheetApp({ actor: initialActor, contextConnector }) {
123
+ const [actor, setActor] = useState(initialActor);
124
+
125
+ useEffect(() => {
126
+ // Re-render whenever Foundry re-renders the sheet
127
+ const off = contextConnector.onUpdate(({ document }) => {
128
+ setActor(document);
129
+ });
130
+ return off; // unsubscribe on unmount
131
+ }, [contextConnector]);
132
+
133
+ return <h1>{actor.name}</h1>;
134
+ }
135
+ ```
136
+
137
+ You can debounce noisy update streams with Foundry's helper:
138
+
139
+ ```jsx
140
+ const handleUpdate = foundry.utils.debounce(({ document }) => {
141
+ setActor(document);
142
+ }, 200);
143
+ const off = contextConnector.onUpdate(handleUpdate);
144
+ ```
145
+
146
+ `ContextConnector<T>` **API**
147
+
148
+
149
+ | Method | Returns | Description |
150
+ | ------------------------- | ------------ | ------------------------------------------------------------------- |
151
+ | `onUpdate(cb)` | `() => void` | Subscribe to context updates. Returns a disposer that unsubscribes. |
152
+ | `tearDown(cb)` | `void` | Unsubscribe a callback previously passed to `onUpdate`. |
153
+ | `on(event, cb)` | `() => void` | Subscribe to a custom event. Returns a disposer. |
154
+ | `off(event, cb)` | `void` | Unsubscribe a callback from a custom event. |
155
+ | `publishContext(context)` | `void` | Emit a context update. Called for you by the mixin on each render. |
156
+
157
+
158
+ > Use the returned disposer **or** `tearDown(cb)` to clean up — either removes the listener.
159
+
160
+ ## Development setup with Vite
161
+
162
+ For a fast dev loop with React Fast Refresh (HMR) inside Foundry, you split your module into **two entry points** and let a Vite dev server serve them:
163
+
164
+ - `src/main.ts` — your **real app** entry (registers hooks, sheets, etc.). This is what gets bundled for production.
165
+ - `src/main.js` — a tiny **dev-only shim** that calls `devSetup`, which injects the React Refresh runtime and then loads `src/main.ts` over HMR.
166
+
167
+ The trick that ties it together: your manifest's `esmodules` points at `dist/main.js`. In a production build that's the bundled app. In development, the Vite dev server is configured so that the same `/modules/<id>/dist/main.js` URL is served from `src/main.js` (the shim) — so **the manifest entry resolves to the dev shim while you develop, and to the real bundle once built.**
168
+
169
+ ### Directory structure
170
+
171
+ ```text
172
+ my-module/
173
+ ├─ module.json # manifest → esmodules: ["dist/main.js"]
174
+ ├─ package.json
175
+ ├─ vite.config.ts
176
+ ├─ src/
177
+ │ ├─ main.js # dev-only shim: calls devSetup() (served at dist/main.js in dev)
178
+ │ ├─ main.ts # real app entry: Hooks, registerSheet, … (build input)
179
+ │ └─ MySheetApp.tsx # your React component(s)
180
+ └─ dist/ # build output (gitignored); created by `vite build`
181
+ ```
182
+
183
+ ### Steps
184
+
185
+ **1. Manifest — point** `esmodules` **at the built path.**
186
+
187
+ ```jsonc
188
+ // module.json
189
+ {
190
+ "id": "my-module",
191
+ "esmodules": ["dist/main.js"],
192
+ "styles": ["dist/main.css"],
193
+ "compatibility": { "minimum": "13", "verified": "13" }
194
+ }
195
+ ```
196
+
197
+ **2. Real app entry (**`src/main.ts`**)** — your normal module bootstrap:
198
+
199
+ ```ts
200
+ import MyActorSheet from "./MyActorSheet";
201
+
202
+ foundry.helpers.Hooks.once("ready", () => {
203
+ foundry.documents.collections.Actors.registerSheet("my-module", MyActorSheet, {
204
+ types: ["character"],
205
+ makeDefault: true,
206
+ });
207
+ });
208
+ ```
209
+
210
+ **3. Dev-only shim (**`src/main.js`**)** — calls `devSetup` with your module id and the path to the real entry:
211
+
212
+ ```js
213
+ import { id as APP_ID } from "../module.json";
214
+ import { devSetup } from "foundry-vtt-react";
215
+
216
+ // Loads @react-refresh + src/main.ts (served at dist/main.ts) with HMR.
217
+ devSetup(APP_ID, "dist/main.ts");
218
+ ```
219
+
220
+ **4. Vite config** — serve `src/` at the manifest's `dist` path and proxy everything else to Foundry:
221
+
222
+ ```ts
223
+ // vite.config.ts
224
+ import { defineConfig } from "vite";
225
+ import react from "@vitejs/plugin-react";
226
+ import path from "path";
227
+ import { id as APP_ID } from "./module.json";
228
+
229
+ export default defineConfig({
230
+ plugins: [react()],
231
+ root: "src/",
232
+ // Serve src/ under the path the manifest references, so
233
+ // /modules/<id>/dist/main.js resolves to src/main.js (the shim),
234
+ // and /modules/<id>/dist/main.ts resolves to src/main.ts (the app).
235
+ base: `/modules/${APP_ID}/dist`,
236
+ server: {
237
+ port: 30001,
238
+ proxy: {
239
+ // Everything that isn't your dev bundle goes to the Foundry server.
240
+ [`^(?!/modules/${APP_ID}/dist)`]: "http://localhost:30000/",
241
+ "/socket.io": { target: "ws://localhost:30000", ws: true },
242
+ },
243
+ },
244
+ build: {
245
+ outDir: path.resolve(__dirname, "dist"),
246
+ emptyOutDir: true,
247
+ rollupOptions: {
248
+ input: path.resolve(__dirname, "src/main.ts"), // build the real entry
249
+ output: { entryFileNames: "[name].js", assetFileNames: "[name].[ext]", format: "es" },
250
+ },
251
+ },
252
+ });
253
+ ```
254
+
255
+ **5. Scripts (**`package.json`**):**
256
+
257
+ ```jsonc
258
+ {
259
+ "scripts": {
260
+ "dev": "vite", // dev server with HMR
261
+ "build": "vite build" // produces dist/main.js for production
262
+ }
263
+ }
264
+ ```
265
+
266
+ **6. Make the module visible to Foundry** — symlink (or copy) your module folder into your Foundry data `Data/modules/` directory so Foundry can read `module.json`.
267
+
268
+ > If you run foundry in docker like me, you can simply mount your local module volume into the docker foundry's modules folder like:
269
+ >
270
+ > ```
271
+ > foundry:
272
+ > image: felddy/foundryvtt:13
273
+ > hostname: localhost
274
+ > volumes:
275
+ > - /path/to/my-module:/data/Data/modules/my-module
276
+ > ```
277
+
278
+ **7. Run it.** Start Foundry (port `30000`), then `npm run dev` and open the Vite server (e.g. `http://localhost:30001`). Foundry loads your module; the manifest's `dist/main.js` is served from `src/main.js`, `devSetup` boots React Refresh, and edits to your components hot-reload.
279
+
280
+ > For production, run `npm run build` and ship `dist/` — `dist/main.js` is now the bundled app, and `devSetup` is not involved.
281
+
282
+ ### `devSetup` reference
283
+
284
+ ```ts
285
+ devSetup(appId: string, entrypoint: string): void
286
+ ```
287
+
288
+
289
+ | Parameter | Description |
290
+ | ------------ | ---------------------------------------------------------------------------------------------------------------------- |
291
+ | `appId` | Your module's `id` (from `module.json`). Used to build the served paths and to namespace the injected `<script>` tags. |
292
+ | `entrypoint` | Path to your real app entry, relative to the module root (e.g. `dist/main.ts`), as served by the Vite dev server. |
293
+
294
+
295
+ It injects (idempotently — safe to call more than once):
296
+
297
+ - A `<script type="module">` preamble that wires up the React Refresh global hooks, loading `@react-refresh` from `/modules/<appId>/dist/@react-refresh`.
298
+ - A `<script type="module" src="/modules/<appId>/<entrypoint>">` that loads your app.
299
+
300
+ ## Exports
301
+
302
+ ```js
303
+ import {
304
+ ReactApplicationV2,
305
+ ReactActorSheetV2,
306
+ ContextConnector,
307
+ devSetup,
308
+ } from "foundry-vtt-react";
309
+ ```
310
+
package/dist/index.d.mts CHANGED
@@ -1,12 +1,27 @@
1
1
  export { devSetup } from './util/dev-setup.mjs';
2
2
 
3
+ /**
4
+ * Event-based bridge for pushing Foundry context updates into React.
5
+ *
6
+ * A `ReactApplicationMixin` instance creates one connector and calls
7
+ * {@link ContextConnector.publishContext} on every render. React components
8
+ * subscribe with {@link ContextConnector.onUpdate} to re-render on changes.
9
+ */
3
10
  declare class ContextConnector<T> extends EventTarget {
11
+ #private;
4
12
  static UPDATE: string;
5
- constructor();
6
13
  publishContext(context: T): void;
7
- on(event: string, callback: (data: T) => void): void;
8
- onUpdate(callback: (data: T) => void): void;
9
- tearDown(fn: (data: T) => void): void;
14
+ /**
15
+ * Subscribe to an event. Returns a disposer that removes the listener,
16
+ * convenient for React `useEffect` cleanup.
17
+ */
18
+ on(event: string, callback: (data: T) => void): () => void;
19
+ /** Subscribe to context updates. Returns a disposer. */
20
+ onUpdate(callback: (data: T) => void): () => void;
21
+ /** Remove a listener previously added with {@link on}. */
22
+ off(event: string, callback: (data: T) => void): void;
23
+ /** Remove a context-update listener previously added with {@link onUpdate}. */
24
+ tearDown(callback: (data: T) => void): void;
10
25
  }
11
26
 
12
27
  declare const ReactApplicationV2_base: {
package/dist/index.mjs CHANGED
@@ -5,22 +5,44 @@ import {
5
5
  // lib/context-connector.ts
6
6
  var ContextConnector = class _ContextConnector extends EventTarget {
7
7
  static UPDATE = "contextUpdate";
8
- constructor() {
9
- super();
10
- }
8
+ /**
9
+ * Maps each subscriber callback to the wrapper actually registered with
10
+ * `addEventListener`, so `off`/`tearDown` can remove it by the original
11
+ * callback reference.
12
+ */
13
+ #wrappers = /* @__PURE__ */ new Map();
11
14
  publishContext(context) {
12
- this.dispatchEvent(new CustomEvent("contextUpdate", { detail: context }));
15
+ this.dispatchEvent(
16
+ new CustomEvent(_ContextConnector.UPDATE, { detail: context })
17
+ );
13
18
  }
19
+ /**
20
+ * Subscribe to an event. Returns a disposer that removes the listener,
21
+ * convenient for React `useEffect` cleanup.
22
+ */
14
23
  on(event, callback) {
15
- this.addEventListener(event, (e) => {
16
- callback(e.detail);
17
- });
24
+ if (!this.#wrappers.has(callback)) {
25
+ const wrapper = (e) => callback(e.detail);
26
+ this.#wrappers.set(callback, wrapper);
27
+ this.addEventListener(event, wrapper);
28
+ }
29
+ return () => this.off(event, callback);
18
30
  }
31
+ /** Subscribe to context updates. Returns a disposer. */
19
32
  onUpdate(callback) {
20
- this.on(_ContextConnector.UPDATE, callback);
33
+ return this.on(_ContextConnector.UPDATE, callback);
34
+ }
35
+ /** Remove a listener previously added with {@link on}. */
36
+ off(event, callback) {
37
+ const wrapper = this.#wrappers.get(callback);
38
+ if (wrapper) {
39
+ this.removeEventListener(event, wrapper);
40
+ this.#wrappers.delete(callback);
41
+ }
21
42
  }
22
- tearDown(fn) {
23
- this.removeEventListener(_ContextConnector.UPDATE, fn);
43
+ /** Remove a context-update listener previously added with {@link onUpdate}. */
44
+ tearDown(callback) {
45
+ this.off(_ContextConnector.UPDATE, callback);
24
46
  }
25
47
  };
26
48
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../lib/context-connector.ts","../lib/util/mount-app.tsx","../lib/react-application-mixin.ts","../lib/react-application-v2.ts","../lib/react-actor-sheet-v2.ts"],"sourcesContent":["export class ContextConnector<T> extends EventTarget {\n static UPDATE = \"contextUpdate\";\n\n constructor() {\n super();\n }\n\n publishContext(context: T) {\n this.dispatchEvent(new CustomEvent(\"contextUpdate\", { detail: context }));\n }\n\n on(event: string, callback: (data: T) => void) {\n this.addEventListener(event, (e: CustomEventInit<T>) => {\n callback(e.detail!);\n });\n }\n\n onUpdate(callback: (data: T) => void) {\n this.on(ContextConnector.UPDATE, callback);\n }\n\n tearDown(fn: (data: T) => void) {\n this.removeEventListener(ContextConnector.UPDATE, fn as EventListener);\n }\n}\n","import { createRoot } from \"react-dom/client\";\n\nexport function mountApp({\n App,\n element,\n initialProps = {},\n innerSelector,\n}: {\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n App: React.ComponentType<any>;\n element: Element;\n initialProps?: {};\n innerSelector: string;\n}) {\n const root = createRoot(element);\n root.render(\n <div id={innerSelector}>\n <App {...initialProps} />\n </div>\n );\n}\n","import { ContextConnector } from \"./context-connector\";\nimport { mountApp } from \"./util/mount-app\";\n\n/**\n * A mixin that integrates React components with Foundry VTT's Application class.\n * This mixin enables the use of React applications within Foundry VTT's application framework.\n *\n * @param superclass - The base class to extend, typically a Foundry VTT Application class\n * @returns A class that extends the superclass with React integration capabilities\n *\n * @remarks\n * This mixin provides the following features:\n * - Mounting React components within Foundry VTT applications\n * - Context management through ContextConnector\n * - Automatic cleanup and rendering lifecycle management\n * - Default window options for position and configuration\n *\n * @example\n * ```typescript\n * class MyReactApp extends ReactApplicationMixin(Application) {\n * constructor(options) {\n * super({\n * reactApp: MyReactComponent,\n * initialProps: { data: \"example\" },\n * ...options\n * });\n * }\n * }\n * ```\n */\n\ntype ReactApplicationProps = {\n reactApp: React.ComponentType<any>;\n initialProps?: Record<string, any>;\n};\n\nfunction ReactApplicationMixin(Superclass: any) {\n return class ReactApplication extends Superclass {\n reactApp: React.ComponentType<any>;\n uuid = foundry.utils.randomID(12);\n rootId = `react-app-root-${this.uuid}`;\n innerSelector = `react-application-inner-${this.rootId}`;\n contextConnector: ContextConnector<any>;\n\n static DEFAULT_OPTIONS = {\n position: {\n width: 400,\n height: 300,\n },\n window: {\n title: \"Hello React-powered Foundry applications\",\n resizable: true,\n minimizable: true,\n },\n };\n\n initialProps = {};\n\n constructor({ reactApp, initialProps, ...options }: ReactApplicationProps & any) {\n super(options);\n this.reactApp = reactApp;\n this.contextConnector = new ContextConnector();\n this.initialProps = initialProps || {};\n }\n\n get appIsRendered() {\n return !!document.querySelector(`#${this.innerSelector}`);\n }\n\n async _onRender(context: any, options: any) {\n await super._onRender(context, options);\n const el = this.element.querySelectorAll(`#${this.rootId}`);\n\n if (el && !this.appIsRendered) {\n mountApp({\n App: this.reactApp,\n element: el[0],\n initialProps: context.initialProps,\n innerSelector: this.innerSelector,\n });\n }\n this.contextConnector.publishContext(context);\n }\n\n _replaceHTML(result: HTMLElement, content: HTMLElement) {\n if (!this.appIsRendered) {\n content.appendChild(result);\n }\n }\n\n async _prepareContext(options: any) {\n const context = (await super._prepareContext(options)) as any;\n context.initialProps = this.initialProps;\n return context;\n }\n\n async _renderHTML() {\n const tempEl = document.createElement(\"div\");\n tempEl.id = this.rootId;\n tempEl.innerHTML = `<span>Uh oh, something went wrong</span>`;\n return tempEl;\n }\n };\n}\n\nexport default ReactApplicationMixin;\n","import ReactApplicationMixin from \"./react-application-mixin\";\n\n/**\n * A Foundry VTT Application class that integrates React components with the Foundry application framework.\n * Extends ApplicationV2 to provide seamless React app mounting and rendering capabilities.\n *\n * @class FoundryReactApplication\n * @extends foundry.applications.api.ApplicationV2\n *\n * @example\n * // Create a new React-powered Foundry application\n * const app = new FoundryReactApplication({\n * reactApp: MyReactComponent,\n * initialProps: { data: 'example' },\n * window: { title: \"My React App\" },\n * position: { width: 600, height: 400 }\n * });\n *\n * @property {React.Component} reactApp - The React component to be mounted\n * @property {string} template - Path to the Handlebars template for the application shell\n * @property {Object} initialProps - Initial properties passed to the React component\n * @property {string} rootId - ID added to the root element where the React app will be mounted\n */\nexport class ReactApplicationV2 extends ReactApplicationMixin(foundry.applications.api.ApplicationV2) {}\n","/**\n * A React-enabled version of Foundry VTT's ActorSheetV2 class.\n *\n * This class extends the core ActorSheetV2 functionality by applying the ReactApplicationMixin,\n * which enables React component rendering within the actor sheet application.\n *\n * @extends {foundry.applications.sheets.ActorSheetV2}\n * @mixes ReactApplicationMixin\n *\n * @example\n * ```typescript\n * class MyActorSheet extends ReactActorSheetV2 {\n * // Your custom React-enabled actor sheet implementation\n * }\n * ```\n */\nimport ReactApplicationMixin from \"./react-application-mixin\";\n\nexport class ReactActorSheetV2 extends ReactApplicationMixin(foundry.applications.sheets.ActorSheetV2) {}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../lib/context-connector.ts","../lib/util/mount-app.tsx","../lib/react-application-mixin.ts","../lib/react-application-v2.ts","../lib/react-actor-sheet-v2.ts"],"sourcesContent":["/**\n * Event-based bridge for pushing Foundry context updates into React.\n *\n * A `ReactApplicationMixin` instance creates one connector and calls\n * {@link ContextConnector.publishContext} on every render. React components\n * subscribe with {@link ContextConnector.onUpdate} to re-render on changes.\n */\nexport class ContextConnector<T> extends EventTarget {\n static UPDATE = \"contextUpdate\";\n\n /**\n * Maps each subscriber callback to the wrapper actually registered with\n * `addEventListener`, so `off`/`tearDown` can remove it by the original\n * callback reference.\n */\n #wrappers = new Map<(data: T) => void, EventListener>();\n\n publishContext(context: T) {\n this.dispatchEvent(\n new CustomEvent(ContextConnector.UPDATE, { detail: context })\n );\n }\n\n /**\n * Subscribe to an event. Returns a disposer that removes the listener,\n * convenient for React `useEffect` cleanup.\n */\n on(event: string, callback: (data: T) => void): () => void {\n if (!this.#wrappers.has(callback)) {\n const wrapper: EventListener = (e) =>\n callback((e as CustomEvent<T>).detail);\n this.#wrappers.set(callback, wrapper);\n this.addEventListener(event, wrapper);\n }\n return () => this.off(event, callback);\n }\n\n /** Subscribe to context updates. Returns a disposer. */\n onUpdate(callback: (data: T) => void): () => void {\n return this.on(ContextConnector.UPDATE, callback);\n }\n\n /** Remove a listener previously added with {@link on}. */\n off(event: string, callback: (data: T) => void) {\n const wrapper = this.#wrappers.get(callback);\n if (wrapper) {\n this.removeEventListener(event, wrapper);\n this.#wrappers.delete(callback);\n }\n }\n\n /** Remove a context-update listener previously added with {@link onUpdate}. */\n tearDown(callback: (data: T) => void) {\n this.off(ContextConnector.UPDATE, callback);\n }\n}\n","import { createRoot } from \"react-dom/client\";\n\nexport function mountApp({\n App,\n element,\n initialProps = {},\n innerSelector,\n}: {\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n App: React.ComponentType<any>;\n element: Element;\n initialProps?: {};\n innerSelector: string;\n}) {\n const root = createRoot(element);\n root.render(\n <div id={innerSelector}>\n <App {...initialProps} />\n </div>\n );\n}\n","import { ContextConnector } from \"./context-connector\";\nimport { mountApp } from \"./util/mount-app\";\n\n/**\n * A mixin that integrates React components with Foundry VTT's Application class.\n * This mixin enables the use of React applications within Foundry VTT's application framework.\n *\n * @param superclass - The base class to extend, typically a Foundry VTT Application class\n * @returns A class that extends the superclass with React integration capabilities\n *\n * @remarks\n * This mixin provides the following features:\n * - Mounting React components within Foundry VTT applications\n * - Context management through ContextConnector\n * - Automatic cleanup and rendering lifecycle management\n * - Default window options for position and configuration\n *\n * @example\n * ```typescript\n * class MyReactApp extends ReactApplicationMixin(Application) {\n * constructor(options) {\n * super({\n * reactApp: MyReactComponent,\n * initialProps: { data: \"example\" },\n * ...options\n * });\n * }\n * }\n * ```\n */\n\ntype ReactApplicationProps = {\n reactApp: React.ComponentType<any>;\n initialProps?: Record<string, any>;\n};\n\nfunction ReactApplicationMixin(Superclass: any) {\n return class ReactApplication extends Superclass {\n reactApp: React.ComponentType<any>;\n uuid = foundry.utils.randomID(12);\n rootId = `react-app-root-${this.uuid}`;\n innerSelector = `react-application-inner-${this.rootId}`;\n contextConnector: ContextConnector<any>;\n\n static DEFAULT_OPTIONS = {\n position: {\n width: 400,\n height: 300,\n },\n window: {\n title: \"Hello React-powered Foundry applications\",\n resizable: true,\n minimizable: true,\n },\n };\n\n initialProps = {};\n\n constructor({ reactApp, initialProps, ...options }: ReactApplicationProps & any) {\n super(options);\n this.reactApp = reactApp;\n this.contextConnector = new ContextConnector();\n this.initialProps = initialProps || {};\n }\n\n get appIsRendered() {\n return !!document.querySelector(`#${this.innerSelector}`);\n }\n\n async _onRender(context: any, options: any) {\n await super._onRender(context, options);\n const el = this.element.querySelectorAll(`#${this.rootId}`);\n\n if (el && !this.appIsRendered) {\n mountApp({\n App: this.reactApp,\n element: el[0],\n initialProps: context.initialProps,\n innerSelector: this.innerSelector,\n });\n }\n this.contextConnector.publishContext(context);\n }\n\n _replaceHTML(result: HTMLElement, content: HTMLElement) {\n if (!this.appIsRendered) {\n content.appendChild(result);\n }\n }\n\n async _prepareContext(options: any) {\n const context = (await super._prepareContext(options)) as any;\n context.initialProps = this.initialProps;\n return context;\n }\n\n async _renderHTML() {\n const tempEl = document.createElement(\"div\");\n tempEl.id = this.rootId;\n tempEl.innerHTML = `<span>Uh oh, something went wrong</span>`;\n return tempEl;\n }\n };\n}\n\nexport default ReactApplicationMixin;\n","import ReactApplicationMixin from \"./react-application-mixin\";\n\n/**\n * A Foundry VTT Application class that integrates React components with the Foundry application framework.\n * Extends ApplicationV2 to provide seamless React app mounting and rendering capabilities.\n *\n * @class FoundryReactApplication\n * @extends foundry.applications.api.ApplicationV2\n *\n * @example\n * // Create a new React-powered Foundry application\n * const app = new FoundryReactApplication({\n * reactApp: MyReactComponent,\n * initialProps: { data: 'example' },\n * window: { title: \"My React App\" },\n * position: { width: 600, height: 400 }\n * });\n *\n * @property {React.Component} reactApp - The React component to be mounted\n * @property {string} template - Path to the Handlebars template for the application shell\n * @property {Object} initialProps - Initial properties passed to the React component\n * @property {string} rootId - ID added to the root element where the React app will be mounted\n */\nexport class ReactApplicationV2 extends ReactApplicationMixin(foundry.applications.api.ApplicationV2) {}\n","/**\n * A React-enabled version of Foundry VTT's ActorSheetV2 class.\n *\n * This class extends the core ActorSheetV2 functionality by applying the ReactApplicationMixin,\n * which enables React component rendering within the actor sheet application.\n *\n * @extends {foundry.applications.sheets.ActorSheetV2}\n * @mixes ReactApplicationMixin\n *\n * @example\n * ```typescript\n * class MyActorSheet extends ReactActorSheetV2 {\n * // Your custom React-enabled actor sheet implementation\n * }\n * ```\n */\nimport ReactApplicationMixin from \"./react-application-mixin\";\n\nexport class ReactActorSheetV2 extends ReactApplicationMixin(foundry.applications.sheets.ActorSheetV2) {}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foundry-vtt-react",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "description": "Extensions of various FoundryVTT classes to support React applications.",
5
5
  "packageManager": "pnpm@10.30.3",
6
6
  "main": "dist/index.mjs",
@@ -24,8 +24,16 @@
24
24
  "prepublishOnly": "pnpm build"
25
25
  },
26
26
  "keywords": [],
27
- "author": "",
28
- "license": "ISC",
27
+ "author": "Tim Sandberg",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/tasandberg/foundry-vtt-react.git"
32
+ },
33
+ "homepage": "https://github.com/tasandberg/foundry-vtt-react#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/tasandberg/foundry-vtt-react/issues"
36
+ },
29
37
  "devDependencies": {
30
38
  "fvtt-types": "^13",
31
39
  "tsup": "^8.5.1",