@jtfmumm/patchwork-standalone-frame 0.2.0 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
  import type { AutomergeUrl } from "@automerge/automerge-repo";
2
- import { type AutomergeRepoKeyhive } from "@automerge/automerge-repo-keyhive";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jtfmumm/patchwork-standalone-frame",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Reusable standalone frame for patchwork tools with keyhive, doc history, and access control",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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 keyhiveDocId = docIdFromAutomergeUrl(docUrl);
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 keyhiveDocId = createMemo(() => docIdFromAutomergeUrl(props.docUrl));
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
- const currentUserHexId = createMemo(() => {
83
- const id = props.hive.active.individual.id;
84
- return id ? uint8ArrayToHex(id.toBytes()) : null;
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
- const syncServerHexId = createMemo(() => {
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
- const publicHexId = createMemo(() => {
96
- const publicId = Identifier.publicId();
97
- return uint8ArrayToHex(publicId.toBytes());
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
- return accessList[publicHexId()] || null;
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, keyhiveDocId());
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 contactCard = ContactCard.fromJson(input);
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 access = Access.tryFromString("write");
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, publicHexId());
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 === currentUserHexId();
298
- const isSyncServer = hexId === syncServerHexId();
299
- const isPublic = hexId === publicHexId();
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;