@mevdragon/vidfarm-devcli 0.20.5 → 0.20.8

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 (154) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +15 -1
  2. package/.agents/skills/vidfarm-director/SKILL.md +106 -0
  3. package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
  4. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
  5. package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
  6. package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
  7. package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +111 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -0
  9. package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
  10. package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
  11. package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
  12. package/.agents/skills/vidfarm-director/references/primitives.md +265 -0
  13. package/SKILL.director.md +521 -261
  14. package/dist/src/cli.js +404 -96
  15. package/dist/src/devcli/clips.js +15 -10
  16. package/dist/src/devcli/doctor.js +2 -2
  17. package/dist/src/devcli/local-backend.js +46 -2
  18. package/dist/src/services/clip-curation/cost.js +5 -1
  19. package/dist/src/services/clip-curation/gemini.js +10 -0
  20. package/package.json +30 -10
  21. package/SKILL.platform.md +0 -432
  22. package/demo/README.md +0 -28
  23. package/demo/dist/app.css +0 -1
  24. package/demo/dist/app.js +0 -1850
  25. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  26. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  27. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  28. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  29. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  30. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  31. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  32. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  33. package/demo/dist/favicon.ico +0 -0
  34. package/demo/dist/icons/timeline/audio.svg +0 -7
  35. package/demo/dist/icons/timeline/captions.svg +0 -5
  36. package/demo/dist/icons/timeline/composition.svg +0 -12
  37. package/demo/dist/icons/timeline/image.svg +0 -18
  38. package/demo/dist/icons/timeline/music.svg +0 -10
  39. package/demo/dist/icons/timeline/text.svg +0 -3
  40. package/demo/dist/index.html +0 -15
  41. package/dist/src/account-pages-legacy.js +0 -9396
  42. package/dist/src/account-pages.js +0 -61
  43. package/dist/src/app.js +0 -21602
  44. package/dist/src/composition-runtime.js +0 -1053
  45. package/dist/src/config.js +0 -217
  46. package/dist/src/context.js +0 -447
  47. package/dist/src/dev-app-legacy.js +0 -739
  48. package/dist/src/dev-app.js +0 -6
  49. package/dist/src/devcli/migrate-local.js +0 -140
  50. package/dist/src/devcli/sync.js +0 -368
  51. package/dist/src/domain.js +0 -5
  52. package/dist/src/editor-chat-history.js +0 -82
  53. package/dist/src/editor-chat.js +0 -756
  54. package/dist/src/editor-dark-theme.js +0 -1128
  55. package/dist/src/frontend/debug.js +0 -71
  56. package/dist/src/frontend/discover-client.js +0 -130
  57. package/dist/src/frontend/discover-store.js +0 -23
  58. package/dist/src/frontend/file-directory.js +0 -1018
  59. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  60. package/dist/src/frontend/homepage-client.js +0 -446
  61. package/dist/src/frontend/homepage-shared.js +0 -201
  62. package/dist/src/frontend/homepage-store.js +0 -66
  63. package/dist/src/frontend/homepage-view.js +0 -705
  64. package/dist/src/frontend/page-runtime-client.js +0 -132
  65. package/dist/src/frontend/page-runtime-store.js +0 -9
  66. package/dist/src/frontend/sentry.js +0 -42
  67. package/dist/src/frontend/template-editor-chat.js +0 -4181
  68. package/dist/src/help-page.js +0 -346
  69. package/dist/src/homepage.js +0 -1458
  70. package/dist/src/index.js +0 -16
  71. package/dist/src/instrument.js +0 -30
  72. package/dist/src/landing-page.js +0 -384
  73. package/dist/src/page-runtime.js +0 -2
  74. package/dist/src/page-shell.js +0 -1452
  75. package/dist/src/primitive-context.js +0 -416
  76. package/dist/src/primitive-registry.js +0 -3940
  77. package/dist/src/primitive-sdk.js +0 -4
  78. package/dist/src/primitives/hyperframes-media.js +0 -108
  79. package/dist/src/react-page-shell.js +0 -35
  80. package/dist/src/ready-post-schedule-component.js +0 -1540
  81. package/dist/src/registry.js +0 -296
  82. package/dist/src/reskin/agency-page.js +0 -299
  83. package/dist/src/reskin/calendar-page.js +0 -568
  84. package/dist/src/reskin/chat-page.js +0 -942
  85. package/dist/src/reskin/discover-page.js +0 -1788
  86. package/dist/src/reskin/document.js +0 -1587
  87. package/dist/src/reskin/help-page.js +0 -357
  88. package/dist/src/reskin/index-page.js +0 -62
  89. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  90. package/dist/src/reskin/inpaint-page.js +0 -2554
  91. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  92. package/dist/src/reskin/job-runs-page.js +0 -477
  93. package/dist/src/reskin/library-page.js +0 -1634
  94. package/dist/src/reskin/login-page.js +0 -262
  95. package/dist/src/reskin/portfolio-page.js +0 -687
  96. package/dist/src/reskin/pricing-page.js +0 -390
  97. package/dist/src/reskin/settings-page.js +0 -732
  98. package/dist/src/reskin/theme.js +0 -711
  99. package/dist/src/runtime.js +0 -35
  100. package/dist/src/services/api-call-history.js +0 -249
  101. package/dist/src/services/auth.js +0 -152
  102. package/dist/src/services/billing-pricing.js +0 -39
  103. package/dist/src/services/billing.js +0 -241
  104. package/dist/src/services/cast.js +0 -127
  105. package/dist/src/services/chat-threads.js +0 -92
  106. package/dist/src/services/clip-records.js +0 -250
  107. package/dist/src/services/clip-search.js +0 -77
  108. package/dist/src/services/clip-vectors.js +0 -125
  109. package/dist/src/services/composition-sanitize.js +0 -124
  110. package/dist/src/services/composition-watch.js +0 -79
  111. package/dist/src/services/elevenlabs.js +0 -222
  112. package/dist/src/services/file-directory.js +0 -117
  113. package/dist/src/services/fork-access.js +0 -93
  114. package/dist/src/services/fork-manifest.js +0 -42
  115. package/dist/src/services/ghostcut.js +0 -179
  116. package/dist/src/services/hyperframes.js +0 -3517
  117. package/dist/src/services/job-capacity.js +0 -14
  118. package/dist/src/services/job-logs.js +0 -197
  119. package/dist/src/services/jobs.js +0 -136
  120. package/dist/src/services/local-dynamo.js +0 -0
  121. package/dist/src/services/media-processing.js +0 -766
  122. package/dist/src/services/primitive-media-lambda.js +0 -280
  123. package/dist/src/services/providers.js +0 -2748
  124. package/dist/src/services/rate-limits.js +0 -262
  125. package/dist/src/services/scene-annotations.js +0 -32
  126. package/dist/src/services/serverless-auth.js +0 -382
  127. package/dist/src/services/serverless-jobs.js +0 -1084
  128. package/dist/src/services/serverless-provider-keys.js +0 -409
  129. package/dist/src/services/serverless-records.js +0 -1515
  130. package/dist/src/services/serverless-template-configs.js +0 -75
  131. package/dist/src/services/storage.js +0 -461
  132. package/dist/src/services/swipe-customize.js +0 -437
  133. package/dist/src/services/template-certification.js +0 -413
  134. package/dist/src/services/template-loader.js +0 -99
  135. package/dist/src/services/template-runtime-bundles.js +0 -217
  136. package/dist/src/services/template-sources.js +0 -1017
  137. package/dist/src/services/upstream.js +0 -248
  138. package/dist/src/services/video-normalization.js +0 -2
  139. package/dist/src/services/webhooks.js +0 -62
  140. package/dist/src/template-editor-pages.js +0 -2576
  141. package/dist/src/template-editor-shell.js +0 -2893
  142. package/dist/src/template-sdk.js +0 -4
  143. package/dist/src/worker.js +0 -17
  144. package/public/assets/discover-client-app.js +0 -1
  145. package/public/assets/file-directory-app.js +0 -3
  146. package/public/assets/homepage-app.js +0 -54
  147. package/public/assets/homepage-client-app.js +0 -80
  148. package/public/assets/page-runtime-client-app.js +0 -94
  149. package/public/assets/placeholders/scene-placeholder.png +0 -0
  150. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  151. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  152. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  153. package/src/assets/favicon.ico +0 -0
  154. package/src/assets/logo-vidfarm.png +0 -0
