@jtfmumm/patchwork-standalone-frame 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,34 +1,203 @@
1
1
  # @jtfmumm/patchwork-standalone-frame
2
2
 
3
- A reusable standalone frame for patchwork tools. Provides keyhive initialization, repo setup, doc history, access control, top bar UI, and sharing modals.
3
+ A standalone frame for patchwork tools. Provides repo setup, doc history, top bar UI, sharing modals, and optional keyhive access control.
4
4
 
5
- ## Usage
5
+ ## Quick start: standalone app from an automerge tool
6
6
 
7
+ This guide walks through building a standalone web app from a patchwork tool published as an automerge FolderDoc.
8
+
9
+ ### Prerequisites
10
+
11
+ - Node.js 18+
12
+ - pnpm
13
+
14
+ ### 1. Create the project
15
+
16
+ ```
17
+ mkdir my-standalone-app && cd my-standalone-app
18
+ pnpm init
19
+ ```
20
+
21
+ ### 2. Install dependencies
22
+
23
+ ```
24
+ pnpm add @jtfmumm/patchwork-standalone-frame \
25
+ @automerge/automerge @automerge/automerge-repo \
26
+ @automerge/automerge-repo-network-websocket \
27
+ @automerge/automerge-repo-storage-indexeddb \
28
+ solid-js
29
+
30
+ pnpm add -D @jtfmumm/automerge-deps @jtfmumm/patchwork-standalone-vite vite
31
+ ```
32
+
33
+ ### 3. Configure automerge-deps
34
+
35
+ Create `automerge-deps.json` to specify the tool and sync server for fetching the tool:
36
+
37
+ ```json
38
+ {
39
+ "syncServers": ["wss://your-sync-server.example.com"],
40
+ "dependencies": [
41
+ { "name": "my-tool", "url": "automerge:<tool-url>" }
42
+ ]
43
+ }
44
+ ```
45
+
46
+ ### 4. Fetch the tool
47
+
48
+ ```
49
+ npx automerge-deps install
50
+ ```
51
+
52
+ This downloads the tool's FolderDoc from the sync server and writes it to `node_modules/my-tool/`.
53
+
54
+ ### 5. Create the entry point
55
+
56
+ **`src/main.ts`**
7
57
  ```typescript
8
- import { mountStandaloneApp, type ToolRegistration } from "@jtfmumm/patchwork-standalone-frame";
58
+ import { Repo } from "@automerge/automerge-repo";
59
+ import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
60
+ import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
61
+ import { mountStandaloneApp } from "@jtfmumm/patchwork-standalone-frame";
62
+ import { plugins } from "my-tool";
63
+
64
+ const repo = new Repo({
65
+ storage: new IndexedDBStorageAdapter(),
66
+ network: [new WebSocketClientAdapter("wss://your-sync-server.example.com")],
67
+ });
68
+
69
+ const root = document.getElementById("root");
70
+ if (root) {
71
+ mountStandaloneApp(root, plugins, { legacyMode: true, repo });
72
+ }
73
+ ```
74
+
75
+ ### 6. Create the HTML shell
76
+
77
+ **`index.html`**
78
+ ```html
79
+ <!doctype html>
80
+ <html lang="en">
81
+ <head>
82
+ <meta charset="UTF-8" />
83
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
84
+ <title>My Tool</title>
85
+ <style>
86
+ * { margin: 0; padding: 0; }
87
+ html, body, #root { height: 100%; }
88
+ </style>
89
+ </head>
90
+ <body>
91
+ <div id="root"></div>
92
+ <script type="module" src="/src/main.ts"></script>
93
+ </body>
94
+ </html>
95
+ ```
96
+
97
+ ### 7. Configure Vite
98
+
99
+ **`vite.config.ts`**
100
+ ```typescript
101
+ import { defineConfig } from "vite";
102
+ import { patchworkStandalone } from "@jtfmumm/patchwork-standalone-vite";
103
+
104
+ export default defineConfig({
105
+ plugins: [patchworkStandalone({ tools: ["my-tool"] })],
106
+ });
107
+ ```
108
+
109
+ The `tools` option lists tool packages fetched by `automerge-deps`. This enables serving of the tool's code-split chunks during development.
9
110
 
