fauna-api 0.0.0-stage → 1.0.676

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.
Files changed (171) hide show
  1. package/dist/brands.d.ts +2 -0
  2. package/dist/brands.js +2 -0
  3. package/dist/index.d.ts +64 -0
  4. package/dist/index.js +121 -0
  5. package/dist/src/ant/chat.d.ts +130 -0
  6. package/dist/src/ant/chat.js +467 -0
  7. package/dist/src/ant/format.d.ts +40 -0
  8. package/dist/src/ant/format.js +143 -0
  9. package/dist/src/ant/graph.d.ts +130 -0
  10. package/dist/src/ant/graph.js +449 -0
  11. package/dist/src/ant/index.d.ts +16 -0
  12. package/dist/src/ant/index.js +61 -0
  13. package/dist/src/ant/persona.d.ts +151 -0
  14. package/dist/src/ant/persona.js +136 -0
  15. package/dist/src/ant/timeline.d.ts +283 -0
  16. package/dist/src/ant/timeline.js +1025 -0
  17. package/dist/src/authentification.d.ts +45 -0
  18. package/dist/src/authentification.js +194 -0
  19. package/dist/src/billing.d.ts +135 -0
  20. package/dist/src/billing.js +128 -0
  21. package/dist/src/calendar.d.ts +83 -0
  22. package/dist/src/calendar.js +133 -0
  23. package/dist/src/chats.d.ts +113 -0
  24. package/dist/src/chats.js +100 -0
  25. package/dist/src/cicada/contacts.d.ts +31 -0
  26. package/dist/src/cicada/contacts.js +28 -0
  27. package/dist/src/cicada/conversations.d.ts +73 -0
  28. package/dist/src/cicada/conversations.js +236 -0
  29. package/dist/src/cicada/inboxes.d.ts +48 -0
  30. package/dist/src/cicada/inboxes.js +82 -0
  31. package/dist/src/cicada/index.d.ts +16 -0
  32. package/dist/src/cicada/index.js +61 -0
  33. package/dist/src/cicada/phone.d.ts +48 -0
  34. package/dist/src/cicada/phone.js +306 -0
  35. package/dist/src/drive.d.ts +80 -0
  36. package/dist/src/drive.js +472 -0
  37. package/dist/src/extensions/index.d.ts +4 -0
  38. package/dist/src/extensions/index.js +5 -0
  39. package/dist/src/extensions/web/index.d.ts +6 -0
  40. package/dist/src/extensions/web/index.js +7 -0
  41. package/dist/src/extensions/web/session.d.ts +106 -0
  42. package/dist/src/extensions/web/session.js +469 -0
  43. package/dist/src/extensions/web/tools.d.ts +13 -0
  44. package/dist/src/extensions/web/tools.js +72 -0
  45. package/dist/src/friends.d.ts +39 -0
  46. package/dist/src/friends.js +119 -0
  47. package/dist/src/groups.d.ts +82 -0
  48. package/dist/src/groups.js +269 -0
  49. package/dist/src/inbox.d.ts +11 -0
  50. package/dist/src/inbox.js +35 -0
  51. package/dist/src/integrations.d.ts +52 -0
  52. package/dist/src/integrations.js +205 -0
  53. package/dist/src/magpie/explorer.d.ts +14 -0
  54. package/dist/src/magpie/explorer.js +36 -0
  55. package/dist/src/magpie/index.d.ts +16 -0
  56. package/dist/src/magpie/index.js +61 -0
  57. package/dist/src/magpie/portfolio.d.ts +52 -0
  58. package/dist/src/magpie/portfolio.js +179 -0
  59. package/dist/src/magpie/quotes.d.ts +35 -0
  60. package/dist/src/magpie/quotes.js +47 -0
  61. package/dist/src/magpie/room.d.ts +61 -0
  62. package/dist/src/magpie/room.js +238 -0
  63. package/dist/src/maps.d.ts +159 -0
  64. package/dist/src/maps.js +171 -0
  65. package/dist/src/marmot/browser.d.ts +139 -0
  66. package/dist/src/marmot/browser.js +389 -0
  67. package/dist/src/marmot/index.d.ts +20 -0
  68. package/dist/src/marmot/index.js +65 -0
  69. package/dist/src/marmot/media.d.ts +56 -0
  70. package/dist/src/marmot/media.js +265 -0
  71. package/dist/src/marmot/research.d.ts +337 -0
  72. package/dist/src/marmot/research.js +1138 -0
  73. package/dist/src/marmot/scraper.d.ts +59 -0
  74. package/dist/src/marmot/scraper.js +94 -0
  75. package/dist/src/marmot/web.d.ts +402 -0
  76. package/dist/src/marmot/web.js +2586 -0
  77. package/dist/src/marmot/workspace.d.ts +81 -0
  78. package/dist/src/marmot/workspace.js +263 -0
  79. package/dist/src/parrot/agent.d.ts +53 -0
  80. package/dist/src/parrot/agent.js +93 -0
  81. package/dist/src/parrot/chat.d.ts +84 -0
  82. package/dist/src/parrot/chat.js +237 -0
  83. package/dist/src/parrot/format.d.ts +128 -0
  84. package/dist/src/parrot/format.js +639 -0
  85. package/dist/src/parrot/generative.d.ts +261 -0
  86. package/dist/src/parrot/generative.js +185 -0
  87. package/dist/src/parrot/index.d.ts +22 -0
  88. package/dist/src/parrot/index.js +67 -0
  89. package/dist/src/parrot/room.d.ts +225 -0
  90. package/dist/src/parrot/room.js +466 -0
  91. package/dist/src/parrot/routing.d.ts +90 -0
  92. package/dist/src/parrot/routing.js +78 -0
  93. package/dist/src/parrot/share.d.ts +126 -0
  94. package/dist/src/parrot/share.js +261 -0
  95. package/dist/src/parrot/socket.d.ts +41 -0
  96. package/dist/src/parrot/socket.js +478 -0
  97. package/dist/src/payments.d.ts +73 -0
  98. package/dist/src/payments.js +107 -0
  99. package/dist/src/private/cards.css +75 -0
  100. package/dist/src/rooms.d.ts +76 -0
  101. package/dist/src/rooms.js +168 -0
  102. package/dist/src/shared/schema.d.ts +24 -0
  103. package/dist/src/shared/schema.js +1 -0
  104. package/dist/src/shared/server.d.ts +65 -0
  105. package/dist/src/shared/server.js +302 -0
  106. package/dist/src/termite/environment.d.ts +110 -0
  107. package/dist/src/termite/environment.js +263 -0
  108. package/dist/src/termite/index.d.ts +14 -0
  109. package/dist/src/termite/index.js +59 -0
  110. package/dist/src/termite/os.d.ts +326 -0
  111. package/dist/src/termite/os.js +572 -0
  112. package/dist/src/termite/versioning.d.ts +94 -0
  113. package/dist/src/termite/versioning.js +217 -0
  114. package/dist/src/tickets.d.ts +43 -0
  115. package/dist/src/tickets.js +138 -0
  116. package/dist/src/travel.d.ts +85 -0
  117. package/dist/src/travel.js +44 -0
  118. package/dist/src/ui/authentification.d.ts +24 -0
  119. package/dist/src/ui/authentification.js +177 -0
  120. package/dist/src/ui/brands.css +11 -0
  121. package/dist/src/ui/brands.d.ts +7 -0
  122. package/dist/src/ui/brands.js +14 -0
  123. package/dist/src/ui/browser.d.ts +74 -0
  124. package/dist/src/ui/browser.js +470 -0
  125. package/dist/src/ui/chat.d.ts +183 -0
  126. package/dist/src/ui/chat.js +2379 -0
  127. package/dist/src/ui/chatPanel.d.ts +109 -0
  128. package/dist/src/ui/chatPanel.js +768 -0
  129. package/dist/src/ui/chatSequence.d.ts +36 -0
  130. package/dist/src/ui/chatSequence.js +42 -0
  131. package/dist/src/ui/crown.d.ts +19 -0
  132. package/dist/src/ui/crown.js +199 -0
  133. package/dist/src/ui/cursor.d.ts +73 -0
  134. package/dist/src/ui/cursor.js +175 -0
  135. package/dist/src/ui/device.d.ts +33 -0
  136. package/dist/src/ui/device.js +49 -0
  137. package/dist/src/ui/drive.d.ts +69 -0
  138. package/dist/src/ui/drive.js +1243 -0
  139. package/dist/src/ui/generative.d.ts +39 -0
  140. package/dist/src/ui/generative.js +313 -0
  141. package/dist/src/ui/hover.d.ts +6 -0
  142. package/dist/src/ui/hover.js +210 -0
  143. package/dist/src/ui/index.d.ts +42 -0
  144. package/dist/src/ui/index.js +87 -0
  145. package/dist/src/ui/integrations.d.ts +13 -0
  146. package/dist/src/ui/integrations.js +175 -0
  147. package/dist/src/ui/maps.d.ts +87 -0
  148. package/dist/src/ui/maps.js +547 -0
  149. package/dist/src/ui/roster.d.ts +32 -0
  150. package/dist/src/ui/roster.js +163 -0
  151. package/dist/src/ui/talk.d.ts +12 -0
  152. package/dist/src/ui/talk.js +560 -0
  153. package/dist/src/ui/tickets.d.ts +13 -0
  154. package/dist/src/ui/tickets.js +966 -0
  155. package/dist/src/ui/toast.d.ts +7 -0
  156. package/dist/src/ui/toast.js +258 -0
  157. package/dist/src/ui/tokenText.d.ts +18 -0
  158. package/dist/src/ui/tokenText.js +96 -0
  159. package/dist/src/ui/tokenized.d.ts +42 -0
  160. package/dist/src/ui/tokenized.js +622 -0
  161. package/dist/src/ui/ui.css +4763 -0
  162. package/dist/src/ui/view.d.ts +23 -0
  163. package/dist/src/ui/view.js +584 -0
  164. package/dist/src/view.d.ts +113 -0
  165. package/dist/src/view.js +386 -0
  166. package/dist/src/webhooks.d.ts +66 -0
  167. package/dist/src/webhooks.js +78 -0
  168. package/dist/ui.d.ts +1 -0
  169. package/dist/ui.js +1 -0
  170. package/package.json +89 -6
  171. package/README.md +0 -3
