@mevdragon/vidfarm-devcli 0.20.6 → 0.20.9

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 (157) 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 +117 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +245 -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 +308 -0
  13. package/SKILL.director.md +517 -250
  14. package/dist/src/cli.js +563 -99
  15. package/dist/src/devcli/clips.js +29 -12
  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 +14 -2
  20. package/dist/src/services/clip-curation/hunt.js +56 -0
  21. package/dist/src/services/clip-curation/index.js +1 -1
  22. package/dist/src/services/clip-curation/scan.js +5 -3
  23. package/package.json +30 -10
  24. package/SKILL.platform.md +0 -432
  25. package/demo/README.md +0 -28
  26. package/demo/dist/app.css +0 -1
  27. package/demo/dist/app.js +0 -1850
  28. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  29. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  30. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  31. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  32. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  33. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  34. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  35. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  36. package/demo/dist/favicon.ico +0 -0
  37. package/demo/dist/icons/timeline/audio.svg +0 -7
  38. package/demo/dist/icons/timeline/captions.svg +0 -5
  39. package/demo/dist/icons/timeline/composition.svg +0 -12
  40. package/demo/dist/icons/timeline/image.svg +0 -18
  41. package/demo/dist/icons/timeline/music.svg +0 -10
  42. package/demo/dist/icons/timeline/text.svg +0 -3
  43. package/demo/dist/index.html +0 -15
  44. package/dist/src/account-pages-legacy.js +0 -9396
  45. package/dist/src/account-pages.js +0 -61
  46. package/dist/src/app.js +0 -21603
  47. package/dist/src/composition-runtime.js +0 -1053
  48. package/dist/src/config.js +0 -217
  49. package/dist/src/context.js +0 -447
  50. package/dist/src/dev-app-legacy.js +0 -739
  51. package/dist/src/dev-app.js +0 -6
  52. package/dist/src/devcli/migrate-local.js +0 -140
  53. package/dist/src/devcli/sync.js +0 -368
  54. package/dist/src/domain.js +0 -5
  55. package/dist/src/editor-chat-history.js +0 -82
  56. package/dist/src/editor-chat.js +0 -828
  57. package/dist/src/editor-dark-theme.js +0 -1128
  58. package/dist/src/frontend/debug.js +0 -71
  59. package/dist/src/frontend/discover-client.js +0 -130
  60. package/dist/src/frontend/discover-store.js +0 -23
  61. package/dist/src/frontend/file-directory.js +0 -1018
  62. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  63. package/dist/src/frontend/homepage-client.js +0 -446
  64. package/dist/src/frontend/homepage-shared.js +0 -201
  65. package/dist/src/frontend/homepage-store.js +0 -66
  66. package/dist/src/frontend/homepage-view.js +0 -705
  67. package/dist/src/frontend/page-runtime-client.js +0 -132
  68. package/dist/src/frontend/page-runtime-store.js +0 -9
  69. package/dist/src/frontend/sentry.js +0 -42
  70. package/dist/src/frontend/template-editor-chat.js +0 -4181
  71. package/dist/src/help-page.js +0 -346
  72. package/dist/src/homepage.js +0 -1458
  73. package/dist/src/index.js +0 -16
  74. package/dist/src/instrument.js +0 -30
  75. package/dist/src/landing-page.js +0 -384
  76. package/dist/src/page-runtime.js +0 -2
  77. package/dist/src/page-shell.js +0 -1452
  78. package/dist/src/primitive-context.js +0 -416
  79. package/dist/src/primitive-registry.js +0 -3940
  80. package/dist/src/primitive-sdk.js +0 -4
  81. package/dist/src/primitives/hyperframes-media.js +0 -108
  82. package/dist/src/react-page-shell.js +0 -35
  83. package/dist/src/ready-post-schedule-component.js +0 -1540
  84. package/dist/src/registry.js +0 -296
  85. package/dist/src/reskin/agency-page.js +0 -299
  86. package/dist/src/reskin/calendar-page.js +0 -568
  87. package/dist/src/reskin/chat-page.js +0 -942
  88. package/dist/src/reskin/discover-page.js +0 -1788
  89. package/dist/src/reskin/document.js +0 -1587
  90. package/dist/src/reskin/help-page.js +0 -357
  91. package/dist/src/reskin/index-page.js +0 -62
  92. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  93. package/dist/src/reskin/inpaint-page.js +0 -2554
  94. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  95. package/dist/src/reskin/job-runs-page.js +0 -477
  96. package/dist/src/reskin/library-page.js +0 -1634
  97. package/dist/src/reskin/login-page.js +0 -262
  98. package/dist/src/reskin/portfolio-page.js +0 -687
  99. package/dist/src/reskin/pricing-page.js +0 -390
  100. package/dist/src/reskin/settings-page.js +0 -732
  101. package/dist/src/reskin/theme.js +0 -711
  102. package/dist/src/runtime.js +0 -35
  103. package/dist/src/services/api-call-history.js +0 -249
  104. package/dist/src/services/auth.js +0 -152
  105. package/dist/src/services/billing-pricing.js +0 -39
  106. package/dist/src/services/billing.js +0 -241
  107. package/dist/src/services/cast.js +0 -127
  108. package/dist/src/services/chat-threads.js +0 -92
  109. package/dist/src/services/clip-records.js +0 -250
  110. package/dist/src/services/clip-search.js +0 -77
  111. package/dist/src/services/clip-vectors.js +0 -125
  112. package/dist/src/services/composition-sanitize.js +0 -124
  113. package/dist/src/services/composition-watch.js +0 -79
  114. package/dist/src/services/elevenlabs.js +0 -222
  115. package/dist/src/services/file-directory.js +0 -117
  116. package/dist/src/services/fork-access.js +0 -93
  117. package/dist/src/services/fork-manifest.js +0 -42
  118. package/dist/src/services/ghostcut.js +0 -179
  119. package/dist/src/services/hyperframes.js +0 -3654
  120. package/dist/src/services/job-capacity.js +0 -14
  121. package/dist/src/services/job-logs.js +0 -197
  122. package/dist/src/services/jobs.js +0 -136
  123. package/dist/src/services/local-dynamo.js +0 -0
  124. package/dist/src/services/media-processing.js +0 -766
  125. package/dist/src/services/primitive-media-lambda.js +0 -280
  126. package/dist/src/services/providers.js +0 -2748
  127. package/dist/src/services/rate-limits.js +0 -262
  128. package/dist/src/services/scene-annotations.js +0 -32
  129. package/dist/src/services/serverless-auth.js +0 -382
  130. package/dist/src/services/serverless-jobs.js +0 -1084
  131. package/dist/src/services/serverless-provider-keys.js +0 -409
  132. package/dist/src/services/serverless-records.js +0 -1515
  133. package/dist/src/services/serverless-template-configs.js +0 -75
  134. package/dist/src/services/storage.js +0 -461
  135. package/dist/src/services/swipe-customize.js +0 -437
  136. package/dist/src/services/template-certification.js +0 -413
  137. package/dist/src/services/template-loader.js +0 -99
  138. package/dist/src/services/template-runtime-bundles.js +0 -217
  139. package/dist/src/services/template-sources.js +0 -1017
  140. package/dist/src/services/upstream.js +0 -248
  141. package/dist/src/services/video-normalization.js +0 -2
  142. package/dist/src/services/webhooks.js +0 -62
  143. package/dist/src/template-editor-pages.js +0 -2576
  144. package/dist/src/template-editor-shell.js +0 -2893
  145. package/dist/src/template-sdk.js +0 -4
  146. package/dist/src/worker.js +0 -17
  147. package/public/assets/discover-client-app.js +0 -1
  148. package/public/assets/file-directory-app.js +0 -3
  149. package/public/assets/homepage-app.js +0 -54
  150. package/public/assets/homepage-client-app.js +0 -80
  151. package/public/assets/page-runtime-client-app.js +0 -94
  152. package/public/assets/placeholders/scene-placeholder.png +0 -0
  153. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  154. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  155. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  156. package/src/assets/favicon.ico +0 -0
  157. 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