@jtfmumm/patchwork-standalone-frame 0.2.0 → 0.3.1
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 +167 -17
- package/dist/index.d.ts +2 -0
- package/dist/index.js +462 -444
- package/dist/share-modal.d.ts +1 -1
- package/package.json +1 -1
- package/src/frame.tsx +1 -1
- package/src/index.ts +2 -0
- package/src/share-modal.tsx +64 -37
package/dist/share-modal.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AutomergeUrl } from "@automerge/automerge-repo";
|
|
2
|
-
import {
|
|
2
|
+
import type { AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
|
|
3
3
|
interface ShareModalProps {
|
|
4
4
|
isOpen: boolean;
|
|
5
5
|
docUrl: AutomergeUrl;
|
package/package.json
CHANGED
package/src/frame.tsx
CHANGED
|
@@ -257,7 +257,7 @@ export function StandaloneApp<D>(props: { tool: ToolRegistration<D>; config?: St
|
|
|
257
257
|
const keyhiveStorage = new IndexedDBStorageAdapter(`${tool.id}-keyhive`);
|
|
258
258
|
const envSyncUrl = (import.meta as unknown as Record<string, Record<string, string>>).env?.VITE_SYNC_URL;
|
|
259
259
|
const networkAdapter = new BrowserWebSocketClientAdapter(
|
|
260
|
-
envSyncUrl || tool.syncUrl || "ws://localhost:3030"
|
|
260
|
+
props.config?.syncUrl || envSyncUrl || tool.syncUrl || "ws://localhost:3030"
|
|
261
261
|
);
|
|
262
262
|
const peerIdSuffix = `${tool.id}-${Math.random().toString(36).slice(2)}`;
|
|
263
263
|
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface StandaloneFrameConfig {
|
|
|
39
39
|
legacyMode?: boolean;
|
|
40
40
|
/** Pre-built repo for legacy mode. Required when legacyMode is true. */
|
|
41
41
|
repo?: FrameRepo;
|
|
42
|
+
/** WebSocket sync server URL. Overrides tool.syncUrl. */
|
|
43
|
+
syncUrl?: string;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export interface PluginDescription {
|
package/src/share-modal.tsx
CHANGED
|
@@ -7,14 +7,7 @@ import {
|
|
|
7
7
|
createMemo,
|
|
8
8
|
} from "solid-js";
|
|
9
9
|
import type { AutomergeUrl } from "@automerge/automerge-repo";
|
|
10
|
-
import {
|
|
11
|
-
Access,
|
|
12
|
-
ContactCard,
|
|
13
|
-
docIdFromAutomergeUrl,
|
|
14
|
-
Identifier,
|
|
15
|
-
uint8ArrayToHex,
|
|
16
|
-
type AutomergeRepoKeyhive,
|
|
17
|
-
} from "@automerge/automerge-repo-keyhive";
|
|
10
|
+
import type { AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
|
|
18
11
|
import { overlayStyle, cardStyle } from "./modal-styles.ts";
|
|
19
12
|
|
|
20
13
|
type DocAccessList = Record<string, string>;
|
|
@@ -26,17 +19,34 @@ interface ShareModalProps {
|
|
|
26
19
|
onClose: () => void;
|
|
27
20
|
}
|
|
28
21
|
|
|
22
|
+
// Lazy-loaded keyhive utilities — resolved once on first use
|
|
23
|
+
let keyhiveModule: {
|
|
24
|
+
docIdFromAutomergeUrl: (url: AutomergeUrl) => unknown;
|
|
25
|
+
uint8ArrayToHex: (bytes: Uint8Array) => string;
|
|
26
|
+
ContactCard: { fromJson: (json: string) => unknown };
|
|
27
|
+
Identifier: { publicId: () => { toBytes: () => Uint8Array } };
|
|
28
|
+
Access: { tryFromString: (s: string) => unknown };
|
|
29
|
+
} | null = null;
|
|
30
|
+
|
|
31
|
+
async function getKeyhive() {
|
|
32
|
+
if (!keyhiveModule) {
|
|
33
|
+
keyhiveModule = await import("@automerge/automerge-repo-keyhive") as unknown as typeof keyhiveModule;
|
|
34
|
+
}
|
|
35
|
+
return keyhiveModule!;
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
const ACCESS_LEVELS = ["Pull", "Read", "Write", "Admin"] as const;
|
|
30
39
|
|
|
31
40
|
async function fetchAccessList(
|
|
32
41
|
hive: AutomergeRepoKeyhive,
|
|
33
42
|
docUrl: AutomergeUrl
|
|
34
43
|
): Promise<DocAccessList> {
|
|
35
|
-
const
|
|
44
|
+
const kh = await getKeyhive();
|
|
45
|
+
const keyhiveDocId = kh.docIdFromAutomergeUrl(docUrl);
|
|
36
46
|
const accessList: DocAccessList = {};
|
|
37
|
-
const members = await hive.docMemberCapabilities(keyhiveDocId);
|
|
47
|
+
const members = await hive.docMemberCapabilities(keyhiveDocId as Parameters<typeof hive.docMemberCapabilities>[0]);
|
|
38
48
|
members.forEach((capability) => {
|
|
39
|
-
const hexId = uint8ArrayToHex(capability.who.id.toBytes());
|
|
49
|
+
const hexId = kh.uint8ArrayToHex(capability.who.id.toBytes());
|
|
40
50
|
accessList[hexId] = capability.can.toString();
|
|
41
51
|
});
|
|
42
52
|
return accessList;
|
|
@@ -77,41 +87,56 @@ export function ShareModal(props: ShareModalProps) {
|
|
|
77
87
|
const [isLoadingAccessList, setIsLoadingAccessList] = createSignal(true);
|
|
78
88
|
const [currentUserAccess, setCurrentUserAccess] = createSignal<string | undefined>(undefined);
|
|
79
89
|
const [isSubmitting, setIsSubmitting] = createSignal(false);
|
|
80
|
-
const
|
|
90
|
+
const [keyhiveDocIdVal, setKeyhiveDocIdVal] = createSignal<unknown>(null);
|
|
91
|
+
const [currentUserHexIdVal, setCurrentUserHexIdVal] = createSignal<string | null>(null);
|
|
92
|
+
const [syncServerHexIdVal, setSyncServerHexIdVal] = createSignal<string | null>(null);
|
|
93
|
+
const [publicHexIdVal, setPublicHexIdVal] = createSignal<string>("");
|
|
81
94
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
// Load keyhive and compute derived values when modal opens
|
|
96
|
+
createEffect(() => {
|
|
97
|
+
if (!props.isOpen) return;
|
|
98
|
+
let cancelled = false;
|
|
99
|
+
(async () => {
|
|
100
|
+
const kh = await getKeyhive();
|
|
101
|
+
if (cancelled) return;
|
|
86
102
|
|
|
87
|
-
|
|
88
|
-
const syncServer = props.hive.syncServer;
|
|
89
|
-
if (!syncServer) return null;
|
|
90
|
-
const contactCard = ContactCard.fromJson(syncServer.contactCard.toJson());
|
|
91
|
-
if (!contactCard) return null;
|
|
92
|
-
return uint8ArrayToHex(contactCard.individualId.bytes);
|
|
93
|
-
});
|
|
103
|
+
setKeyhiveDocIdVal(kh.docIdFromAutomergeUrl(props.docUrl));
|
|
94
104
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
const id = props.hive.active.individual.id;
|
|
106
|
+
setCurrentUserHexIdVal(id ? kh.uint8ArrayToHex(id.toBytes()) : null);
|
|
107
|
+
|
|
108
|
+
const syncServer = props.hive.syncServer;
|
|
109
|
+
if (syncServer) {
|
|
110
|
+
const contactCard = kh.ContactCard.fromJson(syncServer.contactCard.toJson());
|
|
111
|
+
if (contactCard) {
|
|
112
|
+
setSyncServerHexIdVal(kh.uint8ArrayToHex((contactCard as { individualId: { bytes: Uint8Array } }).individualId.bytes));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const publicId = kh.Identifier.publicId();
|
|
117
|
+
setPublicHexIdVal(kh.uint8ArrayToHex(publicId.toBytes()));
|
|
118
|
+
})();
|
|
119
|
+
onCleanup(() => { cancelled = true; });
|
|
98
120
|
});
|
|
99
121
|
|
|
100
122
|
const currentPublicAccess = createMemo(() => {
|
|
101
123
|
const accessList = docAccessList();
|
|
102
|
-
|
|
124
|
+
const pubId = publicHexIdVal();
|
|
125
|
+
return pubId ? (accessList[pubId] || null) : null;
|
|
103
126
|
});
|
|
104
127
|
|
|
105
128
|
const isAdmin = createMemo(() => currentUserAccess() === "Admin");
|
|
106
129
|
|
|
107
130
|
createEffect(() => {
|
|
108
131
|
if (!props.isOpen) return;
|
|
132
|
+
const docId = keyhiveDocIdVal();
|
|
133
|
+
if (!docId) return;
|
|
109
134
|
let cancelled = false;
|
|
110
135
|
(async () => {
|
|
111
136
|
const id = props.hive.active.individual.id;
|
|
112
137
|
if (!id) { if (!cancelled) setCurrentUserAccess(undefined); return; }
|
|
113
138
|
try {
|
|
114
|
-
const access = await props.hive.accessForDoc(id,
|
|
139
|
+
const access = await props.hive.accessForDoc(id, docId as Parameters<typeof props.hive.accessForDoc>[1]);
|
|
115
140
|
if (!cancelled) setCurrentUserAccess(access ? access.toString() : undefined);
|
|
116
141
|
} catch (err) {
|
|
117
142
|
if (!cancelled) { console.error("[ShareModal] Error checking access:", err); setCurrentUserAccess(undefined); }
|
|
@@ -148,11 +173,12 @@ export function ShareModal(props: ShareModalProps) {
|
|
|
148
173
|
if (!input) return;
|
|
149
174
|
setIsSubmitting(true);
|
|
150
175
|
try {
|
|
151
|
-
const
|
|
176
|
+
const kh = await getKeyhive();
|
|
177
|
+
const contactCard = kh.ContactCard.fromJson(input);
|
|
152
178
|
if (!contactCard) throw new Error("Invalid ContactCard JSON");
|
|
153
|
-
const access = Access.tryFromString("write");
|
|
179
|
+
const access = kh.Access.tryFromString("write");
|
|
154
180
|
if (!access) throw new Error("Invalid access level");
|
|
155
|
-
await props.hive.addMemberToDoc(props.docUrl, contactCard, access);
|
|
181
|
+
await props.hive.addMemberToDoc(props.docUrl, contactCard as Parameters<typeof props.hive.addMemberToDoc>[1], access as Parameters<typeof props.hive.addMemberToDoc>[2]);
|
|
156
182
|
setContactCardInput("");
|
|
157
183
|
} catch (err) {
|
|
158
184
|
console.error("[ShareModal]", err);
|
|
@@ -176,9 +202,10 @@ export function ShareModal(props: ShareModalProps) {
|
|
|
176
202
|
|
|
177
203
|
const handleMakePublic = async () => {
|
|
178
204
|
try {
|
|
179
|
-
const
|
|
205
|
+
const kh = await getKeyhive();
|
|
206
|
+
const access = kh.Access.tryFromString("write");
|
|
180
207
|
if (!access) throw new Error("Invalid access level");
|
|
181
|
-
await props.hive.setPublicAccess(props.docUrl, access);
|
|
208
|
+
await props.hive.setPublicAccess(props.docUrl, access as Parameters<typeof props.hive.setPublicAccess>[1]);
|
|
182
209
|
} catch (err) {
|
|
183
210
|
console.error("[ShareModal]", err);
|
|
184
211
|
} finally {
|
|
@@ -189,7 +216,7 @@ export function ShareModal(props: ShareModalProps) {
|
|
|
189
216
|
|
|
190
217
|
const handleMakePrivate = async () => {
|
|
191
218
|
try {
|
|
192
|
-
await props.hive.revokeMemberFromDoc(props.docUrl,
|
|
219
|
+
await props.hive.revokeMemberFromDoc(props.docUrl, publicHexIdVal());
|
|
193
220
|
} catch (err) {
|
|
194
221
|
console.error("[ShareModal]", err);
|
|
195
222
|
} finally {
|
|
@@ -294,9 +321,9 @@ export function ShareModal(props: ShareModalProps) {
|
|
|
294
321
|
<div>
|
|
295
322
|
<For each={sortedMembers()}>
|
|
296
323
|
{([hexId, access]) => {
|
|
297
|
-
const isCurrentUser = hexId ===
|
|
298
|
-
const isSyncServer = hexId ===
|
|
299
|
-
const isPublic = hexId ===
|
|
324
|
+
const isCurrentUser = hexId === currentUserHexIdVal();
|
|
325
|
+
const isSyncServer = hexId === syncServerHexIdVal();
|
|
326
|
+
const isPublic = hexId === publicHexIdVal();
|
|
300
327
|
const myAccessIdx = ACCESS_LEVELS.indexOf(currentUserAccess() as typeof ACCESS_LEVELS[number]);
|
|
301
328
|
const memberAccessIdx = ACCESS_LEVELS.indexOf(access as typeof ACCESS_LEVELS[number]);
|
|
302
329
|
const canRemove = myAccessIdx >= 0 && memberAccessIdx >= 0 && memberAccessIdx <= myAccessIdx && !isCurrentUser && !isSyncServer;
|