@mevdragon/vidfarm-devcli 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.agents/skills/farmville-saas-ux/SKILL.md +156 -0
  2. package/.agents/skills/farmville-saas-ux/assets/starter.html +294 -0
  3. package/.agents/skills/farmville-saas-ux/references/components.md +340 -0
  4. package/.agents/skills/farmville-saas-ux/references/porting-guide.md +121 -0
  5. package/.agents/skills/farmville-saas-ux/references/tokens.md +271 -0
  6. package/.agents/skills/vidfarm-media/SKILL.md +4 -3
  7. package/.agents/skills/vidfarm-media/references/tts.md +20 -1
  8. package/SKILL.director.md +20 -20
  9. package/SKILL.platform.md +4 -4
  10. package/dist/src/account-pages-legacy.js +2 -2
  11. package/dist/src/app.js +721 -101
  12. package/dist/src/cli.js +11 -10
  13. package/dist/src/devcli/clips.js +64 -64
  14. package/dist/src/editor-chat.js +2 -2
  15. package/dist/src/frontend/homepage-client.js +162 -2
  16. package/dist/src/frontend/homepage-store.js +30 -1
  17. package/dist/src/frontend/homepage-view.js +111 -4
  18. package/dist/src/homepage.js +184 -1
  19. package/dist/src/landing-page.js +367 -0
  20. package/dist/src/primitive-registry.js +278 -2
  21. package/dist/src/reskin/agency-page.js +299 -0
  22. package/dist/src/reskin/calendar-page.js +567 -0
  23. package/dist/src/reskin/chat-page.js +607 -0
  24. package/dist/src/reskin/discover-page.js +1096 -0
  25. package/dist/src/reskin/document.js +663 -0
  26. package/dist/src/reskin/help-page.js +356 -0
  27. package/dist/src/reskin/index-page.js +62 -0
  28. package/dist/src/reskin/inpaint-page.js +541 -0
  29. package/dist/src/reskin/job-runs-page.js +477 -0
  30. package/dist/src/reskin/library-page.js +688 -0
  31. package/dist/src/reskin/login-page.js +262 -0
  32. package/dist/src/reskin/pricing-page.js +388 -0
  33. package/dist/src/reskin/settings-page.js +687 -0
  34. package/dist/src/reskin/theme.js +362 -0
  35. package/dist/src/services/serverless-records.js +54 -0
  36. package/dist/src/services/swipe-customize.js +434 -0
  37. package/package.json +1 -1
  38. package/public/assets/homepage-client-app.js +22 -22
