npm package: @jtfmumm/patchwork-standalone-frame
Description: Reusable standalone frame for patchwork tools with keyhive, doc history, and access control
Latest version: 0.5.4

# @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-standalone
```

This 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 build
```

For development:

```
pnpm dev
```

## How 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 into
- **`toolOrPlugins`**: either a `ToolRegistration<D>` object or a `Plugin<D>[]` array (patchwork plugin convention)
- **`config`**: optional `StandaloneFrameConfig`

### Plugin convention

Tools that export a `plugins` array with `patchwork:tool` and `patchwork:datatype` entries can be passed directly:

```typescript
import { plugins } from "my-tool";
mountStandaloneApp(root, plugins);
```

### ToolRegistration

For tools that don't use the plugin convention:

```typescript
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.

```typescript
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-keyhive
```

Then your entry point is simpler, with no repo setup needed:

```typescript
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:

1. `config.syncUrl`
2. `VITE_SYNC_URL` environment variable
3. `tool.syncUrl` from the registration
4. `ws://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 vite
```

### 2. Configure automerge-deps

Create `automerge-deps.json` to specify the tool and sync server for fetching the tool:

```json
{
  "syncServers": ["wss://your-sync-server.example.com"],
  "dependencies": [
    { "name": "my-tool", "url": "automerge:<tool-url>" }
  ]
}
```

### 3. Fetch the tool

```
npx automerge-deps install
```

### 4. Create the entry point

**`src/main.ts`** (legacy mode shown; see keyhive mode above for the alternative)
```typescript
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`**
```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`**
```typescript
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

```json
{
  "scripts": {
    "fetch-deps": "automerge-deps install",
    "dev": "vite",
    "build": "pnpm fetch-deps && vite build",
    "preview": "vite preview"
  }
}
```

### 8. Run

```
pnpm dev
```

For production builds:

```
pnpm build
pnpm preview
```