@@ -1,1018 +0,0 @@
1
- // Reusable file-directory explorer component (browser client).
2
- //
3
- // One framework-free component that browses the unified virtual directory —
4
- // /files, /temp, /raws — as a real folder tree, with a per-row "…" menu
5
- // (Copy Path / Attach / View / Preview), default-click = Copy Path, folder
6
- // Open, multi-file + folder select, and unified search (semantic + substring +
7
- // absolute-path). It talks to the /me/directory[/search] endpoints and returns
8
- // the one canonical DirectoryItem shape from every backend.
9
- //
10
- // It's a COMPONENT, not a one-off: mount it anywhere with
11
- // `window.__vidfarmDirectory.mount(el, opts)`, or drop a `<div data-rk-directory>`
12
- // and it auto-mounts. The chat dock hosts it in "attach" mode; /library/raws
13
- // hosts the same code in "standalone" mode scoped to /raws — no duplication.
14
- //
15
- // Host coupling (chat dock chips / preview modal) is passed IN via opts
16
- // callbacks, or, for the auto-mounted dock instance, read from a
17
- // `window.__rkDirectoryHost` hook the dock's inline script sets synchronously
18
- // before this deferred module runs.
19
- import { buildDirectoryPath, DIRECTORY_ROOT_KEYS, DIRECTORY_ROOTS, normalizeFolderPath, parseDirectoryPath } from "../services/file-directory.js";
20
- const DEFAULT_API_BASE = "/api/v1/user/me";
21
- // ── small DOM helpers ────────────────────────────────────────────────────────
22
- function el(tag, cls, text) {
23
- const node = document.createElement(tag);
24
- if (cls)
25
- node.className = cls;
26
- if (text != null)
27
- node.textContent = text;
28
- return node;
29
- }
30
- function fmtBytes(b) {
31
- if (!b || b <= 0)
32
- return "";
33
- const u = ["B", "KB", "MB", "GB"];
34
- let v = b, i = 0;
35
- while (v >= 1024 && i < u.length - 1) {
36
- v /= 1024;
37
- i++;
38
- }
39
- return (v >= 10 || i === 0 ? Math.round(v) : v.toFixed(1)) + " " + u[i];
40
- }
41
- function fmtDur(s) {
42
- if (typeof s !== "number" || !(s > 0))
43
- return "";
44
- s = Math.round(s);
45
- const m = Math.floor(s / 60), r = s % 60;
46
- return m + ":" + (r < 10 ? "0" + r : r);
47
- }
48
- function iconFor(it) {
49
- const ic = el("span", "rk-aichat-fic " + (it.kind === "folder" ? "rk-aichat-fic-folder" : "rk-aichat-fic-file"));
50
- if (it.kind === "folder") {
51
- ic.textContent = "/";
52
- return ic;
53
- }
54
- if (it.thumbUrl) {
55
- const t = el("img");
56
- t.src = it.thumbUrl;
57
- t.alt = "";
58
- t.loading = "lazy";
59
- ic.appendChild(t);
60
- return ic;
61
- }
62
- const ct = it.contentType || "";
63
- if (it.viewUrl && ct.indexOf("image/") === 0) {
64
- const im = el("img");
65
- im.src = it.viewUrl;
66
- im.alt = "";
67
- im.loading = "lazy";
68
- ic.appendChild(im);
69
- return ic;
70
- }
71
- if (it.viewUrl && ct.indexOf("video/") === 0) {
72
- const v = el("video");
73
- v.src = it.viewUrl;
74
- v.muted = true;
75
- v.playsInline = true;
76
- ic.appendChild(v);
77
- return ic;
78
- }
79
- ic.textContent = ((it.name.split(".").pop() || it.name.slice(0, 1)) || "?").slice(0, 3).toUpperCase();
80
- return ic;
81
- }
82
- // ── shared floating layers (toast + open kebab menu) ─────────────────────────
83
- let toastTimer;
84
- function showToast(msg) {
85
- let t = document.getElementById("rk-dir-toast");
86
- if (!t) {
87
- t = el("div");
88
- t.id = "rk-dir-toast";
89
- t.className = "rk-dir-toast";
90
- document.body.appendChild(t);
91
- }
92
- t.textContent = msg;
93
- t.classList.add("is-on");
94
- if (toastTimer)
95
- window.clearTimeout(toastTimer);
96
- toastTimer = window.setTimeout(() => t && t.classList.remove("is-on"), 1800);
97
- }
98
- function copyToClipboard(text) {
99
- if (navigator.clipboard && navigator.clipboard.writeText)
100
- return navigator.clipboard.writeText(text);
101
- return new Promise((resolve, reject) => {
102
- try {
103
- const ta = el("textarea");
104
- ta.value = text;
105
- ta.style.position = "fixed";
106
- ta.style.opacity = "0";
107
- document.body.appendChild(ta);
108
- ta.select();
109
- document.execCommand("copy");
110
- document.body.removeChild(ta);
111
- resolve();
112
- }
113
- catch (e) {
114
- reject(e);
115
- }
116
- });
117
- }
118
- let closeOpenMenu = null;
119
- function openMenuAt(anchor, actions) {
120
- if (closeOpenMenu)
121
- closeOpenMenu();
122
- const menu = el("div", "rk-dir-menu");
123
- actions.forEach((a) => {
124
- const b = el("button", "rk-dir-menuitem" + (a.danger ? " is-danger" : ""), a.label);
125
- b.type = "button";
126
- b.addEventListener("click", (e) => { e.stopPropagation(); close(); a.run(); });
127
- menu.appendChild(b);
128
- });
129
- document.body.appendChild(menu);
130
- const r = anchor instanceof HTMLElement ? anchor.getBoundingClientRect() : anchor;
131
- // Prefer below-right of the anchor; flip up/left if it would overflow.
132
- const mw = menu.offsetWidth, mh = menu.offsetHeight;
133
- let top = r.bottom + 4, left = r.right - mw;
134
- if (left < 8)
135
- left = r.left;
136
- if (top + mh > window.innerHeight - 8)
137
- top = Math.max(8, r.top - mh - 4);
138
- menu.style.top = top + "px";
139
- menu.style.left = Math.max(8, left) + "px";
140
- function close() {
141
- menu.remove();
142
- document.removeEventListener("click", onDoc, true);
143
- document.removeEventListener("keydown", onKey, true);
144
- window.removeEventListener("scroll", close, true);
145
- if (closeOpenMenu === close)
146
- closeOpenMenu = null;
147
- }
148
- function onDoc(e) { if (!menu.contains(e.target))
149
- close(); }
150
- function onKey(e) { if (e.key === "Escape")
151
- close(); }
152
- setTimeout(() => {
153
- document.addEventListener("click", onDoc, true);
154
- document.addEventListener("keydown", onKey, true);
155
- window.addEventListener("scroll", close, true);
156
- }, 0);
157
- closeOpenMenu = close;
158
- }
159
- // ── the explorer ─────────────────────────────────────────────────────────────
160
- export function createDirectoryExplorer(mount, opts = {}) {
161
- const apiBase = opts.apiBase || DEFAULT_API_BASE;
162
- const roots = (opts.roots && opts.roots.length ? opts.roots : DIRECTORY_ROOT_KEYS.slice());
163
- const singleRoot = roots.length === 1 ? roots[0] : null;
164
- const mode = opts.mode || "attach";
165
- const showSearch = opts.showSearch !== false;
166
- const showMultiSelect = opts.showMultiSelect !== false;
167
- const showUpload = opts.showUpload != null ? opts.showUpload : mode === "attach";
168
- const primaryClick = opts.primaryClick || "copyPath";
169
- const cloudAvailable = opts.cloudAvailable != null
170
- ? opts.cloudAvailable
171
- : typeof window !== "undefined" && window.__vidfarmCloudSpace === true;
172
- const host = window.__rkDirectoryHost || {};
173
- const emitAttach = (items) => (opts.onAttach || host.onAttach || (() => showToast("Attached " + items.length + " item(s)")))(items);
174
- const emitPreview = (items, i) => {
175
- const cb = opts.onPreview || host.onPreview;
176
- if (cb)
177
- cb(items, i);
178
- else if (items[i] && items[i].viewUrl)
179
- window.open(items[i].viewUrl, "_blank", "noopener");
180
- };
181
- const toast = (msg) => (opts.onToast || host.onToast || showToast)(msg);
182
- const copyPath = (p) => copyToClipboard(p).then(() => toast("Copied " + p)).catch(() => toast("Couldn't copy path"));
183
- // ── state ──
184
- let currentPath = normalizeInitial(opts.initialPath, singleRoot);
185
- let status = "idle";
186
- let folders = [];
187
- let files = [];
188
- let nextOffset = null;
189
- let loadingMore = false;
190
- const selection = new Map();
191
- let searchMode = false;
192
- let searchResults = [];
193
- let searchStatus = "idle";
194
- let destroyed = false;
195
- // Which backend the explorer reads: "local" (this box's disk backend) or
196
- // "cloud" (vidfarm.cc via the serve-box passthrough). Only meaningful when
197
- // cloudAvailable; the /cloud fetches hit /me/cloud/directory*.
198
- let space = "local";
199
- function spacePrefix() { return space === "cloud" ? "/cloud" : ""; }
200
- function normalizeInitial(p, single) {
201
- const parsed = parseDirectoryPath(p || "");
202
- if (parsed.root)
203
- return buildDirectoryPath(parsed.root, parsed.folderPath);
204
- return single ? "/" + single : "/";
205
- }
206
- // ── scaffold ──
207
- mount.classList.add("rk-dir");
208
- mount.innerHTML = "";
209
- const searchWrap = el("div", "rk-dir-search");
210
- const searchInput = el("input", "rk-dir-search-input");
211
- searchInput.type = "search";
212
- searchInput.placeholder = "Search files — meaning, name, or path…";
213
- searchWrap.appendChild(searchInput);
214
- const crumbsEl = el("div", "rk-aichat-crumbs rk-dir-crumbs");
215
- const selbar = el("div", "rk-dir-selbar");
216
- selbar.hidden = true;
217
- const bodyEl = el("div", "rk-aichat-files-body rk-dir-body");
218
- const dropEl = el("label", "rk-aichat-drop rk-dir-drop");
219
- dropEl.hidden = true;
220
- const dropText = el("span", "rk-aichat-drop-text", "Drop files here");
221
- const dropInput = el("input");
222
- dropInput.type = "file";
223
- dropInput.multiple = true;
224
- dropInput.hidden = true;
225
- dropEl.appendChild(dropText);
226
- dropEl.appendChild(dropInput);
227
- // Local ⇄ Cloud space switch (serve boxes with an upstream only).
228
- const spaceBar = el("div", "rk-dir-space");
229
- const localBtn = el("button", "rk-dir-space-btn is-on", "Local");
230
- const cloudBtn = el("button", "rk-dir-space-btn", "Cloud");
231
- localBtn.type = "button";
232
- cloudBtn.type = "button";
233
- spaceBar.appendChild(localBtn);
234
- spaceBar.appendChild(cloudBtn);
235
- function setSpace(next) {
236
- if (space === next)
237
- return;
238
- space = next;
239
- localBtn.classList.toggle("is-on", next === "local");
240
- cloudBtn.classList.toggle("is-on", next === "cloud");
241
- // Reset to the space's root view and clear any active search.
242
- searchMode = false;
243
- searchResults = [];
244
- searchInput.value = "";
245
- selection.clear();
246
- currentPath = singleRoot ? "/" + singleRoot : "/";
247
- status = "idle";
248
- load(true);
249
- }
250
- localBtn.addEventListener("click", () => setSpace("local"));
251
- cloudBtn.addEventListener("click", () => setSpace("cloud"));
252
- if (cloudAvailable)
253
- mount.appendChild(spaceBar);
254
- if (showSearch)
255
- mount.appendChild(searchWrap);
256
- mount.appendChild(crumbsEl);
257
- if (showMultiSelect)
258
- mount.appendChild(selbar);
259
- mount.appendChild(bodyEl);
260
- if (showUpload)
261
- mount.appendChild(dropEl);
262
- // ── fetch ──
263
- function dirUrl(path, offset) {
264
- const u = new URL(apiBase + spacePrefix() + "/directory", window.location.href);
265
- u.searchParams.set("path", path);
266
- if (offset)
267
- u.searchParams.set("offset", String(offset));
268
- return u.toString();
269
- }
270
- function load(force) {
271
- if (destroyed)
272
- return;
273
- if (!force && status === "ready") {
274
- render();
275
- return;
276
- }
277
- status = "loading";
278
- render();
279
- fetch(dirUrl(currentPath), { credentials: "same-origin", headers: { accept: "application/json" } })
280
- .then((r) => {
281
- if (r.status === 401 || r.status === 403) {
282
- status = "auth";
283
- return null;
284
- }
285
- if (!r.ok)
286
- throw new Error("http " + r.status);
287
- return r.json();
288
- })
289
- .then((j) => {
290
- if (destroyed)
291
- return;
292
- if (!j) {
293
- render();
294
- return;
295
- }
296
- folders = filterRoots(j.folders || []);
297
- files = j.files || [];
298
- nextOffset = typeof j.next_offset === "number" ? j.next_offset : null;
299
- status = "ready";
300
- render();
301
- })
302
- .catch(() => { if (!destroyed) {
303
- status = "error";
304
- render();
305
- } });
306
- }
307
- // At the virtual root with a roots subset, hide roots we weren't asked to show.
308
- function filterRoots(list) {
309
- if (currentPath !== "/")
310
- return list;
311
- return list.filter((f) => roots.indexOf(f.root) !== -1);
312
- }
313
- function loadMore() {
314
- if (nextOffset == null || loadingMore)
315
- return;
316
- loadingMore = true;
317
- render();
318
- fetch(dirUrl(currentPath, nextOffset), { credentials: "same-origin", headers: { accept: "application/json" } })
319
- .then((r) => (r.ok ? r.json() : null))
320
- .then((j) => {
321
- loadingMore = false;
322
- if (destroyed || !j) {
323
- render();
324
- return;
325
- }
326
- files = files.concat(j.files || []);
327
- nextOffset = typeof j.next_offset === "number" ? j.next_offset : null;
328
- render();
329
- })
330
- .catch(() => { loadingMore = false; render(); });
331
- }
332
- // ── search ──
333
- let searchDebounce;
334
- function onSearchInput() {
335
- const q = searchInput.value.trim();
336
- if (searchDebounce)
337
- window.clearTimeout(searchDebounce);
338
- if (!q) {
339
- searchMode = false;
340
- searchResults = [];
341
- render();
342
- return;
343
- }
344
- searchDebounce = window.setTimeout(() => runSearch(q), 260);
345
- }
346
- function runSearch(q) {
347
- searchMode = true;
348
- searchStatus = "loading";
349
- render();
350
- const scope = singleRoot ? "/" + singleRoot : (currentPath !== "/" ? currentPath : "");
351
- fetch(apiBase + spacePrefix() + "/directory/search", {
352
- method: "POST",
353
- credentials: "same-origin",
354
- headers: { "content-type": "application/json", accept: "application/json" },
355
- body: JSON.stringify({ query: q, path: scope, mode: "auto", limit: 40 })
356
- })
357
- .then((r) => (r.ok ? r.json() : null))
358
- .then((j) => {
359
- if (destroyed)
360
- return;
361
- searchResults = (j && j.results) || [];
362
- searchStatus = "ready";
363
- render();
364
- })
365
- .catch(() => { if (!destroyed) {
366
- searchStatus = "error";
367
- render();
368
- } });
369
- }
370
- // ── navigation ──
371
- function navigate(path) {
372
- const parsed = parseDirectoryPath(path);
373
- currentPath = parsed.root ? buildDirectoryPath(parsed.root, parsed.folderPath) : "/";
374
- status = "idle";
375
- searchMode = false;
376
- searchInput.value = "";
377
- (opts.onNavigate || (() => { }))(currentPath);
378
- load(true);
379
- }
380
- function currentRoot() { return parseDirectoryPath(currentPath).root; }
381
- function currentFolder() { return parseDirectoryPath(currentPath).folderPath; }
382
- // /projects mirrors the user's composition forks — its folders and files are
383
- // backed by fork storage, not user-managed, so no create/rename/upload there.
384
- function canWriteRoot(r) { return !!r && r !== "projects"; }
385
- // Create an empty folder under `parentFolder` (defaults to the current folder).
386
- // Only valid inside a root — at the virtual root there's nowhere to put it.
387
- function createFolder(parentFolder) {
388
- const root = currentRoot();
389
- if (!root) {
390
- toast("Open a root (/files, /temp, /raws) first");
391
- return;
392
- }
393
- if (!canWriteRoot(root)) {
394
- toast("/projects is read-only");
395
- return;
396
- }
397
- const name = (window.prompt("New folder name") || "").trim();
398
- if (!name)
399
- return;
400
- const parent = normalizeFolderPath(parentFolder != null ? parentFolder : currentFolder());
401
- const target = buildDirectoryPath(root, parent ? parent + "/" + name : name);
402
- fetch(apiBase + "/directory/folders", {
403
- method: "POST",
404
- credentials: "same-origin",
405
- headers: { "content-type": "application/json", accept: "application/json" },
406
- body: JSON.stringify({ path: target })
407
- })
408
- .then((r) => (r.ok ? r.json() : r.json().then((j) => Promise.reject(new Error(j.error || "http " + r.status)))))
409
- .then((res) => { navigate(res.path || (root === currentRoot() ? currentPath : "/" + root)); toast("Created " + (res.path || name)); })
410
- .catch((e) => toast(e.message || "Couldn't create folder"));
411
- }
412
- // Rename a file or folder in place via POST /me/directory/rename. Files carry
413
- // their backend id (display-name-only rename); folders rename their trailing
414
- // path segment (files + nested subfolders move with them server-side).
415
- function renameItem(it) {
416
- const isFolder = it.kind === "folder";
417
- const next = (window.prompt(isFolder ? "Rename folder" : "Rename file", it.name) || "").trim();
418
- if (!next || next === it.name)
419
- return;
420
- fetch(apiBase + "/directory/rename", {
421
- method: "POST",
422
- credentials: "same-origin",
423
- headers: { "content-type": "application/json", accept: "application/json" },
424
- body: JSON.stringify({ path: it.path, new_name: next, ...(isFolder ? {} : { file_id: it.id }) })
425
- })
426
- .then((r) => (r.ok ? r.json() : r.json().then((j) => Promise.reject(new Error(j.error || "http " + r.status)))))
427
- .then(() => {
428
- // Renaming shifts the item's path, so drop any stale selection and
429
- // reload the current folder (the renamed child re-renders in place).
430
- if (selection.has(it.path)) {
431
- selection.delete(it.path);
432
- (opts.onSelectionChange || (() => { }))(Array.from(selection.values()));
433
- }
434
- load(true);
435
- toast("Renamed to " + next);
436
- })
437
- .catch((e) => toast(e.message || "Couldn't rename"));
438
- }
439
- // ── selection ──
440
- function toggleSelect(it, on) {
441
- if (on)
442
- selection.set(it.path, it);
443
- else
444
- selection.delete(it.path);
445
- (opts.onSelectionChange || (() => { }))(Array.from(selection.values()));
446
- renderSelbar();
447
- // reflect checkbox state without full re-render churn
448
- const row = bodyEl.querySelector('[data-path="' + cssEscape(it.path) + '"]');
449
- if (row)
450
- row.classList.toggle("is-selected", on);
451
- }
452
- function clearSelection() {
453
- selection.clear();
454
- (opts.onSelectionChange || (() => { }))([]);
455
- render();
456
- }
457
- // ── rendering ──
458
- function crumb(label, current, onClick) {
459
- const b = el("button", "rk-aichat-crumb" + (current ? " is-current" : ""), label);
460
- b.type = "button";
461
- b.addEventListener("click", onClick);
462
- return b;
463
- }
464
- function renderCrumbs() {
465
- crumbsEl.innerHTML = "";
466
- const root = currentRoot();
467
- const folder = currentFolder();
468
- if (!singleRoot) {
469
- crumbsEl.appendChild(crumb("Files", currentPath === "/", () => navigate("/")));
470
- }
471
- if (!root)
472
- return;
473
- const rootLabel = (DIRECTORY_ROOTS.find((r) => r.key === root) || { label: root }).label;
474
- if (!singleRoot)
475
- crumbsEl.appendChild(sep());
476
- crumbsEl.appendChild(crumb(rootLabel, !folder, () => navigate("/" + root)));
477
- let acc = "";
478
- folder.split("/").filter(Boolean).forEach((part, idx, arr) => {
479
- crumbsEl.appendChild(sep());
480
- acc = acc ? acc + "/" + part : part;
481
- const target = "/" + root + "/" + acc;
482
- crumbsEl.appendChild(crumb(part, idx === arr.length - 1, () => navigate(target)));
483
- });
484
- // Discoverable "New folder" affordance (right-click also works anywhere).
485
- // Read-only roots (/projects) don't get it.
486
- if (canWriteRoot(root)) {
487
- const nf = el("button", "rk-dir-newfolder", "+ Folder");
488
- nf.type = "button";
489
- nf.title = "New folder here (or right-click)";
490
- nf.addEventListener("click", () => createFolder());
491
- crumbsEl.appendChild(nf);
492
- }
493
- }
494
- function sep() { return el("span", "rk-aichat-crumb-sep", "/"); }
495
- function renderSelbar() {
496
- if (!showMultiSelect)
497
- return;
498
- const n = selection.size;
499
- selbar.hidden = n === 0;
500
- if (n === 0)
501
- return;
502
- selbar.innerHTML = "";
503
- selbar.appendChild(el("span", "rk-dir-selcount", n + " selected"));
504
- const picked = () => Array.from(selection.values());
505
- // Primary action stays on the bar; everything else tucks into a "⋯" menu so
506
- // the toolbar never overflows in the narrow editor dock.
507
- const cp = el("button", "rk-dir-selbtn is-primary", "Copy paths");
508
- cp.type = "button";
509
- cp.addEventListener("click", () => copyPath(picked().map((s) => s.path).join("\n")));
510
- selbar.appendChild(cp);
511
- const more = [];
512
- if (mode === "attach")
513
- more.push({ label: "Attach", run: () => { emitAttach(picked()); clearSelection(); } });
514
- more.push({ label: "Copy paths", run: () => copyPath(picked().map((s) => s.path).join("\n")) });
515
- more.push({ label: "Clear selection", run: clearSelection });
516
- const menuBtn = el("button", "rk-dir-selbtn is-ghost rk-dir-selmore", "⋯");
517
- menuBtn.type = "button";
518
- menuBtn.setAttribute("aria-label", "More actions");
519
- menuBtn.addEventListener("click", (e) => { e.stopPropagation(); openMenuAt(menuBtn, more); });
520
- selbar.appendChild(menuBtn);
521
- }
522
- function checkbox(it) {
523
- const box = el("input", "rk-dir-check");
524
- box.type = "checkbox";
525
- box.checked = selection.has(it.path);
526
- box.setAttribute("aria-label", "Select " + it.name);
527
- box.addEventListener("click", (e) => e.stopPropagation());
528
- box.addEventListener("change", () => toggleSelect(it, box.checked));
529
- return box;
530
- }
531
- function folderRowEl(it) {
532
- const row = el("div", "rk-aichat-frow rk-dir-row" + (selection.has(it.path) ? " is-selected" : ""));
533
- row.setAttribute("data-path", it.path);
534
- if (showMultiSelect)
535
- row.appendChild(checkbox(it));
536
- // Name button: default click COPIES the (relative) folder path.
537
- const b = el("button", "rk-aichat-fmainbtn");
538
- b.type = "button";
539
- b.title = "Copy path " + it.path;
540
- b.appendChild(iconFor(it));
541
- const main = el("span", "rk-aichat-fmain");
542
- main.appendChild(el("span", "rk-aichat-fname", it.name));
543
- const desc = it.meta && typeof it.meta.description === "string" ? it.meta.description : "";
544
- main.appendChild(el("span", "rk-aichat-fmeta", desc || "folder"));
545
- b.appendChild(main);
546
- b.addEventListener("click", () => copyPath(it.path));
547
- row.appendChild(b);
548
- // Open button (the primary navigation affordance).
549
- const open = el("button", "rk-aichat-fadd rk-dir-open", "Open ›");
550
- open.type = "button";
551
- open.title = "Open folder";
552
- open.addEventListener("click", (e) => { e.stopPropagation(); navigate(it.path); });
553
- row.appendChild(open);
554
- // Kebab. Create/rename only for writable roots (not /projects forks).
555
- const folderActions = [{ label: "Open folder", run: () => navigate(it.path) }];
556
- if (canWriteRoot(it.root)) {
557
- folderActions.push({ label: "New subfolder", run: () => createFolder(it.folderPath) });
558
- folderActions.push({ label: "Rename", run: () => renameItem(it) });
559
- }
560
- folderActions.push({ label: "Copy path", run: () => copyPath(it.path) });
561
- row.appendChild(kebab(it, folderActions));
562
- return row;
563
- }
564
- function fileRowEl(it, siblings, index, showPath) {
565
- const row = el("div", "rk-aichat-frow rk-dir-row" + (selection.has(it.path) ? " is-selected" : ""));
566
- row.setAttribute("data-path", it.path);
567
- if (showMultiSelect)
568
- row.appendChild(checkbox(it));
569
- const b = el("button", "rk-aichat-fmainbtn");
570
- b.type = "button";
571
- const primaryLabel = primaryClick === "copyPath" ? "Copy path " : primaryClick === "attach" ? "Attach " : "Preview ";
572
- b.title = primaryLabel + it.name;
573
- b.appendChild(iconFor(it));
574
- const main = el("span", "rk-aichat-fmain");
575
- main.appendChild(el("span", "rk-aichat-fname", it.name));
576
- const metaText = showPath ? it.path : (fmtDur(it.durationSec) || fmtBytes(it.sizeBytes) || it.contentType || "file");
577
- main.appendChild(el("span", "rk-aichat-fmeta", metaText));
578
- b.appendChild(main);
579
- b.addEventListener("click", () => {
580
- if (primaryClick === "attach")
581
- emitAttach([it]);
582
- else if (primaryClick === "preview")
583
- emitPreview(siblings, index);
584
- else
585
- copyPath(it.path);
586
- });
587
- row.appendChild(b);
588
- // When the row's primary click previews (chat dock), keep attaching one click
589
- // away with a dedicated inline "+" button so files stay easy to add to the
590
- // composer without going through the kebab.
591
- if (mode === "attach" && primaryClick === "preview") {
592
- const add = el("button", "rk-aichat-fadd rk-dir-attach", "+");
593
- add.type = "button";
594
- add.title = "Attach " + it.name;
595
- add.setAttribute("aria-label", "Attach " + it.name);
596
- add.addEventListener("click", (e) => { e.stopPropagation(); emitAttach([it]); });
597
- row.appendChild(add);
598
- }
599
- // Kebab: Attach / View / Preview / Copy path.
600
- const actions = [];
601
- if (mode === "attach")
602
- actions.push({ label: "Attach", run: () => emitAttach([it]) });
603
- if (it.viewUrl)
604
- actions.push({ label: "View", run: () => window.open(it.viewUrl, "_blank", "noopener") });
605
- if (it.viewUrl)
606
- actions.push({ label: "Preview", run: () => emitPreview(siblings, index) });
607
- // Raws (source-named) and approved posts (title-named) can't be file-renamed;
608
- // only /files + /temp files rename in place. Folders rename in every root.
609
- if ((it.root === "files" || it.root === "temp") && it.id)
610
- actions.push({ label: "Rename", run: () => renameItem(it) });
611
- actions.push({ label: "Copy path", run: () => copyPath(it.path) });
612
- row.appendChild(kebab(it, actions));
613
- return row;
614
- }
615
- function kebab(it, actions) {
616
- void it;
617
- const k = el("button", "rk-dir-kebab", "⋯");
618
- k.type = "button";
619
- k.setAttribute("aria-label", "More actions");
620
- k.addEventListener("click", (e) => { e.stopPropagation(); openMenuAt(k, actions); });
621
- return k;
622
- }
623
- function note(text, link, retry) {
624
- const wrap = el("div", "rk-aichat-fstate");
625
- wrap.appendChild(el("div", undefined, text));
626
- if (link) {
627
- const a = el("a");
628
- a.href = link.href;
629
- a.textContent = link.label;
630
- wrap.appendChild(a);
631
- }
632
- if (retry) {
633
- const btn = el("button", undefined, "Try again");
634
- btn.type = "button";
635
- btn.addEventListener("click", () => load(true));
636
- wrap.appendChild(btn);
637
- }
638
- return wrap;
639
- }
640
- function render() {
641
- if (destroyed)
642
- return;
643
- renderCrumbs();
644
- renderSelbar();
645
- updateDrop();
646
- bodyEl.innerHTML = "";
647
- if (searchMode) {
648
- renderSearch();
649
- return;
650
- }
651
- if (status === "loading" && !files.length && !folders.length) {
652
- bodyEl.appendChild(note("Loading your files…"));
653
- return;
654
- }
655
- if (status === "auth") {
656
- bodyEl.appendChild(note("Sign in to browse your files.", { href: "/login", label: "Sign in" }));
657
- return;
658
- }
659
- if (status === "error") {
660
- bodyEl.appendChild(note("Couldn't reach your files.", undefined, true));
661
- return;
662
- }
663
- if (!folders.length && !files.length) {
664
- bodyEl.appendChild(note(currentPath === "/" ? "No files yet." : "This folder is empty."));
665
- return;
666
- }
667
- folders.forEach((f) => bodyEl.appendChild(folderRowEl(f)));
668
- files.forEach((f, i) => bodyEl.appendChild(fileRowEl(f, files, i)));
669
- if (nextOffset != null) {
670
- const more = el("button", "rk-aichat-loadmore", loadingMore ? "Loading…" : "Load more");
671
- more.type = "button";
672
- more.disabled = loadingMore;
673
- more.addEventListener("click", loadMore);
674
- bodyEl.appendChild(more);
675
- }
676
- }
677
- function renderSearch() {
678
- if (searchStatus === "loading" && !searchResults.length) {
679
- bodyEl.appendChild(note("Searching…"));
680
- return;
681
- }
682
- if (searchStatus === "error") {
683
- bodyEl.appendChild(note("Search failed."));
684
- return;
685
- }
686
- if (!searchResults.length) {
687
- bodyEl.appendChild(note("No matches."));
688
- return;
689
- }
690
- const fileHits = searchResults.filter((r) => r.kind !== "folder");
691
- searchResults.forEach((r, i) => {
692
- if (r.kind === "folder")
693
- bodyEl.appendChild(folderRowEl(r));
694
- else
695
- bodyEl.appendChild(fileRowEl(r, fileHits, fileHits.indexOf(r), true));
696
- });
697
- }
698
- // ── upload (attach hosts only; not for raws) ──
699
- let uploading = false;
700
- function uploadEndpoint() {
701
- const root = currentRoot();
702
- if (root === "files")
703
- return apiBase + "/attachments/upload";
704
- if (root === "temp")
705
- return apiBase + "/temporary-files/upload";
706
- return null; // raws are scanned/imported, not plain-uploaded
707
- }
708
- function updateDrop() {
709
- if (!showUpload)
710
- return;
711
- const endpoint = uploadEndpoint();
712
- dropEl.hidden = !endpoint || status === "auth";
713
- if (!dropEl.hidden && !uploading) {
714
- const folder = currentFolder();
715
- const rootLabel = currentRoot() ? (DIRECTORY_ROOTS.find((r) => r.key === currentRoot()) || { label: "" }).label : "";
716
- dropText.textContent = "Drop files into " + rootLabel + (folder ? " / " + folder : "");
717
- }
718
- }
719
- function uploadFiles(list) {
720
- const endpoint = uploadEndpoint();
721
- if (!endpoint || uploading || !list || !list.length)
722
- return;
723
- uploading = true;
724
- const folder = currentFolder();
725
- const root = currentRoot();
726
- const arr = Array.from(list);
727
- let done = 0, failed = 0;
728
- // The server may re-home an upload (e.g. /temp files land in a dated
729
- // subfolder). Track where the bytes actually landed so we can reveal them —
730
- // otherwise the refresh shows the folder we uploaded FROM, which looks empty.
731
- let landedFolder = null;
732
- dropText.textContent = "Uploading " + arr.length + " file(s)…";
733
- const finish = () => {
734
- uploading = false;
735
- if (failed)
736
- toast(failed + " of " + arr.length + " failed");
737
- const target = root && landedFolder != null ? buildDirectoryPath(root, landedFolder) : null;
738
- if (target && target !== currentPath)
739
- navigate(target); // reveal the uploads
740
- else
741
- load(true);
742
- };
743
- const next = (i) => {
744
- if (i >= arr.length) {
745
- finish();
746
- return;
747
- }
748
- const fd = new FormData();
749
- fd.append("file", arr[i]);
750
- fd.append("folder_path", folder);
751
- fetch(endpoint, { method: "POST", credentials: "same-origin", body: fd })
752
- .then((r) => { if (!r.ok)
753
- throw new Error("http " + r.status); return r.json().catch(() => null); })
754
- .then((j) => {
755
- done++;
756
- const fp = j && ((j.file && j.file.folderPath) ?? (j.attachment && j.attachment.folderPath));
757
- if (typeof fp === "string" && landedFolder == null)
758
- landedFolder = fp;
759
- })
760
- .catch(() => { failed++; })
761
- .then(() => next(i + 1));
762
- };
763
- next(0);
764
- }
765
- // ── right-click context menu: New folder anywhere, plus row actions ──
766
- function findItem(path) {
767
- return folders.concat(files, searchResults).find((it) => it.path === path);
768
- }
769
- bodyEl.addEventListener("contextmenu", (e) => {
770
- const rootSet = !!currentRoot();
771
- const rowEl = e.target.closest(".rk-dir-row");
772
- const at = { top: e.clientY, bottom: e.clientY, left: e.clientX, right: e.clientX };
773
- const actions = [];
774
- const it = rowEl ? findItem(rowEl.getAttribute("data-path") || "") : undefined;
775
- if (it && it.kind === "folder") {
776
- actions.push({ label: "Open folder", run: () => navigate(it.path) });
777
- if (canWriteRoot(it.root))
778
- actions.push({ label: "New subfolder", run: () => createFolder(it.folderPath) });
779
- if (canWriteRoot(it.root))
780
- actions.push({ label: "Rename", run: () => renameItem(it) });
781
- actions.push({ label: "Copy path", run: () => copyPath(it.path) });
782
- }
783
- else if (it) {
784
- if (mode === "attach")
785
- actions.push({ label: "Attach", run: () => emitAttach([it]) });
786
- if (it.viewUrl)
787
- actions.push({ label: "View", run: () => window.open(it.viewUrl, "_blank", "noopener") });
788
- if (it.viewUrl)
789
- actions.push({ label: "Preview", run: () => emitPreview(files, files.indexOf(it)) });
790
- if (it.root !== "raws" && canWriteRoot(it.root) && it.id)
791
- actions.push({ label: "Rename", run: () => renameItem(it) });
792
- actions.push({ label: "Copy path", run: () => copyPath(it.path) });
793
- }
794
- // Always offer "New folder" in the current folder when inside a writable root.
795
- if (rootSet && canWriteRoot(currentRoot()))
796
- actions.push({ label: "New folder here", run: () => createFolder() });
797
- if (!actions.length)
798
- return; // virtual root, empty space → let the browser menu show
799
- e.preventDefault();
800
- openMenuAt(at, actions);
801
- });
802
- // ── wire events ──
803
- searchInput.addEventListener("input", onSearchInput);
804
- dropEl.addEventListener("click", (e) => { if (e.target === dropEl || e.target === dropText)
805
- dropInput.click(); });
806
- dropInput.addEventListener("change", () => { uploadFiles(dropInput.files); dropInput.value = ""; });
807
- dropEl.addEventListener("dragover", (e) => { e.preventDefault(); dropEl.classList.add("is-drag"); });
808
- dropEl.addEventListener("dragleave", (e) => { if (e.target === dropEl)
809
- dropEl.classList.remove("is-drag"); });
810
- dropEl.addEventListener("drop", (e) => {
811
- e.preventDefault();
812
- dropEl.classList.remove("is-drag");
813
- if (e.dataTransfer && e.dataTransfer.files)
814
- uploadFiles(e.dataTransfer.files);
815
- });
816
- (opts.onNavigate || (() => { }))(currentPath);
817
- load(true);
818
- return {
819
- refresh: () => load(true),
820
- navigate,
821
- getSelection: () => Array.from(selection.values()),
822
- getCurrentPath: () => currentPath,
823
- destroy: () => { destroyed = true; if (closeOpenMenu)
824
- closeOpenMenu(); mount.innerHTML = ""; }
825
- };
826
- }
827
- function cssEscape(s) {
828
- if (window.CSS && window.CSS.escape)
829
- return window.CSS.escape(s);
830
- return s.replace(/["\\]/g, "\\$&");
831
- }
832
- // ── pop-panel picker (modal) ──────────────────────────────────────────────────
833
- // A self-contained modal wrapper around createDirectoryExplorer so any surface
834
- // (editor "Replace media", inpaint "Save to Files", …) can pick a file or a
835
- // destination folder without reimplementing the overlay + CSS each time.
836
- const PICKER_STYLE_ID = "rk-dirpick-style";
837
- function ensurePickerStyle() {
838
- if (document.getElementById(PICKER_STYLE_ID))
839
- return;
840
- const s = document.createElement("style");
841
- s.id = PICKER_STYLE_ID;
842
- // Uses reskin --rk-* tokens where present, with plain fallbacks so the modal
843
- // is legible even on a page that hasn't loaded the design-system variables.
844
- s.textContent = [
845
- ".rk-dirpick{position:fixed;inset:0;z-index:1200;display:grid;place-items:center;padding:20px;",
846
- "background:rgba(28,25,23,.44);backdrop-filter:blur(2px);animation:rk-dirpick-fade .16s ease}",
847
- "@keyframes rk-dirpick-fade{from{opacity:0}to{opacity:1}}",
848
- ".rk-dirpick-panel{width:min(560px,100%);max-height:min(82vh,700px);display:flex;flex-direction:column;",
849
- "background:var(--rk-surface,#fff);border:1px solid var(--rk-border,#e6e0d6);border-radius:var(--rk-r-3xl,20px);",
850
- "box-shadow:var(--rk-shadow-lg,0 24px 60px rgba(28,25,23,.28));overflow:hidden}",
851
- ".rk-dirpick-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 18px;",
852
- "border-bottom:1px solid var(--rk-border,#e6e0d6)}",
853
- ".rk-dirpick-title{font-family:var(--rk-font-display,inherit);font-weight:800;font-size:15px;color:var(--rk-ink,#1c1917)}",
854
- ".rk-dirpick-close{border:0;background:transparent;font-size:22px;line-height:1;color:var(--rk-text-muted,#78716c);cursor:pointer;padding:0 4px}",
855
- ".rk-dirpick-close:hover{color:var(--rk-ink,#1c1917)}",
856
- ".rk-dirpick-hint{padding:10px 18px 0;font-size:12.5px;line-height:1.4;color:var(--rk-text-muted,#78716c)}",
857
- ".rk-dirpick-mount{padding:12px 14px;overflow:auto;flex:1;min-height:0}",
858
- ".rk-dirpick-foot{display:flex;align-items:center;gap:10px;padding:12px 18px;border-top:1px solid var(--rk-border,#e6e0d6)}",
859
- ".rk-dirpick-dest{flex:1;min-width:0;font-size:12.5px;color:var(--rk-text-muted,#78716c);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
860
- ".rk-dirpick-dest b{color:var(--rk-ink,#1c1917);font-weight:700}",
861
- ".rk-dirpick-btn{flex:none;min-height:36px;padding:0 16px;border-radius:999px;border:0;cursor:pointer;",
862
- "font:inherit;font-size:13px;font-weight:800;background:var(--rk-gold-500,var(--rk-gold-600,#fcb900));color:var(--rk-ink,#1c1917)}",
863
- ".rk-dirpick-btn:hover:not(:disabled){background:var(--rk-gold-600,#e0a500)}",
864
- ".rk-dirpick-btn:disabled{opacity:.5;cursor:default}",
865
- ".rk-dirpick-btn.is-ghost{background:transparent;border:1px solid var(--rk-border,#e6e0d6);color:var(--rk-ink,#1c1917)}"
866
- ].join("");
867
- document.head.appendChild(s);
868
- }
869
- export function openDirectoryPicker(opts = {}) {
870
- ensurePickerStyle();
871
- const select = opts.select === "folder" ? "folder" : "file";
872
- let done = false;
873
- let explorer = null;
874
- const overlay = el("div", "rk-dirpick");
875
- const panel = el("div", "rk-dirpick-panel");
876
- const head = el("div", "rk-dirpick-head");
877
- head.appendChild(el("span", "rk-dirpick-title", opts.title || (select === "folder" ? "Choose a folder" : "Choose a file")));
878
- const closeBtn = el("button", "rk-dirpick-close");
879
- closeBtn.type = "button";
880
- closeBtn.setAttribute("aria-label", "Close");
881
- closeBtn.innerHTML = "&times;";
882
- head.appendChild(closeBtn);
883
- panel.appendChild(head);
884
- if (opts.hint)
885
- panel.appendChild(el("div", "rk-dirpick-hint", opts.hint));
886
- const mountEl = el("div", "rk-dirpick-mount");
887
- panel.appendChild(mountEl);
888
- // Folder mode gets a footer that commits the currently-open folder.
889
- let destEl = null;
890
- let confirmBtn = null;
891
- function finish(result) {
892
- if (done)
893
- return;
894
- done = true;
895
- close();
896
- if (result)
897
- (opts.onPick || (() => { }))(result);
898
- else
899
- (opts.onCancel || (() => { }))();
900
- }
901
- function folderItemFor(path) {
902
- const parsed = parseDirectoryPath(path);
903
- if (!parsed.root)
904
- return null; // the virtual root "/" isn't a real folder
905
- const folder = parsed.folderPath || "";
906
- const name = folder ? folder.split("/").filter(Boolean).pop() || folder : parsed.root;
907
- return { path: buildDirectoryPath(parsed.root, folder), root: parsed.root, folderPath: folder, name, kind: "folder" };
908
- }
909
- function syncFooter(path) {
910
- if (!destEl || !confirmBtn)
911
- return;
912
- const item = folderItemFor(path);
913
- if (item) {
914
- destEl.innerHTML = "";
915
- destEl.appendChild(document.createTextNode("Save to "));
916
- const b = el("b", undefined, item.path);
917
- destEl.appendChild(b);
918
- confirmBtn.disabled = false;
919
- }
920
- else {
921
- destEl.textContent = "Open a folder (Files, Temp, …) to choose a destination.";
922
- confirmBtn.disabled = true;
923
- }
924
- }
925
- if (select === "folder") {
926
- const foot = el("div", "rk-dirpick-foot");
927
- destEl = el("div", "rk-dirpick-dest");
928
- const cancel = el("button", "rk-dirpick-btn is-ghost", "Cancel");
929
- cancel.type = "button";
930
- cancel.addEventListener("click", () => finish(null));
931
- confirmBtn = el("button", "rk-dirpick-btn", opts.confirmLabel || "Use this folder");
932
- confirmBtn.type = "button";
933
- confirmBtn.addEventListener("click", () => {
934
- const item = explorer ? folderItemFor(explorer.getCurrentPath()) : null;
935
- if (item)
936
- finish(item);
937
- });
938
- foot.appendChild(destEl);
939
- foot.appendChild(cancel);
940
- foot.appendChild(confirmBtn);
941
- panel.appendChild(foot);
942
- }
943
- overlay.appendChild(panel);
944
- document.body.appendChild(overlay);
945
- function onKey(e) { if (e.key === "Escape")
946
- finish(null); }
947
- function close() {
948
- document.removeEventListener("keydown", onKey, true);
949
- try {
950
- explorer && explorer.destroy();
951
- }
952
- catch { /* noop */ }
953
- overlay.remove();
954
- }
955
- closeBtn.addEventListener("click", () => finish(null));
956
- overlay.addEventListener("click", (e) => { if (e.target === overlay)
957
- finish(null); });
958
- document.addEventListener("keydown", onKey, true);
959
- explorer = createDirectoryExplorer(mountEl, {
960
- roots: opts.roots,
961
- initialPath: opts.initialPath,
962
- apiBase: opts.apiBase,
963
- // Standalone hides Attach chrome; picking is our own affordance.
964
- mode: "standalone",
965
- showSearch: true,
966
- showMultiSelect: false,
967
- // Opt-in upload: when allowed, the explorer shows its drop zone so users can
968
- // add a file to the currently-open folder (Files/Temp), then click it to pick.
969
- showUpload: opts.allowUpload === true,
970
- // In file mode a name-click resolves the pick; folders still navigate.
971
- primaryClick: select === "file" ? "attach" : "copyPath",
972
- onNavigate: (p) => syncFooter(p),
973
- onAttach: (items) => {
974
- if (select !== "file")
975
- return;
976
- const it = items && items[0];
977
- if (!it)
978
- return;
979
- if (opts.accept && !opts.accept(it)) {
980
- showToast(opts.rejectMessage || "That file can't be used here.");
981
- return;
982
- }
983
- finish(it);
984
- }
985
- });
986
- return { close: () => finish(null) };
987
- }
988
- // ── auto-mount + global bridge ───────────────────────────────────────────────
989
- function readDataOpts(node) {
990
- const d = node.dataset;
991
- const rootsAttr = (d.roots || "").split(",").map((s) => s.trim()).filter(Boolean);
992
- return {
993
- roots: rootsAttr.length ? rootsAttr : undefined,
994
- initialPath: d.initialPath || undefined,
995
- mode: d.mode === "standalone" ? "standalone" : "attach",
996
- showSearch: d.showSearch !== "false",
997
- showMultiSelect: d.showMultiselect !== "false",
998
- showUpload: d.showUpload != null ? d.showUpload === "true" : undefined,
999
- primaryClick: d.primaryClick || "copyPath",
1000
- cloudAvailable: d.cloudAvailable != null ? d.cloudAvailable === "true" : undefined
1001
- };
1002
- }
1003
- function autoMount() {
1004
- const nodes = Array.prototype.slice.call(document.querySelectorAll("[data-rk-directory]"));
1005
- nodes.forEach((node) => {
1006
- if (node.getAttribute("data-rk-dir-mounted") === "1")
1007
- return;
1008
- node.setAttribute("data-rk-dir-mounted", "1");
1009
- window.__rkDirectoryInstance = createDirectoryExplorer(node, readDataOpts(node));
1010
- });
1011
- }
1012
- window.__vidfarmDirectory = { mount: createDirectoryExplorer, pick: openDirectoryPicker };
1013
- if (document.readyState === "loading")
1014
- document.addEventListener("DOMContentLoaded", autoMount);
1015
- else
1016
- autoMount();
1017
- window.dispatchEvent(new CustomEvent("vidfarm:directory-ready"));
1018
- //# sourceMappingURL=file-directory.js.map