diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..ac1944e7
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,30 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ ci:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "22"
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ # Every top-level folder must stay pullable on its own — no workspace-only
+ # dependency specifiers (workspace:/catalog:/link:/file:).
+ - name: Lint standalone packages
+ run: pnpm lint:standalone
+
+ - name: Run tests
+ run: pnpm -r test
diff --git a/account-history/package.json b/account-history/package.json
index 52d5d7c3..e39681de 100644
--- a/account-history/package.json
+++ b/account-history/package.json
@@ -7,7 +7,9 @@
"scripts": {
"build": "vite build",
"dev": "vite build --watch",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"dependencies": {
"@automerge/automerge-repo-solid-primitives": "2.5.2"
@@ -22,10 +24,12 @@
"@inkandswitch/patchwork-filesystem": "^0.0.4",
"@inkandswitch/patchwork-plugins": "^0.0.6",
"babel-preset-solid": "^1.9.9",
+ "happy-dom": "^15.11.7",
"solid-js": "^1.9.9",
"typescript": "^5.9.3",
"vite": "^7.1.9",
+ "vite-plugin-css-injected-by-js": "^3.5.2",
"vite-plugin-solid": "^2.11.10",
- "vite-plugin-css-injected-by-js": "^3.5.2"
+ "vitest": "^3.2.4"
}
}
diff --git a/account-history/vitest.config.ts b/account-history/vitest.config.ts
new file mode 100644
index 00000000..715c53e5
--- /dev/null
+++ b/account-history/vitest.config.ts
@@ -0,0 +1,14 @@
+import {defineConfig} from "vitest/config"
+import solid from "vite-plugin-solid"
+
+export default defineConfig({
+ plugins: [solid()],
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+ resolve: {
+ conditions: ["development", "browser"],
+ },
+})
diff --git a/account-picker/package.json b/account-picker/package.json
index 2959579a..96e11eae 100644
--- a/account-picker/package.json
+++ b/account-picker/package.json
@@ -14,7 +14,9 @@
"build": "node esbuild/build.ts",
"dev": "node esbuild/watch.ts",
"pushwatch": "pnpm dev pushwork",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"dependencies": {
"@automerge/automerge": "^3.2.5",
@@ -34,6 +36,10 @@
"@babel/parser": "7.28.4",
"babel-preset-solid": "^1.9.9",
"esbuild": "^0.23.1",
- "typescript": "^5.8.3"
+ "happy-dom": "^15.11.7",
+ "typescript": "^5.8.3",
+ "vite": "^7.1.9",
+ "vite-plugin-solid": "^2.11.10",
+ "vitest": "^3.2.4"
}
}
diff --git a/account-picker/vitest.config.ts b/account-picker/vitest.config.ts
new file mode 100644
index 00000000..715c53e5
--- /dev/null
+++ b/account-picker/vitest.config.ts
@@ -0,0 +1,14 @@
+import {defineConfig} from "vitest/config"
+import solid from "vite-plugin-solid"
+
+export default defineConfig({
+ plugins: [solid()],
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+ resolve: {
+ conditions: ["development", "browser"],
+ },
+})
diff --git a/add-doc-to-sidebar-button/package.json b/add-doc-to-sidebar-button/package.json
index f59bda98..571cd2ae 100644
--- a/add-doc-to-sidebar-button/package.json
+++ b/add-doc-to-sidebar-button/package.json
@@ -5,12 +5,18 @@
"main": "./index.js",
"scripts": {
"sync": "pushwork sync",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"dependencies": {
"@automerge/automerge": "^3.2.5",
"@automerge/automerge-repo": "^2.6.0-subduction.9",
"@inkandswitch/patchwork-filesystem": "^0.0.4",
"@inkandswitch/patchwork-plugins": "^0.0.6"
+ },
+ "devDependencies": {
+ "happy-dom": "^15.11.7",
+ "vitest": "^3.2.4"
}
}
diff --git a/add-doc-to-sidebar-button/vitest.config.ts b/add-doc-to-sidebar-button/vitest.config.ts
new file mode 100644
index 00000000..44198156
--- /dev/null
+++ b/add-doc-to-sidebar-button/vitest.config.ts
@@ -0,0 +1,9 @@
+import {defineConfig} from "vitest/config"
+
+export default defineConfig({
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+})
diff --git a/codemirror-base/package.json b/codemirror-base/package.json
index c8acf38b..5f340753 100644
--- a/codemirror-base/package.json
+++ b/codemirror-base/package.json
@@ -22,7 +22,9 @@
"build": "node esbuild/build.ts",
"dev": "node esbuild/watch.ts",
"pushwatch": "pnpm dev pushwork",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"dependencies": {
"@automerge/automerge-codemirror": "^0.2.0",
@@ -49,7 +51,11 @@
"esbuild": "^0.23.1",
"eslint": "^9.36.0",
"globals": "^16.4.0",
+ "happy-dom": "^15.11.7",
"typescript": "~5.9.3",
- "typescript-eslint": "^8.45.0"
+ "typescript-eslint": "^8.45.0",
+ "vite": "^7.1.9",
+ "vite-plugin-solid": "^2.11.10",
+ "vitest": "^3.2.4"
}
}
diff --git a/codemirror-base/vitest.config.ts b/codemirror-base/vitest.config.ts
new file mode 100644
index 00000000..715c53e5
--- /dev/null
+++ b/codemirror-base/vitest.config.ts
@@ -0,0 +1,14 @@
+import {defineConfig} from "vitest/config"
+import solid from "vite-plugin-solid"
+
+export default defineConfig({
+ plugins: [solid()],
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+ resolve: {
+ conditions: ["development", "browser"],
+ },
+})
diff --git a/codemirror-markdown/package.json b/codemirror-markdown/package.json
index 564c75a2..7d9e1312 100644
--- a/codemirror-markdown/package.json
+++ b/codemirror-markdown/package.json
@@ -30,7 +30,9 @@
"build": "node esbuild/build.ts",
"dev": "node esbuild/watch.ts",
"pushwatch": "pnpm dev pushwork",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"dependencies": {
"@automerge/automerge": "^3.2.5",
@@ -56,7 +58,9 @@
"esbuild": "^0.23.1",
"eslint": "^9.36.0",
"globals": "^16.4.0",
+ "happy-dom": "^15.11.7",
"typescript": "~5.9.3",
- "typescript-eslint": "^8.45.0"
+ "typescript-eslint": "^8.45.0",
+ "vitest": "^3.2.4"
}
}
diff --git a/codemirror-markdown/vitest.config.ts b/codemirror-markdown/vitest.config.ts
new file mode 100644
index 00000000..44198156
--- /dev/null
+++ b/codemirror-markdown/vitest.config.ts
@@ -0,0 +1,9 @@
+import {defineConfig} from "vitest/config"
+
+export default defineConfig({
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+})
diff --git a/commands/package.json b/commands/package.json
index 5eda98db..a16fafb5 100644
--- a/commands/package.json
+++ b/commands/package.json
@@ -14,7 +14,9 @@
"build": "node esbuild/build.ts",
"dev": "node esbuild/watch.ts",
"pushwatch": "pnpm dev pushwork",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"dependencies": {
"@inkandswitch/patchwork-bootloader": "^0.0.4",
@@ -23,6 +25,8 @@
},
"devDependencies": {
"@automerge/automerge-repo": "^2.6.0-subduction.9",
- "esbuild": "^0.23.1"
+ "esbuild": "^0.23.1",
+ "happy-dom": "^15.11.7",
+ "vitest": "^3.2.4"
}
}
diff --git a/commands/vitest.config.ts b/commands/vitest.config.ts
new file mode 100644
index 00000000..44198156
--- /dev/null
+++ b/commands/vitest.config.ts
@@ -0,0 +1,9 @@
+import {defineConfig} from "vitest/config"
+
+export default defineConfig({
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+})
diff --git a/comments-view/package.json b/comments-view/package.json
index 06b7ead7..c10baaf0 100644
--- a/comments-view/package.json
+++ b/comments-view/package.json
@@ -8,7 +8,9 @@
"dev": "vite build --watch",
"build": "vite build",
"sync": "vite build && pushwork sync",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"dependencies": {
"@automerge/automerge": "3.3.0-fragments.1",
@@ -27,9 +29,11 @@
"@typescript-eslint/parser": "^8.40.0",
"eslint": "^9.34.0",
"globals": "^16.4.0",
+ "happy-dom": "^15.11.7",
"typescript-eslint": "^8.41.0",
"vite": "^7.1.9",
"vite-plugin-css-injected-by-js": "^3.5.2",
- "vite-plugin-solid": "^2.11.10"
+ "vite-plugin-solid": "^2.11.10",
+ "vitest": "^3.2.4"
}
}
diff --git a/comments-view/vitest.config.ts b/comments-view/vitest.config.ts
new file mode 100644
index 00000000..715c53e5
--- /dev/null
+++ b/comments-view/vitest.config.ts
@@ -0,0 +1,14 @@
+import {defineConfig} from "vitest/config"
+import solid from "vite-plugin-solid"
+
+export default defineConfig({
+ plugins: [solid()],
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+ resolve: {
+ conditions: ["development", "browser"],
+ },
+})
diff --git a/contact/package.json b/contact/package.json
index 4a12948b..237d709e 100644
--- a/contact/package.json
+++ b/contact/package.json
@@ -16,7 +16,9 @@
"build": "node esbuild/build.ts",
"dev": "node esbuild/watch.ts",
"pushwatch": "pnpm dev pushwork",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"keywords": [],
"author": "Ink & Switch",
@@ -29,7 +31,7 @@
"@inkandswitch/patchwork-providers": "^0.2.1",
"@inkandswitch/patchwork-providers-react": "^0.2.1",
"@radix-ui/react-avatar": "^1.1.10",
-"lucide-react": "^0.469.0",
+ "lucide-react": "^0.469.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
@@ -41,6 +43,9 @@
"@vitejs/plugin-react": "^4.5.1",
"esbuild": "^0.23.1",
"eslint-plugin-react-hooks": "^4.6.2",
- "typescript": "^5.8.3"
+ "happy-dom": "^15.11.7",
+ "typescript": "^5.8.3",
+ "vite": "^7.1.9",
+ "vitest": "^3.2.4"
}
}
diff --git a/contact/vitest.config.ts b/contact/vitest.config.ts
new file mode 100644
index 00000000..1dd6c6a6
--- /dev/null
+++ b/contact/vitest.config.ts
@@ -0,0 +1,11 @@
+import {defineConfig} from "vitest/config"
+import react from "@vitejs/plugin-react"
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+})
diff --git a/doc-openwith/package.json b/doc-openwith/package.json
index 9633edb6..4671ba8f 100644
--- a/doc-openwith/package.json
+++ b/doc-openwith/package.json
@@ -1,5 +1,13 @@
{
"name": "document open with",
"type": "module",
- "main": "doc-openwith.js"
+ "main": "doc-openwith.js",
+ "devDependencies": {
+ "happy-dom": "^15.11.7",
+ "vitest": "^3.2.4"
+ },
+ "scripts": {
+ "test": "vitest run",
+ "test:watch": "vitest"
+ }
}
diff --git a/doc-openwith/vitest.config.ts b/doc-openwith/vitest.config.ts
new file mode 100644
index 00000000..44198156
--- /dev/null
+++ b/doc-openwith/vitest.config.ts
@@ -0,0 +1,9 @@
+import {defineConfig} from "vitest/config"
+
+export default defineConfig({
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+})
diff --git a/doc-presence/package.json b/doc-presence/package.json
index 81e9e3c8..09ed84ec 100644
--- a/doc-presence/package.json
+++ b/doc-presence/package.json
@@ -1,5 +1,13 @@
{
"name": "document presence",
"type": "module",
- "main": "doc-presence.js"
+ "main": "doc-presence.js",
+ "devDependencies": {
+ "happy-dom": "^15.11.7",
+ "vitest": "^3.2.4"
+ },
+ "scripts": {
+ "test": "vitest run",
+ "test:watch": "vitest"
+ }
}
diff --git a/doc-presence/vitest.config.ts b/doc-presence/vitest.config.ts
new file mode 100644
index 00000000..44198156
--- /dev/null
+++ b/doc-presence/vitest.config.ts
@@ -0,0 +1,9 @@
+import {defineConfig} from "vitest/config"
+
+export default defineConfig({
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+})
diff --git a/doc-title/package.json b/doc-title/package.json
index 09464d8c..9a1a93b3 100644
--- a/doc-title/package.json
+++ b/doc-title/package.json
@@ -7,7 +7,9 @@
"dev": "vite build --watch",
"build": "vite build",
"sync": "vite build && pushwork sync",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"dependencies": {
"@automerge/automerge": "^3.2.5",
@@ -23,8 +25,10 @@
"@typescript-eslint/parser": "^8.40.0",
"eslint": "^9.34.0",
"globals": "^16.4.0",
+ "happy-dom": "^15.11.7",
"typescript-eslint": "^8.41.0",
"vite": "^7.1.9",
- "vite-plugin-css-injected-by-js": "^3.5.2"
+ "vite-plugin-css-injected-by-js": "^3.5.2",
+ "vitest": "^3.2.4"
}
}
diff --git a/doc-title/vitest.config.ts b/doc-title/vitest.config.ts
new file mode 100644
index 00000000..44198156
--- /dev/null
+++ b/doc-title/vitest.config.ts
@@ -0,0 +1,9 @@
+import {defineConfig} from "vitest/config"
+
+export default defineConfig({
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+})
diff --git a/drafts/package.json b/drafts/package.json
index e392c66d..6377b99b 100644
--- a/drafts/package.json
+++ b/drafts/package.json
@@ -8,7 +8,9 @@
"dev": "vite build --watch",
"build": "vite build",
"sync": "vite build && pushwork sync",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"dependencies": {
"@automerge/automerge": "3.3.0-fragments.1",
@@ -26,9 +28,11 @@
"@typescript-eslint/parser": "^8.40.0",
"eslint": "^9.34.0",
"globals": "^16.4.0",
+ "happy-dom": "^15.11.7",
"typescript-eslint": "^8.41.0",
"vite": "^7.1.9",
"vite-plugin-css-injected-by-js": "^3.5.2",
- "vite-plugin-solid": "^2.11.10"
+ "vite-plugin-solid": "^2.11.10",
+ "vitest": "^3.2.4"
}
}
diff --git a/drafts/vitest.config.ts b/drafts/vitest.config.ts
new file mode 100644
index 00000000..715c53e5
--- /dev/null
+++ b/drafts/vitest.config.ts
@@ -0,0 +1,14 @@
+import {defineConfig} from "vitest/config"
+import solid from "vite-plugin-solid"
+
+export default defineConfig({
+ plugins: [solid()],
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+ resolve: {
+ conditions: ["development", "browser"],
+ },
+})
diff --git a/file/package.json b/file/package.json
index af47917e..895fd3a5 100644
--- a/file/package.json
+++ b/file/package.json
@@ -18,7 +18,9 @@
"build": "vite build",
"dev": "vite build --watch",
"push": "pnpm build && pushwork sync",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"keywords": [],
"author": "Ink & Switch",
@@ -54,8 +56,10 @@
},
"devDependencies": {
"@inkandswitch/patchwork-bootloader": "^0.0.4",
+ "happy-dom": "^15.11.7",
"typescript": "^5.8.3",
"vite": "^5.4.19",
- "vite-plugin-solid": "^2.10.0"
+ "vite-plugin-solid": "^2.10.0",
+ "vitest": "^3.2.4"
}
}
diff --git a/file/vitest.config.ts b/file/vitest.config.ts
new file mode 100644
index 00000000..715c53e5
--- /dev/null
+++ b/file/vitest.config.ts
@@ -0,0 +1,14 @@
+import {defineConfig} from "vitest/config"
+import solid from "vite-plugin-solid"
+
+export default defineConfig({
+ plugins: [solid()],
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+ resolve: {
+ conditions: ["development", "browser"],
+ },
+})
diff --git a/folder/package.json b/folder/package.json
index a6fee2c7..9ffab513 100644
--- a/folder/package.json
+++ b/folder/package.json
@@ -15,7 +15,9 @@
"dev": "node esbuild/watch.ts",
"pushwatch": "pnpm dev pushwork",
"sync": "pnpm build && npx pushwork sync",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"keywords": [],
"author": "Ink & Switch",
@@ -27,6 +29,8 @@
},
"devDependencies": {
"@automerge/automerge-repo": "^2.6.0-subduction.9",
- "esbuild": "^0.23.1"
+ "esbuild": "^0.23.1",
+ "happy-dom": "^15.11.7",
+ "vitest": "^3.2.4"
}
}
diff --git a/folder/vitest.config.ts b/folder/vitest.config.ts
new file mode 100644
index 00000000..44198156
--- /dev/null
+++ b/folder/vitest.config.ts
@@ -0,0 +1,9 @@
+import {defineConfig} from "vitest/config"
+
+export default defineConfig({
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+})
diff --git a/frame-configurator/package.json b/frame-configurator/package.json
index a91589f0..1676d49a 100644
--- a/frame-configurator/package.json
+++ b/frame-configurator/package.json
@@ -8,7 +8,9 @@
"watch": "vite --port 4444",
"build": "vite build",
"sync": "vite build && pushwork sync",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"dependencies": {
"@automerge/automerge": "^3.2.5",
@@ -26,9 +28,11 @@
"babel-preset-solid": "^1.9.9",
"eslint": "^9.34.0",
"globals": "^16.4.0",
+ "happy-dom": "^15.11.7",
"typescript-eslint": "^8.41.0",
"vite": "^7.1.9",
"vite-plugin-css-injected-by-js": "^3.5.2",
- "vite-plugin-solid": "^2.11.10"
+ "vite-plugin-solid": "^2.11.10",
+ "vitest": "^3.2.4"
}
}
diff --git a/frame-configurator/src/FrameConfigurator.tsx b/frame-configurator/src/FrameConfigurator.tsx
index 2fe9e5f9..03781ab4 100644
--- a/frame-configurator/src/FrameConfigurator.tsx
+++ b/frame-configurator/src/FrameConfigurator.tsx
@@ -16,7 +16,11 @@ import {
} from "solid-js";
import { createStore, reconcile } from "solid-js/store";
import { render } from "solid-js/web";
-import type { TinyPatchworkLayoutDoc } from "./types";
+import type {
+ TinyPatchworkLayoutDoc,
+ ThreepaneConfigDoc,
+ ToolRef,
+} from "./types";
type ModuleOption = {
id: string;
@@ -465,14 +469,18 @@ function FrameConfiguratorUI(props: {
() => props.handle.url
);
+ // The doctitle + contextbar config now live in the threepane config doc; we
+ // edit it here. Entries are [toolId, docId] pairs — the docid is the account
+ // doc (a placeholder; the frame feeds doctitle the selected doc).
+ const [threepaneDoc, threepaneHandle] = useDocument(
+ () => accountDoc()?.tools?.["threepane"]
+ );
+
const allTools = useToolDescriptions();
const frameOptions = createMemo(() =>
filterToolsByTag([...allTools], "frame-tool")
);
- const sidebarOptions = createMemo(() =>
- filterToolsByTag([...allTools], "sidebar-account")
- );
const documentToolbarOptions = createMemo(() =>
filterToolsByTag([...allTools], "titlebar-tool")
);
@@ -482,6 +490,22 @@ function FrameConfiguratorUI(props: {
const docUrl = props.handle.url;
+ const doctitleIds = () =>
+ threepaneDoc()?.doctitle?.tools?.map((ref) => ref[0]);
+ const contextIds = () => threepaneDoc()?.contextbar?.tabs?.map((ref) => ref[0]);
+
+ const toPairs = (ids: string[]): ToolRef[] =>
+ ids.map((id) => [id, docUrl]);
+
+ const setDoctitle = (next: string[]) =>
+ threepaneHandle()?.change((doc) => {
+ doc.doctitle.tools = toPairs(next);
+ });
+ const setContext = (next: string[]) =>
+ threepaneHandle()?.change((doc) => {
+ doc.contextbar.tabs = toPairs(next);
+ });
+
const setField = (
key: K,
value: TinyPatchworkLayoutDoc[K]
@@ -491,16 +515,6 @@ function FrameConfiguratorUI(props: {
});
};
- const setArrayField = (
- key: keyof TinyPatchworkLayoutDoc,
- next: string[]
- ) => {
- props.handle.change((doc: any) => {
- const arr = doc[key];
- arr.splice(0, arr.length, ...next);
- });
- };
-
return (
- setField("accountSidebarToolId", v as any)}
- options={sidebarOptions()}
- docUrl={docUrl}
- />
-
- setArrayField("documentToolbarToolIds", next)}
- allOptions={documentToolbarOptions()}
- docUrl={docUrl}
- />
-
- setArrayField("contextToolIds", next)}
- allOptions={contextToolOptions()}
- />
+ Preparing layout configuration…
+ }
+ >
+
+
+
+
diff --git a/frame-configurator/src/types.ts b/frame-configurator/src/types.ts
index fc8990de..d246dbf7 100644
--- a/frame-configurator/src/types.ts
+++ b/frame-configurator/src/types.ts
@@ -5,7 +5,18 @@ export type TinyPatchworkLayoutDoc = {
moduleSettingsUrl: AutomergeUrl;
frameToolId: string;
- accountSidebarToolId: string;
- contextToolIds: string[];
- documentToolbarToolIds: string[];
+ /** @deprecated legacy fields, migrated into the threepane config doc */
+ accountSidebarToolId?: string;
+ contextToolIds?: string[];
+ documentToolbarToolIds?: string[];
+
+ tools?: Record
;
+};
+
+export type ToolRef = [toolId: string, docId: AutomergeUrl];
+
+export type ThreepaneConfigDoc = {
+ sidebar: { widgets: ToolRef[] };
+ contextbar: { tabs: ToolRef[] };
+ doctitle: { tools: ToolRef[] };
};
diff --git a/frame-configurator/vitest.config.ts b/frame-configurator/vitest.config.ts
new file mode 100644
index 00000000..715c53e5
--- /dev/null
+++ b/frame-configurator/vitest.config.ts
@@ -0,0 +1,14 @@
+import {defineConfig} from "vitest/config"
+import solid from "vite-plugin-solid"
+
+export default defineConfig({
+ plugins: [solid()],
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+ resolve: {
+ conditions: ["development", "browser"],
+ },
+})
diff --git a/history-view/package.json b/history-view/package.json
index 5a1f8728..8ed682a7 100644
--- a/history-view/package.json
+++ b/history-view/package.json
@@ -7,7 +7,9 @@
"dev": "vite build --watch",
"build": "vite build",
"sync": "vite build && pushwork sync",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"dependencies": {
"@automerge/automerge": "3.3.0-fragments.1",
@@ -27,10 +29,12 @@
"@typescript-eslint/parser": "^8.40.0",
"eslint": "^9.34.0",
"globals": "^16.4.0",
+ "happy-dom": "^15.11.7",
"solid-js": "^1.9.10",
"typescript-eslint": "^8.41.0",
"vite": "^7.1.9",
"vite-plugin-css-injected-by-js": "^3.5.2",
- "vite-plugin-solid": "^2.11.10"
+ "vite-plugin-solid": "^2.11.10",
+ "vitest": "^3.2.4"
}
}
diff --git a/history-view/vitest.config.ts b/history-view/vitest.config.ts
new file mode 100644
index 00000000..715c53e5
--- /dev/null
+++ b/history-view/vitest.config.ts
@@ -0,0 +1,14 @@
+import {defineConfig} from "vitest/config"
+import solid from "vite-plugin-solid"
+
+export default defineConfig({
+ plugins: [solid()],
+ test: {
+ environment: "happy-dom",
+ globals: true,
+ passWithNoTests: true,
+ },
+ resolve: {
+ conditions: ["development", "browser"],
+ },
+})
diff --git a/module-settings-manager/package.json b/module-settings-manager/package.json
index f11098d0..88934d24 100644
--- a/module-settings-manager/package.json
+++ b/module-settings-manager/package.json
@@ -23,7 +23,9 @@
"build": "node esbuild/build.ts",
"dev": "node esbuild/watch.ts",
"pushwatch": "pnpm dev pushwork",
- "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\""
+ "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"dependencies": {
"@inkandswitch/patchwork-bootloader": "^0.0.8",
@@ -39,7 +41,11 @@
"@inkandswitch/patchwork-plugins": "^0.0.6",
"babel-preset-solid": "^1.9.9",
"esbuild": "^0.23.1",
+ "happy-dom": "^15.11.7",
"solid-js": "^1.9.10",
- "typescript": "~5.9.3"
+ "typescript": "~5.9.3",
+ "vite": "^7.1.9",
+ "vite-plugin-solid": "^2.11.10",
+ "vitest": "^3.2.4"
}
}
diff --git a/module-settings-manager/src/module-settings/components/ModuleControls.tsx b/module-settings-manager/src/module-settings/components/ModuleControls.tsx
index 86ae46d2..cb6bd175 100644
--- a/module-settings-manager/src/module-settings/components/ModuleControls.tsx
+++ b/module-settings-manager/src/module-settings/components/ModuleControls.tsx
@@ -17,9 +17,12 @@ import {
import {
DEFAULT_BRANCH,
chosenBranchFor,
+ chosenPinFor,
getModuleEntryKind,
+ publishVersions,
type BranchesDoc,
type ModuleSettingsDocWithBranches,
+ type UrlHeads,
} from "../utils/module-types.ts";
import { unregisterPlugins } from "@inkandswitch/patchwork-plugins";
import {
@@ -75,6 +78,12 @@ export function ModuleControls(props: ModuleControlsProps) {
/>
+
;
+ userSettingsHandle?: DocHandle;
+}
+
+/**
+ * Pin a folder module to a frozen previous version. Pins are written into the
+ * user's own settings doc (like branch choices) under `pinned`, keyed by the
+ * folder URL. The watcher then loads that exact, already-cached version instead
+ * of the latest, and stops hot-reloading it until unpinned.
+ */
+function VersionControls(props: VersionControlsProps) {
+ // Write pins to the user's own settings doc so they're user-local.
+ const targetHandle = () => props.userSettingsHandle ?? props.settingsHandle;
+ const targetDoc = createMemo(() => makeDocumentProjection(targetHandle()));
+
+ const [folderHandle] = createResource(
+ () => props.folderUrl,
+ (url) => props.repo.find<{ lastSyncAt?: number }>(url)
+ );
+
+ const versions = createMemo(() => {
+ const handle = folderHandle();
+ return handle ? publishVersions(handle) : [];
+ });
+
+ const currentPin = createMemo(() =>
+ chosenPinFor([targetDoc()], props.folderUrl)
+ );
+
+ const isPinned = createMemo(() => !!currentPin());
+
+ const sameHeads = (a: UrlHeads | undefined, b: UrlHeads | undefined) =>
+ !!a && !!b && a.length === b.length && a.every((h, i) => h === b[i]);
+
+ // The