@mevdragon/vidfarm-devcli 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.agents/skills/vidfarm-media/SKILL.md +41 -4
  2. package/SKILL.director.md +17 -3
  3. package/SKILL.platform.md +3 -3
  4. package/demo/dist/app.js +69 -69
  5. package/demo/dist/favicon.ico +0 -0
  6. package/dist/src/app.js +1550 -187
  7. package/dist/src/cli.js +227 -7
  8. package/dist/src/devcli/clip-store.js +29 -2
  9. package/dist/src/devcli/clips.js +52 -9
  10. package/dist/src/devcli/composition-edit.js +262 -0
  11. package/dist/src/devcli/timeline-edit.js +283 -0
  12. package/dist/src/editor-chat.js +29 -6
  13. package/dist/src/frontend/discover-client.js +130 -0
  14. package/dist/src/frontend/discover-store.js +23 -0
  15. package/dist/src/frontend/file-directory.js +744 -0
  16. package/dist/src/frontend/template-editor-chat.js +22 -19
  17. package/dist/src/landing-page.js +24 -7
  18. package/dist/src/page-shell.js +25 -1
  19. package/dist/src/reskin/agency-page.js +1 -1
  20. package/dist/src/reskin/calendar-page.js +2 -1
  21. package/dist/src/reskin/chat-page.js +319 -72
  22. package/dist/src/reskin/discover-page.js +486 -31
  23. package/dist/src/reskin/document.js +1171 -384
  24. package/dist/src/reskin/help-page.js +1 -0
  25. package/dist/src/reskin/inpaint-page.js +10 -4
  26. package/dist/src/reskin/library-page.js +918 -220
  27. package/dist/src/reskin/login-page.js +6 -6
  28. package/dist/src/reskin/pricing-page.js +2 -0
  29. package/dist/src/reskin/settings-page.js +55 -10
  30. package/dist/src/reskin/theme.js +337 -17
  31. package/dist/src/services/clip-curation/gemini.js +5 -0
  32. package/dist/src/services/clip-curation/hunt.js +79 -1
  33. package/dist/src/services/clip-curation/index.js +2 -1
  34. package/dist/src/services/clip-curation/local-agent.js +4 -3
  35. package/dist/src/services/clip-curation/media-select.js +85 -0
  36. package/dist/src/services/clip-curation/query.js +5 -1
  37. package/dist/src/services/clip-curation/refine.js +50 -20
  38. package/dist/src/services/clip-curation/scan.js +10 -3
  39. package/dist/src/services/clip-curation/taxonomy.js +3 -1
  40. package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
  41. package/dist/src/services/clip-records.js +14 -1
  42. package/dist/src/services/clip-search.js +43 -13
  43. package/dist/src/services/file-directory.js +114 -0
  44. package/dist/src/services/storage.js +24 -1
  45. package/dist/src/services/upstream.js +5 -5
  46. package/dist/src/template-editor-shell.js +16 -2
  47. package/package.json +1 -1
  48. package/public/assets/discover-client-app.js +1 -0
  49. package/public/assets/file-directory-app.js +2 -0
  50. package/public/assets/homepage-client-app.js +12 -12
  51. package/public/assets/page-runtime-client-app.js +24 -24
  52. package/src/assets/favicon.ico +0 -0
  53. package/src/assets/logo-vidfarm.png +0 -0
@@ -37,6 +37,26 @@ const EMPTY_LIBRARY_INPUT = {
37
37
  };
38
38
  const PLAY = `<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13l11-6.5-11-6.5Z"/></svg>`;
39
39
  const SEARCH_ICON = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>`;
40
+ // Compact per-card action icons + viewer-modal chrome (ported from the /discover
41
+ // reskin TikTok viewer so the library modal reads identical).
42
+ const ICON_EDIT = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>`;
43
+ const ICON_SHARE = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>`;
44
+ const ICON_DL = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M5 21h14"/></svg>`;
45
+ const CROSS_ICON = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>`;
46
+ const SOUND_ON_ICON = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5Z"/><path d="M15.5 8.5a5 5 0 0 1 0 7"/><path d="M18.5 5.5a9 9 0 0 1 0 13"/></svg>`;
47
+ const SOUND_OFF_ICON = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H2v6h4l5 4V5Z"/><path d="m23 9-6 6"/><path d="m17 9 6 6"/></svg>`;
48
+ const CHEV_UP_ICON = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 15 6-6 6 6"/></svg>`;
49
+ const CHEV_DOWN_ICON = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>`;
50
+ const ICON_MORE = `<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.9"/><circle cx="12" cy="12" r="1.9"/><circle cx="12" cy="19" r="1.9"/></svg>`;
51
+ const ICON_COPY = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>`;
52
+ const ICON_TRASH = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6M14 11v6"/></svg>`;
53
+ const ICON_FOLDER = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z"/></svg>`;
54
+ const ICON_CHEVLEFT = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>`;
55
+ const ICON_LOGS = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01M3 12h.01M3 18h.01"/></svg>`;
56
+ // Logs is no longer a primary tab — it's a right-floated ghost button that rides
57
+ // the topbar (to the right of the tab's quick-search). Shared by the Approved,
58
+ // Raws and Files topbars so the row reads identically everywhere.
59
+ const LOGS_BTN = `<a class="rk-btn rk-btn-ghost rk-btn-sm rk-library-logs-btn" href="/library/logs">${ICON_LOGS}<span>Logs</span></a>`;
40
60
  /* ──────────────────────────── Approved (projects) tab ─────────────────────── */