111
+ ### 8. Add scripts to package.json
112
+
113
+ ```json
114
+ {
115
+ "scripts": {
116
+ "fetch-deps": "automerge-deps install",
117
+ "dev": "vite",
118
+ "build": "pnpm fetch-deps && vite build",
119
+ "preview": "vite preview"
120
+ }
121
+ }
122
+ ```
123
+
124
+ ### 9. Run
125
+
126
+ ```
127
+ pnpm dev
128
+ ```
129
+
130
+ For production builds:
131
+
132
+ ```
133
+ pnpm build
134
+ pnpm preview
135
+ ```
136
+
137
+ ## API
138
+
139
+ ### `mountStandaloneApp(root, toolOrPlugins, config?)`
140
+
141
+ Mounts the standalone frame into a DOM element.
142
+
143
+ - **`root`** — the HTML element to mount into
144
+ - **`toolOrPlugins`** — either a `ToolRegistration<D>` object or a `Plugin<D>[]` array (patchwork plugin convention)
145
+ - **`config`** — optional `StandaloneFrameConfig`
146
+
147
+ ### ToolRegistration
148
+
149
+ For tools that don't use the patchwork plugin convention:
150
+
151
+ ```typescript
10
152
  const myTool: ToolRegistration<MyDoc> = {
11
- id: "my-tool", // matches @patchwork.type
12
- name: "My Tool", // display name
13
- defaultTitle: "Untitled", // placeholder for new docs
14
- init: (doc, repo) => {
15
- // Initialize a blank document
16
- },
153
+ id: "my-tool",
154
+ name: "My Tool",
155
+ defaultTitle: "Untitled",
156
+ init: (doc, repo) => { /* initialize a blank document */ },
17
157
  getTitle: (doc) => doc.title || "Untitled",
18
158
  setTitle: (doc, title) => { doc.title = title; },
19
- isDocReady: (doc) => !!doc?.title,
20
- render: (handle, element) => {
21
- // Mount your UI into `element`, return a cleanup function
22
- },
159
+ render: (handle, element) => { /* mount UI, return cleanup function */ },
23
160
  };
24
161
 
25
- const root = document.getElementById("root");
26
- if (root) mountStandaloneApp(root, myTool);
162
+ mountStandaloneApp(root, myTool);
27
163
  ```
28
164
 
29
- ## Sync server
165
+ ### Plugin convention
166
+
167
+ Tools that export a `plugins` array with `patchwork:tool` and `patchwork:datatype` entries can be passed directly:
168
+
169
+ ```typescript
170
+ import { plugins } from "my-tool";
171
+ mountStandaloneApp(root, plugins);
172
+ ```
173
+
174
+ ### Legacy mode
175
+
176
+ Pass `{ legacyMode: true, repo }` to use a plain automerge repo without keyhive. You construct and configure the repo yourself. This is the recommended approach for standalone deployments.
177
+
178
+ ### Keyhive mode (default)
179
+
180
+ When `legacyMode` is not set, the frame initializes keyhive automatically, including WASM loading, repo creation, and access control.
181
+
182
+ ```typescript
183
+ import { plugins } from "my-tool";
184
+
185
+ mountStandaloneApp(root, plugins, {
186
+ syncUrl: "wss://your-keyhive-sync-server.example.com",
187
+ });
188
+ ```
189
+
190
+ ### Config options
191
+
192
+ | Option | Description |
193
+ |--------|-------------|
194
+ | `legacyMode` | Use plain automerge docs without keyhive. Default: `false` |
195
+ | `repo` | Pre-built repo for legacy mode. Required when `legacyMode` is `true` |
196
+ | `syncUrl` | WebSocket sync server URL for keyhive mode |
30
197
 
31
198
  The sync server URL is resolved in this order:
32
- 1. `VITE_SYNC_URL` environment variable
33
- 2. `tool.syncUrl` from the registration
34
- 3. `ws://localhost:3030` (default)
199
+
200
+ 1. `config.syncUrl`
201
+ 2. `VITE_SYNC_URL` environment variable
202
+ 3. `tool.syncUrl` from the registration
203
+ 4. `ws://localhost:3030` (default)
package/dist/index.d.ts CHANGED
@@ -15,17 +15,16 @@ export interface ToolRegistration<D = unknown> {
15
15
  isDocReady?: (doc: D) => boolean;
16
16
  render: (handle: DocHandle<D>, element: ToolElement) => (() => void);
17
17
  }
18
- /** Duck-typed doc handle — structurally compatible with automerge-repo DocHandle */
18
+ /** Duck-typed doc handle — structurally compatible with automerge-repo 2.5.x DocHandle */
19
19
  export interface FrameDocHandle<D = unknown> {
20
20
  url: string;
21
21
  doc(): D | undefined;
22
- whenReady(): Promise<unknown>;
23
22
  on(event: string, cb: (...args: unknown[]) => void): void;
24
23
  off(event: string, cb: (...args: unknown[]) => void): void;
25
24
  }
26
- /** Duck-typed repo — structurally compatible with automerge-repo Repo */
25
+ /** Duck-typed repo — structurally compatible with automerge-repo 2.5.x Repo */
27
26
  export interface FrameRepo {
28
- find<D = unknown>(url: string): FrameDocHandle<D>;
27
+ find<D = unknown>(url: string): Promise<FrameDocHandle<D>>;
29
28
  create<D = unknown>(initialValue: D): FrameDocHandle<D>;
30
29
  delete(url: string): void;
31
30
  }
@@ -34,6 +33,8 @@ export interface StandaloneFrameConfig {
34
33
  legacyMode?: boolean;
35
34
  /** Pre-built repo for legacy mode. Required when legacyMode is true. */
36
35
  repo?: FrameRepo;
36
+ /** WebSocket sync server URL. Overrides tool.syncUrl. */
37
+ syncUrl?: string;
37
38
  }
