@jtfmumm/patchwork-standalone-frame 0.1.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.
@@ -0,0 +1,17 @@
1
+ export declare const overlayStyle: {
2
+ readonly position: "fixed";
3
+ readonly inset: "0";
4
+ readonly background: "rgba(0,0,0,0.5)";
5
+ readonly display: "flex";
6
+ readonly "align-items": "center";
7
+ readonly "justify-content": "center";
8
+ readonly "z-index": "2000";
9
+ };
10
+ export declare const cardStyle: {
11
+ readonly background: "#191e24";
12
+ readonly color: "#edf2f7";
13
+ readonly "border-radius": "8px";
14
+ readonly padding: "20px 24px";
15
+ readonly "box-shadow": "0 8px 24px rgba(0,0,0,0.5)";
16
+ readonly "font-family": "system-ui, sans-serif";
17
+ };
@@ -0,0 +1,2 @@
1
+ import type { ToolRegistration } from "./index.ts";
2
+ export declare function mountStandaloneApp<D>(rootElement: HTMLElement, tool: ToolRegistration<D>): void;
@@ -0,0 +1,8 @@
1
+ interface NewDocModalProps {
2
+ isOpen: boolean;
3
+ defaultTitle: string;
4
+ onConfirm: (title: string) => void;
5
+ onCancel: () => void;
6
+ }
7
+ export declare function NewDocModal(props: NewDocModalProps): import("solid-js").JSX.Element;
8
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { AutomergeUrl } from "@automerge/automerge-repo";
2
+ import { type AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
3
+ interface ShareModalProps {
4
+ isOpen: boolean;
5
+ docUrl: AutomergeUrl;
6
+ hive: AutomergeRepoKeyhive;
7
+ onClose: () => void;
8
+ }
9
+ export declare function ShareModal(props: ShareModalProps): import("solid-js").JSX.Element;
10
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { AutomergeUrl } from "@automerge/automerge-repo";
2
+ export declare function getDocUrlFromHash(): AutomergeUrl | null;
3
+ export declare function setDocUrlInHash(url: AutomergeUrl): void;
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@jtfmumm/patchwork-standalone-frame",
3
+ "version": "0.1.0",
4
+ "description": "Reusable standalone frame for patchwork tools with keyhive, doc history, and access control",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "peerDependencies": {
19
+ "@automerge/automerge-repo": "^2.5.2-alpha.0",
20
+ "@automerge/automerge-repo-keyhive": "0.1.0-alpha.17za",
21
+ "@automerge/automerge-repo-network-websocket": "^2.5.2-alpha.0",
22
+ "@automerge/automerge-repo-storage-indexeddb": "^2.5.2-alpha.0",
23
+ "solid-js": "^1.9.9"
24
+ },
25
+ "devDependencies": {
26
+ "@automerge/automerge-repo": "^2.5.2-alpha.0",
27
+ "@automerge/automerge-repo-keyhive": "0.1.0-alpha.17za",
28
+ "@automerge/automerge-repo-network-websocket": "^2.5.2-alpha.0",
29
+ "@automerge/automerge-repo-storage-indexeddb": "^2.5.2-alpha.0",
30
+ "solid-js": "^1.9.9",
31
+ "typescript": "^5.9.2",
32
+ "vite": "^6.0.3",
33
+ "vite-plugin-solid": "^2.11.10"
34
+ },
35
+ "scripts": {
36
+ "build": "vite build && tsc -p tsconfig.app.json --noEmit false --emitDeclarationOnly --outDir dist",
37
+ "typecheck": "tsc --noEmit"
38
+ }
39
+ }
@@ -0,0 +1,68 @@
1
+ import { Show, createEffect, onCleanup } from "solid-js";
2
+ import { overlayStyle, cardStyle } from "./modal-styles.ts";
3
+
4
+ interface ConfirmModalProps {
5
+ isOpen: boolean;
6
+ title: string;
7
+ message: string;
8
+ confirmLabel?: string;
9
+ onConfirm: () => void;
10
+ onCancel: () => void;
11
+ }
12
+
13
+ export function ConfirmModal(props: ConfirmModalProps) {
14
+ createEffect(() => {
15
+ if (!props.isOpen) return;
16
+ const handleEscape = (e: KeyboardEvent) => {
17
+ if (e.key === "Escape") props.onCancel();
18
+ };
19
+ document.addEventListener("keydown", handleEscape);
20
+ onCleanup(() => document.removeEventListener("keydown", handleEscape));
21
+ });
22
+
23
+ return (
24
+ <Show when={props.isOpen}>
25
+ <div
26
+ onClick={(e) => { if (e.target === e.currentTarget) props.onCancel(); }}
27
+ style={overlayStyle}
28
+ >
29
+ <div
30
+ style={{ ...cardStyle, "min-width": "300px", "max-width": "400px" }}
31
+ >
32
+ <h3 style={{ margin: "0 0 12px", "font-size": "15px" }}>{props.title}</h3>
33
+ <p style={{ margin: "0 0 20px", "font-size": "13px", color: "#6b7280" }}>{props.message}</p>
34
+ <div style={{ display: "flex", "justify-content": "flex-end", gap: "8px" }}>
35
+ <button
36
+ onClick={() => props.onCancel()}
37
+ style={{
38
+ background: "none",
39
+ border: "1px solid #2a323c",
40
+ color: "#edf2f7",
41
+ padding: "4px 14px",
42
+ "border-radius": "4px",
43
+ cursor: "pointer",
44
+ "font-size": "12px",
45
+ }}
46
+ >
47
+ Cancel
48
+ </button>
49
+ <button
50
+ onClick={() => props.onConfirm()}
51
+ style={{
52
+ background: "#944",
53
+ border: "1px solid #a55",
54
+ color: "#fff",
55
+ padding: "4px 14px",
56
+ "border-radius": "4px",
57
+ cursor: "pointer",
58
+ "font-size": "12px",
59
+ }}
60
+ >
61
+ {props.confirmLabel ?? "Confirm"}
62
+ </button>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </Show>
67
+ );
68
+ }
@@ -0,0 +1,58 @@
1
+ export interface DocHistoryEntry {
2
+ url: string;
3
+ title: string;
4
+ lastOpened: number;
5
+ }
6
+
7
+ const HISTORY_CAP = 100;
8
+
9
+ function historyKey(toolId: string, identityHexId: string): string {
10
+ return `${toolId}-doc-history-${identityHexId}`;
11
+ }
12
+
13
+ export function activeDocKey(toolId: string, identityHexId: string): string {
14
+ return `${toolId}-standalone-${identityHexId}`;
15
+ }
16
+
17
+ export function loadHistory(toolId: string, identityHexId: string): DocHistoryEntry[] {
18
+ try {
19
+ const raw = localStorage.getItem(historyKey(toolId, identityHexId));
20
+ return raw ? JSON.parse(raw) : [];
21
+ } catch {
22
+ return [];
23
+ }
24
+ }
25
+
26
+ function saveHistory(toolId: string, identityHexId: string, entries: DocHistoryEntry[]): void {
27
+ localStorage.setItem(historyKey(toolId, identityHexId), JSON.stringify(entries));
28
+ }
29
+
30
+ export function upsertHistory(toolId: string, identityHexId: string, url: string, title: string): DocHistoryEntry[] {
31
+ const entries = loadHistory(toolId, identityHexId);
32
+ const idx = entries.findIndex((e) => e.url === url);
33
+ if (idx !== -1) {
34
+ entries[idx].title = title;
35
+ entries[idx].lastOpened = Date.now();
36
+ } else {
37
+ entries.push({ url, title, lastOpened: Date.now() });
38
+ }
39
+ entries.sort((a, b) => b.lastOpened - a.lastOpened);
40
+ const capped = entries.slice(0, HISTORY_CAP);
41
+ saveHistory(toolId, identityHexId, capped);
42
+ return capped;
43
+ }
44
+
45
+ export function removeFromHistory(toolId: string, identityHexId: string, url: string): DocHistoryEntry[] {
46
+ const entries = loadHistory(toolId, identityHexId).filter((e) => e.url !== url);
47
+ saveHistory(toolId, identityHexId, entries);
48
+ return entries;
49
+ }
50
+
51
+ export function truncateUrl(url: string): string {
52
+ const prefix = "automerge:";
53
+ if (url.startsWith(prefix)) {
54
+ const hash = url.slice(prefix.length);
55
+ return prefix + hash.slice(0, 12) + "...";
56
+ }
57
+ return url;
58
+ }