@jtfmumm/patchwork-standalone-frame
v0.5.4
Published
Reusable standalone frame for patchwork tools with keyhive, doc history, and access control
Downloads
1,338
Readme
@jtfmumm/patchwork-standalone-frame
A standalone frame for patchwork tools. Provides repo setup, doc history, top bar UI, sharing modals, and optional keyhive access control.
Quick start
Scaffold a new project:
pnpm create @jtfmumm/patchwork-standaloneThis prompts for your tool name, automerge URL, sync server, and mode (legacy or keyhive), then generates a ready-to-run project. After scaffolding:
cd my-standalone-app
pnpm install
pnpm buildFor development:
pnpm devHow it works
The standalone frame takes a patchwork tool and wraps it in an app shell with document management. You need three packages:
@jtfmumm/patchwork-standalone-frame: the frame itself@jtfmumm/automerge-deps: fetches tool modules from a sync server at build time@jtfmumm/patchwork-standalone-vite: Vite plugin handling build configuration
Tools are published as automerge FolderDocs. automerge-deps install downloads them into node_modules/ so they work like normal dependencies.
API
mountStandaloneApp(root, toolOrPlugins, config?)
Mounts the standalone frame into a DOM element.
root: the HTML element to mount intotoolOrPlugins: either aToolRegistration<D>object or aPlugin<D>[]array (patchwork plugin convention)config: optionalStandaloneFrameConfig
Plugin convention
Tools that export a plugins array with patchwork:tool and patchwork:datatype entries can be passed directly:
import { plugins } from "my-tool";
mountStandaloneApp(root, plugins);ToolRegistration
For tools that don't use the plugin convention:
const myTool: ToolRegistration<MyDoc> = {
id: "my-tool",
name: "My Tool",
defaultTitle: "Untitled",
init: (doc, repo) => { /* initialize a blank document */ },
getTitle: (doc) => doc.title || "Untitled",
setTitle: (doc, title) => { doc.title = title; },
render: (handle, element) => { /* mount UI, return cleanup function */ },
};
mountStandaloneApp(root, myTool);Legacy mode
Pass { legacyMode: true, repo } to use a plain automerge repo without keyhive. You construct and configure the repo yourself.
import { Repo } from "@automerge/automerge-repo";
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
import { mountStandaloneApp } from "@jtfmumm/patchwork-standalone-frame";
import { plugins } from "my-tool";
const repo = new Repo({
storage: new IndexedDBStorageAdapter(),
network: [new WebSocketClientAdapter("wss://your-sync-server.example.com")],
});
const root = document.getElementById("root");
if (root) {
mountStandaloneApp(root, plugins, { legacyMode: true, repo });
}Keyhive mode (default)
When legacyMode is not set, the frame initializes keyhive automatically, including WASM loading, repo creation, and access control. You don't need to create a repo yourself.
Keyhive mode requires one additional dependency:
pnpm add @automerge/automerge-repo-keyhiveThen your entry point is simpler, with no repo setup needed:
import { mountStandaloneApp } from "@jtfmumm/patchwork-standalone-frame";
import { plugins } from "my-tool";
const root = document.getElementById("root");
if (root) {
mountStandaloneApp(root, plugins, {
syncUrl: "wss://your-keyhive-sync-server.example.com",
});
}Config options
| Option | Description |
|--------|-------------|
| legacyMode | Use plain automerge docs without keyhive. Default: false |
| repo | Pre-built repo for legacy mode. Required when legacyMode is true |
| syncUrl | WebSocket sync server URL for keyhive mode |
The sync server URL is resolved in this order:
config.syncUrlVITE_SYNC_URLenvironment variabletool.syncUrlfrom the registrationws://localhost:3030(default)
Manual setup
If you prefer to set up a project manually instead of using the scaffolder, here are the steps:
1. Create the project and install dependencies
mkdir my-standalone-app && cd my-standalone-app
pnpm init
pnpm add @jtfmumm/patchwork-standalone-frame \
@automerge/automerge @automerge/automerge-repo \
@automerge/automerge-repo-network-websocket \
@automerge/automerge-repo-storage-indexeddb \
solid-js
pnpm add -D @jtfmumm/automerge-deps @jtfmumm/patchwork-standalone-vite vite2. Configure automerge-deps
Create automerge-deps.json to specify the tool and sync server for fetching the tool:
{
"syncServers": ["wss://your-sync-server.example.com"],
"dependencies": [
{ "name": "my-tool", "url": "automerge:<tool-url>" }
]
}3. Fetch the tool
npx automerge-deps install4. Create the entry point
src/main.ts (legacy mode shown; see keyhive mode above for the alternative)
import { Repo } from "@automerge/automerge-repo";
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
import { mountStandaloneApp } from "@jtfmumm/patchwork-standalone-frame";
import { plugins } from "my-tool";
const repo = new Repo({
storage: new IndexedDBStorageAdapter(),
network: [new WebSocketClientAdapter("wss://your-sync-server.example.com")],
});
const root = document.getElementById("root");
if (root) {
mountStandaloneApp(root, plugins, { legacyMode: true, repo });
}5. Create the HTML shell
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Tool</title>
<style>
* { margin: 0; padding: 0; }
html, body, #root { height: 100%; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>6. Configure Vite
vite.config.ts
import { defineConfig } from "vite";
import { patchworkStandalone } from "@jtfmumm/patchwork-standalone-vite";
export default defineConfig({
plugins: [patchworkStandalone({ tools: ["my-tool"] })],
});7. Add scripts to package.json
{
"scripts": {
"fetch-deps": "automerge-deps install",
"dev": "vite",
"build": "pnpm fetch-deps && vite build",
"preview": "vite preview"
}
}8. Run
pnpm devFor production builds:
pnpm build
pnpm preview