@@ -0,0 +1,1243 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useImperativeHandle, useLayoutEffect, useRef, forwardRef } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { flushSync } from "react-dom";
5
+ import { Fauna } from "fauna-api";
6
+ import gsap from "gsap";
7
+ import { createHoveringElement, createTooltip } from "./hover.js";
8
+ import { sendProgressToast } from "./toast.js";
9
+ // ═══════════════════════════════════════════════════════════════════════════
10
+ // Drive — the file explorer + file card, ported from the original imperative
11
+ // widget to React. The public surface is unchanged: `createFileElement` and
12
+ // `createFileExplorer` are still imperative factories that return a mountable
13
+ // element + control handle, so nothing at the call sites (apps/fauna Drive tab,
14
+ // apps/fauna /view/) has to change. Internally, `createFileExplorer` mounts a
15
+ // React `FileExplorer` component (via a flushSync createRoot) that owns an
16
+ // `ExplorerEngine` instance holding the DOM-building logic lifted verbatim from
17
+ // the original widget, exposed through useImperativeHandle — the same bridge
18
+ // pattern used by chat.tsx.
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+ // ─── MIME maps ─────────────────────────────────────────────────────────────
21
+ // Content-type → extension is sourced from Marmot's public `/media/types`
22
+ // route (the canonical `PARSABLE_CONTENT_TYPES` map) rather than hardcoded, so
23
+ // there's a single server-owned source of truth. `getTypes()` is cached, so
24
+ // hydration is cheap and idempotent; we flatten it into a synchronous lookup so
25
+ // `createFileElement` (which builds DOM synchronously) can read it inline.
26
+ const EXTENSION_CACHE = {
27
+ // Folders are a Drive-only virtual type, not a Marmot-parsable media type,
28
+ // so the server list never carries it — keep it seeded locally.
29
+ 'application/x-directory': 'dir',
30
+ };
31
+ // Guard so the network fetch happens at most once regardless of how many cards
32
+ // or explorers trigger it. `getTypes()` is itself cached, but this also avoids
33
+ // spawning redundant in-flight promises.
34
+ let extensionsHydration = null;
35
+ function hydrateExtensions() {
36
+ if (!extensionsHydration) {
37
+ extensionsHydration = Fauna.Marmot.Media.getTypes().then((grouped) => {
38
+ for (const infos of Object.values(grouped)) {
39
+ for (const info of infos) {
40
+ if (info.extensions[0])
41
+ EXTENSION_CACHE[info.contentType] = info.extensions[0];
42
+ }
43
+ }
44
+ }).catch(() => {
45
+ // Let a later call retry; the filename-extension fallback covers the gap.
46
+ extensionsHydration = null;
47
+ });
48
+ }
49
+ return extensionsHydration;
50
+ }
51
+ // Resolve a display extension for a file. Prefers the server-sourced map, then
52
+ // falls back to the filename's own extension, then a generic 'file' — so an
53
+ // unmapped content type degrades gracefully instead of throwing. Also warms the
54
+ // cache on first use so consumers that build cards without an ExplorerEngine
55
+ // (e.g. previews) still converge to the canonical extension on re-render.
56
+ function extensionFor(contentType, name) {
57
+ void hydrateExtensions();
58
+ return EXTENSION_CACHE[contentType]
59
+ ?? (name.includes('.') ? name.split('.').pop() : 'file');
60
+ }
61
+ // Material-symbol fallback per content type, shown when no thumbnail has been
62
+ // painted onto the preview canvas yet (or the type doesn't have a meaningful
63
+ // visual preview, e.g. folders, audio, generic text).
64
+ export const MIME_TO_ICON = {
65
+ 'application/x-directory': 'folder',
66
+ 'application/pdf': 'picture_as_pdf',
67
+ 'text/plain': 'description',
68
+ 'text/markdown': 'article',
69
+ 'application/json': 'data_object',
70
+ 'text/csv': 'table_chart',
71
+ 'text/html': 'code',
72
+ 'application/xml': 'code',
73
+ 'audio/mpeg': 'graphic_eq',
74
+ 'audio/wav': 'graphic_eq',
75
+ 'image/jpeg': 'image',
76
+ 'image/png': 'image',
77
+ 'image/webp': 'image',
78
+ 'image/heic': 'image',
79
+ 'image/heif': 'image',
80
+ };
81
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
82
+ export function normalizePath(p) {
83
+ return p.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/{2,}/g, "/").replace(/\/$/, "");
84
+ }
85
+ // ─── File card (imperative DOM factory, ported verbatim) ───────────────────────
86
+ export function createFileElement(file, options = {}) {
87
+ function clone(element) {
88
+ const clone = element.cloneNode(true);
89
+ clone.addEventListener('click', () => {
90
+ element.click();
91
+ });
92
+ return clone;
93
+ }
94
+ const view = options.view || 'list';
95
+ var element = document.createElement('div');
96
+ element.className = `file-${view}`;
97
+ const preview = document.createElement('canvas');
98
+ preview.className = `file-${view}-preview`;
99
+ const dpr = window.devicePixelRatio || 1;
100
+ const width = element.offsetWidth * dpr;
101
+ const height = element.offsetHeight * dpr;
102
+ preview.width = width;
103
+ preview.height = height;
104
+ element.appendChild(preview);
105
+ // Centred material-symbol icon shown over the preview canvas while nothing
106
+ // has been painted into it. Subsequent thumbnail rendering can hide this by
107
+ // adding the `drawn` class to the file element.
108
+ const previewIcon = document.createElement('div');
109
+ previewIcon.className = 'file-preview-icon';
110
+ const previewIconGlyph = document.createElement('span');
111
+ previewIconGlyph.className = 'material-symbols-outlined';
112
+ previewIconGlyph.textContent = MIME_TO_ICON[file.contentType] ?? 'draft';
113
+ previewIcon.appendChild(previewIconGlyph);
114
+ element.appendChild(previewIcon);
115
+ // Lazily fetch a server-generated thumbnail and paint it into the preview
116
+ // canvas as a small, contained "sheet" — like a little file resting inside
117
+ // the card — rather than a full-bleed cover image. Once painted, the `drawn`
118
+ // class fades out the fallback icon. The bitmap is cached so the
119
+ // ResizeObserver can cheaply repaint after the canvas backing store resizes.
120
+ let previewBitmap = null;
121
+ let previewRequested = false;
122
+ function drawBoxyImage(ctx, bitmap, x, y, w, h, dpr) {
123
+ ctx.save();
124
+ // The server renders text sheets on white and images on their own bg;
125
+ // fill white behind so any letterboxing reads as paper, not a hole.
126
+ ctx.fillStyle = '#ffffff';
127
+ ctx.fillRect(x, y, w, h);
128
+ // contain-fit inside the box
129
+ ctx.beginPath();
130
+ ctx.rect(x, y, w, h);
131
+ ctx.clip();
132
+ const scale = Math.min(w / bitmap.width, h / bitmap.height);
133
+ const dw = bitmap.width * scale;
134
+ const dh = bitmap.height * scale;
135
+ ctx.drawImage(bitmap, x + (w - dw) / 2, y + (h - dh) / 2, dw, dh);
136
+ ctx.restore();
137
+ // Crisp 1px border so the file reads as a boxy sheet against the card.
138
+ ctx.save();
139
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.28)';
140
+ ctx.lineWidth = Math.max(1, Math.round(dpr));
141
+ ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
142
+ ctx.restore();
143
+ }
144
+ function paintPreview() {
145
+ if (!previewBitmap)
146
+ return;
147
+ const ctx = preview.getContext('2d');
148
+ if (!ctx || preview.width === 0 || preview.height === 0)
149
+ return;
150
+ const cw = preview.width;
151
+ const ch = preview.height;
152
+ const dpr = window.devicePixelRatio || 1;
153
+ ctx.clearRect(0, 0, cw, ch);
154
+ // Layout the sheet in the region above the footer, with even padding so
155
+ // the file "floats" inside the card. Footer is ~44px tall in CSS px.
156
+ const padX = 26 * dpr;
157
+ const padTop = 26 * dpr;
158
+ const footer = 52 * dpr;
159
+ const boxW = cw - padX * 2;
160
+ const boxH = ch - padTop - footer;
161
+ if (boxW <= 0 || boxH <= 0)
162
+ return;
163
+ // Fit the sheet to the source aspect ratio within the available box so a
164
+ // portrait doc stays portrait and a wide image stays wide.
165
+ const iw = previewBitmap.width;
166
+ const ih = previewBitmap.height;
167
+ const fit = Math.min(boxW / iw, boxH / ih);
168
+ const sheetW = iw * fit;
169
+ const sheetH = ih * fit;
170
+ const x = (cw - sheetW) / 2;
171
+ const y = padTop + (boxH - sheetH) / 2;
172
+ // Boxy sheet with a crisp border — no rounding, no shadow.
173
+ drawBoxyImage(ctx, previewBitmap, x, y, sheetW, sheetH, dpr);
174
+ element.classList.add('drawn');
175
+ }
176
+ async function requestPreview() {
177
+ if (previewRequested)
178
+ return;
179
+ // A freshly-wrapped file uploads asynchronously; requesting a preview
180
+ // before the bytes land server-side just 404s and would burn the
181
+ // one-shot flag. Skip until it's online — the upload's completion
182
+ // handler re-invokes this once the file exists.
183
+ //
184
+ // Exception: when `upload: false`, paint a local image preview from
185
+ // cached bytes (design / offline canvas use) instead of hitting the server.
186
+ if (!file.online && options.upload !== false)
187
+ return;
188
+ previewRequested = true;
189
+ try {
190
+ // Browsers generally can't paint HEIC via createImageBitmap — wait for
191
+ // the server WebP preview once the file is online.
192
+ const localImage = !file.online
193
+ && file.contentType.startsWith('image/')
194
+ && file.contentType !== 'image/heic'
195
+ && file.contentType !== 'image/heif';
196
+ if (localImage) {
197
+ const bytes = await file.download();
198
+ previewBitmap = await createImageBitmap(new Blob([new Uint8Array(bytes)], { type: file.contentType }));
199
+ paintPreview();
200
+ return;
201
+ }
202
+ if (!file.online)
203
+ return;
204
+ const bytes = await file.preview();
205
+ if (!bytes)
206
+ return;
207
+ previewBitmap = await createImageBitmap(new Blob([new Uint8Array(bytes)], { type: 'image/webp' }));
208
+ paintPreview();
209
+ }
210
+ catch (err) {
211
+ // Leave the fallback icon in place on any failure.
212
+ }
213
+ }
214
+ const previewViewport = new IntersectionObserver((entries) => {
215
+ for (const entry of entries) {
216
+ if (entry.isIntersecting) {
217
+ previewViewport.disconnect();
218
+ void requestPreview();
219
+ }
220
+ }
221
+ }, { rootMargin: '200px' });
222
+ previewViewport.observe(element);
223
+ const details = document.createElement('div');
224
+ details.className = `file-${view}-details`;
225
+ element.appendChild(details);
226
+ const label = document.createElement('div');
227
+ label.className = `file-${view}-label`;
228
+ details.appendChild(label);
229
+ const name = document.createElement('input');
230
+ name.type = 'text';
231
+ name.placeholder = 'Untitled';
232
+ name.value = file.name;
233
+ name.readOnly = true;
234
+ label.appendChild(name);
235
+ const type = document.createElement('span');
236
+ type.innerHTML = extensionFor(file.contentType, file.name).toUpperCase();
237
+ label.appendChild(type);
238
+ const opts = document.createElement('div');
239
+ opts.className = `file-${view}-options`;
240
+ details.appendChild(opts);
241
+ if (options.remove || options.remove === undefined) {
242
+ const remove = document.createElement('span');
243
+ remove.className = 'material-symbols-outlined';
244
+ remove.innerHTML = 'delete';
245
+ opts.appendChild(remove);
246
+ createTooltip(remove, file.contentType === 'application/x-directory' ? 'Delete folder' : 'Delete file');
247
+ remove.onclick = () => {
248
+ if (options.onRemove) {
249
+ options.onRemove();
250
+ return;
251
+ }
252
+ file.remove();
253
+ gsap.to(element, {
254
+ duration: 0.3,
255
+ opacity: 0,
256
+ display: 'none',
257
+ ease: 'power2.inOut',
258
+ onComplete: () => {
259
+ element.remove();
260
+ }
261
+ });
262
+ };
263
+ }
264
+ if (options.rename || options.rename === undefined) {
265
+ const rename = document.createElement('span');
266
+ rename.className = 'material-symbols-outlined';
267
+ rename.innerHTML = 'edit';
268
+ opts.appendChild(rename);
269
+ createTooltip(rename, 'Rename');
270
+ rename.onclick = () => {
271
+ if (!('path' in file.authorization)) {
272
+ throw new Error('401: Unauthorized request.');
273
+ }
274
+ name.readOnly = false;
275
+ name.focus();
276
+ name.select();
277
+ const commit = async () => {
278
+ if (!('path' in file.authorization)) {
279
+ throw new Error('401: Unauthorized request.');
280
+ }
281
+ name.readOnly = true;
282
+ const nextName = name.value.trim();
283
+ if (!nextName || nextName === file.name) {
284
+ name.value = file.name;
285
+ return;
286
+ }
287
+ const oldPath = normalizePath(file.authorization.path);
288
+ const split = oldPath.split('/');
289
+ split.pop();
290
+ const parent = split.join('/');
291
+ const newPath = (parent ? parent + '/' : '') + nextName + (file.contentType === 'application/x-directory' ? '/' : '');
292
+ try {
293
+ const toast = sendProgressToast(`Renaming ${file.name}...`);
294
+ toast.setProgress(0.1);
295
+ element.style.pointerEvents = 'none';
296
+ await file.copy(newPath);
297
+ toast.setProgress(0.7);
298
+ const newFile = await Fauna.Drive.pull({
299
+ authorization: file.authorization.authorization,
300
+ path: newPath,
301
+ group: 'path' in file.authorization ? file.authorization.group : undefined
302
+ });
303
+ await file.remove();
304
+ toast.setProgress(1);
305
+ file = newFile;
306
+ name.value = newFile.name;
307
+ element.style.pointerEvents = 'auto';
308
+ }
309
+ catch (err) {
310
+ name.value = file.name;
311
+ throw err;
312
+ }
313
+ };
314
+ name.onblur = () => { void commit(); };
315
+ name.onkeydown = (e) => {
316
+ if (e.key === 'Enter') {
317
+ e.preventDefault();
318
+ name.blur();
319
+ }
320
+ else if (e.key === 'Escape') {
321
+ e.preventDefault();
322
+ name.value = file.name;
323
+ name.readOnly = true;
324
+ name.blur();
325
+ }
326
+ };
327
+ };
328
+ }
329
+ if (options.download || options.download === undefined) {
330
+ const download = document.createElement('span');
331
+ download.className = 'material-symbols-outlined';
332
+ download.innerHTML = 'download';
333
+ opts.appendChild(download);
334
+ createTooltip(download, file.contentType === 'application/x-directory' ? 'Download as .zip' : 'Download');
335
+ download.onclick = async () => {
336
+ const toast = sendProgressToast(`Downloading ${file.name}...`);
337
+ const bytes = await file.download(true, (percent) => {
338
+ toast.setProgress(percent);
339
+ });
340
+ const blob = new Blob([new Uint8Array(bytes)], { type: file.contentType === 'application/x-directory' ? 'application/zip' : file.contentType });
341
+ const url = URL.createObjectURL(blob);
342
+ const a = document.createElement('a');
343
+ a.href = url;
344
+ a.download = file.name + (file.contentType === 'application/x-directory' ? '.zip' : '');
345
+ a.click();
346
+ URL.revokeObjectURL(url);
347
+ };
348
+ }
349
+ if ((options.open || options.open === undefined) && (file.contentType !== 'application/x-directory' || options.onFolderOpen)) {
350
+ const open = document.createElement('span');
351
+ open.className = 'material-symbols-outlined';
352
+ open.innerHTML = 'open_in_new';
353
+ opts.appendChild(open);
354
+ createTooltip(open, file.contentType === 'application/x-directory' ? 'Open folder' : 'Open');
355
+ open.onclick = () => {
356
+ if (file.contentType === 'application/x-directory' && options.onFolderOpen) {
357
+ options.onFolderOpen();
358
+ }
359
+ else if (options.onOpen) {
360
+ options.onOpen();
361
+ }
362
+ else if (!('repository' in file.authorization)) {
363
+ if (!('path' in file.authorization)) {
364
+ throw new Error('401: Unauthorized request.');
365
+ }
366
+ window.location.href = `/view/?path=${encodeURIComponent(file.authorization.path)}`;
367
+ }
368
+ };
369
+ }
370
+ if (options.share || options.share === undefined) {
371
+ const share = document.createElement('span');
372
+ share.className = 'material-symbols-outlined';
373
+ share.innerHTML = 'public';
374
+ opts.appendChild(share);
375
+ createTooltip(share, 'Share a public link');
376
+ share.onclick = async () => {
377
+ const accessCode = await file.generateAccessCode();
378
+ if (options.onShare) {
379
+ options.onShare(accessCode);
380
+ }
381
+ else {
382
+ window.location.href = `/view/?access-code=${accessCode}`;
383
+ }
384
+ };
385
+ }
386
+ if (options.remove || options.remove === undefined || options.rename || options.rename === undefined || options.download || options.download === undefined || options.open || options.open === undefined || options.share || options.share === undefined) {
387
+ const expand = document.createElement('span');
388
+ expand.className = 'material-symbols-outlined';
389
+ expand.dataset.role = 'expand';
390
+ expand.innerHTML = view === 'list' ? 'more_vert' : 'more_horiz';
391
+ opts.appendChild(expand);
392
+ createTooltip(expand, 'More actions');
393
+ createHoveringElement([...opts.children].filter((element) => element.dataset.role !== 'expand').map(clone), expand);
394
+ }
395
+ [...opts.children].forEach((element) => {
396
+ element.onmouseenter = () => {
397
+ gsap.to(element, {
398
+ duration: 0.3,
399
+ scale: 1.1,
400
+ rotate: -10,
401
+ ease: 'power2.inOut'
402
+ });
403
+ };
404
+ element.onmouseleave = () => {
405
+ gsap.to(element, {
406
+ duration: 0.3,
407
+ scale: 1,
408
+ rotate: 0,
409
+ ease: 'power2.inOut'
410
+ });
411
+ };
412
+ });
413
+ const isExpandGlyph = (el) => el.dataset.role === 'expand';
414
+ // Collapse the inline action rail behind a single "…" menu when the row is
415
+ // too narrow to comfortably fit the file name alongside every action. Cozy
416
+ // cards always collapse (their footer is tight); list rows collapse below a
417
+ // fixed width threshold. Keying purely off the row's own width (rather than
418
+ // the icons' measured widths) keeps this stable — hiding the icons doesn't
419
+ // change the row width, so the decision never oscillates.
420
+ const COLLAPSE_BELOW = 520;
421
+ let collapsed = null;
422
+ function resize() {
423
+ const rowWidth = element.getBoundingClientRect().width;
424
+ // A freshly-created, not-yet-laid-out row reports 0; don't commit a
425
+ // decision until we have a real measurement.
426
+ if (rowWidth === 0)
427
+ return;
428
+ const next = view === 'cozy' || rowWidth < COLLAPSE_BELOW;
429
+ if (next === collapsed)
430
+ return;
431
+ collapsed = next;
432
+ const actionIcons = [...opts.children].filter((el) => !isExpandGlyph(el));
433
+ const expandIcons = [...opts.children].filter(isExpandGlyph);
434
+ gsap.set(actionIcons, { display: next ? 'none' : 'flex' });
435
+ gsap.set(expandIcons, { display: next ? 'flex' : 'none' });
436
+ }
437
+ const observer = new ResizeObserver(() => {
438
+ const dpr = window.devicePixelRatio || 1;
439
+ preview.width = element.offsetWidth * dpr;
440
+ preview.height = element.offsetHeight * dpr;
441
+ resize();
442
+ paintPreview();
443
+ });
444
+ observer.observe(element);
445
+ requestAnimationFrame(resize);
446
+ const load = document.createElement('div');
447
+ load.className = 'file-load';
448
+ const progress = document.createElement('div');
449
+ progress.className = 'file-load-progress';
450
+ load.appendChild(progress);
451
+ details.appendChild(load);
452
+ const highlightTl = gsap.timeline({ paused: true });
453
+ highlightTl.to(element, {
454
+ duration: 0.3,
455
+ borderColor: getComputedStyle(document.body).getPropertyValue('--accent'),
456
+ ease: 'power2.inOut'
457
+ });
458
+ element.onmouseenter = () => {
459
+ highlightTl.play();
460
+ };
461
+ element.onmouseleave = () => {
462
+ highlightTl.reverse();
463
+ };
464
+ element.onclick = (ev) => {
465
+ const target = ev.target;
466
+ if (!target) {
467
+ return;
468
+ }
469
+ if (opts.contains(target)) {
470
+ return;
471
+ }
472
+ if (element.classList.contains('active')) {
473
+ element.classList.remove('active');
474
+ }
475
+ else {
476
+ element.classList.add('active');
477
+ }
478
+ if (options.onSelect) {
479
+ options.onSelect();
480
+ }
481
+ };
482
+ if (!file.online && options.upload !== false) {
483
+ file.upload((percent) => {
484
+ gsap.to(progress, {
485
+ duration: 0.1,
486
+ width: `${percent * 100}%`,
487
+ ease: 'linear'
488
+ });
489
+ }).then(() => {
490
+ gsap.to(load, {
491
+ duration: 0.5,
492
+ opacity: 0,
493
+ display: 'none',
494
+ ease: 'power2.inOut'
495
+ });
496
+ // Now that the bytes exist server-side, pull the generated preview.
497
+ void requestPreview();
498
+ });
499
+ }
500
+ else {
501
+ gsap.to(load, {
502
+ display: 'none'
503
+ });
504
+ // Offline + upload:false (or already online) — try preview immediately.
505
+ void requestPreview();
506
+ }
507
+ return {
508
+ getElement: () => element,
509
+ getFile: () => file,
510
+ isSelected: () => element.classList.contains('active')
511
+ };
512
+ }
513
+ // The imperative explorer logic, lifted verbatim from the original widget into
514
+ // a class so the React `FileExplorer` component can own its lifecycle (build in
515
+ // a layout effect, tear down on unmount) and expose it via useImperativeHandle.
516
+ class ExplorerEngine {
517
+ constructor(authorization, options) {
518
+ Object.defineProperty(this, "authorization", {
519
+ enumerable: true,
520
+ configurable: true,
521
+ writable: true,
522
+ value: void 0
523
+ });
524
+ Object.defineProperty(this, "options", {
525
+ enumerable: true,
526
+ configurable: true,
527
+ writable: true,
528
+ value: void 0
529
+ });
530
+ Object.defineProperty(this, "currentPath", {
531
+ enumerable: true,
532
+ configurable: true,
533
+ writable: true,
534
+ value: void 0
535
+ });
536
+ Object.defineProperty(this, "currentView", {
537
+ enumerable: true,
538
+ configurable: true,
539
+ writable: true,
540
+ value: void 0
541
+ });
542
+ Object.defineProperty(this, "files", {
543
+ enumerable: true,
544
+ configurable: true,
545
+ writable: true,
546
+ value: []
547
+ });
548
+ Object.defineProperty(this, "clipboard", {
549
+ enumerable: true,
550
+ configurable: true,
551
+ writable: true,
552
+ value: []
553
+ });
554
+ Object.defineProperty(this, "loadGeneration", {
555
+ enumerable: true,
556
+ configurable: true,
557
+ writable: true,
558
+ value: 0
559
+ });
560
+ Object.defineProperty(this, "loading", {
561
+ enumerable: true,
562
+ configurable: true,
563
+ writable: true,
564
+ value: false
565
+ });
566
+ Object.defineProperty(this, "wantedPath", {
567
+ enumerable: true,
568
+ configurable: true,
569
+ writable: true,
570
+ value: null
571
+ });
572
+ Object.defineProperty(this, "destroyed", {
573
+ enumerable: true,
574
+ configurable: true,
575
+ writable: true,
576
+ value: false
577
+ });
578
+ Object.defineProperty(this, "root", {
579
+ enumerable: true,
580
+ configurable: true,
581
+ writable: true,
582
+ value: void 0
583
+ });
584
+ Object.defineProperty(this, "toolbar", {
585
+ enumerable: true,
586
+ configurable: true,
587
+ writable: true,
588
+ value: void 0
589
+ });
590
+ Object.defineProperty(this, "breadcrumbs", {
591
+ enumerable: true,
592
+ configurable: true,
593
+ writable: true,
594
+ value: void 0
595
+ });
596
+ Object.defineProperty(this, "viewListBtn", {
597
+ enumerable: true,
598
+ configurable: true,
599
+ writable: true,
600
+ value: void 0
601
+ });
602
+ Object.defineProperty(this, "viewCozyBtn", {
603
+ enumerable: true,
604
+ configurable: true,
605
+ writable: true,
606
+ value: void 0
607
+ });
608
+ Object.defineProperty(this, "copyBtn", {
609
+ enumerable: true,
610
+ configurable: true,
611
+ writable: true,
612
+ value: null
613
+ });
614
+ Object.defineProperty(this, "pasteBtn", {
615
+ enumerable: true,
616
+ configurable: true,
617
+ writable: true,
618
+ value: null
619
+ });
620
+ Object.defineProperty(this, "body", {
621
+ enumerable: true,
622
+ configurable: true,
623
+ writable: true,
624
+ value: void 0
625
+ });
626
+ Object.defineProperty(this, "loadingEl", {
627
+ enumerable: true,
628
+ configurable: true,
629
+ writable: true,
630
+ value: void 0
631
+ });
632
+ Object.defineProperty(this, "emptyEl", {
633
+ enumerable: true,
634
+ configurable: true,
635
+ writable: true,
636
+ value: void 0
637
+ });
638
+ Object.defineProperty(this, "listEl", {
639
+ enumerable: true,
640
+ configurable: true,
641
+ writable: true,
642
+ value: void 0
643
+ });
644
+ Object.defineProperty(this, "dropzone", {
645
+ enumerable: true,
646
+ configurable: true,
647
+ writable: true,
648
+ value: void 0
649
+ });
650
+ Object.defineProperty(this, "onResize", {
651
+ enumerable: true,
652
+ configurable: true,
653
+ writable: true,
654
+ value: () => { }
655
+ });
656
+ this.authorization = authorization;
657
+ this.options = authorization.repository
658
+ ? { ...options, rename: false, remove: false, copyPaste: false, onShare: undefined }
659
+ : options;
660
+ this.currentPath = authorization.path;
661
+ this.currentView = options.view ?? 'list';
662
+ // Warm the server-sourced content-type → extension map (cached, so this
663
+ // only hits the network once across the whole app). Fire-and-forget:
664
+ // cards rendered before it resolves fall back to the filename extension.
665
+ void hydrateExtensions();
666
+ // ── DOM ──
667
+ const root = document.createElement('div');
668
+ root.className = 'explorer-root';
669
+ this.root = root;
670
+ const toolbar = document.createElement('div');
671
+ toolbar.className = 'explorer-toolbar';
672
+ this.toolbar = toolbar;
673
+ const breadcrumbs = document.createElement('div');
674
+ breadcrumbs.className = 'explorer-breadcrumbs';
675
+ this.breadcrumbs = breadcrumbs;
676
+ if (options.navigate) {
677
+ toolbar.appendChild(breadcrumbs);
678
+ }
679
+ // Right column of the toolbar — stacks view switches above the file
680
+ // actions.
681
+ const actionsCol = document.createElement('div');
682
+ actionsCol.className = 'explorer-actions-col';
683
+ const viewSwitch = document.createElement('div');
684
+ viewSwitch.className = 'explorer-view-switch';
685
+ const viewListBtn = this.createActionIcon('view_list');
686
+ createTooltip(viewListBtn, 'List view');
687
+ if (this.currentView === 'list')
688
+ viewListBtn.classList.add('active');
689
+ viewSwitch.appendChild(viewListBtn);
690
+ this.viewListBtn = viewListBtn;
691
+ const viewCozyBtn = this.createActionIcon('grid_view');
692
+ createTooltip(viewCozyBtn, 'Grid view');
693
+ if (this.currentView === 'cozy')
694
+ viewCozyBtn.classList.add('active');
695
+ viewSwitch.appendChild(viewCozyBtn);
696
+ this.viewCozyBtn = viewCozyBtn;
697
+ viewListBtn.onclick = () => { this.switchView('list'); this.options.onView?.('list'); };
698
+ viewCozyBtn.onclick = () => { this.switchView('cozy'); this.options.onView?.('cozy'); };
699
+ actionsCol.appendChild(viewSwitch);
700
+ const actions = document.createElement('div');
701
+ actions.className = 'explorer-actions';
702
+ if (options.upload) {
703
+ const uploadBtn = this.createActionIcon('upload_file');
704
+ createTooltip(uploadBtn, 'Upload files');
705
+ uploadBtn.onclick = () => this.openFilePicker();
706
+ actions.appendChild(uploadBtn);
707
+ }
708
+ if (options.createFolder) {
709
+ const folderBtn = this.createActionIcon('create_new_folder');
710
+ createTooltip(folderBtn, 'New folder');
711
+ folderBtn.onclick = () => this.createNewFolder();
712
+ actions.appendChild(folderBtn);
713
+ }
714
+ if (options.copyPaste) {
715
+ const copyBtn = this.createActionIcon('content_copy');
716
+ createTooltip(copyBtn, 'Copy selected');
717
+ copyBtn.style.opacity = '0.3';
718
+ copyBtn.style.pointerEvents = 'none';
719
+ copyBtn.onclick = () => {
720
+ this.clipboard = this.files.filter(f => f.selected).map(f => ({ file: f.file, path: f.path }));
721
+ if (this.clipboard.length > 0 && this.pasteBtn) {
722
+ gsap.to(this.pasteBtn, { duration: 0.3, opacity: 1, ease: 'power2.out' });
723
+ this.pasteBtn.style.pointerEvents = 'auto';
724
+ }
725
+ };
726
+ actions.appendChild(copyBtn);
727
+ this.copyBtn = copyBtn;
728
+ const pasteBtn = this.createActionIcon('content_paste');
729
+ createTooltip(pasteBtn, 'Paste');
730
+ pasteBtn.style.opacity = '0.3';
731
+ pasteBtn.style.pointerEvents = 'none';
732
+ pasteBtn.onclick = () => this.pasteFiles();
733
+ actions.appendChild(pasteBtn);
734
+ this.pasteBtn = pasteBtn;
735
+ }
736
+ actionsCol.appendChild(actions);
737
+ toolbar.appendChild(actionsCol);
738
+ root.appendChild(toolbar);
739
+ // Body
740
+ const body = document.createElement('div');
741
+ body.className = 'explorer-body';
742
+ this.body = body;
743
+ const loadingEl = document.createElement('div');
744
+ loadingEl.className = 'explorer-loading';
745
+ for (let i = 0; i < 4; i++) {
746
+ const skeleton = document.createElement('div');
747
+ skeleton.className = 'explorer-loading-skeleton';
748
+ loadingEl.appendChild(skeleton);
749
+ }
750
+ body.appendChild(loadingEl);
751
+ this.loadingEl = loadingEl;
752
+ const emptyEl = document.createElement('div');
753
+ emptyEl.className = 'explorer-empty';
754
+ emptyEl.style.display = 'none';
755
+ const emptyIcon = document.createElement('span');
756
+ emptyIcon.className = 'material-symbols-outlined';
757
+ emptyIcon.textContent = 'folder_open';
758
+ emptyEl.appendChild(emptyIcon);
759
+ const emptyText = document.createElement('div');
760
+ emptyText.textContent = 'No files';
761
+ emptyEl.appendChild(emptyText);
762
+ if (options.upload) {
763
+ const emptyHint = document.createElement('div');
764
+ emptyHint.className = 'explorer-empty-hint';
765
+ emptyHint.textContent = 'Drop files here or click upload';
766
+ emptyEl.appendChild(emptyHint);
767
+ }
768
+ body.appendChild(emptyEl);
769
+ this.emptyEl = emptyEl;
770
+ const listEl = document.createElement('div');
771
+ listEl.className = `explorer-list ${this.currentView === 'cozy' ? 'cozy' : ''}`;
772
+ body.appendChild(listEl);
773
+ this.listEl = listEl;
774
+ const dropzone = document.createElement('div');
775
+ dropzone.className = 'explorer-dropzone';
776
+ const dropIcon = document.createElement('span');
777
+ dropIcon.className = 'material-symbols-outlined';
778
+ dropIcon.textContent = 'upload_file';
779
+ dropzone.appendChild(dropIcon);
780
+ const dropText = document.createElement('span');
781
+ dropText.textContent = 'Drop to upload';
782
+ dropzone.appendChild(dropText);
783
+ body.appendChild(dropzone);
784
+ this.dropzone = dropzone;
785
+ root.appendChild(body);
786
+ // ── Drag-drop upload ──
787
+ if (options.upload) {
788
+ let dragCounter = 0;
789
+ body.addEventListener('dragenter', (e) => {
790
+ e.preventDefault();
791
+ dragCounter++;
792
+ if (dragCounter === 1) {
793
+ dropzone.style.display = 'flex';
794
+ gsap.fromTo(dropzone, { opacity: 0 }, { opacity: 1, duration: 0.2, ease: 'power2.out' });
795
+ }
796
+ });
797
+ body.addEventListener('dragover', (e) => {
798
+ e.preventDefault();
799
+ });
800
+ body.addEventListener('dragleave', () => {
801
+ dragCounter--;
802
+ if (dragCounter <= 0) {
803
+ dragCounter = 0;
804
+ gsap.to(dropzone, { duration: 0.2, opacity: 0, ease: 'power2.in', onComplete: () => { dropzone.style.display = 'none'; } });
805
+ }
806
+ });
807
+ body.addEventListener('drop', (e) => {
808
+ e.preventDefault();
809
+ dragCounter = 0;
810
+ gsap.to(dropzone, { duration: 0.2, opacity: 0, ease: 'power2.in', onComplete: () => { dropzone.style.display = 'none'; } });
811
+ const droppedFiles = e.dataTransfer?.files;
812
+ if (droppedFiles && droppedFiles.length > 0) {
813
+ this.uploadNativeFiles(Array.from(droppedFiles));
814
+ }
815
+ });
816
+ }
817
+ // ── Initial load ──
818
+ this.load(this.currentPath);
819
+ }
820
+ createActionIcon(icon) {
821
+ const span = document.createElement('span');
822
+ span.className = 'material-symbols-outlined';
823
+ span.textContent = icon;
824
+ span.onmouseenter = () => { gsap.to(span, { duration: 0.3, scale: 1.1, rotate: -10, ease: 'power2.inOut' }); };
825
+ span.onmouseleave = () => { gsap.to(span, { duration: 0.3, scale: 1, rotate: 0, ease: 'power2.inOut' }); };
826
+ return span;
827
+ }
828
+ switchView(view) {
829
+ this.currentView = view;
830
+ this.viewListBtn.classList.toggle('active', view === 'list');
831
+ this.viewCozyBtn.classList.toggle('active', view === 'cozy');
832
+ this.listEl.classList.toggle('cozy', view === 'cozy');
833
+ // Rebuild file elements with new view
834
+ this.load(this.currentPath);
835
+ }
836
+ updateBreadcrumbs() {
837
+ if (!this.options.navigate)
838
+ return;
839
+ this.breadcrumbs.innerHTML = '';
840
+ const homeIcon = document.createElement('span');
841
+ homeIcon.className = 'material-symbols-outlined explorer-breadcrumb-home';
842
+ homeIcon.textContent = 'home';
843
+ homeIcon.onclick = () => this.load('');
844
+ this.breadcrumbs.appendChild(homeIcon);
845
+ const normalized = normalizePath(this.currentPath);
846
+ if (!normalized)
847
+ return;
848
+ const parts = normalized.split('/');
849
+ let accumulated = '';
850
+ for (let i = 0; i < parts.length; i++) {
851
+ const sep = document.createElement('span');
852
+ sep.className = 'explorer-breadcrumb-sep';
853
+ sep.textContent = '/';
854
+ this.breadcrumbs.appendChild(sep);
855
+ accumulated += parts[i] + '/';
856
+ const crumb = document.createElement('span');
857
+ crumb.className = 'explorer-breadcrumb';
858
+ crumb.textContent = parts[i];
859
+ const target = accumulated;
860
+ if (i < parts.length - 1) {
861
+ crumb.onclick = () => this.load(target);
862
+ }
863
+ else {
864
+ crumb.classList.add('current');
865
+ }
866
+ this.breadcrumbs.appendChild(crumb);
867
+ }
868
+ // Scroll breadcrumbs to end
869
+ this.breadcrumbs.scrollLeft = this.breadcrumbs.scrollWidth;
870
+ }
871
+ updateSelectionState() {
872
+ const selected = this.files.filter(f => f.selected).map(f => f.file);
873
+ if (this.copyBtn) {
874
+ const hasSelection = selected.length > 0;
875
+ gsap.to(this.copyBtn, { duration: 0.2, opacity: hasSelection ? 1 : 0.3, ease: 'power2.out' });
876
+ this.copyBtn.style.pointerEvents = hasSelection ? 'auto' : 'none';
877
+ }
878
+ this.options.onSelect?.(selected);
879
+ }
880
+ async load(path) {
881
+ if (this.loading)
882
+ return;
883
+ this.loadGeneration++;
884
+ const gen = this.loadGeneration;
885
+ this.loading = true;
886
+ this.currentPath = path;
887
+ // Show loading
888
+ this.listEl.innerHTML = '';
889
+ this.emptyEl.style.display = 'none';
890
+ this.loadingEl.style.display = 'flex';
891
+ gsap.fromTo(this.loadingEl, { opacity: 0 }, { opacity: 1, duration: 0.3 });
892
+ this.files = [];
893
+ this.updateBreadcrumbs();
894
+ this.options.onNavigate?.(path);
895
+ try {
896
+ const repository = this.authorization.repository;
897
+ const pulled = repository
898
+ ? [await repository.open(path)].flat().map((file) => ({ file, path: 'path' in file.authorization ? file.authorization.path : path }))
899
+ : await Promise.all((await Fauna.Drive.list(this.authorization.authorization, path, this.authorization.group)).map(async (p) => {
900
+ try {
901
+ // list() returns full paths, not relative
902
+ const file = await Fauna.Drive.pull({
903
+ authorization: this.authorization.authorization,
904
+ path: p,
905
+ group: this.authorization.group
906
+ });
907
+ return { file, path: p };
908
+ }
909
+ catch {
910
+ return null;
911
+ }
912
+ }));
913
+ if (gen !== this.loadGeneration)
914
+ return;
915
+ // Hide loading
916
+ this.loadingEl.style.display = 'none';
917
+ this.listEl.innerHTML = '';
918
+ this.files = [];
919
+ const validFiles = pulled.filter(Boolean);
920
+ if (validFiles.length === 0) {
921
+ this.emptyEl.style.display = 'flex';
922
+ gsap.from(this.emptyEl, { duration: 0.3, opacity: 0, ease: 'power2.out' });
923
+ }
924
+ else {
925
+ this.emptyEl.style.display = 'none';
926
+ for (let i = 0; i < validFiles.length; i++) {
927
+ const { file, path: filePath } = validFiles[i];
928
+ const isFolder = file.contentType === 'application/x-directory';
929
+ const fileEl = createFileElement(file, {
930
+ view: this.currentView,
931
+ remove: this.options.remove,
932
+ rename: this.options.rename,
933
+ download: this.options.download,
934
+ open: isFolder ? !!this.options.navigate : !!this.options.onOpen,
935
+ share: !!this.options.onShare,
936
+ onShare: this.options.onShare ? (code) => this.options.onShare(fileEl.getFile(), code) : undefined,
937
+ onFolderOpen: isFolder && this.options.navigate ? () => {
938
+ const auth = fileEl.getFile().authorization;
939
+ const livePath = 'path' in auth ? auth.path : filePath;
940
+ this.load(livePath.endsWith('/') ? livePath : livePath + '/');
941
+ } : undefined,
942
+ onOpen: !isFolder && this.options.onOpen ? () => {
943
+ const auth = fileEl.getFile().authorization;
944
+ const livePath = 'path' in auth ? auth.path : filePath;
945
+ this.options.onOpen(fileEl.getFile(), livePath);
946
+ } : undefined,
947
+ onSelect: () => {
948
+ const entry = this.files.find(f => f.file === file);
949
+ if (entry) {
950
+ entry.selected = fileEl.isSelected();
951
+ this.updateSelectionState();
952
+ }
953
+ }
954
+ });
955
+ const element = fileEl.getElement();
956
+ // Double-click: folders navigate, files open
957
+ if (isFolder && this.options.navigate) {
958
+ element.addEventListener('dblclick', (e) => {
959
+ e.stopPropagation();
960
+ const auth = fileEl.getFile().authorization;
961
+ const livePath = 'path' in auth ? auth.path : filePath;
962
+ this.load(livePath.endsWith('/') ? livePath : livePath + '/');
963
+ });
964
+ }
965
+ else if (!isFolder && this.options.onOpen) {
966
+ element.addEventListener('dblclick', (e) => {
967
+ e.stopPropagation();
968
+ const auth = fileEl.getFile().authorization;
969
+ const livePath = 'path' in auth ? auth.path : filePath;
970
+ this.options.onOpen(fileEl.getFile(), livePath);
971
+ });
972
+ }
973
+ this.files.push({ file, path: filePath, element, selected: false });
974
+ this.listEl.appendChild(element);
975
+ gsap.from(element, { duration: 0.3, opacity: 0, y: 10, ease: 'power2.out', delay: i * 0.04 });
976
+ }
977
+ }
978
+ }
979
+ catch (err) {
980
+ if (gen !== this.loadGeneration)
981
+ return;
982
+ this.loadingEl.style.display = 'none';
983
+ this.emptyEl.style.display = 'none';
984
+ const errorEl = document.createElement('div');
985
+ errorEl.className = 'explorer-empty';
986
+ errorEl.style.color = 'var(--error, #DC322F)';
987
+ const errorIcon = document.createElement('span');
988
+ errorIcon.className = 'material-symbols-outlined';
989
+ errorIcon.textContent = 'error';
990
+ errorEl.appendChild(errorIcon);
991
+ const errorText = document.createElement('div');
992
+ errorText.textContent = 'Failed to load files';
993
+ errorEl.appendChild(errorText);
994
+ const retryBtn = document.createElement('div');
995
+ retryBtn.className = 'explorer-retry';
996
+ retryBtn.textContent = 'Retry';
997
+ retryBtn.onclick = () => {
998
+ errorEl.remove();
999
+ this.load(this.currentPath);
1000
+ };
1001
+ errorEl.appendChild(retryBtn);
1002
+ this.listEl.appendChild(errorEl);
1003
+ this.options.onError?.(err instanceof Error ? err : new Error(String(err)));
1004
+ }
1005
+ finally {
1006
+ if (gen === this.loadGeneration) {
1007
+ this.loading = false;
1008
+ const wanted = this.wantedPath;
1009
+ this.wantedPath = null;
1010
+ if (wanted !== null && wanted !== this.currentPath)
1011
+ void this.load(wanted);
1012
+ }
1013
+ }
1014
+ }
1015
+ openFilePicker() {
1016
+ const input = document.createElement('input');
1017
+ input.type = 'file';
1018
+ input.multiple = true;
1019
+ input.style.display = 'none';
1020
+ input.addEventListener('change', () => {
1021
+ if (input.files && input.files.length > 0) {
1022
+ this.uploadNativeFiles(Array.from(input.files));
1023
+ }
1024
+ });
1025
+ document.body.appendChild(input);
1026
+ input.click();
1027
+ input.remove();
1028
+ }
1029
+ async uploadNativeFiles(nativeFiles) {
1030
+ const repository = this.authorization.repository;
1031
+ if (repository && !this.currentPath.startsWith('/repository/')) {
1032
+ this.options.onError?.(new Error('Uploads only go into /repository.'));
1033
+ return;
1034
+ }
1035
+ for (const nativeFile of nativeFiles) {
1036
+ try {
1037
+ const filePath = this.currentPath + nativeFile.name;
1038
+ const wrapped = await Fauna.Drive.wrap(repository
1039
+ ? { authorization: this.authorization.authorization, repository: repository.id, path: filePath, group: this.authorization.group }
1040
+ : { authorization: this.authorization.authorization, path: filePath, group: this.authorization.group }, nativeFile);
1041
+ const fileEl = createFileElement(wrapped, {
1042
+ view: this.currentView,
1043
+ remove: this.options.remove,
1044
+ rename: this.options.rename,
1045
+ download: this.options.download,
1046
+ open: !!this.options.onOpen,
1047
+ share: !!this.options.onShare,
1048
+ onOpen: this.options.onOpen ? () => this.options.onOpen(wrapped, filePath) : undefined,
1049
+ onShare: this.options.onShare ? (code) => this.options.onShare(wrapped, code) : undefined
1050
+ });
1051
+ const element = fileEl.getElement();
1052
+ this.files.push({ file: wrapped, path: filePath, element, selected: false });
1053
+ this.listEl.prepend(element);
1054
+ gsap.from(element, { duration: 0.3, opacity: 0, y: -10, ease: 'power2.out' });
1055
+ // Hide empty state if showing
1056
+ this.emptyEl.style.display = 'none';
1057
+ }
1058
+ catch (err) {
1059
+ this.options.onError?.(err instanceof Error ? err : new Error(String(err)));
1060
+ }
1061
+ }
1062
+ }
1063
+ async createNewFolder() {
1064
+ // Find unique name
1065
+ const existingNames = this.files.map(f => f.file.name);
1066
+ let name = 'Untitled';
1067
+ let counter = 0;
1068
+ while (existingNames.includes(name)) {
1069
+ counter++;
1070
+ name = `Untitled-${counter}`;
1071
+ }
1072
+ const repository = this.authorization.repository;
1073
+ if (repository && !this.currentPath.startsWith('/repository/')) {
1074
+ this.options.onError?.(new Error('Folders can only be created in /repository.'));
1075
+ return;
1076
+ }
1077
+ try {
1078
+ const folderPath = this.currentPath + name + '/';
1079
+ const folderFile = await Fauna.Drive.folder(repository
1080
+ ? { authorization: this.authorization.authorization, repository: repository.id, path: folderPath, group: this.authorization.group }
1081
+ : { authorization: this.authorization.authorization, path: folderPath, group: this.authorization.group });
1082
+ const fileEl = createFileElement(folderFile, {
1083
+ view: this.currentView,
1084
+ remove: this.options.remove,
1085
+ rename: this.options.rename,
1086
+ download: this.options.download,
1087
+ open: !!this.options.navigate,
1088
+ share: !!this.options.onShare,
1089
+ onShare: this.options.onShare ? (code) => this.options.onShare(folderFile, code) : undefined,
1090
+ onFolderOpen: this.options.navigate ? () => {
1091
+ const auth = fileEl.getFile().authorization;
1092
+ const livePath = 'path' in auth ? auth.path : folderPath;
1093
+ this.load(livePath.endsWith('/') ? livePath : livePath + '/');
1094
+ } : undefined
1095
+ });
1096
+ const element = fileEl.getElement();
1097
+ this.files.push({ file: folderFile, path: folderPath, element, selected: false });
1098
+ this.listEl.prepend(element);
1099
+ gsap.from(element, { duration: 0.3, opacity: 0, y: -10, ease: 'power2.out' });
1100
+ this.emptyEl.style.display = 'none';
1101
+ }
1102
+ catch (err) {
1103
+ this.options.onError?.(err instanceof Error ? err : new Error(String(err)));
1104
+ }
1105
+ }
1106
+ async pasteFiles() {
1107
+ if (this.clipboard.length === 0)
1108
+ return;
1109
+ for (const item of this.clipboard) {
1110
+ try {
1111
+ const newPath = this.currentPath + item.file.name + (item.file.contentType === 'application/x-directory' ? '/' : '');
1112
+ await item.file.copy(newPath);
1113
+ const newFile = await Fauna.Drive.pull({
1114
+ authorization: this.authorization.authorization,
1115
+ path: newPath,
1116
+ group: this.authorization.group
1117
+ });
1118
+ const isFolder = newFile.contentType === 'application/x-directory';
1119
+ const fileEl = createFileElement(newFile, {
1120
+ view: this.currentView,
1121
+ remove: this.options.remove,
1122
+ rename: this.options.rename,
1123
+ download: this.options.download,
1124
+ open: isFolder ? !!this.options.navigate : !!this.options.onOpen,
1125
+ share: !!this.options.onShare,
1126
+ onShare: this.options.onShare ? (code) => this.options.onShare(newFile, code) : undefined,
1127
+ onFolderOpen: isFolder && this.options.navigate
1128
+ ? () => {
1129
+ const auth = fileEl.getFile().authorization;
1130
+ const livePath = 'path' in auth ? auth.path : newPath;
1131
+ this.load(livePath.endsWith('/') ? livePath : livePath + '/');
1132
+ }
1133
+ : undefined,
1134
+ onOpen: !isFolder && this.options.onOpen ? () => this.options.onOpen(newFile, newPath) : undefined
1135
+ });
1136
+ const element = fileEl.getElement();
1137
+ this.files.push({ file: newFile, path: newPath, element, selected: false });
1138
+ this.listEl.prepend(element);
1139
+ gsap.from(element, { duration: 0.3, opacity: 0, y: -10, ease: 'power2.out' });
1140
+ this.emptyEl.style.display = 'none';
1141
+ }
1142
+ catch (err) {
1143
+ this.options.onError?.(err instanceof Error ? err : new Error(String(err)));
1144
+ }
1145
+ }
1146
+ this.clipboard = [];
1147
+ if (this.pasteBtn) {
1148
+ gsap.to(this.pasteBtn, { duration: 0.3, opacity: 0, ease: 'power2.in' });
1149
+ this.pasteBtn.style.pointerEvents = 'none';
1150
+ }
1151
+ }
1152
+ // ── Public API ──
1153
+ getElement() {
1154
+ return this.root;
1155
+ }
1156
+ getPath() {
1157
+ return this.currentPath;
1158
+ }
1159
+ /** Go to a folder. One asked for while another loads goes next, instead of being dropped. */
1160
+ async setPath(path) {
1161
+ if (this.loading) {
1162
+ this.wantedPath = path;
1163
+ return;
1164
+ }
1165
+ await this.load(path);
1166
+ }
1167
+ async refresh() {
1168
+ await this.load(this.currentPath);
1169
+ }
1170
+ getSelected() {
1171
+ return this.files.filter(f => f.selected).map(f => f.file);
1172
+ }
1173
+ setView(view) {
1174
+ this.switchView(view);
1175
+ }
1176
+ destroy() {
1177
+ this.destroyed = true;
1178
+ this.loadGeneration++;
1179
+ this.root.remove();
1180
+ }
1181
+ }
1182
+ // Owns an ExplorerEngine for its lifetime: builds it during the commit phase so
1183
+ // the engine's root exists synchronously by the time createFileExplorer()
1184
+ // returns (paired with the flushSync mount below), and tears it down on
1185
+ // unmount. The engine's root is appended into a host <div> ref.
1186
+ export const FileExplorer = forwardRef(function FileExplorer({ authorization, options }, ref) {
1187
+ const hostRef = useRef(null);
1188
+ const engineRef = useRef(null);
1189
+ useLayoutEffect(() => {
1190
+ if (!hostRef.current)
1191
+ return;
1192
+ const engine = new ExplorerEngine(authorization, options);
1193
+ engineRef.current = engine;
1194
+ hostRef.current.appendChild(engine.getElement());
1195
+ return () => {
1196
+ engine.destroy();
1197
+ engineRef.current = null;
1198
+ };
1199
+ // Authorization + options are read once at mount (matching the imperative
1200
+ // widget's one-time configuration); consumers rebuild to change them.
1201
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1202
+ }, []);
1203
+ useImperativeHandle(ref, () => ({
1204
+ getElement: () => engineRef.current?.getElement() ?? hostRef.current,
1205
+ getPath: () => engineRef.current?.getPath() ?? authorization.path,
1206
+ setPath: (path) => engineRef.current?.setPath(path) ?? Promise.resolve(),
1207
+ refresh: () => engineRef.current?.refresh() ?? Promise.resolve(),
1208
+ getSelected: () => engineRef.current?.getSelected() ?? [],
1209
+ setView: (view) => engineRef.current?.setView(view),
1210
+ destroy: () => engineRef.current?.destroy(),
1211
+ }), []);
1212
+ return _jsx("div", { className: "explorer-mount", ref: hostRef, style: { display: 'contents' } });
1213
+ });
1214
+ export function createFileExplorer(authorization, options = {}) {
1215
+ // Host element the React root mounts into. Because <FileExplorer> uses
1216
+ // display:contents and appends the engine root as its own child, this host
1217
+ // is effectively transparent — getElement() returns the engine root, so
1218
+ // consumers that query `.explorer-toolbar` off the returned element (e.g.
1219
+ // the Drive tab's scope-selector injection) work unchanged.
1220
+ const container = document.createElement('div');
1221
+ container.style.display = 'contents';
1222
+ const root = createRoot(container);
1223
+ let instance = null;
1224
+ const ready = [];
1225
+ const handleRef = (inst) => {
1226
+ instance = inst;
1227
+ if (inst)
1228
+ ready.push(inst);
1229
+ };
1230
+ // React 19's createRoot().render() is asynchronous by default; flushSync
1231
+ // forces a synchronous commit + layout effects so the engine (built in a
1232
+ // useLayoutEffect) is live before this function returns.
1233
+ flushSync(() => root.render(_jsx(FileExplorer, { ref: handleRef, authorization: authorization, options: options })));
1234
+ const get = () => instance ?? ready[ready.length - 1] ?? null;
1235
+ return {
1236
+ getElement: () => get()?.getElement() ?? container,
1237
+ getPath: () => get()?.getPath() ?? authorization.path,
1238
+ setPath: (path) => get()?.setPath(path) ?? Promise.resolve(),
1239
+ refresh: () => get()?.refresh() ?? Promise.resolve(),
1240
+ getSelected: () => get()?.getSelected() ?? [],
1241
+ setView: (view) => get()?.setView(view),
1242
+ };
1243
+ }