@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.
- package/README.md +34 -0
- package/dist/confirm-modal.d.ts +10 -0
- package/dist/doc-history.d.ts +10 -0
- package/dist/frame.d.ts +10 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +711 -0
- package/dist/modal-styles.d.ts +17 -0
- package/dist/mount.d.ts +2 -0
- package/dist/new-doc-modal.d.ts +8 -0
- package/dist/share-modal.d.ts +10 -0
- package/dist/url-hash.d.ts +3 -0
- package/package.json +39 -0
- package/src/confirm-modal.tsx +68 -0
- package/src/doc-history.ts +58 -0
- package/src/frame.tsx +660 -0
- package/src/index.ts +25 -0
- package/src/modal-styles.ts +18 -0
- package/src/mount.tsx +10 -0
- package/src/new-doc-modal.tsx +98 -0
- package/src/share-modal.tsx +349 -0
- package/src/url-hash.ts +13 -0
|
@@ -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
|
+
};
|
package/dist/mount.d.ts
ADDED
|
@@ -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 {};
|
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
|
+
}
|