@@ -0,0 +1,687 @@
1
+ // /reskin/settings — the flagship reskin, now MIGRATED to real production data.
2
+ //
3
+ // This is the reference for the /reskin/* UX migration: the renderer accepts the
4
+ // SAME input object the live settings route already builds (via
5
+ // `Parameters<typeof renderSettingsPage>[0]`), so the route wiring is just "fetch
6
+ // real data + call this instead of the legacy renderer". Every form posts to the
7
+ // REAL /settings/* endpoints with the correct account + ids, so profile save,
8
+ // AI-key add/remove, FlockPoster connect, channel resend/delete, attachment
9
+ // upload/delete, and wallet top-up all actually work. Design stays farmville
10
+ // (shared rk-* primitives, isolated CSS); only the data + actions became real.
11
+ import { reskinDocument, rkEscape } from "./document.js";
12
+ const EYE = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>`;
13
+ const SEARCH_ICON = `<svg width="15" height="15" 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>`;
14
+ function money(n) {
15
+ const abs = Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
16
+ return `${n < 0 ? "−" : ""}$${abs}`;
17
+ }
18
+ function fmtBytes(bytes) {
19
+ if (!bytes || bytes < 1024)
20
+ return `${bytes || 0} B`;
21
+ const units = ["KB", "MB", "GB"];
22
+ let v = bytes / 1024;
23
+ let i = 0;
24
+ while (v >= 1024 && i < units.length - 1) {
25
+ v /= 1024;
26
+ i++;
27
+ }
28
+ return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[i]}`;
29
+ }
30
+ function fmtWhen(ms) {
31
+ if (!ms || ms <= 0)
32
+ return "";
33
+ try {
34
+ return new Date(ms).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" });
35
+ }
36
+ catch {
37
+ return "";
38
+ }
39
+ }
40
+ function fmtDate(iso) {
41
+ const t = Date.parse(iso);
42
+ if (Number.isNaN(t))
43
+ return iso;
44
+ return new Date(t).toLocaleDateString("en-US", { month: "short", day: "numeric" });
45
+ }
46
+ function walletEventTitle(e) {
47
+ if (e.entity_type === "wallet_funding_event")
48
+ return "Wallet top-up";
49
+ if (e.cost_center_slug === "hyperframes_lambda" || e.cost_center_slug === "remotion_lambda")
50
+ return "Timeline render usage";
51
+ if (e.cost_center_slug === "wallet_funding")
52
+ return "Wallet funding";
53
+ if (e.cost_center_slug) {
54
+ return e.cost_center_slug.split(/[_-]+/g).filter(Boolean)
55
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(" ");
56
+ }
57
+ return "Usage charge";
58
+ }
59
+ export function renderReskinSettings(input) {
60
+ const acct = `<input type="hidden" name="account" value="${rkEscape(input.currentAccountId)}" />`;
61
+ const displayName = input.name ?? "";
62
+ const activeTab = input.activeTab || "profile";
63
+ const flash = input.error
64
+ ? `<div class="rk-notice rk-notice-err rk-set-flash">${rkEscape(input.error)}</div>`
65
+ : input.notice
66
+ ? `<div class="rk-notice rk-notice-ok rk-set-flash">${rkEscape(input.notice)}</div>`
67
+ : "";
68
+ // ---- Profile ----
69
+ const attachmentCards = input.attachments.length
70
+ ? input.attachments.map((a) => {
71
+ const ext = (a.fileName.split(".").pop() || a.contentType.split("/").pop() || "file").toUpperCase().slice(0, 4);
72
+ return `
73
+ <div class="rk-item">
74
+ <span class="rk-tile rk-tile-txt rk-tile-sky">${rkEscape(ext)}</span>
75
+ <div class="rk-item-main">
76
+ <div class="rk-item-title">${rkEscape(a.fileName)}</div>
77
+ <div class="rk-item-sub">${rkEscape(fmtBytes(a.sizeBytes))} · added ${rkEscape(fmtDate(a.createdAt))}</div>
78
+ </div>
79
+ <a class="rk-btn rk-btn-ghost rk-btn-sm" href="${rkEscape(a.viewUrl)}" target="_blank" rel="noreferrer">Open</a>
80
+ <form method="post" action="/settings/attachments/${encodeURIComponent(a.id)}/delete" data-rk-confirm="Delete ${rkEscape(a.fileName)}?" style="display:inline">
81
+ ${acct}
82
+ <button class="rk-btn rk-btn-danger rk-btn-sm" type="submit">Remove</button>
83
+ </form>
84
+ </div>`;
85
+ }).join("")
86
+ : `<p class="rk-muted rk-set-empty">No brand assets yet — upload logos, fonts, VO, or footage to reuse across projects.</p>`;
87
+ const profile = `
88
+ <div class="rk-card rk-set-panel-card">
89
+ <div class="rk-card-head">
90
+ <span class="rk-eyebrow">Profile</span>
91
+ <h2>Profile, notes &amp; attachments</h2>
92
+ <p class="rk-muted">The product description powers Swipe mode's auto-recut. Attachments are your reusable brand assets.</p>
93
+ </div>
94
+ <form class="rk-stack" method="post" action="/settings/profile">
95
+ ${acct}
96
+ <input type="hidden" name="groupchat_url" value="${rkEscape(input.groupchatUrl ?? "")}" />
97
+ <div class="rk-field">
98
+ <label class="rk-label" for="rk-name">Display name</label>
99
+ <input class="rk-input" id="rk-name" type="text" name="display_name" value="${rkEscape(displayName)}" />
100
+ </div>
101
+ <div class="rk-field">
102
+ <label class="rk-label" for="rk-about">Product description</label>
103
+ <textarea class="rk-textarea" id="rk-about" name="about" placeholder="What it is, who it's for, the promise…">${rkEscape(input.about ?? "")}</textarea>
104
+ <span class="rk-hint">Swipe mode reads this to auto-recut templates into ads for you.</span>
105
+ </div>
106
+ <div class="rk-row">
107
+ <button class="rk-btn rk-btn-ink" type="submit">Save changes</button>
108
+ <span class="rk-hint">Signed in as ${rkEscape(input.email)}</span>
109
+ </div>
110
+ </form>
111
+ </div>
112
+ <div class="rk-card rk-set-panel-card rk-set-myfiles">
113
+ <div class="rk-card-head">
114
+ <span class="rk-eyebrow">My Files</span>
115
+ <h2>Your files</h2>
116
+ <p class="rk-muted">Your private library — logos, fonts, VO, footage, and notes. Open <b>My Files</b> or <b>Temp</b> below and browse into folders.</p>
117
+ </div>
118
+ <form method="post" action="/settings/attachments" enctype="multipart/form-data" class="rk-stack rk-set-upload" data-rk-upload>
119
+ ${acct}
120
+ <label class="rk-set-drop" for="rk-files">
121
+ <div><strong>Choose files</strong> to upload — logos, fonts, VO, footage.</div>
122
+ <input class="rk-set-file" id="rk-files" type="file" name="files" multiple />
123
+ </label>
124
+ <div class="rk-row"><button class="rk-btn rk-btn-gold rk-btn-sm" type="submit" data-rk-upload-btn>Upload files</button></div>
125
+ </form>
126
+ <div class="rk-set-explorer" data-rk-fx>
127
+ <div class="rk-aichat-crumbs rk-set-fx-crumbs" data-rk-fx-crumbs aria-label="Current folder"></div>
128
+ <div class="rk-set-fx-body" data-rk-fx-body></div>
129
+ </div>
130
+ </div>`;
131
+ // ---- Wallet ----
132
+ const walletBalance = input.wallet.unavailableReason
133
+ ? `<div class="rk-notice rk-notice-err">${rkEscape(input.wallet.unavailableReason)}</div>`
134
+ : `<div class="rk-stat-num rk-set-balance-num">${money(input.wallet.balanceUsd)}</div>
135
+ <p class="rk-muted">Available balance${input.wallet.updatedAtMs > 0 ? ` · updated ${rkEscape(fmtWhen(input.wallet.updatedAtMs))}` : ""}</p>`;
136
+ const historyRows = input.walletEvents.unavailableReason
137
+ ? `<div class="rk-notice rk-notice-err">${rkEscape(input.walletEvents.unavailableReason)}</div>`
138
+ : input.walletEvents.events.length
139
+ ? input.walletEvents.events.map((e) => {
140
+ const credit = e.entity_type === "wallet_funding_event" || e.amount_usd > 0;
141
+ const sub = [e.cost_center_slug || e.entity_type, e.job_id, e.tracer].filter(Boolean).join(" · ");
142
+ return `
143
+ <div class="rk-item">
144
+ <span class="rk-tile rk-tile-sm rk-tile-txt ${credit ? "rk-tile-green" : "rk-tile-ink"}">${credit ? "+" : "−"}</span>
145
+ <div class="rk-item-main">
146
+ <div class="rk-item-title">${rkEscape(walletEventTitle(e))}</div>
147
+ <div class="rk-item-sub">${rkEscape(sub)} · ${rkEscape(fmtWhen(e.occurred_at_ms))}</div>
148
+ </div>
149
+ <div class="rk-set-amount ${credit ? "is-credit" : ""}">${money(e.amount_usd)}</div>
150
+ </div>`;
151
+ }).join("")
152
+ : `<p class="rk-muted rk-set-empty">No wallet activity yet.</p>`;
153
+ const wallet = `
154
+ <div class="rk-stack rk-set-panel">
155
+ <div class="rk-card rk-set-balance">
156
+ <span class="rk-eyebrow">Wallet</span>
157
+ ${walletBalance}
158
+ <div class="rk-row rk-wrap rk-set-balance-cta">
159
+ <button class="rk-btn rk-btn-gold" type="button" data-rk-topup>Top up wallet <span class="rk-arrow">→</span></button>
160
+ </div>
161
+ <div class="rk-hint rk-set-topup-status" data-rk-topup-status hidden></div>
162
+ </div>
163
+ <div class="rk-card">
164
+ <div class="rk-card-head">
165
+ <span class="rk-eyebrow">Ledger</span>
166
+ <h2>Transparent billing history</h2>
167
+ <p class="rk-muted">Every render and top-up, itemized. You're only charged for compute — BYO AI keys are never billed.</p>
168
+ </div>
169
+ <div class="rk-list">${historyRows}</div>
170
+ </div>
171
+ </div>`;
172
+ // ---- Channels ----
173
+ // Mirrors the live /settings#channels ordering: topline heading + Connect
174
+ // button + "…" menu (Add Email Channel), then a channel search, then the
175
+ // channel list. The FlockPoster key + email channel forms live in dialogs.
176
+ const emailCards = input.emailChannels.map((ch) => {
177
+ const status = ch.status || "Pending";
178
+ const statusPill = /connect|active|verified/i.test(status) ? "rk-pill-green" : "rk-pill-gold";
179
+ const hay = [ch.title, ch.platform || "email", ch.handle].filter(Boolean).join(" ").toLowerCase();
180
+ return `<div class="rk-item" data-rk-channel-hay="${rkEscape(hay)}">
181
+ <span class="rk-tile rk-tile-txt rk-tile-sky">${rkEscape((ch.platform || "E").charAt(0).toUpperCase())}</span>
182
+ <div class="rk-item-main">
183
+ <div class="rk-item-title">${rkEscape(ch.title)}</div>
184
+ <div class="rk-item-sub">${rkEscape(ch.platform || "Email")} · ${rkEscape(ch.handle || "")}</div>
185
+ </div>
186
+ <span class="rk-pill ${statusPill}">${rkEscape(status)}</span>
187
+ <form method="post" action="/settings/channels/email/${encodeURIComponent(ch.rawId ?? "")}/resend" style="display:inline">${acct}<button class="rk-btn rk-btn-ghost rk-btn-sm" type="submit">Resend</button></form>
188
+ <form method="post" action="/settings/channels/email/${encodeURIComponent(ch.rawId ?? "")}/delete" data-rk-confirm="Remove this channel?" style="display:inline">${acct}<button class="rk-btn rk-btn-danger rk-btn-sm" type="submit">Remove</button></form>
189
+ </div>`;
190
+ }).join("");
191
+ const flockCards = input.flockposterChannels.map((ch) => {
192
+ const status = ch.status || "Connected";
193
+ const tint = /tiktok/i.test(ch.platform || "") ? "rk-tile-ink" : /insta/i.test(ch.platform || "") ? "rk-tile-coral" : "rk-tile-sky";
194
+ const hay = [ch.title, ch.platform, ch.handle].filter(Boolean).join(" ").toLowerCase();
195
+ return `<div class="rk-item" data-rk-channel-hay="${rkEscape(hay)}">
196
+ <span class="rk-tile rk-tile-txt ${tint}">${rkEscape((ch.platform || "F").charAt(0).toUpperCase())}</span>
197
+ <div class="rk-item-main">
198
+ <div class="rk-item-title">${rkEscape(ch.title)}</div>
199
+ <div class="rk-item-sub">${rkEscape(ch.platform || "")} · ${rkEscape(ch.handle || "")}</div>
200
+ </div>
201
+ <span class="rk-pill rk-pill-green">${rkEscape(status)}</span>
202
+ </div>`;
203
+ }).join("");
204
+ const channelCards = flockCards + emailCards;
205
+ const channelCount = input.emailChannels.length + input.flockposterChannels.length;
206
+ const hasFlockKey = Boolean(input.flockposterApiKey);
207
+ const channels = `
208
+ <div class="rk-stack rk-set-panel">
209
+ ${input.flockposterChannelsError ? `<div class="rk-notice rk-notice-err">${rkEscape(input.flockposterChannelsError)}</div>` : ""}
210
+ <div class="rk-card">
211
+ <div class="rk-spread rk-set-channels-topline">
212
+ <div class="rk-card-head rk-set-channels-headcopy">
213
+ <span class="rk-eyebrow">Channels</span>
214
+ <h2>${channelCount} connected channel${channelCount === 1 ? "" : "s"}</h2>
215
+ <p class="rk-muted">Where approved cuts get published. Connect via FlockPoster, or add an email destination.</p>
216
+ </div>
217
+ <div class="rk-row rk-set-channels-actions">
218
+ <button class="rk-btn rk-btn-gold rk-btn-sm" type="button" data-rk-open-flock>Connect</button>
219
+ <div class="rk-set-menu-wrap">
220
+ <button class="rk-btn rk-btn-ghost rk-btn-sm rk-set-menu-btn" type="button" data-rk-channel-menu aria-expanded="false" aria-label="More channel actions">…</button>
221
+ <div class="rk-set-menu" data-rk-channel-menu-pop hidden>
222
+ <button type="button" data-rk-open-email>Add Email Channel</button>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ </div>
227
+ <label class="rk-set-chsearch">
228
+ <span class="rk-set-chsearch-ic" aria-hidden="true">${SEARCH_ICON}</span>
229
+ <input class="rk-input" data-rk-channel-search type="search"
230
+ placeholder="${channelCount ? "Search channels by name, handle, or platform" : "Add an email channel or connect FlockPoster to search channels"}"
231
+ autocomplete="off" spellcheck="false" aria-label="Search channels"${channelCount ? "" : " disabled"} />
232
+ </label>
233
+ <div class="rk-list" data-rk-channel-list>${channelCards || `<p class="rk-muted rk-set-empty">No channels connected yet.</p>`}</div>
234
+ <p class="rk-muted rk-set-empty" data-rk-channel-noresults hidden>No channels match your search.</p>
235
+ </div>
236
+ </div>
237
+ <dialog class="rk-set-dialog" data-rk-flock-dialog${input.openFlockPosterDialog ? ` data-rk-autoopen="1"` : ""}>
238
+ <form class="rk-set-dialog-form" method="post" action="/settings/channels/flockposter">
239
+ ${acct}
240
+ <div class="rk-card-head">
241
+ <span class="rk-eyebrow">FlockPoster settings</span>
242
+ <h3>FlockPoster API key</h3>
243
+ <p class="rk-muted">${hasFlockKey ? "Update the stored key and retest the connection before saving." : "Paste a FlockPoster API key to connect this workspace and load its channels."}</p>
244
+ </div>
245
+ <div class="rk-field">
246
+ <label class="rk-label" for="rk-fp">API key</label>
247
+ <div class="rk-input-group">
248
+ <input class="rk-input" id="rk-fp" type="password" name="flockposter_api_key" placeholder="${hasFlockKey ? "•••••••• (stored) — paste to replace" : "fp_live_…"}" autocomplete="off" />
249
+ <button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-reveal="rk-fp" aria-label="Show API key">${EYE}</button>
250
+ </div>
251
+ <span class="rk-hint">${hasFlockKey ? "The key stays hidden by default — changing it triggers a connection test on save." : "We test the key before storing it. If the connection succeeds, this tab loads your channels."}</span>
252
+ </div>
253
+ <div class="rk-set-dialog-actions">
254
+ <a class="rk-btn rk-btn-ghost rk-btn-sm" href="https://flockposter.com" target="_blank" rel="noreferrer">Visit App</a>
255
+ <button type="button" class="rk-btn rk-btn-ghost" data-rk-dialog-close>Close</button>
256
+ <button type="submit" class="rk-btn rk-btn-gold">${hasFlockKey ? "Save" : "Connect"} &amp; test</button>
257
+ </div>
258
+ </form>
259
+ </dialog>
260
+ <dialog class="rk-set-dialog" data-rk-email-dialog>
261
+ <form class="rk-set-dialog-form" method="post" action="/settings/channels/email">
262
+ ${acct}
263
+ <div class="rk-card-head">
264
+ <span class="rk-eyebrow">Email channel</span>
265
+ <h3>Add Email Channel</h3>
266
+ <p class="rk-muted">We'll send a consent confirmation before this email can be used for scheduled delivery.</p>
267
+ </div>
268
+ <div class="rk-field"><label class="rk-label" for="rk-em-title">Title</label><input class="rk-input" id="rk-em-title" name="title" required maxlength="160" placeholder="Client newsletter" /></div>
269
+ <div class="rk-field"><label class="rk-label" for="rk-em-addr">Email</label><input class="rk-input" id="rk-em-addr" name="email" type="email" required maxlength="320" placeholder="name@example.com" /></div>
270
+ <div class="rk-field"><label class="rk-label" for="rk-em-note">Note</label><textarea class="rk-textarea rk-set-email-note" id="rk-em-note" name="note" maxlength="2000" placeholder="Internal notes for team"></textarea></div>
271
+ <div class="rk-set-dialog-actions">
272
+ <button type="button" class="rk-btn rk-btn-ghost" data-rk-dialog-close>Close</button>
273
+ <button type="submit" class="rk-btn rk-btn-ink">Send confirmation</button>
274
+ </div>
275
+ </form>
276
+ </dialog>`;
277
+ // ---- Developer ----
278
+ const keyRows = input.providerKeys.length
279
+ ? input.providerKeys.map((k, i) => {
280
+ const id = `rk-pk-${i}`;
281
+ const tint = k.provider === "openai" ? "rk-tile-green" : k.provider === "gemini" ? "rk-tile-sky" : "rk-tile-violet";
282
+ const status = k.status || "active";
283
+ const lastUsed = k.last_used_at ? `used ${rkEscape(fmtDate(k.last_used_at))}` : "never used";
284
+ return `<div class="rk-item">
285
+ <span class="rk-tile rk-tile-txt ${tint}">${rkEscape((k.provider || "?").charAt(0).toUpperCase())}</span>
286
+ <div class="rk-item-main">
287
+ <div class="rk-item-title">${rkEscape(k.label || `${k.provider} key`)} <span class="rk-pill rk-pill-green">${rkEscape(status)}</span></div>
288
+ <div class="rk-item-sub"><span class="rk-mono" id="${id}" data-rk-secret="${rkEscape(k.secret)}">${"•".repeat(18)}</span> · ${rkEscape(k.provider)} · ${lastUsed}</div>
289
+ </div>
290
+ <button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-reveal-text="${id}" aria-label="Show key">${EYE}</button>
291
+ <form method="post" action="/settings/provider-keys/${encodeURIComponent(k.id)}/delete" data-rk-confirm="Remove this AI key?" style="display:inline">
292
+ ${acct}
293
+ <button class="rk-btn rk-btn-danger rk-btn-sm" type="submit">Remove</button>
294
+ </form>
295
+ </div>`;
296
+ }).join("")
297
+ : `<p class="rk-muted rk-set-empty">No AI keys yet — add one to enable clip tagging, recaption, and Swipe.</p>`;
298
+ const developer = `
299
+ <div class="rk-stack rk-set-panel">
300
+ <div class="rk-card">
301
+ <div class="rk-card-head">
302
+ <span class="rk-eyebrow">Developer</span>
303
+ <h2>Your vidfarm API key</h2>
304
+ <p class="rk-muted">Authenticates the <code>vidfarm</code> CLI and REST calls. Keep it secret.</p>
305
+ </div>
306
+ <div class="rk-field">
307
+ <div class="rk-input-group">
308
+ <input class="rk-input rk-mono" id="rk-vfkey" type="password" value="${rkEscape(input.vidfarmApiKey)}" readonly />
309
+ <button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-reveal="rk-vfkey" aria-label="Show key">${EYE}</button>
310
+ <button class="rk-btn rk-btn-ink rk-btn-sm" type="button" data-rk-copy="rk-vfkey">Copy</button>
311
+ </div>
312
+ </div>
313
+ <div class="rk-row rk-wrap rk-set-skillrow">
314
+ <button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-skill-copy data-rk-skill-content="${rkEscape(input.directorSkill || "")}">Copy Director SKILL.md</button>
315
+ <span class="rk-hint">The full REST + CLI playbook for driving vidfarm from an agent — one click, paste into your agent.</span>
316
+ </div>
317
+ </div>
318
+ <div class="rk-card">
319
+ <div class="rk-card-head">
320
+ <span class="rk-eyebrow">AI keys</span>
321
+ <h2>Bring your own model keys</h2>
322
+ <p class="rk-muted">Stored per workspace. Used for clip tagging, recaption, and Swipe — never billed by vidfarm.</p>
323
+ </div>
324
+ <div class="rk-list rk-set-keys">${keyRows}</div>
325
+ <form class="rk-set-addkey" method="post" action="/settings/provider-keys">
326
+ ${acct}
327
+ <div class="rk-grid rk-grid-3 rk-set-addkey-grid">
328
+ <div class="rk-field">
329
+ <label class="rk-label" for="rk-prov">Provider</label>
330
+ <select class="rk-select" id="rk-prov" name="provider">
331
+ <option value="openai">OpenAI</option><option value="gemini">Gemini</option><option value="openrouter">OpenRouter</option><option value="anthropic">Anthropic</option>
332
+ </select>
333
+ </div>
334
+ <div class="rk-field">
335
+ <label class="rk-label" for="rk-prov-label">Label</label>
336
+ <input class="rk-input" id="rk-prov-label" name="label" placeholder="Optional" />
337
+ </div>
338
+ <div class="rk-field">
339
+ <label class="rk-label" for="rk-prov-secret">API key</label>
340
+ <input class="rk-input" id="rk-prov-secret" type="password" name="secret" placeholder="sk-…" />
341
+ </div>
342
+ </div>
343
+ <button class="rk-btn rk-btn-gold" type="submit">Add AI key</button>
344
+ </form>
345
+ </div>
346
+ </div>`;
347
+ // ---- Log out ----
348
+ const logout = `
349
+ <div class="rk-card rk-set-panel-card rk-set-logout">
350
+ <div class="rk-card-head">
351
+ <span class="rk-eyebrow">Session</span>
352
+ <h2>Log out</h2>
353
+ <p class="rk-muted">End this browser session. You'll need to sign back in as <b>${rkEscape(input.email)}</b> to return to your studio.</p>
354
+ </div>
355
+ <form method="post" action="/logout" class="rk-row rk-wrap rk-set-logout-actions">
356
+ ${acct}
357
+ <button class="rk-btn rk-btn-danger" type="submit">Log out</button>
358
+ <button class="rk-btn rk-btn-ghost" type="button" data-rk-tab="profile">Cancel</button>
359
+ </form>
360
+ </div>`;
361
+ const tab = (slug, label, danger = false) => `<button type="button" class="rk-side-item${danger ? " rk-side-item-danger" : ""}${slug === activeTab ? " is-active" : ""}" data-rk-tab="${slug}">${label}</button>`;
362
+ const body = `
363
+ <main class="rk-container rk-page">
364
+ <div class="rk-page-head">
365
+ <h1 class="rk-h1">Settings</h1>
366
+ <p class="rk-sub">Manage your profile, wallet, publishing channels, and developer access.</p>
367
+ </div>
368
+ ${flash}
369
+ <div class="rk-shell">
370
+ <nav class="rk-side" aria-label="Settings sections">
371
+ ${tab("profile", "Profile")}
372
+ ${tab("wallet", "Wallet")}
373
+ ${tab("channels", "Channels")}
374
+ ${tab("developer", "Developer")}
375
+ <div class="rk-side-sep"></div>
376
+ <a class="rk-side-item" href="https://billing.stripe.com/p/login/fZecQubkxcwOgDuaEE" target="_blank" rel="noopener noreferrer">Subscription</a>
377
+ ${tab("logout", "Log out", true)}
378
+ </nav>
379
+ <div class="rk-set-panels">
380
+ <section class="rk-reveal" data-rk-panel="profile"${activeTab === "profile" ? "" : " hidden"}>${profile}</section>
381
+ <section class="rk-reveal" data-rk-panel="wallet"${activeTab === "wallet" ? "" : " hidden"}>${wallet}</section>
382
+ <section class="rk-reveal" data-rk-panel="channels"${activeTab === "channels" ? "" : " hidden"}>${channels}</section>
383
+ <section class="rk-reveal" data-rk-panel="developer"${activeTab === "developer" ? "" : " hidden"}>${developer}</section>
384
+ <section class="rk-reveal" data-rk-panel="logout" hidden>${logout}</section>
385
+ </div>
386
+ </div>
387
+ </main>`;
388
+ const pageCss = `
389
+ .rk-tile-txt{font-family:var(--rk-font-display);font-weight:700;font-size:14px;letter-spacing:.01em;line-height:1}
390
+ .rk-tile-txt.rk-tile-sm{font-size:15px}
391
+ .rk-set-flash{max-width:760px;margin-bottom:20px}
392
+ .rk-set-panel-card,.rk-set-panel{max-width:760px}
393
+ .rk-set-panel-card + .rk-set-panel-card{margin-top:20px}
394
+ .rk-set-empty{padding:8px 2px}
395
+ .rk-set-drop{display:flex;align-items:center;gap:14px;padding:20px;border:1.5px dashed var(--rk-border-strong);border-radius:var(--rk-r-2xl);background:var(--rk-n-50);font-size:14px;color:var(--rk-text-muted);cursor:pointer}
396
+ .rk-set-drop:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint)}
397
+ .rk-set-file{margin-left:auto;font-size:13px;max-width:220px}
398
+ .rk-set-attach{margin-top:4px}
399
+ .rk-set-balance{display:grid;gap:10px;align-content:start}
400
+ .rk-set-balance-num{font-size:var(--rk-text-5xl)}
401
+ .rk-set-balance-cta{margin-top:6px}
402
+ .rk-set-topup-status{margin-top:4px}
403
+ .rk-set-amount{font-family:var(--rk-font-display);font-weight:700;font-size:15px;color:var(--rk-n-600);white-space:nowrap}
404
+ .rk-set-amount.is-credit{color:#16a34a}
405
+ .rk-set-keys{margin-bottom:20px}
406
+ .rk-set-addkey{border-top:1px solid var(--rk-border);padding-top:20px;display:grid;gap:16px}
407
+ .rk-set-addkey-grid{gap:14px}
408
+ .rk-set-skillrow{margin-top:14px;padding-top:14px;border-top:1px solid var(--rk-border)}
409
+ .rk-set-skillrow .rk-hint{flex:1;min-width:200px}
410
+
411
+ /* log-out tab — danger-styled side item + a hairline separator above it */
412
+ .rk-side-sep{height:1px;background:var(--rk-border);margin:8px 6px}
413
+ @media(max-width:860px){.rk-side-sep{display:none}}
414
+ .rk-side-item-danger{color:var(--rk-red)}
415
+ .rk-side-item-danger:hover{background:var(--rk-red-tint);color:#b91c1c}
416
+ .rk-side-item-danger.is-active{background:var(--rk-red-tint);color:#b91c1c}
417
+ .rk-set-logout{max-width:760px}
418
+ .rk-set-logout-actions{margin-top:2px}
419
+
420
+ /* channels topline: heading left, Connect + "…" menu right (mirrors live) */
421
+ .rk-set-channels-topline{align-items:flex-start;gap:16px}
422
+ .rk-set-channels-headcopy{margin-bottom:14px;flex:1;min-width:0}
423
+ .rk-set-channels-actions{flex:none;gap:8px}
424
+ .rk-set-menu-wrap{position:relative}
425
+ .rk-set-menu-btn{width:34px;padding:7px 0;font-weight:800;letter-spacing:.04em}
426
+ .rk-set-menu{position:absolute;right:0;top:calc(100% + 6px);z-index:20;min-width:190px;background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);box-shadow:var(--rk-shadow-lg);padding:6px;display:grid}
427
+ .rk-set-menu[hidden]{display:none}
428
+ .rk-set-menu button{display:block;width:100%;text-align:left;padding:9px 12px;border:0;background:transparent;border-radius:var(--rk-r-md);font-family:var(--rk-font-body);font-size:13.5px;font-weight:600;color:var(--rk-n-700);cursor:pointer}
429
+ .rk-set-menu button:hover{background:var(--rk-n-100);color:var(--rk-ink)}
430
+ .rk-set-chsearch{position:relative;display:block;margin-bottom:14px}
431
+ .rk-set-chsearch-ic{position:absolute;left:13px;top:50%;transform:translateY(-50%);color:var(--rk-n-400);pointer-events:none;display:flex}
432
+ .rk-set-chsearch .rk-input{padding-left:38px;border-radius:var(--rk-r-full)}
433
+ .rk-set-chsearch .rk-input:disabled{background:var(--rk-n-100);cursor:not-allowed}
434
+ [data-rk-channel-hay][hidden]{display:none}
435
+
436
+ /* shared dialog chrome (FlockPoster connect + email channel) */
437
+ .rk-set-dialog{width:min(30rem,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)}
438
+ .rk-set-dialog::backdrop{background:rgba(23,23,23,.34);backdrop-filter:blur(6px)}
439
+ .rk-set-dialog-form{display:grid;gap:16px;margin:0;padding:26px}
440
+ .rk-set-dialog-actions{display:flex;justify-content:flex-end;align-items:center;gap:10px;margin-top:2px}
441
+ .rk-set-dialog-actions .rk-btn:first-child{margin-right:auto}
442
+ .rk-set-email-note{min-height:76px}
443
+
444
+ /* My Files — inline, always-open file explorer (reuses the chat panel's
445
+ rk-aichat-f* directory-nav visuals) with My Files + Temp as root folders */
446
+ .rk-set-myfiles{display:grid;gap:16px}
447
+ .rk-set-upload{flex:none}
448
+ .rk-set-explorer{border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);background:var(--rk-n-50);overflow:hidden}
449
+ .rk-set-fx-crumbs{padding:12px 14px 8px}
450
+ .rk-set-fx-body{max-height:360px;min-height:120px;overflow-y:auto;padding:2px 10px 12px;display:grid;gap:2px;align-content:start}
451
+ `;
452
+ const script = `
453
+ (function(){
454
+ var root=document;
455
+ function show(tab){
456
+ root.querySelectorAll('[data-rk-tab]').forEach(function(b){b.classList.toggle('is-active',b.getAttribute('data-rk-tab')===tab)});
457
+ root.querySelectorAll('[data-rk-panel]').forEach(function(p){p.hidden=p.getAttribute('data-rk-panel')!==tab});
458
+ if(history.replaceState)history.replaceState(null,'','#'+tab);
459
+ }
460
+ root.querySelectorAll('[data-rk-tab]').forEach(function(btn){
461
+ btn.addEventListener('click',function(){show(btn.getAttribute('data-rk-tab'));});
462
+ });
463
+ var hash=(location.hash||'').replace('#','');
464
+ if(hash&&root.querySelector('[data-rk-tab="'+hash+'"]'))show(hash);
465
+ // reveal password inputs
466
+ root.querySelectorAll('[data-rk-reveal]').forEach(function(btn){
467
+ btn.addEventListener('click',function(){var el=root.getElementById(btn.getAttribute('data-rk-reveal'));if(el)el.type=el.type==='password'?'text':'password';});
468
+ });
469
+ // reveal masked secret text
470
+ root.querySelectorAll('[data-rk-reveal-text]').forEach(function(btn){
471
+ btn.addEventListener('click',function(){var el=root.getElementById(btn.getAttribute('data-rk-reveal-text'));if(!el)return;var real=el.getAttribute('data-rk-secret');var shown=el.getAttribute('data-rk-shown')==='1';el.textContent=shown?'\\u2022'.repeat(18):real;el.setAttribute('data-rk-shown',shown?'0':'1');});
472
+ });
473
+ // copy
474
+ root.querySelectorAll('[data-rk-copy]').forEach(function(btn){
475
+ btn.addEventListener('click',function(){var el=root.getElementById(btn.getAttribute('data-rk-copy'));if(!el||!navigator.clipboard)return;navigator.clipboard.writeText(el.value||el.textContent||'').then(function(){var t=btn.textContent;btn.textContent='Copied!';setTimeout(function(){btn.textContent=t;},1200);});});
476
+ });
477
+ // destructive form confirmations
478
+ root.querySelectorAll('form[data-rk-confirm]').forEach(function(f){
479
+ f.addEventListener('submit',function(e){if(!window.confirm(f.getAttribute('data-rk-confirm')||'Are you sure?'))e.preventDefault();});
480
+ });
481
+ // director skill — 1-click copy (content travels in the attribute, like live)
482
+ var skillCopy=root.querySelector('[data-rk-skill-copy]');
483
+ if(skillCopy){
484
+ skillCopy.addEventListener('click',function(){
485
+ var text=skillCopy.getAttribute('data-rk-skill-content')||'';
486
+ if(!navigator.clipboard)return;
487
+ navigator.clipboard.writeText(text).then(function(){
488
+ var t=skillCopy.textContent;skillCopy.textContent='Copied!';
489
+ setTimeout(function(){skillCopy.textContent=t;},1200);
490
+ });
491
+ });
492
+ }
493
+ // My Files — inline, always-open file explorer. Same directory-nav model as
494
+ // the chat pop panel: a single virtual root whose top-level entries are the
495
+ // "My Files" and "Temp" folders; navigate into them natively. Files open in
496
+ // a new tab (view URL); no drawer, no source tabs.
497
+ (function(){
498
+ var fx=root.querySelector('[data-rk-fx]');
499
+ if(!fx) return;
500
+ var crumbsEl=fx.querySelector('[data-rk-fx-crumbs]');
501
+ var bodyEl=fx.querySelector('[data-rk-fx-body]');
502
+ var ROOTS={
503
+ my_files:{url:'/api/v1/user/me/attachments',key:'attachments',label:'My Files'},
504
+ temp:{url:'/api/v1/user/me/temporary-files',key:'files',label:'Temp'}
505
+ };
506
+ var VROOTS=[{key:'my_files',label:'My Files'},{key:'temp',label:'Temp'}];
507
+ var st={root:'',path:{my_files:'',temp:''},data:{my_files:null,temp:null},status:{my_files:'idle',temp:'idle'}};
508
+
509
+ function normFolder(v){ return String(v||'').split('/').map(function(s){return s.trim();}).filter(Boolean).join('/'); }
510
+ function normItem(raw){
511
+ var id=raw.id||raw.attachment_id||raw.file_id||''; var name=raw.fileName||raw.file_name||'';
512
+ if(!id||!name) return null;
513
+ return {id:id,name:name,contentType:raw.contentType||raw.content_type||'',viewUrl:raw.viewUrl||raw.view_url||'',
514
+ sizeBytes:typeof raw.sizeBytes==='number'?raw.sizeBytes:raw.size_bytes,folderPath:normFolder(raw.folderPath||raw.folder_path||'')};
515
+ }
516
+ function fmtBytes(b){ if(!b||b<=0) return ''; var u=['B','KB','MB','GB'],v=b,i=0; while(v>=1024&&i<u.length-1){v/=1024;i++;} return (v>=10||i===0?Math.round(v):v.toFixed(1))+' '+u[i]; }
517
+ function immediateFolders(data,current){
518
+ var cur=normFolder(current); var prefix=cur?cur+'/':''; var names={};
519
+ function consider(fp){ var n=normFolder(fp); if(!n||(cur&&n===cur)||n.indexOf(prefix)!==0) return; var child=n.slice(prefix.length).split('/')[0]; if(child) names[child]=prefix+child; }
520
+ (data.folders||[]).forEach(consider);
521
+ (data.items||[]).forEach(function(it){ consider(it.folderPath); });
522
+ return Object.keys(names).sort().map(function(n){ return {name:n,path:names[n]}; });
523
+ }
524
+ function loadRoot(rootKey,force){
525
+ if(!ROOTS[rootKey]) return;
526
+ if(!force&&st.status[rootKey]==='ready'){ render(); return; }
527
+ st.status[rootKey]='loading'; render();
528
+ fetch(ROOTS[rootKey].url,{credentials:'same-origin',headers:{accept:'application/json'}})
529
+ .then(function(r){ if(r.status===401||r.status===403){ st.status[rootKey]='auth'; return null; } if(!r.ok) throw new Error('http '+r.status); return r.json(); })
530
+ .then(function(j){ if(!j){ render(); return; } var raw=j[ROOTS[rootKey].key]||[]; var items=[]; for(var i=0;i<raw.length;i++){ var n=normItem(raw[i]); if(n) items.push(n); } st.data[rootKey]={items:items,folders:j.folders||[]}; st.status[rootKey]='ready'; render(); })
531
+ .catch(function(){ st.status[rootKey]='error'; render(); });
532
+ }
533
+ function stateNode(text,opts){
534
+ var wrap=document.createElement('div'); wrap.className='rk-aichat-fstate';
535
+ var p=document.createElement('div'); p.textContent=text; wrap.appendChild(p);
536
+ if(opts&&opts.href){ var a=document.createElement('a'); a.href=opts.href; a.textContent=opts.linkText||'Open'; wrap.appendChild(a); }
537
+ if(opts&&opts.retry){ var b=document.createElement('button'); b.type='button'; b.textContent='Try again'; b.addEventListener('click',function(){ loadRoot(st.root,true); }); wrap.appendChild(b); }
538
+ return wrap;
539
+ }
540
+ function folderRow(label,onOpen,sub){
541
+ var b=document.createElement('button'); b.type='button'; b.className='rk-aichat-frow';
542
+ var ic=document.createElement('span'); ic.className='rk-aichat-fic rk-aichat-fic-folder'; ic.textContent='/';
543
+ var main=document.createElement('span'); main.className='rk-aichat-fmain';
544
+ var name=document.createElement('span'); name.className='rk-aichat-fname'; name.textContent=label; main.appendChild(name);
545
+ if(sub){ var meta=document.createElement('span'); meta.className='rk-aichat-fmeta'; meta.textContent=sub; main.appendChild(meta); }
546
+ var chev=document.createElement('span'); chev.className='rk-aichat-fadd'; chev.textContent='Open \\u203a';
547
+ b.appendChild(ic); b.appendChild(main); b.appendChild(chev);
548
+ b.addEventListener('click',onOpen);
549
+ return b;
550
+ }
551
+ function vrootRow(v){
552
+ var sub=''; var s=st.status[v.key]; var d=st.data[v.key];
553
+ if(s==='ready'&&d) sub=d.items.length+(d.items.length===1?' file':' files');
554
+ else if(s==='loading') sub='Loading\\u2026'; else if(s==='auth') sub='Sign in to view';
555
+ return folderRow(v.label,function(){ enterRoot(v.key); },sub);
556
+ }
557
+ function fileRow(it){
558
+ var open=it.viewUrl?document.createElement('a'):document.createElement('div');
559
+ open.className='rk-aichat-frow';
560
+ if(it.viewUrl){ open.href=it.viewUrl; open.target='_blank'; open.rel='noopener'; }
561
+ var ic=document.createElement('span'); ic.className='rk-aichat-fic rk-aichat-fic-file';
562
+ if(it.viewUrl&&it.contentType.indexOf('image/')===0){ var img=document.createElement('img'); img.src=it.viewUrl; img.alt=''; img.loading='lazy'; ic.appendChild(img); }
563
+ else if(it.viewUrl&&it.contentType.indexOf('video/')===0){ var vid=document.createElement('video'); vid.src=it.viewUrl; vid.muted=true; vid.playsInline=true; ic.appendChild(vid); }
564
+ else { ic.textContent=(it.name.split('.').pop()||it.name.slice(0,1)).slice(0,3).toUpperCase(); }
565
+ var main=document.createElement('span'); main.className='rk-aichat-fmain';
566
+ var name=document.createElement('span'); name.className='rk-aichat-fname'; name.textContent=it.name;
567
+ var meta=document.createElement('span'); meta.className='rk-aichat-fmeta'; meta.textContent=fmtBytes(it.sizeBytes)||it.contentType||'file';
568
+ main.appendChild(name); main.appendChild(meta);
569
+ var add=document.createElement('span'); add.className='rk-aichat-fadd'; add.textContent=it.viewUrl?'Open \\u2197':'';
570
+ open.appendChild(ic); open.appendChild(main); open.appendChild(add);
571
+ return open;
572
+ }
573
+ function crumb(label,current,onClick){
574
+ var b=document.createElement('button'); b.type='button'; b.className='rk-aichat-crumb'+(current?' is-current':''); b.textContent=label; b.addEventListener('click',onClick); return b;
575
+ }
576
+ function crumbSep(){ var s=document.createElement('span'); s.className='rk-aichat-crumb-sep'; s.textContent='/'; return s; }
577
+ function renderCrumbs(){
578
+ crumbsEl.innerHTML='';
579
+ var atVirtual=!st.root;
580
+ crumbsEl.appendChild(crumb('Files',atVirtual,goVirtualRoot));
581
+ if(atVirtual) return;
582
+ if(st.status[st.root]!=='ready') return;
583
+ var cur=normFolder(st.path[st.root]);
584
+ crumbsEl.appendChild(crumbSep());
585
+ crumbsEl.appendChild(crumb(ROOTS[st.root].label,!cur,function(){ goPath(''); }));
586
+ var parts=cur.split('/').filter(Boolean); var acc='';
587
+ parts.forEach(function(part,idx){ crumbsEl.appendChild(crumbSep()); acc=acc?acc+'/'+part:part; var pth=acc; crumbsEl.appendChild(crumb(part,idx===parts.length-1,function(){ goPath(pth); })); });
588
+ }
589
+ var rendering=false;
590
+ function render(){ if(rendering) return; rendering=true; try{ renderInner(); } finally{ rendering=false; } }
591
+ function renderInner(){
592
+ renderCrumbs(); bodyEl.innerHTML='';
593
+ if(!st.root){
594
+ VROOTS.forEach(function(v){ if(st.status[v.key]==='idle') loadRoot(v.key,false); });
595
+ VROOTS.forEach(function(v){ bodyEl.appendChild(vrootRow(v)); });
596
+ return;
597
+ }
598
+ var s=st.status[st.root];
599
+ if(s==='idle'){ loadRoot(st.root,false); return; }
600
+ if(s==='loading'){ bodyEl.appendChild(stateNode('Loading your files\\u2026')); return; }
601
+ if(s==='auth'){ bodyEl.appendChild(stateNode('Sign in to browse your files.',{href:'/login',linkText:'Sign in'})); return; }
602
+ if(s==='error'){ bodyEl.appendChild(stateNode('Couldn\\u2019t reach your files.',{retry:true})); return; }
603
+ var data=st.data[st.root]||{items:[],folders:[]};
604
+ var cur=normFolder(st.path[st.root]);
605
+ var folders=immediateFolders(data,cur);
606
+ var here=data.items.filter(function(it){ return normFolder(it.folderPath)===cur; });
607
+ if(!folders.length&&!here.length){ bodyEl.appendChild(stateNode(cur?'This folder is empty.':'No files in '+ROOTS[st.root].label+' yet.')); return; }
608
+ folders.forEach(function(f){ bodyEl.appendChild(folderRow(f.name,function(){ goPath(f.path); })); });
609
+ here.forEach(function(it){ bodyEl.appendChild(fileRow(it)); });
610
+ }
611
+ function goPath(p){ st.path[st.root]=normFolder(p); render(); }
612
+ function goVirtualRoot(){ st.root=''; render(); }
613
+ function enterRoot(rootKey){ if(!ROOTS[rootKey]) return; st.root=rootKey; st.path[rootKey]=''; loadRoot(rootKey,false); render(); }
614
+ render();
615
+ })();
616
+ // channels: quick search filter
617
+ var chSearch=root.querySelector('[data-rk-channel-search]');
618
+ var chList=root.querySelector('[data-rk-channel-list]');
619
+ var chNone=root.querySelector('[data-rk-channel-noresults]');
620
+ if(chSearch&&chList){
621
+ chSearch.addEventListener('input',function(){
622
+ var q=(chSearch.value||'').trim().toLowerCase();
623
+ var shown=0;
624
+ chList.querySelectorAll('[data-rk-channel-hay]').forEach(function(row){
625
+ var ok=!q||(row.getAttribute('data-rk-channel-hay')||'').indexOf(q)>-1;
626
+ row.hidden=!ok;if(ok)shown++;
627
+ });
628
+ if(chNone)chNone.hidden=!(q&&shown===0);
629
+ });
630
+ }
631
+ // channels: "…" actions menu
632
+ var menuBtn=root.querySelector('[data-rk-channel-menu]');
633
+ var menuPop=root.querySelector('[data-rk-channel-menu-pop]');
634
+ if(menuBtn&&menuPop){
635
+ menuBtn.addEventListener('click',function(e){
636
+ e.stopPropagation();
637
+ var open=menuPop.hidden;
638
+ menuPop.hidden=!open;
639
+ menuBtn.setAttribute('aria-expanded',open?'true':'false');
640
+ });
641
+ root.addEventListener('click',function(e){
642
+ if(!menuPop.hidden&&!menuPop.contains(e.target)&&e.target!==menuBtn){menuPop.hidden=true;menuBtn.setAttribute('aria-expanded','false');}
643
+ });
644
+ }
645
+ // channel dialogs (FlockPoster connect + email channel)
646
+ function wireDialog(dlgSel,openSel){
647
+ var dlg=root.querySelector(dlgSel);
648
+ if(!dlg)return null;
649
+ root.querySelectorAll(openSel).forEach(function(btn){
650
+ btn.addEventListener('click',function(){
651
+ if(menuPop)menuPop.hidden=true;
652
+ if(typeof dlg.showModal==='function'&&!dlg.open)dlg.showModal();
653
+ });
654
+ });
655
+ dlg.addEventListener('click',function(e){
656
+ var r=dlg.getBoundingClientRect();
657
+ if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)dlg.close();
658
+ });
659
+ dlg.querySelectorAll('[data-rk-dialog-close]').forEach(function(btn){btn.addEventListener('click',function(){dlg.close();});});
660
+ return dlg;
661
+ }
662
+ var flockDlg=wireDialog('[data-rk-flock-dialog]','[data-rk-open-flock]');
663
+ wireDialog('[data-rk-email-dialog]','[data-rk-open-email]');
664
+ if(flockDlg&&flockDlg.getAttribute('data-rk-autoopen')==='1'&&typeof flockDlg.showModal==='function'&&!flockDlg.open){flockDlg.showModal();}
665
+ // wallet top-up → real funding link
666
+ var topBtn=root.querySelector('[data-rk-topup]');
667
+ var topStatus=root.querySelector('[data-rk-topup-status]');
668
+ if(topBtn){
669
+ topBtn.addEventListener('click',function(){
670
+ topBtn.disabled=true;if(topStatus){topStatus.hidden=false;topStatus.textContent='Starting secure checkout…';}
671
+ fetch('/settings/wallet/funding-link',{method:'POST',headers:{'content-type':'application/json'},credentials:'same-origin',body:'{}'})
672
+ .then(function(r){return r.json();})
673
+ .then(function(j){if(j&&j.checkout_url){window.location.href=j.checkout_url;}else{throw new Error(j&&j.error||'Unable to start funding');}})
674
+ .catch(function(err){topBtn.disabled=false;if(topStatus){topStatus.hidden=false;topStatus.textContent=String(err.message||err);}});
675
+ });
676
+ }
677
+ })();`;
678
+ return reskinDocument({
679
+ title: "vidfarm reskin — Settings",
680
+ description: "Vidfarm settings — profile, wallet, channels, developer.",
681
+ activeSlug: "settings",
682
+ pageCss,
683
+ body,
684
+ script
685
+ });
686
+ }
687
+ //# sourceMappingURL=settings-page.js.map