41
61
  const STATUS = {
42
62
  ready: { short: "Ready", pill: "rk-pill-green" },
@@ -68,7 +88,13 @@ function downloadAsset(post) {
68
88
  const v = post.media.find((a) => isVideoAsset(a));
69
89
  return v || primaryAsset(post);
70
90
  }
71
- function shareHref(post) {
91
+ // "View post" now always opens the account-scoped public post page in a new
92
+ // tab: /u/:accountId/approved/posts/:postId (falls back to the un-scoped path or
93
+ // the stored share_url when the account id is unknown).
94
+ function shareHref(post, accountId) {
95
+ if (accountId) {
96
+ return `/u/${encodeURIComponent(accountId)}/approved/posts/${encodeURIComponent(post.post_id)}`;
97
+ }
72
98
  return post.share_url && post.share_url.length
73
99
  ? post.share_url
74
100
  : `/approved/posts/${encodeURIComponent(post.post_id)}`;
@@ -89,11 +115,8 @@ function posterMedia(post) {
89
115
  }
90
116
  return `<span class="rk-library-noprev">No preview</span>`;
91
117
  }
92
- function projectCard(post, accountId) {
118
+ function projectCard(post, accountId, index) {
93
119
  const s = statusInfo(post.status);
94
- const href = editorHref(post, accountId) || shareHref(post);
95
- const dl = downloadAsset(post);
96
- const canDownload = dl && isVideoAsset(dl);
97
120
  const title = post.title || "Published render";
98
121
  const subLine = post.tracer
99
122
  ? post.tracer
@@ -102,29 +125,44 @@ function projectCard(post, accountId) {
102
125
  : "Ready-to-post cut";
103
126
  const searchHay = [title, post.caption, post.tracer, post.template_id].filter(Boolean).join(" ").toLowerCase();
104
127
  const editHref = editorHref(post, accountId);
105
- const primaryAction = post.status === "scheduled"
106
- ? `<a class="rk-btn rk-btn-ghost rk-btn-sm" href="/calendar">Reschedule</a>`
107
- : `<a class="rk-btn rk-btn-gold rk-btn-sm" href="/calendar">Schedule <span class="rk-arrow">→</span></a>`;
128
+ // Compact one-row action bar: "Share Post" is the labelled primary (opens the
129
+ // approved-post preview page in a new tab); "Edit" is the labelled secondary
130
+ // (opens the composition in the editor in a new tab). A right-floated ""
131
+ // overflow menu carries Copy ID + Delete (with a popconfirm).
108
132
  const actions = [
109
- editHref ? `<a class="rk-btn rk-btn-ghost rk-btn-sm" href="${rkEscape(editHref)}">Open in editor</a>` : "",
110
- `<a class="rk-btn rk-btn-ghost rk-btn-sm" href="${rkEscape(shareHref(post))}" target="_blank" rel="noreferrer">View post</a>`,
111
- canDownload ? `<a class="rk-btn rk-btn-ghost rk-btn-sm" href="${rkEscape(dl.url)}" download="${rkEscape(dl.file_name || "render.mp4")}">Download</a>` : ""
133
+ `<a class="rk-btn rk-btn-gold rk-library-act" href="${rkEscape(shareHref(post, accountId))}" target="_blank" rel="noreferrer">Share Post</a>`,
134
+ editHref ? `<a class="rk-btn rk-btn-ghost rk-library-act" href="${rkEscape(editHref)}" target="_blank" rel="noreferrer" title="Open in editor">Edit</a>` : ""
112
135
  ].filter(Boolean).join("");
113
- return `<article class="rk-library-card rk-card-hover" data-status="${rkEscape(post.status)}" data-title="${rkEscape(searchHay)}">
114
- <a class="rk-library-poster" href="${rkEscape(href)}" aria-label="${rkEscape(title)}">
136
+ // Delete targets the real account-scoped DELETE route; only wired when we know
137
+ // the owning account (always true for the authed library).
138
+ const delUrl = accountId
139
+ ? `/u/${encodeURIComponent(accountId)}/approved/posts/${encodeURIComponent(post.post_id)}`
140
+ : "";
141
+ const menu = `<div class="rk-library-menu" data-rk-menu>
142
+ <button type="button" class="rk-library-kebab" data-rk-menu-btn aria-label="More actions" aria-haspopup="true" aria-expanded="false">${ICON_MORE}</button>
143
+ <div class="rk-library-menu-pop" data-rk-menu-pop hidden role="menu">
144
+ <button type="button" class="rk-library-menu-item" data-rk-copy-id="${rkEscape(post.post_id)}" role="menuitem">${ICON_COPY}<span>Copy ID</span></button>
145
+ <button type="button" class="rk-library-menu-item is-danger" data-rk-del="${rkEscape(delUrl)}" role="menuitem">${ICON_TRASH}<span>Delete</span></button>
146
+ <div class="rk-library-menu-confirm" data-rk-confirm hidden>
147
+ <p class="rk-library-menu-confirm-q">Delete this post?</p>
148
+ <div class="rk-library-menu-confirm-row">
149
+ <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-rk-confirm-no>Cancel</button>
150
+ <button type="button" class="rk-btn rk-btn-danger rk-btn-sm" data-rk-confirm-yes>Delete</button>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>`;
155
+ return `<article class="rk-library-card rk-card-hover" data-status="${rkEscape(post.status)}" data-title="${rkEscape(searchHay)}" data-post-id="${rkEscape(post.post_id)}"
156
+ <button type="button" class="rk-library-poster" data-rk-view="${index}" aria-label="Play ${rkEscape(title)}">
115
157
  ${posterMedia(post)}
116
158
  <span class="rk-pill ${s.pill} rk-library-pstatus"><span class="rk-library-dot"></span>${rkEscape(s.short)}</span>
117
159
  <span class="rk-library-dur">9:16</span>
118
160
  <span class="rk-library-play">${PLAY}</span>
119
- </a>
161
+ </button>
120
162
  <div class="rk-library-meta">
121
163
  <h3 class="rk-library-title">${rkEscape(title)}</h3>
122
- <div class="rk-library-origin"><span class="rk-library-origin-txt">${rkEscape(subLine)}</span></div>
123
- <div class="rk-spread rk-library-foot">
124
- <span class="rk-library-time">${rkEscape(fmtDate(post.created_at))}</span>
125
- ${primaryAction}
126
- </div>
127
- <div class="rk-library-actions">${actions}</div>
164
+ <div class="rk-library-origin"><span class="rk-library-time">${rkEscape(fmtDate(post.created_at))}</span><span class="rk-library-origin-sep">·</span><span class="rk-library-origin-txt">${rkEscape(subLine)}</span></div>
165
+ <div class="rk-library-actions">${actions}${menu}</div>
128
166
  </div>
129
167
  </article>`;
130
168
  }
@@ -133,108 +171,137 @@ function approvedPanel(input, tabs) {
133
171
  const accountId = input.currentAccountId;
134
172
  // The quickfilter searchbar mirrors the Clips tab's pill search — always
135
173
  // rendered so the topbar reads the same on both tabs (disabled when empty).
174
+ const qCount = posts.length;
136
175
  const searchBar = `<label class="rk-clips-search rk-library-qsearch">
137
176
  <span class="rk-clips-search-ic">${SEARCH_ICON}</span>
138
177
  <input class="rk-input rk-clips-search-input" data-rk-search type="search"
139
- placeholder="Search cuts title, caption, or tracer…" autocomplete="off" aria-label="Search cuts"${posts.length ? "" : " disabled"} />
178
+ placeholder="Search ${qCount} approved post${qCount === 1 ? "" : "s"}…" autocomplete="off" aria-label="Search approved posts"${posts.length ? "" : " disabled"} />
140
179
  </label>`;
141
180
  if (posts.length === 0) {
142
181
  return `
143
182
  <div class="rk-library-topbar">
144
183
  ${tabs}
145
184
  ${searchBar}
185
+ ${LOGS_BTN}
146
186
  </div>
147
187
 
148
188
  <div class="rk-library-empty is-visible">
149
189
  <h3>No published cuts yet</h3>
150
190
  <p class="rk-muted">Approve a finished render and it lands here — ready to schedule, share, or download. Start from a template or a prompt.</p>
151
- <a class="rk-btn rk-btn-gold" href="/discover">Start a project <span class="rk-arrow">→</span></a>
191
+ <a class="rk-btn rk-btn-gold" href="/editor/original/fork/new">Start a project <span class="rk-arrow">→</span></a>
152
192
  </div>`;
153
193
  }
154
- const readyCount = posts.filter((p) => p.status === "ready").length;
155
- const scheduledCount = posts.filter((p) => p.status === "scheduled").length;
156
- const chipDefs = [
157
- ["", "All"],
158
- ["ready", "Ready to post"],
159
- ["scheduled", "Scheduled"]
160
- ];
161
- const chips = chipDefs
162
- .map(([value, label], i) => `<button type="button" class="rk-library-chip${i === 0 ? " is-active" : ""}" data-rk-filter="${value}">${rkEscape(label)}</button>`)
163
- .join("");
164
- const newTile = `<a class="rk-library-new" data-rk-newtile href="/discover">
165
- <span class="rk-tile rk-tile-lg rk-tile-gold rk-library-new-plus">+</span>
166
- <div>
167
- <div class="rk-library-new-title">New project</div>
168
- <div class="rk-library-new-sub">Fork a template or start from a prompt</div>
169
- </div>
170
- </a>`;
171
- const cards = posts.map((p) => projectCard(p, accountId)).join("");
194
+ const cards = posts.map((p, i) => projectCard(p, accountId, i)).join("");
195
+ // Data the client viewer needs to build the vertical scroll deck (order MUST
196
+ // match the cards' data-rk-view index). Videos play in the modal; image-only
197
+ // posts show their poster still.
198
+ const viewerData = posts.map((p) => {
199
+ const asset = primaryAsset(p);
200
+ const isVideo = Boolean(asset && isVideoAsset(asset));
201
+ const poster = p.media.find((a) => isImageAsset(a));
202
+ return {
203
+ title: p.title || "Published render",
204
+ sub: p.tracer || (p.template_id ? `Composition ${p.template_id}` : "Ready-to-post cut"),
205
+ url: asset ? asset.url : "",
206
+ is_video: isVideo,
207
+ poster: poster ? poster.url : "",
208
+ post_href: shareHref(p, accountId),
209
+ editor_href: editorHref(p, accountId)
210
+ };
211
+ });
172
212
  return `
173
213
  <div class="rk-library-topbar">
174
214
  ${tabs}
175
215
  ${searchBar}
176
- </div>
177
-
178
- <div class="rk-library-toolbar">
179
- <div class="rk-library-chips" role="tablist" aria-label="Filter by status">${chips}</div>
180
- <div class="rk-library-summary"><b>${posts.length}</b> published cut${posts.length === 1 ? "" : "s"}&nbsp;·&nbsp;<b>${readyCount}</b> ready to post&nbsp;·&nbsp;<b>${scheduledCount}</b> scheduled</div>
216
+ ${LOGS_BTN}
181
217
  </div>
182
218
 
183
219
  <div class="rk-library-grid" data-rk-grid>
184
- ${newTile}
185
220
  ${cards}
186
221
  </div>
187
222
 
188
223
  <div class="rk-library-empty" data-rk-empty>
189
224
  <h3>Nothing here yet</h3>
190
- <p class="rk-muted">No cuts match this filter. Try another status or spin up a fresh render.</p>
191
- <a class="rk-btn rk-btn-gold" href="/discover">Start a project <span class="rk-arrow">→</span></a>
192
- </div>`;
225
+ <p class="rk-muted">No cuts match your search. Try a different term or spin up a fresh render.</p>
226
+ <a class="rk-btn rk-btn-gold" href="/editor/original/fork/new">Start a project <span class="rk-arrow">→</span></a>
227
+ </div>
228
+
229
+ <script type="application/json" data-rk-library-data>${JSON.stringify(viewerData).replace(/</g, "\\u003c")}</script>
230
+ ${LIBRARY_VIEWER_HTML}`;
193
231
  }
232
+ // Fullscreen TikTok-style viewer shell — ported from the /discover reskin so the
233
+ // library modal reads identically. The client fills [data-viewer-track] on open.
234
+ const LIBRARY_VIEWER_HTML = `
235
+ <div class="rk-library-viewer" data-rk-viewer hidden role="dialog" aria-modal="true" aria-label="Published cut viewer">
236
+ <div class="rk-library-viewer-scrim" data-viewer-close></div>
237
+ <div class="rk-library-viewer-track" data-viewer-track></div>
238
+ <div class="rk-library-viewer-ui">
239
+ <button type="button" class="rk-library-viewer-btn" data-viewer-mute aria-label="Toggle sound"><span class="rk-lvm-on">${SOUND_ON_ICON}</span><span class="rk-lvm-off">${SOUND_OFF_ICON}</span></button>
240
+ <button type="button" class="rk-library-viewer-btn" data-viewer-close aria-label="Close viewer">${CROSS_ICON}</button>
241
+ </div>
242
+ <div class="rk-library-viewer-nav">
243
+ <button type="button" class="rk-library-viewer-btn" data-viewer-prev aria-label="Previous cut">${CHEV_UP_ICON}</button>
244
+ <button type="button" class="rk-library-viewer-btn" data-viewer-next aria-label="Next cut">${CHEV_DOWN_ICON}</button>
245
+ </div>
246
+ </div>`;
194
247
  /* ──────────────────────────────── Clips tab ───────────────────────────────── */
195
248
  // The clip library is NOT part of the library route's input — the client fetches
196
249
  // the REAL endpoints on load: GET /clips/feed (library), POST /clips/search
197
250
  // (hybrid semantic search), POST /clips/scan + GET /clips/scan/:id (mine a
198
251
  // source), download via /clips/:clipId/download.
199
- // Filter chips double as real one-tap semantic searches (each hits /clips/search
200
- // with its phrase); "All clips" reloads the feed.
201
- const CLIP_PRESETS = [
202
- { slug: "all", label: "All raws", q: "" },
203
- { slug: "reactions", label: "Reactions", q: "someone reacting, a surprised or emotional reaction close-up" },
204
- { slug: "hooks", label: "Hooks & hot takes", q: "a strong opening hook or hot take, talking to camera" },
205
- { slug: "money", label: "Money shots", q: "money, cash, coins or payment in close-up" },
206
- { slug: "talking", label: "Talking head", q: "a person talking to the camera, medium shot" },
207
- { slug: "broll", label: "B-roll", q: "b-roll cutaway footage with no one talking" },
208
- { slug: "establishing", label: "Establishing", q: "a wide establishing shot of a place" }
209
- ];
210
252
  function clipsPanel(tabs) {
211
- const filterChips = CLIP_PRESETS
212
- .map((f, i) => `<button type="button" class="rk-clips-chip${i === 0 ? " is-active" : ""}" data-filter="${rkEscape(f.slug)}" data-q="${rkEscape(f.q)}">${rkEscape(f.label)}</button>`)
213
- .join("");
253
+ // Raws render as a file-directory explorer: raws are foldered by their source
254
+ // video (source_video_id / source_filename). The root shows folders; opening a
255
+ // folder shows its raws as big scannable thumbnails; typing a query flips to a
256
+ // flat semantic-search results view. The clip count lives in the search
257
+ // placeholder (kept in sync by the client) — no separate summary/count row.
214
258
  return `
215
259
  <div class="rk-library-topbar">
216
260
  ${tabs}
217
- <form class="rk-clips-search" id="rkClipsSearch">
261
+ <form class="rk-clips-search rk-library-qsearch" id="rkClipsSearch">
218
262
  <span class="rk-clips-search-ic">${SEARCH_ICON}</span>
219
263
  <input class="rk-input rk-clips-search-input" id="rkClipsQuery" type="search"
220
- placeholder="Search raws — e.g. &quot;someone looks confused after reading a message&quot;" autocomplete="off" />
221
- <button class="rk-btn rk-btn-ink rk-btn-sm rk-clips-search-btn" type="submit">Search</button>
264
+ placeholder="Search raw clips…" autocomplete="off" aria-label="Search raw clips" />
222
265
  </form>
266
+ ${LOGS_BTN}
223
267
  </div>
224
268
 
225
- <div class="rk-clips-toolbar">
226
- <div class="rk-clips-chipsrow">
227
- <div class="rk-clips-chips" id="rkClipsChips">${filterChips}</div>
228
- <div class="rk-library-summary" id="rkClipsSummary">Loading your raws library…</div>
229
- </div>
230
- <div class="rk-clips-status" id="rkClipsStatus">Loading your raws…</div>
269
+ <div class="rk-raws-bar">
270
+ <nav class="rk-raws-crumbs" id="rkRawsCrumbs" aria-label="Breadcrumb"></nav>
271
+ <div class="rk-clips-status" id="rkClipsStatus"></div>
231
272
  </div>
232
273
 
233
- <div class="rk-clips-grid" id="rkClipsGrid"></div>
274
+ <div class="rk-raws-explorer" id="rkClipsGrid"></div>
234
275
 
235
276
  <div class="rk-clips-empty" id="rkClipsEmpty" hidden>
236
- <h3>No clips yet</h3>
277
+ <h3>No raws yet</h3>
237
278
  <p class="rk-muted">Click <b>Import source</b> to mine a long video into tagged, searchable short clips.</p>
279
+ </div>
280
+
281
+ <div class="rk-raws-hover" id="rkRawsHover" hidden aria-hidden="true">
282
+ <video class="rk-raws-hover-vid" muted loop playsinline preload="none"></video>
283
+ <div class="rk-raws-hover-cap" id="rkRawsHoverCap"></div>
284
+ </div>
285
+ ${LIBRARY_VIEWER_HTML}`;
286
+ }
287
+ /* ──────────────────────────────── Files tab ───────────────────────────────── */
288
+ // The Files tab is the full-page mount of the reusable file-directory explorer
289
+ // (src/frontend/file-directory.ts). It opens at the virtual root so the three
290
+ // backends read as folders — Files / Temp / Raws — with the component's own
291
+ // unified search (semantic vector + substring + absolute-path, mode=auto),
292
+ // breadcrumb navigation, multi-select, and drag-drop upload. The bundle
293
+ // (/assets/file-directory-app.js) already ships on every full-chrome reskin
294
+ // page and auto-mounts any `[data-rk-directory]`, so no page script is needed;
295
+ // preview routes through the chrome's `window.__rkDirectoryHost.onPreview`.
296
+ function filesPanel(tabs) {
297
+ return `
298
+ <div class="rk-library-topbar">
299
+ ${tabs}
300
+ ${LOGS_BTN}
301
+ </div>
302
+
303
+ <div class="rk-files-explorer">
304
+ <div data-rk-directory data-mode="standalone" data-show-upload="true" data-show-multiselect="true" data-primary-click="preview"></div>
238
305
  </div>`;
239
306
  }
240
307
  const CLIPS_DIALOG = `
@@ -242,19 +309,27 @@ const CLIPS_DIALOG = `
242
309
  <form class="rk-clips-dialog-form" id="rkClipsScanForm" method="dialog">
243
310
  <div class="rk-card-head">
244
311
  <span class="rk-eyebrow">Import source</span>
245
- <h2>Mine a video into clips</h2>
246
- <p class="rk-muted">Paste a video URL — TikTok, YouTube, Instagram, X, or a direct link. We'll scan it and add the matching moments to your raws library.</p>
312
+ <h2>Import a video</h2>
313
+ <p class="rk-muted">Paste a video URL — TikTok, YouTube, Instagram, X, or a direct link. We'll add it to your raws library.</p>
247
314
  </div>
248
315
  <div class="rk-field">
249
316
  <label class="rk-label" for="rkScanUrl">Video URL</label>
250
317
  <input class="rk-input" id="rkScanUrl" name="url" type="url" placeholder="https://youtube.com/watch?v=… or https://…/video.mp4" autocomplete="off" />
251
318
  </div>
252
319
  <div class="rk-field">
253
- <label class="rk-label" for="rkScanPrompt">What to clip for</label>
254
- <textarea class="rk-textarea" id="rkScanPrompt" name="prompt" placeholder="Clip scenes of people reacting, no text on screen, vertical, between 5–10 secs long"></textarea>
255
- <span class="rk-hint">Guides the AI tagger toward the moments you actually want.</span>
320
+ <label class="rk-label" for="rkScanTracer">Tracer <span class="rk-hint">optional</span></label>
321
+ <input class="rk-input" id="rkScanTracer" name="tracer" type="text" placeholder="my-campaign-001" autocomplete="off" />
256
322
  </div>
257
- <div class="rk-grid rk-grid-2 rk-clips-dialog-grid">
323
+ <label class="rk-clips-toggle" for="rkScanClip">
324
+ <input type="checkbox" id="rkScanClip" name="clip" />
325
+ <span><b>Clip raws</b> — mine this video into matching moments with AI, instead of importing the whole thing.</span>
326
+ </label>
327
+ <div class="rk-clips-advanced" id="rkClipsAdvanced" hidden>
328
+ <div class="rk-field">
329
+ <label class="rk-label" for="rkScanPrompt">What to clip for</label>
330
+ <textarea class="rk-textarea" id="rkScanPrompt" name="prompt" placeholder="Clip scenes of people reacting, no text on screen, vertical, between 5–10 secs long"></textarea>
331
+ <span class="rk-hint">Guides the AI tagger toward the moments you actually want.</span>
332
+ </div>
258
333
  <div class="rk-field">
259
334
  <label class="rk-label" for="rkScanProvider">AI provider</label>
260
335
  <select class="rk-select" id="rkScanProvider" name="provider">
@@ -264,15 +339,11 @@ const CLIPS_DIALOG = `
264
339
  <option value="openrouter">OpenRouter</option>
265
340
  </select>
266
341
  </div>
267
- <div class="rk-field">
268
- <label class="rk-label" for="rkScanTracer">Tracer <span class="rk-hint">optional</span></label>
269
- <input class="rk-input" id="rkScanTracer" name="tracer" type="text" placeholder="my-campaign-001" autocomplete="off" />
270
- </div>
271
342
  </div>
272
- <p class="rk-hint rk-clips-dialog-note" id="rkScanNote">AI tagging runs on your own key (BYOK) — vidfarm only bills the clip-scan compute.</p>
343
+ <p class="rk-hint rk-clips-dialog-note" id="rkScanNote">We'll pull the whole video into your raws library.</p>
273
344
  <div class="rk-clips-dialog-actions">
274
345
  <button type="button" class="rk-btn rk-btn-ghost" id="rkClipsScanCancel">Cancel</button>
275
- <button type="submit" class="rk-btn rk-btn-gold" id="rkClipsScanSubmit">Start scan <span class="rk-arrow">→</span></button>
346
+ <button type="submit" class="rk-btn rk-btn-gold" id="rkClipsScanSubmit">Import video <span class="rk-arrow">→</span></button>
276
347
  </div>
277
348
  </form>
278
349
  </dialog>`;
@@ -280,35 +351,28 @@ const CLIPS_DIALOG = `
280
351
  const APPROVED_SCRIPT = `
281
352
  (function(){
282
353
  var root=document;
283
- var grid=root.querySelector('[data-rk-grid]');
284
354
  var empty=root.querySelector('[data-rk-empty]');
285
- var chips=Array.prototype.slice.call(root.querySelectorAll('[data-rk-filter]'));
286
- var newTile=root.querySelector('[data-rk-newtile]');
287
355
  var search=root.querySelector('[data-rk-search]');
356
+ var grid=root.querySelector('[data-rk-grid]');
288
357
  var cards=Array.prototype.slice.call(root.querySelectorAll('.rk-library-card'));
289
- var activeFilter='';
358
+ var total=cards.length;
359
+ function syncPlaceholder(){
360
+ if(!search)return;
361
+ search.setAttribute('placeholder','Search '+total+' approved post'+(total===1?'':'s')+String.fromCharCode(8230));
362
+ }
290
363
  function apply(){
291
364
  var q=(search&&search.value||'').trim().toLowerCase();
292
365
  var shown=0;
293
366
  cards.forEach(function(card){
294
- var okStatus=activeFilter===''||card.getAttribute('data-status')===activeFilter;
295
- var okSearch=!q||(card.getAttribute('data-title')||'').indexOf(q)!==-1;
296
- var visible=okStatus&&okSearch;
367
+ if(card.getAttribute('data-removed'))return;
368
+ var visible=!q||(card.getAttribute('data-title')||'').indexOf(q)!==-1;
297
369
  card.hidden=!visible;
298
370
  if(visible)shown++;
299
371
  });
300
- if(newTile)newTile.hidden=!(activeFilter===''&&!q);
301
372
  if(empty)empty.classList.toggle('is-visible',shown===0);
302
373
  }
303
- chips.forEach(function(btn){
304
- btn.addEventListener('click',function(){
305
- activeFilter=btn.getAttribute('data-rk-filter')||'';
306
- chips.forEach(function(b){b.classList.toggle('is-active',b===btn)});
307
- apply();
308
- });
309
- });
310
374
  if(search)search.addEventListener('input',apply);
311
- // hover-to-play the real MP4 previews
375
+ // hover-to-play the real MP4 previews on the grid cards
312
376
  root.querySelectorAll('video.rk-library-video').forEach(function(v){
313
377
  var card=v.closest('.rk-library-card');
314
378
  if(!card)return;
@@ -316,76 +380,529 @@ const APPROVED_SCRIPT = `
316
380
  card.addEventListener('mouseleave',function(){ if(v.pause)v.pause(); });
317
381
  });
318
382
  apply();
383
+
384
+ // ── per-card "⋯" overflow menu: Copy ID + Delete (popconfirm) ────────────
385
+ function closeAllMenus(except){
386
+ root.querySelectorAll('[data-rk-menu].is-open').forEach(function(m){
387
+ if(m===except)return;
388
+ m.classList.remove('is-open');
389
+ var pop=m.querySelector('[data-rk-menu-pop]'); if(pop)pop.hidden=true;
390
+ var cf=m.querySelector('[data-rk-confirm]'); if(cf)cf.hidden=true;
391
+ var btn=m.querySelector('[data-rk-menu-btn]'); if(btn)btn.setAttribute('aria-expanded','false');
392
+ });
393
+ }
394
+ function flashKebab(menu,text){
395
+ var btn=menu&&menu.querySelector('[data-rk-menu-btn]'); if(!btn)return;
396
+ btn.classList.add('is-ok'); var prev=btn.getAttribute('title')||'';
397
+ btn.setAttribute('title',text||'Done');
398
+ setTimeout(function(){ btn.classList.remove('is-ok'); btn.setAttribute('title',prev); },1100);
399
+ }
400
+ document.addEventListener('click',function(e){
401
+ var t=e.target;
402
+ var btn=t&&t.closest?t.closest('[data-rk-menu-btn]'):null;
403
+ if(btn){
404
+ e.preventDefault(); e.stopPropagation();
405
+ var menu=btn.closest('[data-rk-menu]');
406
+ var open=menu.classList.contains('is-open');
407
+ closeAllMenus(open?null:menu);
408
+ if(open){ menu.classList.remove('is-open'); btn.setAttribute('aria-expanded','false'); return; }
409
+ menu.classList.add('is-open'); btn.setAttribute('aria-expanded','true');
410
+ var pop=menu.querySelector('[data-rk-menu-pop]'); if(pop)pop.hidden=false;
411
+ var cf=menu.querySelector('[data-rk-confirm]'); if(cf)cf.hidden=true;
412
+ return;
413
+ }
414
+ // Copy ID
415
+ var copy=t&&t.closest?t.closest('[data-rk-copy-id]'):null;
416
+ if(copy){
417
+ e.preventDefault();
418
+ var id=copy.getAttribute('data-rk-copy-id')||'';
419
+ var menuC=copy.closest('[data-rk-menu]');
420
+ var done=function(){ flashKebab(menuC,'Copied ID'); closeAllMenus(null); };
421
+ if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(id).then(done,done); }
422
+ else { try{ var ta=document.createElement('textarea'); ta.value=id; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); }catch(err){} done(); }
423
+ return;
424
+ }
425
+ // Delete → reveal popconfirm
426
+ var del=t&&t.closest?t.closest('[data-rk-del]'):null;
427
+ if(del){
428
+ e.preventDefault();
429
+ var m2=del.closest('[data-rk-menu]');
430
+ var cf2=m2.querySelector('[data-rk-confirm]'); if(cf2)cf2.hidden=false;
431
+ return;
432
+ }
433
+ // Cancel popconfirm
434
+ var no=t&&t.closest?t.closest('[data-rk-confirm-no]'):null;
435
+ if(no){ e.preventDefault(); var mN=no.closest('[data-rk-menu]'); closeAllMenus(null); void mN; return; }
436
+ // Confirm delete → real DELETE
437
+ var yes=t&&t.closest?t.closest('[data-rk-confirm-yes]'):null;
438
+ if(yes){
439
+ e.preventDefault();
440
+ var m3=yes.closest('[data-rk-menu]');
441
+ var card=m3.closest('.rk-library-card');
442
+ var delBtn=m3.querySelector('[data-rk-del]');
443
+ var url=delBtn&&delBtn.getAttribute('data-rk-del')||'';
444
+ if(!url){ window.alert('Cannot delete this post.'); return; }
445
+ yes.disabled=true; yes.classList.add('is-loading');
446
+ fetch(url,{method:'DELETE',credentials:'same-origin',headers:{Accept:'application/json'}})
447
+ .then(function(r){ if(!r.ok)throw new Error('delete failed'); return r.json().catch(function(){return {};}); })
448
+ .then(function(){
449
+ if(card){ card.setAttribute('data-removed','1'); card.hidden=true; card.parentNode&&card.parentNode.removeChild(card); }
450
+ total=Math.max(0,total-1); syncPlaceholder(); apply();
451
+ })
452
+ .catch(function(){ yes.disabled=false; yes.classList.remove('is-loading'); window.alert('Could not delete that post. Please try again.'); });
453
+ return;
454
+ }
455
+ // click elsewhere closes any open menu
456
+ if(!(t&&t.closest&&t.closest('[data-rk-menu]')))closeAllMenus(null);
457
+ });
458
+ document.addEventListener('keydown',function(e){ if(e.key==='Escape')closeAllMenus(null); });
459
+
460
+ // ── TikTok-style viewer modal (mirrors the /discover reskin viewer) ──────
461
+ var DATA=[]; try{ var el=root.querySelector('[data-rk-library-data]'); if(el) DATA=JSON.parse(el.textContent||'[]'); }catch(e){ DATA=[]; }
462
+ function esc(t){ var d=document.createElement('div'); d.textContent=(t==null?'':String(t)); return d.innerHTML; }
463
+ function attr(t){ return esc(t).replace(/"/g,'&quot;'); }
464
+ var viewer=root.querySelector('[data-rk-viewer]');
465
+ var vTrack=viewer&&viewer.querySelector('[data-viewer-track]');
466
+ if(viewer&&vTrack){
467
+ var vItems=[]; var vOpen=false; var vActive=-1; var vSound=true; var vIO=null;
468
+ function slideHtml(t,i){
469
+ var media=t.is_video
470
+ ? '<video class="rk-library-viewer-media" data-vsrc="'+attr(t.url)+'"'+(t.poster?' poster="'+attr(t.poster)+'"':'')+' loop playsinline preload="none"></video>'
471
+ : '<img class="rk-library-viewer-media" src="'+attr(t.url||t.poster)+'" alt="'+attr(t.title||'Published cut')+'">';
472
+ var cta=t.editor_href
473
+ ? '<a class="rk-btn rk-btn-gold rk-btn-sm rk-library-viewer-cta" href="'+attr(t.editor_href)+'">Open in editor</a>'
474
+ : '';
475
+ var view=t.post_href
476
+ ? '<a class="rk-btn rk-btn-ghost-light rk-btn-sm rk-library-viewer-cta" href="'+attr(t.post_href)+'" target="_blank" rel="noreferrer">View post</a>'
477
+ : '';
478
+ return '<section class="rk-library-viewer-slide" data-vslide="'+i+'">'
479
+ +'<figure class="rk-library-viewer-stage">'
480
+ + media
481
+ +'<span class="rk-library-viewer-pause">'+${JSON.stringify(PLAY)}+'</span>'
482
+ +'<div class="rk-library-viewer-shade"></div>'
483
+ +'<div class="rk-library-viewer-info">'
484
+ +'<div class="rk-library-viewer-title">'+esc(t.title)+'</div>'
485
+ +(t.sub?'<div class="rk-library-viewer-desc">'+esc(t.sub)+'</div>':'')
486
+ +'<div class="rk-library-viewer-actions">'+cta+view+'</div>'
487
+ +'</div>'
488
+ +'</figure>'
489
+ +'</section>';
490
+ }
491
+ function hydrate(slide){ var v=slide.querySelector('video[data-vsrc]'); if(!v||v.getAttribute('src'))return; v.preload='auto'; v.setAttribute('src',v.getAttribute('data-vsrc')); }
492
+ function updateMuteUi(){ viewer.classList.toggle('is-muted',!vSound); }
493
+ function setActive(idx){
494
+ vActive=idx;
495
+ [].forEach.call(vTrack.children,function(s,i){
496
+ if(Math.abs(i-idx)<=1) hydrate(s);
497
+ s.classList.remove('is-paused');
498
+ var v=s.querySelector('video'); if(!v)return;
499
+ if(i===idx){ v.muted=!vSound; var pr=v.play(); if(pr&&pr.catch) pr.catch(function(){ v.muted=true; vSound=false; updateMuteUi(); var p2=v.play(); if(p2&&p2.catch)p2.catch(function(){}); }); }
500
+ else { v.pause(); }
501
+ });
502
+ updateMuteUi();
503
+ }
504
+ function step(d){ if(!vOpen)return; var n=Math.max(0,Math.min(vItems.length-1,vActive+d)); if(n===vActive)return; vTrack.scrollTo({top:n*vTrack.clientHeight,behavior:'smooth'}); }
505
+ function open(clickedIdx){
506
+ vItems=[]; var start=0;
507
+ DATA.forEach(function(t,i){ if(!t.url&&!t.poster)return; if(i===clickedIdx)start=vItems.length; vItems.push(t); });
508
+ if(!vItems.length)return;
509
+ vTrack.innerHTML=vItems.map(slideHtml).join('');
510
+ vOpen=true; vSound=true; viewer.hidden=false;
511
+ document.documentElement.classList.add('rk-noscroll');
512
+ if(vIO)vIO.disconnect();
513
+ vIO=('IntersectionObserver' in window)?new IntersectionObserver(function(entries){ entries.forEach(function(en){ if(!en.isIntersecting)return; var i=Number(en.target.getAttribute('data-vslide')); if(i!==vActive)setActive(i); }); },{root:vTrack,threshold:0.6}):null;
514
+ if(vIO)[].forEach.call(vTrack.children,function(s){ vIO.observe(s); });
515
+ vTrack.scrollTop=start*vTrack.clientHeight;
516
+ setActive(start);
517
+ }
518
+ function close(){
519
+ if(!vOpen)return; vOpen=false; vActive=-1;
520
+ if(vIO){ vIO.disconnect(); vIO=null; }
521
+ vTrack.querySelectorAll('video').forEach(function(v){ v.pause(); });
522
+ vTrack.innerHTML=''; viewer.hidden=true;
523
+ document.documentElement.classList.remove('rk-noscroll');
524
+ }
525
+ // open from a card poster
526
+ document.addEventListener('click',function(e){
527
+ var b=(e.target&&e.target.closest)?e.target.closest('[data-rk-view]'):null;
528
+ if(!b)return; e.preventDefault(); open(Number(b.getAttribute('data-rk-view')));
529
+ });
530
+ viewer.querySelectorAll('[data-viewer-close]').forEach(function(b){ b.addEventListener('click',close); });
531
+ var muteBtn=viewer.querySelector('[data-viewer-mute]');
532
+ if(muteBtn)muteBtn.addEventListener('click',function(){ vSound=!vSound; updateMuteUi(); var s=vTrack.children[vActive]; var v=s&&s.querySelector('video'); if(v){ v.muted=!vSound; if(vSound&&v.paused){ var pr=v.play(); if(pr&&pr.catch)pr.catch(function(){}); s.classList.remove('is-paused'); } } });
533
+ var prevBtn=viewer.querySelector('[data-viewer-prev]'); if(prevBtn)prevBtn.addEventListener('click',function(){ step(-1); });
534
+ var nextBtn=viewer.querySelector('[data-viewer-next]'); if(nextBtn)nextBtn.addEventListener('click',function(){ step(1); });
535
+ vTrack.addEventListener('click',function(e){
536
+ var t=e.target;
537
+ if(t&&t.closest&&t.closest('a'))return;
538
+ var media=(t&&t.closest)?t.closest('.rk-library-viewer-media'):null;
539
+ if(media&&media.tagName==='VIDEO'){ var slide=media.closest('.rk-library-viewer-slide'); if(media.paused){ var pr=media.play(); if(pr&&pr.catch)pr.catch(function(){}); if(slide)slide.classList.remove('is-paused'); } else { media.pause(); if(slide)slide.classList.add('is-paused'); } return; }
540
+ if(t&&t.classList&&(t.classList.contains('rk-library-viewer-slide')||t===vTrack))close();
541
+ });
542
+ document.addEventListener('keydown',function(e){ if(!vOpen)return; if(e.key==='Escape'){ e.preventDefault(); close(); } else if(e.key==='ArrowDown'||e.key==='PageDown'){ e.preventDefault(); step(1); } else if(e.key==='ArrowUp'||e.key==='PageUp'){ e.preventDefault(); step(-1); } });
543
+ window.addEventListener('resize',function(){ if(vOpen&&vActive>=0)vTrack.scrollTop=vActive*vTrack.clientHeight; });
544
+ }
319
545
  })();`;
320
546
  const CLIPS_SCRIPT = `
321
547
  (function(){
322
548
  var root=document;
323
549
  var grid=root.getElementById('rkClipsGrid');
324
550
  var statusEl=root.getElementById('rkClipsStatus');
325
- var summaryEl=root.getElementById('rkClipsSummary');
551
+ var crumbsEl=root.getElementById('rkRawsCrumbs');
326
552
  var empty=root.getElementById('rkClipsEmpty');
327
- var chips=root.getElementById('rkClipsChips');
328
553
  var form=root.getElementById('rkClipsSearch');
329
554
  var query=root.getElementById('rkClipsQuery');
555
+ var hoverEl=root.getElementById('rkRawsHover');
556
+ var hoverVid=hoverEl&&hoverEl.querySelector('.rk-raws-hover-vid');
557
+ var hoverCap=root.getElementById('rkRawsHoverCap');
558
+ var canHover=!(window.matchMedia&&window.matchMedia('(hover:none)').matches);
559
+
560
+ // ── file-directory state ────────────────────────────────────────────────
561
+ var ALL=[]; // every raw in the library
562
+ var FOLDERS=[]; // [{id,name,clips:[...]}] grouped by source video
563
+ var VIEW=[]; // clips currently on screen (drives viewer + hover, by index)
564
+ var MODE='root'; // 'root' (folders) | 'folder' | 'search'
565
+ var CUR_FOLDER=''; // active folder id when MODE==='folder'
566
+ var LAST_Q=''; // active query when MODE==='search'
567
+ var PENDING=[]; // optimistic import cards: {id,url,tracer,clip,label,error}
330
568
 
331
569
  function esc(t){ var d=document.createElement('div'); d.textContent=(t==null?'':String(t)); return d.innerHTML; }
570
+ function attr(t){ return esc(t).replace(/"/g,'&quot;'); }
571
+ function cssUrl(u){ return "'"+String(u||'').replace(/['")\\\\]/g,'').replace(/\\s/g,'%20')+"'"; }
332
572
  function fmtClock(sec){ sec=Number(sec)||0; var m=Math.floor(sec/60), s=Math.floor(sec%60); return m+':'+(s<10?'0':'')+s; }
333
573
  function topTags(tags){
334
574
  if(!tags||typeof tags!=='object') return [];
335
575
  var out=[];
336
- ['subject','action','emotion','composition'].forEach(function(k){ (tags[k]||[]).slice(0,1).forEach(function(v){ out.push(v); }); });
576
+ // content_type leads — it's the shot-kind filter the chips key off.
577
+ (tags.content_type||[]).slice(0,2).forEach(function(v){ out.push(v.replace(/_/g,' ')); });
578
+ ['subject','action','emotion'].forEach(function(k){ (tags[k]||[]).slice(0,1).forEach(function(v){ out.push(v); }); });
337
579
  if(tags.energy) out.push('energy:'+tags.energy);
338
- return out.slice(0,4);
580
+ return out.slice(0,5);
339
581
  }
340
582
  function setGridEmpty(show){ if(empty)empty.hidden=!show; if(grid)grid.style.display=show?'none':''; }
583
+ function syncPlaceholder(){
584
+ if(!query)return;
585
+ var n=ALL.length;
586
+ query.setAttribute('placeholder','Search '+n+' raw clip'+(n===1?'':'s')+String.fromCharCode(8230));
587
+ }
588
+
589
+ // Group raws into folders by their source video (source_video_id, falling back
590
+ // to source_filename). First-seen order is preserved.
591
+ function buildFolders(){
592
+ var map={}; FOLDERS=[];
593
+ ALL.forEach(function(c){
594
+ var id=String(c.source_video_id||c.source_filename||'ungrouped');
595
+ var f=map[id];
596
+ if(!f){ f={id:id,name:c.source_filename||'Untitled source',clips:[]}; map[id]=f; FOLDERS.push(f); }
597
+ f.clips.push(c);
598
+ });
599
+ }
600
+
601
+ // Optimistic "importing…" placeholder card (spinner, or an error state with a
602
+ // Dismiss button). Kept in PENDING and re-prepended on every root repaint so a
603
+ // feed reload never wipes a still-running import.
604
+ function importCardHtml(p){
605
+ var label=p.error?'Import failed':(p.label||'Importing…');
606
+ var sub=p.error?p.error:(p.url||'');
607
+ var badge=p.error
608
+ ? '<button type="button" class="rk-clips-dl" data-import-dismiss="'+attr(p.id)+'">Dismiss</button>'
609
+ : '<span class="rk-clips-dl rk-clips-importing-tag">'+(p.clip?'Mining':'Importing')+'</span>';
610
+ return '<article class="rk-clips-card rk-clips-importing'+(p.error?' is-error':'')+'" data-import-id="'+attr(p.id)+'">'
611
+ + '<div class="rk-clips-thumb">'
612
+ + (p.error?'<span class="rk-clips-importing-x">'+${JSON.stringify(CROSS_ICON)}+'</span>':'<span class="rk-clips-importing-spin" aria-hidden="true"></span>')
613
+ + '</div>'
614
+ + '<div class="rk-clips-body">'
615
+ + '<h3 class="rk-clips-title">'+esc(label)+'</h3>'
616
+ + '<div class="rk-clips-foot"><span class="rk-clips-src" title="'+attr(sub)+'">'+esc(sub)+'</span>'+badge+'</div>'
617
+ + '</div></article>';
618
+ }
619
+
620
+ // Folder tile — a 2×2 thumbnail mosaic + name + clip count.
621
+ function folderCardHtml(folder){
622
+ var thumbs=folder.clips.filter(function(c){ return c.thumbnail_url; }).slice(0,4);
623
+ var mosaic;
624
+ if(thumbs.length){
625
+ mosaic='<span class="rk-raws-folder-mosaic'+(thumbs.length===1?' is-single':'')+'">'
626
+ + thumbs.map(function(c){ return '<span class="rk-raws-folder-cell" style="background-image:url('+cssUrl(c.thumbnail_url)+')"></span>'; }).join('')
627
+ + '</span>';
628
+ } else {
629
+ mosaic='<span class="rk-raws-folder-mosaic is-empty">'+${JSON.stringify(ICON_FOLDER)}+'</span>';
630
+ }
631
+ var n=folder.clips.length;
632
+ return '<button type="button" class="rk-raws-folder rk-card-hover" data-folder-id="'+attr(folder.id)+'">'
633
+ + mosaic
634
+ + '<span class="rk-raws-folder-meta">'
635
+ + '<span class="rk-raws-folder-ic">'+${JSON.stringify(ICON_FOLDER)}+'</span>'
636
+ + '<span class="rk-raws-folder-txt"><span class="rk-raws-folder-name" title="'+attr(folder.name)+'">'+esc(folder.name)+'</span>'
637
+ + '<span class="rk-raws-folder-count">'+n+' clip'+(n===1?'':'s')+'</span></span>'
638
+ + '</span></button>';
639
+ }
341
640
 
342
- function card(clip){
641
+ function card(clip,i){
343
642
  var range=fmtClock(clip.start_time_sec)+'–'+fmtClock(clip.end_time_sec);
344
643
  var dur=(clip.duration_sec!=null)?(Number(clip.duration_sec).toFixed(1)+'s'):'';
345
644
  var media=clip.thumbnail_url
346
645
  ? '<img class="rk-clips-thumb-img" loading="lazy" src="'+esc(clip.thumbnail_url)+'" alt="" />'
347
646
  : '<span class="rk-clips-noimg">No preview</span>';
348
- var vid=clip.view_url ? '<video class="rk-clips-thumb-vid" muted loop playsinline preload="none" src="'+esc(clip.view_url)+'"></video>' : '';
349
647
  var tags=topTags(clip.tags).map(function(t){ return '<span class="rk-clips-tag">'+esc(t)+'</span>'; }).join('');
350
648
  var desc=clip.description || (clip.tags&&clip.tags.transcript) || '(untagged clip)';
351
649
  var dl='/raws/'+encodeURIComponent(clip.clip_id)+'/download';
352
650
  return '<article class="rk-clips-card rk-card-hover">'
353
- + '<div class="rk-clips-thumb">'+media+vid
651
+ + '<button type="button" class="rk-clips-thumb" data-rk-clip-view="'+i+'" aria-label="Open raw">'+media
354
652
  + '<span class="rk-clips-dur">'+esc(range+(dur?' · '+dur:''))+'</span>'
355
- + '<button type="button" class="rk-clips-play" aria-label="Preview clip">'+String.fromCharCode(9654)+'</button>'
356
- + '</div>'
653
+ + '<span class="rk-clips-play" aria-hidden="true">'+${JSON.stringify(PLAY)}+'</span>'
654
+ + '</button>'
357
655
  + '<div class="rk-clips-body">'
358
656
  + '<h3 class="rk-clips-title">'+esc(desc)+'</h3>'
359
657
  + (tags?'<div class="rk-clips-tags">'+tags+'</div>':'')
360
- + '<div class="rk-clips-foot"><span class="rk-clips-src" title="'+esc(clip.source_filename)+'">'+esc(clip.source_filename||'')+'</span>'
361
- + '<a class="rk-clips-dl" href="'+dl+'">Download</a></div>'
658
+ + '<div class="rk-clips-foot"><span class="rk-clips-src" title="'+attr(clip.source_filename)+'">'+esc(clip.source_filename||'')+'</span>'
659
+ + '<a class="rk-clips-dl" href="'+dl+'" target="_blank" rel="noopener">Download</a></div>'
362
660
  + '</div></article>';
363
661
  }
364
662
 
663
+ /* ── mouseover preview popover ────────────────────────────────────────────── */
664
+ var hoverTimer=null;
665
+ function hideHover(){
666
+ if(hoverTimer){ clearTimeout(hoverTimer); hoverTimer=null; }
667
+ if(!hoverEl)return;
668
+ hoverEl.hidden=true; hoverEl.classList.remove('is-visible');
669
+ if(hoverVid){ try{ hoverVid.pause(); }catch(e){} hoverVid.removeAttribute('src'); hoverVid.load&&hoverVid.load(); }
670
+ }
671
+ function positionHover(rect){
672
+ if(!hoverEl)return;
673
+ var gap=12, bw=hoverEl.offsetWidth||250, bh=hoverEl.offsetHeight||330;
674
+ var x=rect.right+gap;
675
+ if(x+bw>window.innerWidth-8) x=rect.left-gap-bw;
676
+ if(x<8) x=Math.max(8,Math.min(rect.left,window.innerWidth-bw-8));
677
+ var y=rect.top+rect.height/2-bh/2;
678
+ y=Math.max(8,Math.min(y,window.innerHeight-bh-8));
679
+ hoverEl.style.left=Math.round(x)+'px';
680
+ hoverEl.style.top=Math.round(y)+'px';
681
+ }
682
+ function showHover(clip,rect){
683
+ if(!hoverEl||!clip||(!clip.view_url&&!clip.thumbnail_url))return;
684
+ if(hoverCap){ hoverCap.textContent=clip.description||(clip.tags&&clip.tags.transcript)||clip.source_filename||'Raw clip'; }
685
+ if(hoverVid){
686
+ hoverVid.poster=clip.thumbnail_url||'';
687
+ if(clip.view_url){ hoverVid.setAttribute('src',clip.view_url); hoverVid.load&&hoverVid.load(); var p=hoverVid.play&&hoverVid.play(); if(p&&p.catch)p.catch(function(){}); }
688
+ else { hoverVid.removeAttribute('src'); }
689
+ }
690
+ hoverEl.hidden=false; hoverEl.classList.add('is-visible');
691
+ positionHover(rect);
692
+ }
365
693
  function bindHover(){
366
- if(!grid)return;
367
- Array.prototype.slice.call(grid.querySelectorAll('.rk-clips-thumb')).forEach(function(thumb){
368
- var v=thumb.querySelector('.rk-clips-thumb-vid');
369
- if(!v)return;
370
- thumb.addEventListener('mouseenter',function(){ v.style.display='block'; var p=v.play&&v.play(); if(p&&p.catch)p.catch(function(){}); });
371
- thumb.addEventListener('mouseleave',function(){ if(v.pause)v.pause(); v.style.display='none'; });
694
+ if(!grid||!canHover)return;
695
+ Array.prototype.slice.call(grid.querySelectorAll('.rk-clips-thumb[data-rk-clip-view]')).forEach(function(thumb){
696
+ var idx=Number(thumb.getAttribute('data-rk-clip-view'));
697
+ thumb.addEventListener('mouseenter',function(){
698
+ var clip=VIEW[idx]; if(!clip)return;
699
+ var rect=thumb.getBoundingClientRect();
700
+ if(hoverTimer)clearTimeout(hoverTimer);
701
+ hoverTimer=setTimeout(function(){ showHover(clip,rect); },90);
702
+ });
703
+ thumb.addEventListener('mouseleave',hideHover);
372
704
  });
373
705
  }
706
+ if(hoverEl){
707
+ window.addEventListener('scroll',hideHover,true);
708
+ window.addEventListener('resize',hideHover);
709
+ }
374
710
 
375
- function render(clips,label){
376
- clips=clips||[];
377
- if(clips.length===0){
378
- if(statusEl)statusEl.textContent=label||'';
379
- if(summaryEl)summaryEl.innerHTML='<b>0</b> clips in your library';
380
- if(grid)grid.innerHTML='';
381
- setGridEmpty(true);
382
- return;
711
+ /* ── breadcrumb + view rendering ──────────────────────────────────────────── */
712
+ function renderCrumbs(){
713
+ if(!crumbsEl)return;
714
+ var html='';
715
+ if(MODE==='root'){
716
+ html='<span class="rk-raws-crumb is-current">All raws</span>';
717
+ } else {
718
+ html='<button type="button" class="rk-raws-crumb rk-raws-crumb-link" data-crumb-root>'+${JSON.stringify(ICON_CHEVLEFT)}+'<span>All raws</span></button>';
719
+ html+='<span class="rk-raws-crumb-sep" aria-hidden="true">/</span>';
720
+ if(MODE==='folder'){
721
+ var f=folderById(CUR_FOLDER);
722
+ html+='<span class="rk-raws-crumb is-current">'+esc(f?f.name:'Folder')+' <span class="rk-raws-crumb-n">'+(f?f.clips.length:0)+'</span></span>';
723
+ } else {
724
+ html+='<span class="rk-raws-crumb is-current">Search '+String.fromCharCode(8220)+esc(LAST_Q)+String.fromCharCode(8221)+' <span class="rk-raws-crumb-n">'+VIEW.length+'</span></span>';
725
+ }
383
726
  }
727
+ crumbsEl.innerHTML=html;
728
+ }
729
+ function folderById(id){ for(var i=0;i<FOLDERS.length;i++){ if(FOLDERS[i].id===id)return FOLDERS[i]; } return null; }
730
+
731
+ function paintRoot(){
732
+ MODE='root'; CUR_FOLDER=''; VIEW=[];
733
+ if(!grid)return;
734
+ buildFolders();
735
+ grid.classList.add('is-folders');
736
+ if(FOLDERS.length===0 && PENDING.length===0){ grid.innerHTML=''; setGridEmpty(true); renderCrumbs(); if(statusEl)statusEl.textContent=''; return; }
737
+ grid.innerHTML=PENDING.map(importCardHtml).join('')+FOLDERS.map(folderCardHtml).join('');
384
738
  setGridEmpty(false);
385
- if(statusEl)statusEl.textContent=(label||'')+clips.length+' clip'+(clips.length===1?'':'s');
386
- if(summaryEl)summaryEl.innerHTML='<b>'+clips.length+'</b> clip'+(clips.length===1?'':'s')+' shown';
387
- if(grid)grid.innerHTML=clips.map(card).join('');
388
- bindHover();
739
+ renderCrumbs();
740
+ if(statusEl)statusEl.textContent=FOLDERS.length+' folder'+(FOLDERS.length===1?'':'s');
741
+ }
742
+ function paintFolder(id){
743
+ var f=folderById(id); if(!f){ paintRoot(); return; }
744
+ MODE='folder'; CUR_FOLDER=id; VIEW=f.clips;
745
+ if(!grid)return;
746
+ grid.classList.remove('is-folders');
747
+ grid.innerHTML=VIEW.map(card).join('');
748
+ setGridEmpty(VIEW.length===0);
749
+ renderCrumbs(); bindHover();
750
+ if(statusEl)statusEl.textContent='';
751
+ }
752
+ function paintSearch(results,q){
753
+ MODE='search'; LAST_Q=q; VIEW=results||[];
754
+ if(!grid)return;
755
+ grid.classList.remove('is-folders');
756
+ if(VIEW.length===0){ grid.innerHTML=''; setGridEmpty(true); renderCrumbs(); if(statusEl)statusEl.textContent='No raws match '+String.fromCharCode(8220)+q+String.fromCharCode(8221)+'.'; return; }
757
+ grid.innerHTML=VIEW.map(card).join('');
758
+ setGridEmpty(false);
759
+ renderCrumbs(); bindHover();
760
+ if(statusEl)statusEl.textContent='';
761
+ }
762
+ // Repaint whatever view is active (used after an optimistic import lands).
763
+ function repaint(){
764
+ if(MODE==='folder')paintFolder(CUR_FOLDER);
765
+ else if(MODE==='search')paintSearch(VIEW,LAST_Q);
766
+ else paintRoot();
767
+ }
768
+
769
+ /* ── optimistic import lifecycle ─────────────────────────────────────────── */
770
+ function getPending(id){ for(var i=0;i<PENDING.length;i++){ if(PENDING[i].id===id)return PENDING[i]; } return null; }
771
+ function removeImport(id){ PENDING=PENDING.filter(function(p){ return p.id!==id; }); }
772
+ function updateImportCard(id){
773
+ var p=getPending(id); if(!p)return;
774
+ var el=grid&&grid.querySelector('[data-import-id="'+id+'"]');
775
+ if(el){ el.outerHTML=importCardHtml(p); } else { repaint(); }
776
+ }
777
+ function finishImport(id){ removeImport(id); loadFeed(); }
778
+ function failImport(id,msg){ var p=getPending(id); if(p){ p.error=msg; } updateImportCard(id); if(statusEl)statusEl.textContent=msg; }
779
+ function onScanResult(id,clip,res){
780
+ if(!res.ok){ failImport(id,(res.data&&res.data.error)||(clip?'Could not start the scan.':'Could not import that video.')); return; }
781
+ var d=res.data||{};
782
+ if(d.scan_id && d.status!=='complete'){
783
+ var p=getPending(id); if(p){ p.label=clip?'Mining clips…':'Importing…'; updateImportCard(id); }
784
+ pollImport(id,d.scan_id);
785
+ } else {
786
+ finishImport(id);
787
+ }
788
+ }
789
+ function pollImport(id,scanId){
790
+ var tries=0;
791
+ var t=setInterval(function(){
792
+ tries++;
793
+ if(tries>200){ clearInterval(t); var p=getPending(id); if(p){ p.error='Still working in the background — refresh shortly.'; updateImportCard(id); } return; }
794
+ fetch('/raws/scan/'+encodeURIComponent(scanId),{credentials:'same-origin',headers:{Accept:'application/json'}})
795
+ .then(function(r){ return r.ok?r.json():null; })
796
+ .then(function(d){
797
+ if(!d)return;
798
+ var src=d.source||{}; var status=src.status||(d.scan&&d.scan.status)||'running';
799
+ if(status==='complete'){ clearInterval(t); finishImport(id); }
800
+ else if(status==='failed'||status==='error'){ clearInterval(t); failImport(id,src.error||(d.scan&&d.scan.error)||'The source could not be processed.'); }
801
+ }).catch(function(){});
802
+ },4000);
803
+ }
804
+ if(grid){
805
+ grid.addEventListener('click',function(e){
806
+ var d=e.target&&e.target.closest?e.target.closest('[data-import-dismiss]'):null;
807
+ if(!d)return; e.preventDefault(); removeImport(d.getAttribute('data-import-dismiss')); repaint();
808
+ });
809
+ }
810
+
811
+ /* ── fullscreen scroll-snap viewer (click a raw → vertical deck) ──────────── */
812
+ var viewer=root.querySelector('[data-rk-viewer]');
813
+ var vTrack=viewer&&viewer.querySelector('[data-viewer-track]');
814
+ if(viewer&&vTrack){
815
+ var vItems=[]; var vOpen=false; var vActive=-1; var vSound=true; var vIO=null;
816
+ function slideHtml(t,i){
817
+ var media=t.view_url
818
+ ? '<video class="rk-library-viewer-media" data-vsrc="'+attr(t.view_url)+'"'+(t.thumbnail_url?' poster="'+attr(t.thumbnail_url)+'"':'')+' loop playsinline preload="none"></video>'
819
+ : '<img class="rk-library-viewer-media" src="'+attr(t.thumbnail_url||'')+'" alt="'+attr(t.description||'Raw clip')+'">';
820
+ var desc=t.description || (t.tags&&t.tags.transcript) || t.source_filename || 'Raw clip';
821
+ var range=fmtClock(t.start_time_sec)+'–'+fmtClock(t.end_time_sec);
822
+ var meta=(t.source_filename?esc(t.source_filename)+' · ':'')+esc(range);
823
+ var dl='/raws/'+encodeURIComponent(t.clip_id)+'/download';
824
+ return '<section class="rk-library-viewer-slide" data-vslide="'+i+'">'
825
+ +'<figure class="rk-library-viewer-stage">'
826
+ + media
827
+ +'<span class="rk-library-viewer-pause">'+${JSON.stringify(PLAY)}+'</span>'
828
+ +'<div class="rk-library-viewer-shade"></div>'
829
+ +'<div class="rk-library-viewer-info">'
830
+ +'<div class="rk-library-viewer-title">'+esc(desc)+'</div>'
831
+ +'<div class="rk-library-viewer-desc">'+meta+'</div>'
832
+ +'<div class="rk-library-viewer-actions"><a class="rk-btn rk-btn-gold rk-btn-sm rk-library-viewer-cta" href="'+dl+'" target="_blank" rel="noopener">Download</a></div>'
833
+ +'</div>'
834
+ +'</figure>'
835
+ +'</section>';
836
+ }
837
+ function hydrate(slide){ var v=slide.querySelector('video[data-vsrc]'); if(!v||v.getAttribute('src'))return; v.preload='auto'; v.setAttribute('src',v.getAttribute('data-vsrc')); }
838
+ function updateMuteUi(){ viewer.classList.toggle('is-muted',!vSound); }
839
+ function setActive(idx){
840
+ vActive=idx;
841
+ [].forEach.call(vTrack.children,function(s,i){
842
+ if(Math.abs(i-idx)<=1) hydrate(s);
843
+ s.classList.remove('is-paused');
844
+ var v=s.querySelector('video'); if(!v)return;
845
+ if(i===idx){ v.muted=!vSound; var pr=v.play(); if(pr&&pr.catch) pr.catch(function(){ v.muted=true; vSound=false; updateMuteUi(); var p2=v.play(); if(p2&&p2.catch)p2.catch(function(){}); }); }
846
+ else { v.pause(); }
847
+ });
848
+ updateMuteUi();
849
+ }
850
+ function step(d){ if(!vOpen)return; var n=Math.max(0,Math.min(vItems.length-1,vActive+d)); if(n===vActive)return; vTrack.scrollTo({top:n*vTrack.clientHeight,behavior:'smooth'}); }
851
+ function open(clickedIdx){
852
+ hideHover();
853
+ vItems=[]; var start=0;
854
+ VIEW.forEach(function(t,i){ if(!t.view_url&&!t.thumbnail_url)return; if(i===clickedIdx)start=vItems.length; vItems.push(t); });
855
+ if(!vItems.length)return;
856
+ vTrack.innerHTML=vItems.map(slideHtml).join('');
857
+ vOpen=true; vSound=true; viewer.hidden=false;
858
+ document.documentElement.classList.add('rk-noscroll');
859
+ if(vIO)vIO.disconnect();
860
+ vIO=('IntersectionObserver' in window)?new IntersectionObserver(function(entries){ entries.forEach(function(en){ if(!en.isIntersecting)return; var i=Number(en.target.getAttribute('data-vslide')); if(i!==vActive)setActive(i); }); },{root:vTrack,threshold:0.6}):null;
861
+ if(vIO)[].forEach.call(vTrack.children,function(s){ vIO.observe(s); });
862
+ vTrack.scrollTop=start*vTrack.clientHeight;
863
+ setActive(start);
864
+ }
865
+ function close(){
866
+ if(!vOpen)return; vOpen=false; vActive=-1;
867
+ if(vIO){ vIO.disconnect(); vIO=null; }
868
+ vTrack.querySelectorAll('video').forEach(function(v){ v.pause(); });
869
+ vTrack.innerHTML=''; viewer.hidden=true;
870
+ document.documentElement.classList.remove('rk-noscroll');
871
+ }
872
+ if(grid){
873
+ grid.addEventListener('click',function(e){
874
+ var b=e.target&&e.target.closest?e.target.closest('[data-rk-clip-view]'):null;
875
+ if(!b)return; e.preventDefault(); open(Number(b.getAttribute('data-rk-clip-view')));
876
+ });
877
+ }
878
+ viewer.querySelectorAll('[data-viewer-close]').forEach(function(b){ b.addEventListener('click',close); });
879
+ var muteBtn=viewer.querySelector('[data-viewer-mute]');
880
+ if(muteBtn)muteBtn.addEventListener('click',function(){ vSound=!vSound; updateMuteUi(); var s=vTrack.children[vActive]; var v=s&&s.querySelector('video'); if(v){ v.muted=!vSound; if(vSound&&v.paused){ var pr=v.play(); if(pr&&pr.catch)pr.catch(function(){}); s.classList.remove('is-paused'); } } });
881
+ var prevBtn=viewer.querySelector('[data-viewer-prev]'); if(prevBtn)prevBtn.addEventListener('click',function(){ step(-1); });
882
+ var nextBtn=viewer.querySelector('[data-viewer-next]'); if(nextBtn)nextBtn.addEventListener('click',function(){ step(1); });
883
+ vTrack.addEventListener('click',function(e){
884
+ var t=e.target;
885
+ if(t&&t.closest&&t.closest('a'))return;
886
+ var media=(t&&t.closest)?t.closest('.rk-library-viewer-media'):null;
887
+ if(media&&media.tagName==='VIDEO'){ var slide=media.closest('.rk-library-viewer-slide'); if(media.paused){ var pr=media.play(); if(pr&&pr.catch)pr.catch(function(){}); if(slide)slide.classList.remove('is-paused'); } else { media.pause(); if(slide)slide.classList.add('is-paused'); } return; }
888
+ if(t&&t.classList&&(t.classList.contains('rk-library-viewer-slide')||t===vTrack))close();
889
+ });
890
+ document.addEventListener('keydown',function(e){ if(!vOpen)return; if(e.key==='Escape'){ e.preventDefault(); close(); } else if(e.key==='ArrowDown'||e.key==='PageDown'){ e.preventDefault(); step(1); } else if(e.key==='ArrowUp'||e.key==='PageUp'){ e.preventDefault(); step(-1); } });
891
+ window.addEventListener('resize',function(){ if(vOpen&&vActive>=0)vTrack.scrollTop=vActive*vTrack.clientHeight; });
892
+ }
893
+
894
+ // Folder navigation (grid tile) + breadcrumb "All raws" back link.
895
+ if(grid){
896
+ grid.addEventListener('click',function(e){
897
+ var f=e.target&&e.target.closest?e.target.closest('[data-folder-id]'):null;
898
+ if(!f)return; e.preventDefault(); hideHover(); if(query)query.value=''; paintFolder(f.getAttribute('data-folder-id'));
899
+ });
900
+ }
901
+ if(crumbsEl){
902
+ crumbsEl.addEventListener('click',function(e){
903
+ var r=e.target&&e.target.closest?e.target.closest('[data-crumb-root]'):null;
904
+ if(!r)return; e.preventDefault(); hideHover(); if(query)query.value=''; paintRoot();
905
+ });
389
906
  }
390
907
 
391
908
  function handleAuth(res){
@@ -393,7 +910,6 @@ const CLIPS_SCRIPT = `
393
910
  if(grid)grid.innerHTML='';
394
911
  setGridEmpty(false);
395
912
  if(statusEl)statusEl.innerHTML='Sign in to vidfarm to see your raws, then build a library with the Import source button or <code>vidfarm raws scan</code>.';
396
- if(summaryEl)summaryEl.textContent='Sign in to view your raws library';
397
913
  throw new Error('unauthorized');
398
914
  }
399
915
  return res.json();
@@ -401,37 +917,31 @@ const CLIPS_SCRIPT = `
401
917
 
402
918
  function loadFeed(){
403
919
  if(statusEl)statusEl.textContent='Loading your raws…';
404
- fetch('/raws/feed?limit=120',{credentials:'same-origin',headers:{Accept:'application/json'}})
405
- .then(handleAuth).then(function(d){ render(d.clips,''); }).catch(function(){});
920
+ fetch('/raws/feed?limit=250',{credentials:'same-origin',headers:{Accept:'application/json'}})
921
+ .then(handleAuth).then(function(d){
922
+ ALL=d.clips||[]; buildFolders(); syncPlaceholder();
923
+ // stay inside the current folder if it survived the refresh, else root
924
+ if(MODE==='folder' && folderById(CUR_FOLDER)) paintFolder(CUR_FOLDER);
925
+ else paintRoot();
926
+ }).catch(function(){});
406
927
  }
407
928
 
408
- function search(q,label){
929
+ function search(q){
409
930
  if(statusEl)statusEl.textContent='Searching…';
410
931
  fetch('/raws/search',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify({query:q,limit:60})})
411
- .then(handleAuth).then(function(d){
412
- var lbl=(d.semantic?'semantic ':'')+'results for '+String.fromCharCode(8220)+(label||q)+String.fromCharCode(8221)+' — ';
413
- render(d.results,lbl);
414
- }).catch(function(){});
932
+ .then(handleAuth).then(function(d){ paintSearch(d.results||[],q); }).catch(function(){});
415
933
  }
416
934
 
417
935
  if(form){
418
936
  form.addEventListener('submit',function(e){
419
937
  e.preventDefault();
420
938
  var q=(query&&query.value||'').trim();
421
- if(chips)chips.querySelectorAll('.rk-clips-chip').forEach(function(b){b.classList.remove('is-active');});
422
- if(q)search(q,null); else { var all=chips&&chips.querySelector('[data-filter="all"]'); if(all)all.classList.add('is-active'); loadFeed(); }
939
+ if(q)search(q); else paintRoot();
423
940
  });
424
941
  }
425
- if(chips){
426
- chips.addEventListener('click',function(e){
427
- var btn=e.target.closest('.rk-clips-chip');
428
- if(!btn)return;
429
- chips.querySelectorAll('.rk-clips-chip').forEach(function(b){b.classList.toggle('is-active',b===btn);});
430
- var slug=btn.getAttribute('data-filter');
431
- var q=btn.getAttribute('data-q')||'';
432
- if(query)query.value='';
433
- if(slug==='all'||!q)loadFeed(); else search(q,btn.textContent.trim());
434
- });
942
+ if(query){
943
+ // clearing the box returns to the folder root without a round-trip
944
+ query.addEventListener('input',function(){ if(!(query.value||'').trim() && MODE==='search'){ hideHover(); paintRoot(); } });
435
945
  }
436
946
 
437
947
  // ── Scan a source (real clip hunt) ─────────────────────────────────────
@@ -445,9 +955,49 @@ const CLIPS_SCRIPT = `
445
955
  var scanTracer=root.getElementById('rkScanTracer');
446
956
  var scanNote=root.getElementById('rkScanNote');
447
957
  var scanSubmit=root.getElementById('rkClipsScanSubmit');
448
- var scanPoll=null;
958
+ var scanClip=root.getElementById('rkScanClip');
959
+ var scanAdvanced=root.getElementById('rkClipsAdvanced');
960
+ var IMPORT_NOTE="We'll pull the whole video into your raws library.";
961
+ var CLIP_NOTE='AI tagging runs on your own key (BYOK) — vidfarm only bills the clip-scan compute.';
962
+ var SCAN_OPTS=null; // GET /raws/scan-options: which providers/keys can run a hunt
449
963
  function setNote(msg,tone){ if(!scanNote)return; scanNote.textContent=msg; if(tone)scanNote.setAttribute('data-tone',tone); else scanNote.removeAttribute('data-tone'); }
450
- if(dialog){ openBtns.forEach(function(openBtn){ openBtn.addEventListener('click',function(){ setNote('AI tagging runs on your own key (BYOK) — vidfarm only bills the clip-scan compute.'); if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal(); }); }); }
964
+ function providerLabel(p){ return p==='gemini'?'Gemini':p==='openai'?'OpenAI':p==='openrouter'?'OpenRouter':p; }
965
+ // Can a clip hunt actually run? A saved provider key, a local agent CLI, or a
966
+ // linked cloud account all qualify. Unknown (not yet loaded) → don't block.
967
+ function clipReady(){
968
+ if(!SCAN_OPTS)return true;
969
+ if(SCAN_OPTS.default_provider)return true;
970
+ if(SCAN_OPTS.pipeline==='upstream'||SCAN_OPTS.upstream_backup)return true;
971
+ return (SCAN_OPTS.providers||[]).some(function(p){ return p.has_key; });
972
+ }
973
+ function applyScanOpts(){
974
+ if(!SCAN_OPTS||!scanProvider)return;
975
+ Array.prototype.slice.call(scanProvider.options).forEach(function(opt){
976
+ if(!opt.value)return; // leave "Auto" alone
977
+ var pr=null; (SCAN_OPTS.providers||[]).forEach(function(p){ if(p.provider===opt.value)pr=p; });
978
+ if(!pr)return;
979
+ opt.disabled=!pr.has_key;
980
+ opt.textContent=providerLabel(opt.value)+(pr.has_key?' — key saved':' — no key saved');
981
+ });
982
+ if(scanClip&&scanClip.checked)syncClipMode();
983
+ }
984
+ function loadScanOpts(){
985
+ fetch('/raws/scan-options',{credentials:'same-origin',headers:{Accept:'application/json'}})
986
+ .then(function(r){ return r.ok?r.json():null; })
987
+ .then(function(d){ if(d){ SCAN_OPTS=d; applyScanOpts(); } }).catch(function(){});
988
+ }
989
+ function syncClipMode(){
990
+ var on=!!(scanClip&&scanClip.checked);
991
+ if(scanAdvanced)scanAdvanced.hidden=!on;
992
+ if(scanSubmit)scanSubmit.innerHTML=(on?'Start scan':'Import video')+' <span class="rk-arrow">→</span>';
993
+ if(on&&!clipReady()){
994
+ setNote('No AI provider available — add a Gemini, OpenAI, or OpenRouter key in Settings to clip raws (or uncheck to import the whole video).','error');
995
+ } else {
996
+ setNote(on?CLIP_NOTE:IMPORT_NOTE);
997
+ }
998
+ }
999
+ if(scanClip)scanClip.addEventListener('change',syncClipMode);
1000
+ if(dialog){ openBtns.forEach(function(openBtn){ openBtn.addEventListener('click',function(){ if(!SCAN_OPTS)loadScanOpts(); syncClipMode(); if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal(); }); }); }
451
1001
  if(cancelBtn&&dialog){ cancelBtn.addEventListener('click',function(){ if(dialog.open)dialog.close(); }); }
452
1002
  if(dialog){
453
1003
  dialog.addEventListener('click',function(e){
@@ -455,49 +1005,41 @@ const CLIPS_SCRIPT = `
455
1005
  if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)dialog.close();
456
1006
  });
457
1007
  }
458
- function pollScan(scanId){
459
- if(scanPoll)clearInterval(scanPoll);
460
- var tries=0;
461
- scanPoll=setInterval(function(){
462
- tries++;
463
- if(tries>200){ clearInterval(scanPoll); scanPoll=null; if(statusEl)statusEl.textContent='Still scanning in the background — check back shortly.'; return; }
464
- fetch('/raws/scan/'+encodeURIComponent(scanId),{credentials:'same-origin',headers:{Accept:'application/json'}})
465
- .then(function(r){ return r.ok?r.json():null; })
466
- .then(function(d){
467
- if(!d)return;
468
- var src=d.source||{};
469
- var status=src.status||(d.scan&&d.scan.status)||'running';
470
- if(status==='complete'){ clearInterval(scanPoll); scanPoll=null; if(statusEl)statusEl.textContent='Import complete — refreshing your raws…'; loadFeed(); }
471
- else if(status==='failed'||status==='error'){ clearInterval(scanPoll); scanPoll=null; if(statusEl)statusEl.textContent='Import failed: '+(src.error||(d.scan&&d.scan.error)||'the source could not be scanned.'); }
472
- else if(statusEl){ statusEl.textContent='Scanning source… ('+status+')'; }
473
- }).catch(function(){});
474
- },4000);
475
- }
476
1008
  if(scanForm){
477
1009
  scanForm.addEventListener('submit',function(e){
478
1010
  e.preventDefault();
479
1011
  var url=(scanUrl&&scanUrl.value||'').trim();
480
- var prompt=(scanPrompt&&scanPrompt.value||'').trim();
481
1012
  var tracer=(scanTracer&&scanTracer.value||'').trim();
482
- var provider=(scanProvider&&scanProvider.value||'').trim();
1013
+ var clip=!!(scanClip&&scanClip.checked);
1014
+ var prompt=clip?(scanPrompt&&scanPrompt.value||'').trim():'';
1015
+ var provider=clip?(scanProvider&&scanProvider.value||'').trim():'';
483
1016
  if(!url){ setNote('Enter a video URL.','error'); return; }
484
- setNote('Starting scan…');
485
- if(scanSubmit)scanSubmit.disabled=true;
486
- var payload={source_url:url,prompt:prompt};
1017
+ setNote(clip?'Starting scan…':'Importing video…');
1018
+ if(scanSubmit){ scanSubmit.disabled=true; scanSubmit.classList.add('is-loading'); }
1019
+
1020
+ var payload={source_url:url};
1021
+ if(clip){ payload.prompt=prompt; if(provider)payload.provider=provider; } else { payload.import_only=true; }
487
1022
  if(tracer)payload.tracer=tracer;
488
- if(provider)payload.provider=provider;
489
- fetch('/raws/scan',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify(payload)})
490
- .then(function(r){ return r.json().then(function(d){ return {ok:r.ok,data:d}; }); })
491
- .then(function(res){
492
- if(scanSubmit)scanSubmit.disabled=false;
493
- if(!res.ok){ setNote((res.data&&res.data.error)||'Could not start the scan.','error'); return; }
494
- setNote('Scan started this can take a few minutes.','success');
495
- if(statusEl)statusEl.textContent='Scanning source…'+(res.data&&res.data.tracer?' (tracer '+res.data.tracer+')':'');
496
- if(dialog&&dialog.open)dialog.close();
497
- if(scanForm.reset)scanForm.reset();
498
- if(res.data&&res.data.scan_id)pollScan(res.data.scan_id);
499
- })
500
- .catch(function(){ if(scanSubmit)scanSubmit.disabled=false; setNote('Network error — please try again.','error'); });
1023
+
1024
+ // Fire the request now; hold it and only wire the result AFTER the modal
1025
+ // closes and the optimistic card lands (so a fast reply can't race ahead
1026
+ // of its own placeholder).
1027
+ var id='imp'+Date.now()+'-'+Math.floor(Math.random()*1e6);
1028
+ var req=fetch('/raws/scan',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json',Accept:'application/json'},body:JSON.stringify(payload)})
1029
+ .then(function(r){ return r.json().then(function(d){ return {ok:r.ok,data:d}; }); });
1030
+
1031
+ // brief spinner beat, then close and optimistically drop an "importing…" card
1032
+ setTimeout(function(){
1033
+ if(scanSubmit){ scanSubmit.disabled=false; scanSubmit.classList.remove('is-loading'); }
1034
+ if(dialog&&dialog.open)dialog.close();
1035
+ if(scanForm.reset)scanForm.reset();
1036
+ syncClipMode();
1037
+ PENDING.unshift({id:id,url:url,tracer:tracer,clip:clip,label:clip?'Starting scan…':'Importing…'});
1038
+ if(query)query.value='';
1039
+ paintRoot(); // surface the optimistic "importing…" card at the directory root
1040
+ if(statusEl)statusEl.textContent=(clip?'Mining a source…':'Importing a video…')+(tracer?' (tracer '+tracer+')':'');
1041
+ req.then(function(res){ onScanResult(id,clip,res); }).catch(function(){ failImport(id,'Network error — please try again.'); });
1042
+ },650);
501
1043
  });
502
1044
  }
503
1045
 
@@ -506,10 +1048,11 @@ const CLIPS_SCRIPT = `
506
1048
  export function renderReskinLibrary(tab = "approved", input = EMPTY_LIBRARY_INPUT) {
507
1049
  const isRaws = tab === "raws";
508
1050
  const isLogs = tab === "logs";
1051
+ const isFiles = tab === "files";
509
1052
  const tabDef = [
510
1053
  { key: "approved", label: "Approved", href: "/library/approved" },
511
1054
  { key: "raws", label: "Raws", href: "/library/raws" },
512
- { key: "logs", label: "Logs", href: "/library/logs" }
1055
+ { key: "files", label: "Files", href: "/library/files" }
513
1056
  ];
514
1057
  const tabs = `<div class="rk-library-tabs" role="tablist" aria-label="Library view">
515
1058
  ${tabDef.map((t) => `<a class="rk-library-tab${t.key === tab ? " is-active" : ""}" role="tab"${t.key === tab ? ` aria-current="page"` : ""} href="${t.href}">${t.label}</a>`).join("\n ")}
@@ -524,19 +1067,23 @@ export function renderReskinLibrary(tab = "approved", input = EMPTY_LIBRARY_INPU
524
1067
  };
525
1068
  const headSub = isLogs
526
1069
  ? "Every render, clip scan, decompose and primitive job — with status, timing, and the compute it cost."
527
- : isRaws
528
- ? "Long-form videos mined into tagged, searchable short clips every reaction, hook, and money shot, ready to drop into an edit."
529
- : "Every published cut you own — ready-to-post renders and scheduled drops. Open in the editor, share, download, or schedule.";
1070
+ : isFiles
1071
+ ? "Your whole workspace as one navigable directoryfiles, temp scratch, and mined raws. Search by meaning, name, or path."
1072
+ : isRaws
1073
+ ? "Long-form videos mined into tagged, searchable short clips — every reaction, hook, and money shot, ready to drop into an edit."
1074
+ : "Every published cut you own — ready-to-post renders and scheduled drops. Open in the editor, share, download, or schedule.";
530
1075
  const headCta = isRaws
531
1076
  ? `<button type="button" class="rk-btn rk-btn-gold rk-btn-sm rk-library-new-btn" data-rk-open-scan>Import source <span class="rk-arrow">→</span></button>`
532
- : isLogs
1077
+ : isLogs || isFiles
533
1078
  ? ""
534
- : `<a class="rk-btn rk-btn-gold rk-btn-sm rk-library-new-btn" href="/discover">New project <span class="rk-arrow">→</span></a>`;
1079
+ : `<a class="rk-btn rk-btn-gold rk-btn-sm rk-library-new-btn" href="/editor/original/fork/new">New project <span class="rk-arrow">→</span></a>`;
535
1080
  const panel = isLogs
536
1081
  ? `<div class="rk-library-topbar">${tabs}</div>\n ${jobRunsPanel(logsBoot)}`
537
- : isRaws
538
- ? clipsPanel(tabs)
539
- : approvedPanel(input, tabs);
1082
+ : isFiles
1083
+ ? filesPanel(tabs)
1084
+ : isRaws
1085
+ ? clipsPanel(tabs)
1086
+ : approvedPanel(input, tabs);
540
1087
  const body = `
541
1088
  <main class="rk-container-wide rk-page rk-library-page">
542
1089
  <div class="rk-spread rk-page-head rk-library-head">
@@ -561,6 +1108,13 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
561
1108
 
562
1109
  /* topbar row: primary tab toggle + the tab's quick search share one line */
563
1110
  .rk-library-topbar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:22px}
1111
+ /* "Logs" — pulled out of the tab toggle; floats to the far right of the row.
1112
+ When a search bar is present it already grabs margin-left:auto, so Logs just
1113
+ trails it; when there's no search (Files tab) Logs floats right on its own. */
1114
+ .rk-library-logs-btn{flex:none;margin-left:auto;gap:7px}
1115
+ .rk-library-qsearch ~ .rk-library-logs-btn,#rkClipsSearch ~ .rk-library-logs-btn{margin-left:0}
1116
+ .rk-library-logs-btn svg{color:var(--rk-n-500)}
1117
+ @media(max-width:720px){.rk-library-logs-btn{margin-left:auto}}
564
1118
 
565
1119
  /* primary tab toggle (Approved | Clips) */
566
1120
  .rk-library-tabs{display:inline-flex;flex:none;padding:5px;gap:3px;background:var(--rk-n-100);border:1px solid var(--rk-border);border-radius:var(--rk-r-full)}
@@ -572,7 +1126,7 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
572
1126
  /* stats — sits right-aligned in the chips row */
573
1127
  .rk-library-summary{font-size:13.5px;color:var(--rk-text-muted);font-weight:500;line-height:1.7}
574
1128
  .rk-library-summary b{color:var(--rk-ink);font-weight:800;font-family:var(--rk-font-display);font-size:15px;letter-spacing:-.01em}
575
- .rk-library-toolbar .rk-library-summary,.rk-clips-chipsrow .rk-library-summary{margin-left:auto}
1129
+ .rk-library-toolbar .rk-library-summary{margin-left:auto}
576
1130
 
577
1131
  /* toolbar (filter chips + count) */
578
1132
  .rk-library-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:24px}
@@ -589,7 +1143,7 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
589
1143
  .rk-library-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:20px}
590
1144
  .rk-library-card{display:flex;flex-direction:column;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);overflow:hidden;box-shadow:var(--rk-shadow-card)}
591
1145
  .rk-library-card[hidden]{display:none}
592
- .rk-library-poster{position:relative;display:block;aspect-ratio:9/16;overflow:hidden;background:linear-gradient(158deg,#2c2c2c,#171717)}
1146
+ .rk-library-poster{position:relative;display:block;width:100%;aspect-ratio:9/16;overflow:hidden;background:linear-gradient(158deg,#2c2c2c,#171717);border:0;padding:0;margin:0;cursor:pointer;text-align:left;font:inherit;color:inherit}
593
1147
  .rk-library-poster::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,0) 58%,rgba(0,0,0,.22));pointer-events:none}
594
1148
  .rk-library-video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#050505}
595
1149
  .rk-library-noprev{position:absolute;inset:0;display:grid;place-items:center;color:rgba(255,255,255,.72);font-size:12px;font-weight:600;letter-spacing:.01em}
@@ -598,16 +1152,39 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
598
1152
  .rk-library-dur{position:absolute;bottom:12px;right:12px;z-index:2;padding:4px 9px;border-radius:var(--rk-r-full);background:rgba(23,23,23,.72);color:#fff;font-size:11px;font-weight:600;letter-spacing:.01em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}
599
1153
  .rk-library-play{position:absolute;top:50%;left:50%;z-index:2;width:52px;height:52px;border-radius:var(--rk-r-full);background:rgba(255,255,255,.94);display:grid;place-items:center;color:var(--rk-ink);box-shadow:var(--rk-shadow-md);opacity:0;transform:translate(-50%,-50%) scale(.82);transition:opacity var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
600
1154
  .rk-library-card:hover .rk-library-play{opacity:1;transform:translate(-50%,-50%) scale(1)}
1155
+ @media(hover:none){.rk-library-play{opacity:1;transform:translate(-50%,-50%) scale(1)}}
601
1156
  .rk-library-play svg{margin-left:2px}
602
- .rk-library-meta{display:grid;gap:10px;padding:16px 16px 18px}
1157
+ .rk-library-meta{display:grid;gap:9px;padding:14px 14px 15px}
603
1158
  .rk-library-title{font-family:var(--rk-font-display);font-size:15px;font-weight:700;color:var(--rk-ink);line-height:1.24;letter-spacing:-.01em;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:2.48em}
604
- .rk-library-origin{display:flex;align-items:center;gap:8px;min-width:0;font-size:12.5px;font-weight:500;color:var(--rk-text-muted)}
1159
+ .rk-library-origin{display:flex;align-items:center;gap:6px;min-width:0;font-size:12px;font-weight:500;color:var(--rk-text-muted)}
1160
+ .rk-library-origin-sep{color:var(--rk-text-faint);flex:none}
605
1161
  .rk-library-origin-txt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
606
- .rk-library-foot{margin-top:2px;gap:10px}
607
- .rk-library-foot .rk-btn{flex:none}
608
- .rk-library-time{font-size:12px;font-weight:500;color:var(--rk-text-faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
609
- .rk-library-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:2px}
610
- .rk-library-actions .rk-btn{flex:none}
1162
+ .rk-library-time{font-size:12px;font-weight:600;color:var(--rk-text-faint);white-space:nowrap;flex:none}
1163
+ /* compact action row: labelled Schedule + icon-only Open/View/Download */
1164
+ .rk-library-actions{display:flex;align-items:center;gap:6px;margin-top:1px}
1165
+ .rk-library-act{flex:none;min-height:32px;padding:0 12px;font-size:12.5px;border-radius:var(--rk-r-full)}
1166
+ .rk-library-act-ic{width:32px;min-width:32px;padding:0;display:inline-flex;align-items:center;justify-content:center}
1167
+ .rk-library-act-ic svg{display:block}
1168
+
1169
+ /* right-floated "⋯" overflow menu (Copy ID / Delete-with-popconfirm) */
1170
+ .rk-library-menu{position:relative;margin-left:auto;flex:none}
1171
+ .rk-library-kebab{width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);background:var(--rk-surface);color:var(--rk-n-600);cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
1172
+ .rk-library-kebab:hover{background:var(--rk-n-100);color:var(--rk-ink)}
1173
+ .rk-library-menu.is-open .rk-library-kebab{background:var(--rk-n-100);color:var(--rk-ink);border-color:var(--rk-border-strong)}
1174
+ .rk-library-kebab.is-ok{color:#1f7a44;border-color:#bfe6cd;background:#e8f7ee}
1175
+ .rk-library-menu-pop{position:absolute;right:0;bottom:calc(100% + 8px);z-index:30;min-width:172px;padding:6px;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-xl))}
1176
+ .rk-library-menu-pop[hidden]{display:none}
1177
+ .rk-library-menu-item{display:flex;align-items:center;gap:9px;width:100%;padding:9px 11px;border:0;border-radius:var(--rk-r-lg,12px);background:none;color:var(--rk-ink);font:inherit;font-size:13px;font-weight:600;text-align:left;cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
1178
+ .rk-library-menu-item svg{flex:none;color:var(--rk-n-500)}
1179
+ .rk-library-menu-item:hover{background:var(--rk-n-100)}
1180
+ .rk-library-menu-item.is-danger{color:var(--rk-red)}
1181
+ .rk-library-menu-item.is-danger svg{color:var(--rk-red)}
1182
+ .rk-library-menu-item.is-danger:hover{background:var(--rk-red-tint)}
1183
+ .rk-library-menu-confirm{padding:10px 11px 4px}
1184
+ .rk-library-menu-confirm[hidden]{display:none}
1185
+ .rk-library-menu-confirm-q{font-size:12.5px;font-weight:600;color:var(--rk-ink);margin:0 0 9px}
1186
+ .rk-library-menu-confirm-row{display:flex;justify-content:flex-end;gap:7px}
1187
+ .rk-library-menu-confirm-row .rk-btn{min-height:30px}
611
1188
 
612
1189
  /* new-project tile */
613
1190
  .rk-library-new{display:grid;grid-auto-flow:row;place-content:center;justify-items:center;gap:14px;text-align:center;padding:28px 22px;border:1.5px dashed var(--rk-border-strong);border-radius:var(--rk-r-3xl);background:var(--rk-n-50);color:var(--rk-text-muted);transition:border-color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
@@ -625,21 +1202,93 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
625
1202
 
626
1203
  @media(prefers-reduced-motion:reduce){.rk-library-video{}}
627
1204
 
1205
+ /* ── fullscreen TikTok-style viewer (click a card → vertical scroll deck) ── */
1206
+ html.rk-noscroll,html.rk-noscroll body{overflow:hidden}
1207
+ .rk-library-viewer{position:fixed;inset:0;z-index:120}
1208
+ .rk-library-viewer[hidden]{display:none}
1209
+ .rk-library-viewer-scrim{position:absolute;inset:0;background:rgba(15,12,5,.88);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
1210
+ .rk-library-viewer-track{position:absolute;inset:0;overflow-y:auto;scroll-snap-type:y mandatory;overscroll-behavior:contain;scrollbar-width:none}
1211
+ .rk-library-viewer-track::-webkit-scrollbar{display:none}
1212
+ .rk-library-viewer-slide{height:100%;display:grid;place-items:center;scroll-snap-align:start;scroll-snap-stop:always;padding:20px 0}
1213
+ .rk-library-viewer-stage{position:relative;width:min(92vw,calc(93vh * 9 / 16));aspect-ratio:9/16;max-height:100%;margin:0;border-radius:var(--rk-r-3xl);overflow:hidden;background:#000;box-shadow:var(--rk-shadow-xl)}
1214
+ .rk-library-viewer-media{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:block}
1215
+ video.rk-library-viewer-media{cursor:pointer}
1216
+ .rk-library-viewer-pause{position:absolute;inset:0;z-index:2;display:none;place-items:center;color:rgba(255,255,255,.92);pointer-events:none;filter:drop-shadow(0 4px 14px rgba(0,0,0,.55))}
1217
+ .rk-library-viewer-pause svg{width:64px;height:64px}
1218
+ .rk-library-viewer-slide.is-paused .rk-library-viewer-pause{display:grid}
1219
+ .rk-library-viewer-shade{position:absolute;left:0;right:0;bottom:0;height:46%;z-index:1;pointer-events:none;background:linear-gradient(to top,rgba(0,0,0,.74) 0%,rgba(0,0,0,.22) 62%,transparent 100%)}
1220
+ .rk-library-viewer-info{position:absolute;left:0;right:0;bottom:0;z-index:3;padding:20px;display:grid;gap:8px;justify-items:start;pointer-events:none}
1221
+ .rk-library-viewer-title{font-family:var(--rk-font-display);font-weight:700;font-size:1.12rem;line-height:1.2;letter-spacing:-.01em;color:#fff}
1222
+ .rk-library-viewer-desc{font-size:12.5px;font-weight:500;color:rgba(255,255,255,.82);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
1223
+ .rk-library-viewer-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:4px;pointer-events:auto}
1224
+ .rk-library-viewer-cta{flex:none}
1225
+ .rk-library-viewer .rk-btn-ghost-light{background:rgba(255,255,255,.14);color:#fff;border-color:rgba(255,255,255,.24);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);box-shadow:none}
1226
+ .rk-library-viewer .rk-btn-ghost-light:hover{background:rgba(255,255,255,.26);border-color:rgba(255,255,255,.4)}
1227
+ .rk-library-viewer-ui{position:absolute;top:max(16px,env(safe-area-inset-top,0px));right:16px;z-index:5;display:flex;gap:10px}
1228
+ .rk-library-viewer-btn{width:44px;height:44px;border-radius:var(--rk-r-full);border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.12);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fff;display:grid;place-items:center;cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease)}
1229
+ .rk-library-viewer-btn:hover{background:rgba(255,255,255,.24);transform:scale(1.06)}
1230
+ .rk-library-viewer-btn span{display:grid;place-items:center;line-height:0}
1231
+ .rk-library-viewer .rk-lvm-off{display:none}
1232
+ .rk-library-viewer.is-muted .rk-lvm-on{display:none}
1233
+ .rk-library-viewer.is-muted .rk-lvm-off{display:grid}
1234
+ .rk-library-viewer-nav{position:absolute;right:18px;top:50%;transform:translateY(-50%);z-index:5;display:grid;gap:12px}
1235
+ @media(hover:none){.rk-library-viewer-nav{display:none}}
1236
+ @media(max-width:640px){
1237
+ .rk-library-viewer-nav{display:none}
1238
+ .rk-library-viewer-slide{padding:0}
1239
+ .rk-library-viewer-stage{width:100%;height:100%;max-height:none;aspect-ratio:auto;border-radius:0;box-shadow:none}
1240
+ }
1241
+
628
1242
  /* ── clips tab ── */
629
1243
  .rk-clips-toolbar{display:grid;gap:16px;margin-bottom:24px}
630
- .rk-clips-chipsrow{display:flex;flex-wrap:wrap;align-items:center;gap:12px}
631
1244
  .rk-clips-search{position:relative;display:flex;align-items:center;gap:8px;flex:1 1 300px;min-width:240px;margin:0}
632
1245
  .rk-clips-search-ic{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--rk-n-400);pointer-events:none;display:flex}
633
1246
  .rk-clips-search-input{flex:1;padding-left:40px;border-radius:var(--rk-r-full)}
634
1247
  .rk-clips-search-btn{flex:none}
635
1248
  .rk-clips-scan{flex:none}
636
- .rk-clips-chips{display:flex;flex-wrap:wrap;gap:4px}
637
- .rk-clips-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 13px;border-radius:var(--rk-r-full);border:1px solid transparent;background:transparent;font-family:var(--rk-font-body);font-size:13px;font-weight:600;color:var(--rk-text-muted);cursor:pointer;white-space:nowrap;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
638
- .rk-clips-chip:hover{background:var(--rk-n-100);color:var(--rk-ink)}
639
- .rk-clips-chip.is-active{background:var(--rk-ink);color:#fff}
640
1249
  .rk-clips-status{font-size:13px;color:var(--rk-text-muted);font-weight:500}
641
1250
  .rk-clips-status code{font-family:var(--rk-font-mono,monospace);font-size:12px;background:var(--rk-n-100);padding:1px 5px;border-radius:6px}
642
- .rk-clips-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(232px,1fr));gap:22px}
1251
+
1252
+ /* ── raws file-directory explorer ── */
1253
+ /* breadcrumb bar (replaces the old count/summary double-row) */
1254
+ .rk-raws-bar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;min-height:30px;margin-bottom:18px}
1255
+ .rk-raws-crumbs{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
1256
+ .rk-raws-crumb{display:inline-flex;align-items:center;gap:5px;font-size:14px;font-weight:600;color:var(--rk-text-muted);max-width:52ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1257
+ .rk-raws-crumb.is-current{color:var(--rk-ink);font-family:var(--rk-font-display);font-weight:800;letter-spacing:-.01em}
1258
+ .rk-raws-crumb-link{border:0;background:none;padding:4px 8px 4px 6px;margin-left:-6px;border-radius:var(--rk-r-full);cursor:pointer;font-family:inherit;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
1259
+ .rk-raws-crumb-link:hover{background:var(--rk-n-100);color:var(--rk-ink)}
1260
+ .rk-raws-crumb-link svg{flex:none}
1261
+ .rk-raws-crumb-sep{color:var(--rk-text-faint);font-weight:500}
1262
+ .rk-raws-crumb-n{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:19px;padding:0 6px;border-radius:var(--rk-r-full);background:var(--rk-n-100);color:var(--rk-text-muted);font-family:var(--rk-font-mono,monospace);font-size:11px;font-weight:700}
1263
+ .rk-raws-bar .rk-clips-status{margin-left:auto;text-align:right}
1264
+
1265
+ /* the explorer grid switches between clip cards (default) and folder tiles */
1266
+ .rk-raws-explorer{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:20px}
1267
+ .rk-raws-explorer.is-folders{grid-template-columns:repeat(auto-fill,minmax(228px,1fr))}
1268
+
1269
+ /* folder tile — 2×2 thumbnail mosaic + name + count */
1270
+ .rk-raws-folder{display:flex;flex-direction:column;overflow:hidden;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-card);cursor:pointer;text-align:left;font:inherit;color:inherit;padding:0;transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
1271
+ .rk-raws-folder:hover{transform:translateY(-3px);box-shadow:var(--rk-shadow-md);border-color:var(--rk-gold-600)}
1272
+ .rk-raws-folder-mosaic{position:relative;display:grid;grid-template-columns:1fr 1fr;grid-auto-rows:1fr;gap:2px;aspect-ratio:4/3;background:var(--rk-n-100);overflow:hidden}
1273
+ .rk-raws-folder-mosaic.is-single{grid-template-columns:1fr}
1274
+ .rk-raws-folder-mosaic.is-empty{display:grid;grid-template-columns:1fr;place-items:center;color:var(--rk-n-400);background:linear-gradient(150deg,var(--rk-n-100),var(--rk-n-50))}
1275
+ .rk-raws-folder-mosaic.is-empty svg{width:38px;height:38px}
1276
+ .rk-raws-folder-cell{display:block;background-size:cover;background-position:center;background-repeat:no-repeat;background-color:#171717}
1277
+ .rk-raws-folder-meta{display:flex;align-items:center;gap:10px;padding:13px 14px}
1278
+ .rk-raws-folder-ic{flex:none;color:var(--rk-gold-700);display:flex}
1279
+ .rk-raws-folder-txt{display:flex;flex-direction:column;gap:2px;min-width:0}
1280
+ .rk-raws-folder-name{font-family:var(--rk-font-display);font-size:14px;font-weight:700;color:var(--rk-ink);letter-spacing:-.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1281
+ .rk-raws-folder-count{font-size:12px;font-weight:600;color:var(--rk-text-muted)}
1282
+
1283
+ /* mouseover preview popover (floating enlarged media) */
1284
+ .rk-raws-hover{position:fixed;z-index:90;width:250px;pointer-events:none;border-radius:var(--rk-r-2xl);overflow:hidden;background:#0d0d0d;border:1px solid rgba(255,255,255,.12);box-shadow:var(--rk-shadow-xl);opacity:0;transform:scale(.96);transition:opacity .12s var(--rk-ease),transform .12s var(--rk-ease)}
1285
+ .rk-raws-hover.is-visible{opacity:1;transform:scale(1)}
1286
+ .rk-raws-hover[hidden]{display:none}
1287
+ .rk-raws-hover-vid{display:block;width:100%;aspect-ratio:3/4;object-fit:cover;background:#0d0d0d}
1288
+ .rk-raws-hover-cap{padding:9px 12px 11px;font-size:12px;font-weight:600;line-height:1.35;color:rgba(255,255,255,.92);background:linear-gradient(180deg,#171717,#0d0d0d);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
1289
+ @media(hover:none){.rk-raws-hover{display:none!important}}
1290
+
1291
+ .rk-clips-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:20px}
643
1292
  .rk-clips-card{display:flex;flex-direction:column;overflow:hidden;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);box-shadow:var(--rk-shadow-card)}
644
1293
  .rk-clips-thumb{position:relative;aspect-ratio:3/4;overflow:hidden;display:grid;place-items:center;background:linear-gradient(150deg,#2c2c2c,#171717)}
645
1294
  .rk-clips-thumb-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
@@ -648,6 +1297,7 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
648
1297
  .rk-clips-dur{position:absolute;left:10px;bottom:10px;padding:3px 9px;border-radius:var(--rk-r-full);background:rgba(255,255,255,.92);color:var(--rk-ink);font-size:11px;font-weight:700;font-variant-numeric:tabular-nums;box-shadow:var(--rk-shadow-xs);z-index:2}
649
1298
  .rk-clips-play{position:absolute;right:10px;top:10px;width:34px;height:34px;display:grid;place-items:center;padding:0 0 0 2px;border:0;border-radius:var(--rk-r-full);background:rgba(255,255,255,.92);color:var(--rk-ink);font-size:13px;cursor:pointer;opacity:0;transform:scale(.85);box-shadow:var(--rk-shadow-sm);z-index:2;transition:opacity var(--rk-dur) var(--rk-ease),transform var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
650
1299
  .rk-clips-card:hover .rk-clips-play{opacity:1;transform:scale(1)}
1300
+ @media(hover:none){.rk-clips-play{opacity:1;transform:scale(1)}}
651
1301
  .rk-clips-play:hover{background:var(--rk-gold-500)}
652
1302
  .rk-clips-body{display:flex;flex-direction:column;gap:10px;flex:1;padding:16px}
653
1303
  .rk-clips-title{font-family:var(--rk-font-display);font-size:14px;font-weight:700;line-height:1.34;color:var(--rk-ink);letter-spacing:-.01em;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
@@ -655,34 +1305,82 @@ ${isRaws ? CLIPS_DIALOG : ""}`;
655
1305
  .rk-clips-tag{display:inline-flex;align-items:center;padding:2px 8px;border-radius:var(--rk-r-full);background:var(--rk-gold-tint);color:var(--rk-n-700);font-size:11px;font-weight:600;white-space:nowrap}
656
1306
  .rk-clips-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:auto;border-top:1px solid var(--rk-border);padding-top:12px}
657
1307
  .rk-clips-src{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--rk-text-muted);font-size:12px;font-weight:500}
658
- .rk-clips-dl{flex:none;color:var(--rk-gold-700);font-size:12px;font-weight:700}
1308
+ .rk-clips-dl{flex:none;color:var(--rk-gold-700);font-size:12px;font-weight:700;background:none;border:0;padding:0;font-family:inherit;cursor:pointer}
659
1309
  .rk-clips-dl:hover{color:var(--rk-gold-500)}
1310
+ /* the whole thumb is a button that opens the scroll-snap viewer */
1311
+ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer}
1312
+ /* optimistic "importing…" placeholder card */
1313
+ .rk-clips-importing .rk-clips-thumb{aspect-ratio:3/4}
1314
+ .rk-clips-importing-spin{width:36px;height:36px;border-radius:var(--rk-r-full);border:3px solid rgba(255,255,255,.26);border-top-color:#fff;animation:rk-clips-spin .8s linear infinite}
1315
+ @keyframes rk-clips-spin{to{transform:rotate(360deg)}}
1316
+ .rk-clips-importing.is-error .rk-clips-thumb{background:linear-gradient(150deg,#5b2b23,#3a1a15)}
1317
+ .rk-clips-importing-x{color:rgba(255,255,255,.9);display:grid;place-items:center}
1318
+ .rk-clips-importing.is-error .rk-clips-title{color:#8d3b2f}
1319
+ .rk-clips-importing-tag{color:var(--rk-text-faint)!important;cursor:default}
1320
+ /* submit spinner while the request is in flight, before the modal closes */
1321
+ .rk-clips-dialog .rk-btn.is-loading{position:relative;color:transparent!important;pointer-events:none}
1322
+ .rk-clips-dialog .rk-btn.is-loading::after{content:"";position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;border-radius:var(--rk-r-full);border:2px solid rgba(0,0,0,.32);border-top-color:var(--rk-ink);animation:rk-clips-spin .7s linear infinite}
660
1323
  .rk-clips-empty{display:grid;place-items:center;gap:10px;text-align:center;padding:64px 20px;background:#fff;border:1.5px dashed var(--rk-border-strong);border-radius:var(--rk-r-3xl)}
661
1324
  .rk-clips-empty[hidden]{display:none}
662
1325
  .rk-clips-empty h3{font-size:var(--rk-text-xl)}
663
- .rk-clips-dialog{width:min(34rem,calc(100vw - 2rem));padding:0;border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);background:var(--rk-surface);box-shadow:var(--rk-shadow-xl)}
1326
+ .rk-clips-dialog{width:min(34rem,calc(100vw - 2rem));max-height:calc(100dvh - 2rem);padding:0;border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);background:var(--rk-surface);box-shadow:var(--rk-shadow-xl);overflow:hidden}
664
1327
  .rk-clips-dialog::backdrop{background:rgba(23,23,23,.34);backdrop-filter:blur(6px)}
665
- .rk-clips-dialog-form{display:grid;gap:16px;margin:0;padding:28px}
1328
+ /* scroll the form (not the page) so Cancel / Import stay reachable when the
1329
+ "Clip raws" advanced block expands on short / landscape-phone viewports */
1330
+ .rk-clips-dialog-form{display:grid;gap:16px;margin:0;padding:28px;max-height:calc(100dvh - 2rem);overflow-y:auto}
666
1331
  .rk-clips-dialog-grid{gap:14px}
1332
+ .rk-clips-toggle{display:flex;gap:10px;align-items:flex-start;cursor:pointer;font-size:.92rem;line-height:1.4;color:var(--rk-n-700);background:var(--rk-n-50);border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);padding:12px 14px}
1333
+ .rk-clips-toggle input{margin-top:2px;flex:0 0 auto;accent-color:var(--rk-gold);width:16px;height:16px;cursor:pointer}
1334
+ .rk-clips-toggle b{color:var(--rk-ink)}
1335
+ .rk-clips-advanced{display:grid;gap:16px}
1336
+ .rk-clips-advanced[hidden]{display:none}
667
1337
  .rk-clips-dialog-note{background:var(--rk-gold-tint);border-radius:var(--rk-r-xl);padding:10px 13px;color:var(--rk-n-700)}
668
1338
  .rk-clips-dialog-note[data-tone="error"]{background:#fdecea;color:#8d3b2f}
669
1339
  .rk-clips-dialog-note[data-tone="success"]{background:#e8f7ee;color:#1f7a44}
670
1340
  .rk-clips-dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}
1341
+ /* ── files tab: full-page mount of the shared directory-explorer component ──
1342
+ The component (.rk-dir + .rk-aichat-* rows) is tuned for the narrow chat
1343
+ drawer; on a full page we wrap it in a card and scale the search, crumbs and
1344
+ rows up so it reads as a first-class file explorer. Scoped to
1345
+ .rk-files-explorer so the drawer instance is untouched. */
1346
+ .rk-files-explorer{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-card);overflow:hidden;min-height:60vh;display:flex;flex-direction:column}
1347
+ .rk-files-explorer .rk-dir{flex:1;min-height:60vh}
1348
+ .rk-files-explorer .rk-dir-search{padding:18px 20px 12px}
1349
+ .rk-files-explorer .rk-dir-search-input{font-size:15px;padding:12px 16px;border-radius:var(--rk-r-full)}
1350
+ .rk-files-explorer .rk-aichat-crumbs,.rk-files-explorer .rk-dir-crumbs{padding:4px 22px 12px;gap:2px;border-bottom:1px solid var(--rk-border)}
1351
+ .rk-files-explorer .rk-aichat-crumb{font-size:14px;padding:4px 9px}
1352
+ .rk-files-explorer .rk-aichat-crumb-sep{font-size:14px}
1353
+ .rk-files-explorer .rk-dir-newfolder{font-size:12.5px;padding:5px 11px}
1354
+ .rk-files-explorer .rk-dir-selbar{margin:12px 18px 0;padding:9px 14px}
1355
+ .rk-files-explorer .rk-aichat-files-body,.rk-files-explorer .rk-dir-body{padding:12px 14px 20px;gap:3px}
1356
+ .rk-files-explorer .rk-aichat-frow{padding:11px 12px;border-radius:var(--rk-r-xl)}
1357
+ .rk-files-explorer .rk-aichat-fic{width:44px;height:44px;border-radius:var(--rk-r-lg)}
1358
+ .rk-files-explorer .rk-aichat-fname{font-size:14.5px}
1359
+ .rk-files-explorer .rk-aichat-fmeta{font-size:12.5px}
1360
+ .rk-files-explorer .rk-dir-open{font-size:12.5px}
1361
+ .rk-files-explorer .rk-dir-kebab{width:30px;height:30px}
1362
+ .rk-files-explorer .rk-aichat-drop{margin:0 18px 18px}
1363
+ .rk-files-explorer .rk-aichat-fstate{padding:48px 24px;text-align:center}
671
1364
  @media(max-width:620px){
672
1365
  .rk-clips-search{flex:1 1 100%}
673
1366
  .rk-clips-scan{flex:1 1 100%}
674
1367
  .rk-clips-search-input{font-size:16px}
675
- .rk-clips-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
1368
+ .rk-clips-grid,.rk-raws-explorer{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
1369
+ .rk-raws-explorer.is-folders{grid-template-columns:repeat(2,minmax(0,1fr))}
1370
+ .rk-files-explorer .rk-dir-search-input{font-size:16px}
676
1371
  }
677
1372
  `;
678
- const tabTitle = isRaws ? " · Raws" : isLogs ? " · Logs" : "";
1373
+ const tabTitle = isRaws ? " · Raws" : isLogs ? " · Logs" : isFiles ? " · Files" : "";
679
1374
  return reskinDocument({
680
1375
  title: `vidfarm reskin — Library${tabTitle}`,
681
- description: "Reskinned vidfarm library: your published cuts, mined raws library, and job run history.",
1376
+ description: "Reskinned vidfarm library: your published cuts, mined raws library, unified file directory, and job run history.",
682
1377
  activeSlug: "library",
1378
+ account: { name: input.name, email: input.email },
683
1379
  pageCss: pageCss + (isLogs ? "\n" + JOBRUNS_PAGE_CSS : ""),
684
1380
  body,
685
- script: isLogs ? JOBRUNS_SCRIPT : isRaws ? CLIPS_SCRIPT : APPROVED_SCRIPT
1381
+ // Files tab needs no page script the auto-mounted directory-explorer
1382
+ // component (base module on every full-chrome page) owns all its behaviour.
1383
+ script: isFiles ? undefined : isLogs ? JOBRUNS_SCRIPT : isRaws ? CLIPS_SCRIPT : APPROVED_SCRIPT
686
1384
  });
687
1385
  }
688
1386
  //# sourceMappingURL=library-page.js.map