38
39
  export interface PluginDescription {
39
40
  id: string;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { delegateEvents as xe, createComponent as m, insert as f, effect as D, style as E, memo as Z, setStyleProperty as X, template as w, use as Ee, setAttribute as He, render as Pe } from "solid-js/web";
1
+ import { delegateEvents as xe, createComponent as m, insert as f, effect as D, style as E, memo as Z, setStyleProperty as X, template as w, use as Ee, setAttribute as Re, render as Pe } from "solid-js/web";
2
2
  import { createSignal as x, createEffect as ie, onCleanup as q, createMemo as we, Show as _, For as Me, onMount as $e } from "solid-js";
3
3
  const Fe = 100;
4
4
  function Oe(e, t) {
@@ -15,18 +15,18 @@ function ve(e, t) {
15
15
  return [];
16
16
  }
17
17
  }
18
- function Re(e, t, l) {
18
+ function He(e, t, l) {
19
19
  localStorage.setItem(Oe(e, t), JSON.stringify(l));
20
20
  }
21
21
  function Ie(e, t, l, g) {
22
22
  const u = ve(e, t), p = u.findIndex(($) => $.url === l);
23
23
  p !== -1 ? (u[p].title = g, u[p].lastOpened = Date.now()) : u.push({ url: l, title: g, lastOpened: Date.now() }), u.sort(($, v) => v.lastOpened - $.lastOpened);
24
24
  const k = u.slice(0, Fe);
25
- return Re(e, t, k), k;
25
+ return He(e, t, k), k;
26
26
  }
27
27
  function Ne(e, t, l) {
28
28
  const g = ve(e, t).filter((u) => u.url !== l);
29
- return Re(e, t, g), g;
29
+ return He(e, t, g), g;
30
30
  }
31
31
  function Ve(e) {
32
32
  const t = "automerge:";
@@ -85,29 +85,29 @@ const Ze = {
85
85
  "border-radius": "4px",
86
86
  cursor: "pointer",
87
87
  "font-size": "12px"
88
- }, ze = {
88
+ }, Ue = {
89
89
  "font-size": "13px",
90
90
  color: "#6b7280",
91
91
  margin: "16px 0 8px"
92
- }, Ue = {
92
+ }, ze = {
93
93
  border: "none",
94
94
  "border-top": "1px solid #2a323c",
95
95
  margin: "12px 0"
96
96
  };
97
97
  function et(e) {
98
- const [t, l] = x(""), [g, u] = x({}), [p, k] = x(!0), [$, v] = x(void 0), [N, z] = x(!1), [oe, H] = x(null), [A, ae] = x(null), [ee, te] = x(null), [G, be] = x("");
98
+ const [t, l] = x(""), [g, u] = x({}), [p, k] = x(!0), [$, v] = x(void 0), [N, U] = x(!1), [oe, R] = x(null), [A, ae] = x(null), [ee, te] = x(null), [G, be] = x("");
99
99
  ie(() => {
100
100
  if (!e.isOpen) return;
101
101
  let a = !1;
102
102
  (async () => {
103
103
  const s = await ye();
104
104
  if (a) return;
105
- H(s.docIdFromAutomergeUrl(e.docUrl));
105
+ R(s.docIdFromAutomergeUrl(e.docUrl));
106
106
  const C = e.hive.active.individual.id;
107
107
  ae(C ? s.uint8ArrayToHex(C.toBytes()) : null);
108
- const U = e.hive.syncServer;
109
- if (U) {
110
- const T = s.ContactCard.fromJson(U.contactCard.toJson());
108
+ const z = e.hive.syncServer;
109
+ if (z) {
110
+ const T = s.ContactCard.fromJson(z.contactCard.toJson());
111
111
  T && te(s.uint8ArrayToHex(T.individualId.bytes));
112
112
  }
113
113
  const P = s.Identifier.publicId();
@@ -132,10 +132,10 @@ function et(e) {
132
132
  return;
133
133
  }
134
134
  try {
135
- const U = await e.hive.accessForDoc(C, a);
136
- s || v(U ? U.toString() : void 0);
137
- } catch (U) {
138
- s || (console.error("[ShareModal] Error checking access:", U), v(void 0));
135
+ const z = await e.hive.accessForDoc(C, a);
136
+ s || v(z ? z.toString() : void 0);
137
+ } catch (z) {
138
+ s || (console.error("[ShareModal] Error checking access:", z), v(void 0));
139
139
  }
140
140
  })(), q(() => {
141
141
  s = !0;
@@ -165,18 +165,18 @@ function et(e) {
165
165
  a.preventDefault();
166
166
  const s = t().trim();
167
167
  if (s) {
168
- z(!0);
168
+ U(!0);
169
169
  try {
170
- const C = await ye(), U = C.ContactCard.fromJson(s);
171
- if (!U) throw new Error("Invalid ContactCard JSON");
170
+ const C = await ye(), z = C.ContactCard.fromJson(s);
171
+ if (!z) throw new Error("Invalid ContactCard JSON");
172
172
  const P = C.Access.tryFromString("write");
173
173
  if (!P) throw new Error("Invalid access level");
174
- await e.hive.addMemberToDoc(e.docUrl, U, P), l("");
174
+ await e.hive.addMemberToDoc(e.docUrl, z, P), l("");
175
175
  } catch (C) {
176
176
  console.error("[ShareModal]", C);
177
177
  } finally {
178
178
  const C = await de(e.hive, e.docUrl);
179
- u(C), z(!1);
179
+ u(C), U(!1);
180
180
  }
181
181
  }
182
182
  }, he = async (a) => {
@@ -214,7 +214,7 @@ function et(e) {
214
214
  return e.isOpen;
215
215
  },
216
216
  get children() {
217
- var a = Ge(), s = a.firstChild, C = s.firstChild, U = C.firstChild, P = U.nextSibling, T = C.nextSibling, L = T.firstChild, F = L.nextSibling, j = T.nextSibling, ce = j.firstChild;
217
+ var a = Ge(), s = a.firstChild, C = s.firstChild, z = C.firstChild, P = z.nextSibling, T = C.nextSibling, L = T.firstChild, F = L.nextSibling, j = T.nextSibling, ce = j.firstChild;
218
218
  return a.$$click = (i) => {
219
219
  i.target === i.currentTarget && e.onClose();
220
220
  }, s.$$click = (i) => i.stopPropagation(), P.$$click = () => e.onClose(), f(F, m(_, {
@@ -254,7 +254,7 @@ function et(e) {
254
254
  get children() {
255
255
  return [(() => {
256
256
  var i = Le();
257
- return D((S) => E(i, Ue, S)), i;
257
+ return D((S) => E(i, ze, S)), i;
258
258
  })(), (() => {
259
259
  var i = We(), S = i.firstChild, K = S.nextSibling, V = K.firstChild;
260
260
  return i.addEventListener("submit", fe), S.$$input = (M) => l(M.currentTarget.value), f(V, () => N() ? "Adding..." : "Add Member"), D((M) => {
@@ -269,7 +269,7 @@ function et(e) {
269
269
  }), D(() => S.value = t()), i;
270
270
  })(), (() => {
271
271
  var i = Le();
272
- return D((S) => E(i, Ue, S)), i;
272
+ return D((S) => E(i, ze, S)), i;
273
273
  })()];
274
274
  }
275
275
  }), j), f(j, m(_, {
@@ -299,8 +299,8 @@ function et(e) {
299
299
  children: ([S, K]) => {
300
300
  const V = S === A(), M = S === ee(), n = S === G(), r = De.indexOf($()), h = De.indexOf(K), c = r >= 0 && h >= 0 && h <= r && !V && !M, b = () => V ? "You" : M ? "Sync Server" : n ? "Public" : pe(S);
301
301
  return (() => {
302
- var y = Xe(), J = y.firstChild, R = J.firstChild, W = R.nextSibling;
303
- return X(R, "color", V ? "#7ab4f5" : n ? "#b5bd68" : "#edf2f7"), f(R, b), f(W, K), f(y, m(_, {
302
+ var y = Xe(), J = y.firstChild, H = J.firstChild, W = H.nextSibling;
303
+ return X(H, "color", V ? "#7ab4f5" : n ? "#b5bd68" : "#edf2f7"), f(H, b), f(W, K), f(y, m(_, {
304
304
  when: c,
305
305
  get children() {
306
306
  var o = Qe();
@@ -316,7 +316,7 @@ function et(e) {
316
316
  }), null), D((i) => {
317
317
  var S = Ce, K = Ze, V = {
318
318
  ...ue
319
- }, M = ze, n = ze;
319
+ }, M = Ue, n = Ue;
320
320
  return i.e = E(a, S, i.e), i.t = E(s, K, i.t), i.a = E(P, V, i.a), i.o = E(L, M, i.o), i.i = E(ce, n, i.i), i;
321
321
  }, {
322
322
  e: void 0,
@@ -346,10 +346,10 @@ function nt(e) {
346
346
  return t.$$click = (v) => {
347
347
  v.target === v.currentTarget && e.onCancel();
348
348
  }, f(g, () => e.title), f(u, () => e.message), k.$$click = () => e.onCancel(), $.$$click = () => e.onConfirm(), f($, () => e.confirmLabel ?? "Confirm"), D((v) => {
349
- var N = Ce, z = {
349
+ var N = Ce, U = {
350
350
  ...Se
351
351
  };
352
- return v.e = E(t, N, v.e), v.t = E(l, z, v.t), v;
352
+ return v.e = E(t, N, v.e), v.t = E(l, U, v.t), v;
353
353
  }, {
354
354
  e: void 0,
355
355
  t: void 0
@@ -379,18 +379,18 @@ function it(e) {
379
379
  return e.isOpen;
380
380
  },
381
381
  get children() {
382
- var p = rt(), k = p.firstChild, $ = k.firstChild, v = $.nextSibling, N = v.nextSibling, z = N.firstChild, oe = z.nextSibling;
382
+ var p = rt(), k = p.firstChild, $ = k.firstChild, v = $.nextSibling, N = v.nextSibling, U = N.firstChild, oe = U.nextSibling;
383
383
  p.$$click = (A) => {
384
384
  A.target === A.currentTarget && e.onCancel();
385
385
  }, v.$$keydown = (A) => {
386
386
  A.key === "Enter" && u();
387
387
  }, v.$$input = (A) => l(A.currentTarget.value);
388
- var H = g;
389
- return typeof H == "function" ? Ee(H, v) : g = v, z.$$click = () => e.onCancel(), oe.$$click = () => u(), D((A) => {
388
+ var R = g;
389
+ return typeof R == "function" ? Ee(R, v) : g = v, U.$$click = () => e.onCancel(), oe.$$click = () => u(), D((A) => {
390
390
  var ae = Ce, ee = {
391
391
  ...Se
392
392
  }, te = e.defaultTitle;
393
- return A.e = E(p, ae, A.e), A.t = E(k, ee, A.t), te !== A.a && He(v, "placeholder", A.a = te), A;
393
+ return A.e = E(p, ae, A.e), A.t = E(k, ee, A.t), te !== A.a && Re(v, "placeholder", A.a = te), A;
394
394
  }, {
395
395
  e: void 0,
396
396
  t: void 0,
@@ -403,30 +403,30 @@ xe(["click", "input", "keydown"]);
403
403
  const ot = {};
404
404
  var at = /* @__PURE__ */ w('<div style="padding:8px 10px;color:#6b7280;font-style:italic">No documents yet'), lt = /* @__PURE__ */ w('<div style="position:absolute;top:34px;left:0;background:#191e24;border:1px solid #2a323c;border-radius:4px;max-height:400px;overflow-y:auto;min-width:280px;max-width:420px;z-index:1000;box-shadow:0 4px 12px rgba(0,0,0,0.4)">'), ct = /* @__PURE__ */ w('<button style="background:none;border:1px solid #2a323c;color:#edf2f7;font-size:13px;padding:4px 10px;border-radius:4px;cursor:pointer;white-space:nowrap">Share'), st = /* @__PURE__ */ w('<button title="Copy automerge URL"style="background:none;border:1px solid #2a323c;font-size:13px;padding:4px 10px;border-radius:4px;cursor:pointer;white-space:nowrap">'), dt = /* @__PURE__ */ w('<button style="background:none;border:1px solid #2a323c;font-size:13px;padding:4px 10px;border-radius:4px;cursor:pointer;white-space:nowrap">'), ut = /* @__PURE__ */ w('<button style="background:none;border:1px solid #944;color:#c66;font-size:13px;padding:4px 10px;border-radius:4px;cursor:pointer;white-space:nowrap">Remove Doc'), ft = /* @__PURE__ */ w('<div style=display:flex;flex-direction:column;height:100vh;background:#1d232a><div style="display:flex;align-items:center;height:52px;min-height:52px;background:#191e24;color:#edf2f7;font-size:14px;font-family:system-ui, sans-serif;padding:0 12px;border-bottom:1px solid #15191e;box-sizing:border-box"><button style="background:none;border:1px solid #2a323c;color:#edf2f7;font-size:13px;padding:4px 10px;border-radius:4px;cursor:pointer;flex-shrink:0;margin-right:12px;white-space:nowrap">+ New</button><input type=text placeholder="Paste automerge:… URL"style="background:#15191e;border:1px solid #2a323c;color:#edf2f7;font-size:13px;padding:4px 10px;border-radius:4px;width:200px;min-width:80px;flex-shrink:1;margin-right:12px;outline:none"><div data-doc-switcher style=position:relative;flex:1;min-width:120px><button style="background:none;border:none;color:#edf2f7;font-size:14px;cursor:pointer;padding:4px 8px;border-radius:4px;max-width:100%;display:flex;align-items:center;gap:4px"><span style=overflow:hidden;text-overflow:ellipsis;white-space:nowrap></span><span style=flex-shrink:0>▾</span></button></div><div style=display:flex;align-items:center;flex-shrink:0;gap:8px></div></div><div style=flex:1;min-height:0;overflow:hidden>'), ht = /* @__PURE__ */ w('<div style="padding:8px 12px;cursor:pointer;border-bottom:1px solid #15191e;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"> <span style=color:#6b7280;font-size:12px>'), gt = /* @__PURE__ */ w('<div style="width:24px;height:24px;border:2px solid #2a323c;border-top-color:#6b7280;border-radius:50%;animation:spin 0.8s linear infinite">'), pt = /* @__PURE__ */ w('<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;height:100%;background:#1d232a;font-family:system-ui, sans-serif;font-size:14px">'), mt = /* @__PURE__ */ w("<div style=height:100%>");
405
405
  function yt(e) {
406
- const t = e.tool, [l, g] = x(null), [u, p] = x("..."), [k, $] = x([]), [v, N] = x(!1), [z, oe] = x(null), [H, A] = x(""), [ae, ee] = x(!1), [te, G] = x(!1), [be, Q] = x(!1), [le, fe] = x(!1), [he, ge] = x(!1), [ne, pe] = x(!1), [re, a] = x(!1), [s, C] = x(null), [U, P] = x(!1);
406
+ const t = e.tool, [l, g] = x(null), [u, p] = x("..."), [k, $] = x([]), [v, N] = x(!1), [U, oe] = x(null), [R, A] = x(""), [ae, ee] = x(!1), [te, G] = x(!1), [be, Q] = x(!1), [le, fe] = x(!1), [he, ge] = x(!1), [ne, pe] = x(!1), [re, a] = x(!1), [s, C] = x(null), [z, P] = x(!1);
407
407
  let T, L = null, F = 0;
408
408
  function j(n) {
409
409
  L && (L(), L = null);
410
410
  const r = n.doc(), h = r ? t.getTitle(r) : t.defaultTitle;
411
- p(h), $(Ie(t.id, H(), n.url, h));
411
+ p(h), $(Ie(t.id, R(), n.url, h));
412
412
  const c = () => {
413
413
  const b = n.doc();
414
414
  if (b) {
415
415
  const y = t.getTitle(b);
416
- p(y), $(Ie(t.id, H(), n.url, y));
416
+ p(y), $(Ie(t.id, R(), n.url, y));
417
417
  }
418
418
  };
419
419
  n.on("change", c), L = () => n.off("change", c);
420
420
  }
421
421
  async function ce(n) {
422
- const r = z();
422
+ const r = U();
423
423
  if (!r) return !0;
424
424
  try {
425
425
  const {
426
426
  docIdFromAutomergeUrl: h,
427
427
  Identifier: c
428
- } = await import("@automerge/automerge-repo-keyhive"), b = h(n), y = r.active.individual.id, J = c.publicId(), [R, W] = await Promise.all([r.accessForDoc(y, b).catch(() => null), r.accessForDoc(J, b).catch(() => null)]);
429
- return console.log(`[${t.name}] Access check for ${n.slice(0, 30)}...: my=${R}, public=${W}`), !!(R || W);
428
+ } = await import("@automerge/automerge-repo-keyhive"), b = h(n), y = r.active.individual.id, J = c.publicId(), [H, W] = await Promise.all([r.accessForDoc(y, b).catch(() => null), r.accessForDoc(J, b).catch(() => null)]);
429
+ return console.log(`[${t.name}] Access check for ${n.slice(0, 30)}...: my=${H}, public=${W}`), !!(H || W);
430
430
  } catch (h) {
431
431
  return console.error(`[${t.name}] Access check error for ${n}:`, h), !0;
432
432
  }
@@ -445,15 +445,15 @@ function yt(e) {
445
445
  const y = b.doc();
446
446
  return y ? t.isDocReady ? t.isDocReady(y) : !!y : !1;
447
447
  };
448
- let c = T.find(n);
449
- if (await c.whenReady(), r !== F) return;
448
+ let c = await T.find(n);
449
+ if (r !== F) return;
450
450
  if (!h(c)) {
451
451
  console.log(`[${t.name}] Doc incomplete, forcing re-sync for: ${n.slice(0, 30)}...`);
452
452
  try {
453
453
  T.delete(n);
454
454
  } catch {
455
455
  }
456
- if (await new Promise((b) => setTimeout(b, 100)), r !== F || (c = T.find(n), await c.whenReady(), r !== F)) return;
456
+ if (await new Promise((b) => setTimeout(b, 100)), r !== F || (c = await T.find(n), r !== F)) return;
457
457
  }
458
458
  if (!h(c) && (se(n), await new Promise((b) => {
459
459
  const y = () => {
@@ -468,7 +468,7 @@ function yt(e) {
468
468
  }, 3e4);
469
469
  }), r !== F))
470
470
  return;
471
- P(!1), se(n), localStorage.setItem(me(t.id, H()), n), j(c), g(c);
471
+ P(!1), se(n), localStorage.setItem(me(t.id, R()), n), j(c), g(c);
472
472
  } catch (h) {
473
473
  if (r !== F) return;
474
474
  console.error(`[${t.name}] Failed to load doc: ${n}`, h), P(!1), a(!0), C(n), se(n);
@@ -479,21 +479,21 @@ function yt(e) {
479
479
  t.init(r, T), t.setTitle && t.setTitle(r, n), r["@patchwork"] = {
480
480
  type: t.id
481
481
  };
482
- const h = z();
482
+ const h = U();
483
483
  let c;
484
- h ? (c = await T.create2(r), console.log(`[${t.name}] Created document (keyhive): ${c.url}`), await h.addSyncServerPullToDoc(c.url), await h.keyhiveStorage.saveKeyhiveWithHash(h.keyhive)) : (c = T.create(r), console.log(`[${t.name}] Created document (legacy): ${c.url}`)), g(c), se(c.url), localStorage.setItem(me(t.id, H()), c.url), j(c);
484
+ h ? (c = await T.create2(r), console.log(`[${t.name}] Created document (keyhive): ${c.url}`), await h.addSyncServerPullToDoc(c.url), await h.keyhiveStorage.saveKeyhiveWithHash(h.keyhive)) : (c = T.create(r), console.log(`[${t.name}] Created document (legacy): ${c.url}`)), g(c), se(c.url), localStorage.setItem(me(t.id, R()), c.url), j(c);
485
485
  }
486
486
  function K() {
487
- L && (L(), L = null), g(null), p("..."), a(!1), C(null), localStorage.removeItem(me(t.id, H())), window.history.replaceState(null, "", window.location.pathname);
487
+ L && (L(), L = null), g(null), p("..."), a(!1), C(null), localStorage.removeItem(me(t.id, R())), window.history.replaceState(null, "", window.location.pathname);
488
488
  }
489
489
  function V() {
490
490
  const n = l();
491
491
  if (!n) return;
492
492
  const r = n.url;
493
- K(), T.delete(r), $(Ne(t.id, H(), r));
493
+ K(), T.delete(r), $(Ne(t.id, R(), r));
494
494
  }
495
495
  async function M() {
496
- const n = z();
496
+ const n = U();
497
497
  if (n)
498
498
  try {
499
499
  const r = n.active.contactCard.toJson();
@@ -521,7 +521,7 @@ function yt(e) {
521
521
  BrowserWebSocketClientAdapter: b
522
522
  }, y] = await Promise.all([import("@automerge/automerge-repo"), import("@automerge/automerge-repo-storage-indexeddb"), import("@automerge/automerge-repo-network-websocket"), import("@automerge/automerge-repo-keyhive")]);
523
523
  y.initKeyhiveWasm();
524
- const J = new c(`${t.id}-keyhive`), R = ot?.VITE_SYNC_URL, W = new b(R || t.syncUrl || "ws://localhost:3030"), o = `${t.id}-${Math.random().toString(36).slice(2)}`, d = await y.initializeAutomergeRepoKeyhive({
524
+ const J = new c(`${t.id}-keyhive`), H = ot?.VITE_SYNC_URL, W = new b(e.config?.syncUrl || H || t.syncUrl || "ws://localhost:3030"), o = `${t.id}-${Math.random().toString(36).slice(2)}`, d = await y.initializeAutomergeRepoKeyhive({
525
525
  storage: J,
526
526
  peerIdSuffix: o,
527
527
  networkAdapter: W,
@@ -554,7 +554,7 @@ function yt(e) {
554
554
  console.log(`[${t.name}] Loading doc from hash: ${n}`), await i(n);
555
555
  return;
556
556
  }
557
- const r = localStorage.getItem(me(t.id, H()));
557
+ const r = localStorage.getItem(me(t.id, R()));
558
558
  if (r) {
559
559
  console.log(`[${t.name}] Found existing doc: ${r}`), await i(r);
560
560
  return;
@@ -574,7 +574,7 @@ function yt(e) {
574
574
  }), (() => {
575
575
  var n = ft(), r = n.firstChild, h = r.firstChild, c = h.nextSibling, b = c.nextSibling, y = b.firstChild, J = y.firstChild;
576
576
  J.nextSibling;
577
- var R = b.nextSibling, W = r.nextSibling;
577
+ var H = b.nextSibling, W = r.nextSibling;
578
578
  return h.$$click = () => Q(!0), c.$$keydown = (o) => {
579
579
  if (o.key === "Enter") {
580
580
  const d = o.currentTarget.value.trim();
@@ -618,15 +618,15 @@ function yt(e) {
618
618
  }
619
619
  }), null), o;
620
620
  }
621
- }), null), f(R, m(_, {
621
+ }), null), f(H, m(_, {
622
622
  get when() {
623
- return Z(() => !!l())() && z();
623
+ return Z(() => !!l())() && U();
624
624
  },
625
625
  get children() {
626
626
  var o = ct();
627
627
  return o.$$click = () => ee(!0), o;
628
628
  }
629
- }), null), f(R, m(_, {
629
+ }), null), f(H, m(_, {
630
630
  get when() {
631
631
  return l();
632
632
  },
@@ -637,15 +637,15 @@ function yt(e) {
637
637
  d && (await navigator.clipboard.writeText(d.url), ge(!0), setTimeout(() => ge(!1), 1500));
638
638
  }, f(o, () => he() ? "Copied!" : "Copy URL"), D((d) => X(o, "color", he() ? "#b5bd68" : "#edf2f7")), o;
639
639
  }
640
- }), null), f(R, m(_, {
640
+ }), null), f(H, m(_, {
641
641
  get when() {
642
- return z();
642
+ return U();
643
643
  },
644
644
  get children() {
645
645
  var o = dt();
646
646
  return o.$$click = () => void M(), f(o, () => le() ? "Copied!" : "Contact Card"), D((d) => X(o, "color", le() ? "#b5bd68" : "#edf2f7")), o;
647
647
  }
648
- }), null), f(R, m(_, {
648
+ }), null), f(H, m(_, {
649
649
  get when() {
650
650
  return l();
651
651
  },
@@ -663,14 +663,14 @@ function yt(e) {
663
663
  var o = pt();
664
664
  return f(o, m(_, {
665
665
  get when() {
666
- return U() || !ne();
666
+ return z() || !ne();
667
667
  },
668
668
  get children() {
669
669
  return gt();
670
670
  }
671
671
  }), null), f(o, (() => {
672
672
  var d = Z(() => !!re());
673
- return () => d() ? "Document unavailable: you may not have access" : Z(() => !!U())() ? "Loading document..." : Z(() => !!ne())() ? "No document open" : e.config?.legacyMode ? "Initializing..." : "Initializing keyhive...";
673
+ return () => d() ? "Document unavailable: you may not have access" : Z(() => !!z())() ? "Loading document..." : Z(() => !!ne())() ? "No document open" : e.config?.legacyMode ? "Initializing..." : "Initializing keyhive...";
674
674
  })(), null), D((d) => X(o, "color", re() ? "#c66" : "#6b7280")), o;
675
675
  })();
676
676
  },
@@ -685,7 +685,7 @@ function yt(e) {
685
685
  }
686
686
  })), f(n, m(_, {
687
687
  get when() {
688
- return Z(() => !!z())() && l();
688
+ return Z(() => !!U())() && l();
689
689
  },
690
690
  get children() {
691
691
  return m(et, {
@@ -696,7 +696,7 @@ function yt(e) {
696
696
  return l().url;
697
697
  },
698
698
  get hive() {
699
- return z();
699
+ return U();
700
700
  },
701
701
  onClose: () => ee(!1)
702
702
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jtfmumm/patchwork-standalone-frame",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Reusable standalone frame for patchwork tools with keyhive, doc history, and access control",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/frame.tsx CHANGED
@@ -115,8 +115,7 @@ export function StandaloneApp<D>(props: { tool: ToolRegistration<D>; config?: St
115
115
  return tool.isDocReady ? tool.isDocReady(d) : !!d;
116
116
  };
117
117
 
118
- let docHandle = repo.find<D>(url);
119
- await docHandle.whenReady();
118
+ let docHandle = await repo.find<D>(url);
120
119
  if (gen !== loadGeneration) return;
121
120
 
122
121
  if (!isDocReady(docHandle)) {
@@ -125,8 +124,7 @@ export function StandaloneApp<D>(props: { tool: ToolRegistration<D>; config?: St
125
124
  await new Promise((r) => setTimeout(r, 100));
126
125
  if (gen !== loadGeneration) return;
127
126
 
128
- docHandle = repo.find<D>(url);
129
- await docHandle.whenReady();
127
+ docHandle = await repo.find<D>(url);
130
128
  if (gen !== loadGeneration) return;
131
129
  }
132
130
 
@@ -257,7 +255,7 @@ export function StandaloneApp<D>(props: { tool: ToolRegistration<D>; config?: St
257
255
  const keyhiveStorage = new IndexedDBStorageAdapter(`${tool.id}-keyhive`);
258
256
  const envSyncUrl = (import.meta as unknown as Record<string, Record<string, string>>).env?.VITE_SYNC_URL;
259
257
  const networkAdapter = new BrowserWebSocketClientAdapter(
260
- envSyncUrl || tool.syncUrl || "ws://localhost:3030"
258
+ props.config?.syncUrl || envSyncUrl || tool.syncUrl || "ws://localhost:3030"
261
259
  );
262
260
  const peerIdSuffix = `${tool.id}-${Math.random().toString(36).slice(2)}`;
263
261
 
package/src/index.ts CHANGED
@@ -18,18 +18,17 @@ export interface ToolRegistration<D = unknown> {
18
18
  render: (handle: DocHandle<D>, element: ToolElement) => (() => void);
19
19
  }
20
20
 
21
- /** Duck-typed doc handle — structurally compatible with automerge-repo DocHandle */
21
+ /** Duck-typed doc handle — structurally compatible with automerge-repo 2.5.x DocHandle */
22
22
  export interface FrameDocHandle<D = unknown> {
23
23
  url: string;
24
24
  doc(): D | undefined;
25
- whenReady(): Promise<unknown>;
26
25
  on(event: string, cb: (...args: unknown[]) => void): void;
27
26
  off(event: string, cb: (...args: unknown[]) => void): void;
28
27
  }
29
28
 
30
- /** Duck-typed repo — structurally compatible with automerge-repo Repo */
29
+ /** Duck-typed repo — structurally compatible with automerge-repo 2.5.x Repo */
31
30
  export interface FrameRepo {
32
- find<D = unknown>(url: string): FrameDocHandle<D>;
31
+ find<D = unknown>(url: string): Promise<FrameDocHandle<D>>;
33
32
  create<D = unknown>(initialValue: D): FrameDocHandle<D>;
34
33
  delete(url: string): void;
35
34
  }
@@ -39,6 +38,8 @@ export interface StandaloneFrameConfig {
39
38
  legacyMode?: boolean;
40
39
  /** Pre-built repo for legacy mode. Required when legacyMode is true. */
41
40
  repo?: FrameRepo;
41
+ /** WebSocket sync server URL. Overrides tool.syncUrl. */
42
+ syncUrl?: string;
42
43
  }
43
44
 
44
45
  export interface PluginDescription {