@mevdragon/vidfarm-devcli 0.17.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.
@@ -1,131 +1,60 @@
1
- // /reskin/agency — the Agency / client-workspaces screen ported into the
2
- // farmville design system. Uses ONLY shared rk-* primitives + a small
3
- // page-scoped CSS block + a self-contained module script.
1
+ // /reskin/agency — the Agency / client-workspaces screen, MIGRATED to real
2
+ // production data + behavior (farmville design system).
4
3
  //
5
- // Renders with representative SAMPLE data so it can be viewed without auth.
6
- // Faithfully mirrors src/account-pages-legacy.ts renderAgencyPage: an agency
7
- // managing its client sub-accounts. The legacy page is a searchable list of
8
- // cached client accounts (name · email · user id) with "Open Tab" (opens the
9
- // client's /u/:id/library) and "Copy Login" (autologin URL) actions plus an
10
- // "Add Login" button. This reskin keeps that spine and dresses it up with the
11
- // per-client stats the admin agency-link model already tracks (wallet, seats,
12
- // projects) into warm workspace cards.
4
+ // This mirrors src/account-pages-legacy.ts renderAgencyPage: an agency
5
+ // managing its client sub-accounts. The live page's client list is NOT
6
+ // server-fetched it is read CLIENT-SIDE from this browser's localStorage
7
+ // account registry (the same cache the app writes when you sign into a client
8
+ // account), so the server can only render an empty shell + template and the
9
+ // real client cards are built in JS. This reskin replicates that exact read and
10
+ // wires the real per-client actions:
11
+ // "Open workspace" → opens the client's /u/:id/library in a new tab
12
+ // • "Copy login" → copies the client's autologin URL
13
+ // • "Add client login"→ opens /agency-client-login
14
+ // localStorage contract (identical keys/fields to the live page):
15
+ // index key: "vidfarm:agency:accounts:index" → string[] of userIds
16
+ // entry key: "vidfarm:agency:account_<userId>" → { apiKey, email,
17
+ // displayName, lastSeenAt, autologinUrl? }
18
+ // autologinUrl falls back to "/auto-login?api_key=<encodeURIComponent(apiKey)>"
19
+ //
20
+ // The old reskin rendered fabricated per-client stats (projects, scheduled,
21
+ // wallet, seats, plan, status) and aggregate tiles — NONE of those live in the
22
+ // cache, so they are dropped. Cards now show only real cached fields (name,
23
+ // email, user id, last-seen). Avatars are text initials (no emoji).
13
24
  import { reskinDocument, rkEscape } from "./document.js";
14
- const SAMPLE = {
15
- agencyName: "Sunset Social",
16
- ownerEmail: "hey@sunsetsocial.co",
17
- stats: { clients: 7, activeProjects: 34, seatsUsed: 12, seatsTotal: 20, monthlySpendUsd: 486.2 },
18
- clients: [
19
- { name: "Meadow Coffee Co.", email: "ops@meadowcoffee.com", userId: "usr_4b12a9", plan: "Pro", status: "active", tint: "gold", projects: 9, scheduled: 6, walletUsd: 42.8, seats: 3, lastSeen: "2h ago" },
20
- { name: "Northwind Outdoors", email: "team@northwind.gear", userId: "usr_9f0c31", plan: "Pro", status: "active", tint: "green", projects: 7, scheduled: 4, walletUsd: 88.15, seats: 2, lastSeen: "5h ago" },
21
- { name: "Lumen Skincare", email: "grow@lumenskin.co", userId: "usr_2d77e4", plan: "Growth", status: "active", tint: "violet", projects: 6, scheduled: 8, walletUsd: 24.0, seats: 2, lastSeen: "yesterday" },
22
- { name: "Harborline Fitness", email: "studio@harborline.fit", userId: "usr_71aa08", plan: "Starter", status: "trial", tint: "sky", projects: 4, scheduled: 2, walletUsd: 5.0, seats: 1, lastSeen: "3d ago" },
23
- { name: "Cedar & Co. Home", email: "hello@cedarandco.com", userId: "usr_58bb1c", plan: "Pro", status: "active", tint: "coral", projects: 5, scheduled: 3, walletUsd: 61.4, seats: 2, lastSeen: "6h ago" },
24
- { name: "Beacon Legal", email: "marketing@beaconlegal.io", userId: "usr_3c90f7", plan: "Growth", status: "paused", tint: "ink", projects: 2, scheduled: 0, walletUsd: 12.9, seats: 1, lastSeen: "2w ago" },
25
- { name: "Pixel Pantry", email: "founders@pixelpantry.app", userId: "usr_6e14d0", plan: "Starter", status: "trial", tint: "sky", projects: 1, scheduled: 1, walletUsd: 5.0, seats: 1, lastSeen: "1d ago" }
26
- ]
27
- };
28
- const STATUS_PILL = {
29
- active: { cls: "rk-pill-green", label: "Active", dot: "●" },
30
- trial: { cls: "rk-pill-sky", label: "Trial", dot: "◐" },
31
- paused: { cls: "rk-pill-red", label: "Paused", dot: "◌" }
32
- };
33
- function money(n) {
34
- const frac = Number.isInteger(n) ? 0 : 2;
35
- return `$${n.toLocaleString("en-US", { minimumFractionDigits: frac, maximumFractionDigits: 2 })}`;
36
- }
37
- function initials(name) {
38
- const letters = name
39
- .replace(/&/g, " ")
40
- .split(/\s+/)
41
- .map((w) => (w.match(/[a-z0-9]/i) || [""])[0])
42
- .filter(Boolean);
43
- const out = `${letters[0] || ""}${letters[1] || ""}`;
44
- return (out || "?").toUpperCase();
45
- }
46
- function statTile(label, value, sub) {
47
- return `<div class="rk-card rk-card-pad-sm rk-agency-stat">
48
- <div class="rk-stat-label">${rkEscape(label)}</div>
49
- <div class="rk-stat-num">${value}</div>
50
- <div class="rk-agency-stat-sub">${rkEscape(sub)}</div>
51
- </div>`;
52
- }
53
- function clientCard(c) {
54
- const pill = STATUS_PILL[c.status];
55
- const search = `${c.name} ${c.email} ${c.userId}`.toLowerCase();
56
- return `<article class="rk-card rk-card-hover rk-agency-card" data-rk-client="${rkEscape(search)}">
57
- <div class="rk-agency-card-top">
58
- <span class="rk-tile rk-tile-lg rk-tile-${c.tint} rk-agency-avatar">${rkEscape(initials(c.name))}</span>
59
- <div class="rk-agency-card-id">
60
- <h3 class="rk-agency-name">${rkEscape(c.name)}</h3>
61
- <div class="rk-agency-email" role="button" tabindex="0" data-rk-copy-text="${rkEscape(c.email)}" title="Copy email">${rkEscape(c.email)}</div>
62
- </div>
63
- <span class="rk-pill ${pill.cls} rk-agency-status">${pill.dot} ${pill.label}</span>
64
- </div>
65
- <div class="rk-agency-meta">
66
- <span class="rk-pill rk-pill-gold">${rkEscape(c.plan)} plan</span>
67
- <span class="rk-pill">${c.seats} ${c.seats === 1 ? "seat" : "seats"}</span>
68
- <span class="rk-agency-user" role="button" tabindex="0" data-rk-copy-text="${rkEscape(c.userId)}" title="Copy user id">${rkEscape(c.userId)}</span>
69
- </div>
70
- <div class="rk-agency-minis">
71
- <div class="rk-agency-mini">
72
- <div class="rk-agency-mini-num">${c.projects}</div>
73
- <div class="rk-agency-mini-lbl">Projects</div>
74
- </div>
75
- <div class="rk-agency-mini">
76
- <div class="rk-agency-mini-num">${c.scheduled}</div>
77
- <div class="rk-agency-mini-lbl">Scheduled</div>
78
- </div>
79
- <div class="rk-agency-mini">
80
- <div class="rk-agency-mini-num">${money(c.walletUsd)}</div>
81
- <div class="rk-agency-mini-lbl">Wallet</div>
82
- </div>
83
- </div>
84
- <div class="rk-agency-card-foot">
85
- <a class="rk-btn rk-btn-ghost rk-btn-sm rk-agency-open" href="/u/${encodeURIComponent(c.userId)}/library" target="_blank" rel="noreferrer">Open workspace <span class="rk-arrow">→</span></a>
86
- <button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-copy-login="/auto-login?client=${encodeURIComponent(c.userId)}">Copy login</button>
87
- <span class="rk-agency-seen">Active ${rkEscape(c.lastSeen)}</span>
88
- </div>
89
- </article>`;
90
- }
91
- export function renderReskinAgency() {
92
- const a = SAMPLE;
93
- const s = a.stats;
94
- const stats = `
95
- <div class="rk-grid rk-grid-4 rk-agency-stats">
96
- ${statTile("Client workspaces", String(s.clients), "all sub-accounts you manage")}
97
- ${statTile("Active projects", String(s.activeProjects), "across every client")}
98
- ${statTile("Seats used", `${s.seatsUsed}<span class="rk-agency-stat-of"> / ${s.seatsTotal}</span>`, "team members with access")}
99
- ${statTile("Monthly spend", money(s.monthlySpendUsd), "pooled render + clip usage")}
100
- </div>`;
101
- const clientCards = a.clients.map(clientCard).join("");
102
- const inviteCard = `<article class="rk-card rk-agency-invite" data-rk-client="__invite__">
103
- <span class="rk-tile rk-tile-lg rk-tile-gold rk-agency-invite-ic">+</span>
25
+ export function renderReskinAgency(input = { userId: null, currentAccountId: null, name: null, email: null, loggedIn: false }) {
26
+ const loggedIn = input.loggedIn ?? Boolean(input.userId);
27
+ const signedInHint = loggedIn && input.email
28
+ ? `<span class="rk-agency-hint">Managing from ${rkEscape(input.email)}</span>`
29
+ : "";
30
+ const PLUS = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>`;
31
+ // The invite / "add a client workspace" card is a static, always-real CTA to
32
+ // the /agency-client-login flow. It lives inside the grid alongside the
33
+ // JS-injected client cards (which render into [data-rk-agency-cards]).
34
+ const inviteCard = `<article class="rk-card rk-agency-invite" data-rk-agency-invite>
35
+ <span class="rk-tile rk-tile-lg rk-tile-gold rk-agency-invite-ic">${PLUS}</span>
104
36
  <h3 class="rk-agency-invite-title">Add a client workspace</h3>
105
37
  <p class="rk-muted rk-agency-invite-copy">Link a client's account and get an autologin so your team can post on their behalf.</p>
106
- <button class="rk-btn rk-btn-gold" type="button" data-rk-add-client>Add client <span class="rk-arrow">→</span></button>
38
+ <a class="rk-btn rk-btn-gold" href="/agency-client-login" target="_blank" rel="noreferrer">Add client login <span class="rk-arrow">→</span></a>
107
39
  </article>`;
108
40
  const body = `
109
41
  <main class="rk-container rk-page">
110
42
  <div class="rk-page-head rk-agency-head">
111
43
  <div class="rk-agency-head-copy">
112
- <span class="rk-badge">Agency workspace</span>
113
- <h1 class="rk-h1">Agency</h1>
114
- <p class="rk-sub">Manage every client workspace from one seat — ${a.clients.length} accounts, shared seats, and one pooled bill.</p>
44
+ <h1 class="rk-h1">Agency clients</h1>
45
+ <p class="rk-sub">Every client workspace cached on this device — open their library, or copy a one-tap login to post on their behalf.</p>
115
46
  </div>
116
47
  <div class="rk-agency-head-cta">
117
- <a class="rk-btn rk-btn-ghost" href="/agency-client-login" target="_blank" rel="noreferrer">Add login</a>
118
- <button class="rk-btn rk-btn-gold rk-btn-lg" type="button" data-rk-add-client>Add client <span class="rk-arrow">→</span></button>
48
+ ${signedInHint}
49
+ <a class="rk-btn rk-btn-gold rk-btn-lg" href="/agency-client-login" target="_blank" rel="noreferrer">Add client login <span class="rk-arrow">→</span></a>
119
50
  </div>
120
51
  </div>
121
52
 
122
- ${stats}
123
-
124
53
  <section class="rk-agency-panel">
125
54
  <div class="rk-spread rk-wrap rk-agency-toolbar">
126
55
  <div class="rk-agency-toolbar-copy">
127
56
  <span class="rk-eyebrow">Clients</span>
128
- <h2 class="rk-agency-h2">${a.clients.length} client workspaces</h2>
57
+ <h2 class="rk-agency-h2"><span data-rk-agency-count>0</span> client workspace<span data-rk-agency-plural>s</span></h2>
129
58
  </div>
130
59
  <div class="rk-agency-search-wrap">
131
60
  <span class="rk-agency-search-ic" aria-hidden="true"><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></span>
@@ -133,27 +62,21 @@ export function renderReskinAgency() {
133
62
  </div>
134
63
  </div>
135
64
  <div class="rk-grid rk-grid-3 rk-agency-grid" data-rk-agency-grid>
136
- ${clientCards}
65
+ <div class="rk-agency-cards" data-rk-agency-cards></div>
137
66
  ${inviteCard}
138
67
  </div>
139
68
  <div class="rk-agency-empty" data-rk-agency-empty hidden>
140
- <p class="rk-muted">No client workspaces match that search.</p>
69
+ <p class="rk-muted" data-rk-agency-empty-msg>No cached accounts found in this browser yet.</p>
141
70
  </div>
142
71
  </section>
143
72
  </main>`;
144
73
  const pageCss = `
145
74
  .rk-agency-head{grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:20px}
146
- .rk-agency-head-copy{display:grid;gap:8px;min-width:0}
147
- .rk-agency-head-copy .rk-badge{justify-self:start}
148
- .rk-agency-head-cta{display:flex;align-items:center;gap:10px;flex:none}
75
+ .rk-agency-head-copy{display:grid;gap:4px;min-width:0}
76
+ .rk-agency-head-cta{display:flex;align-items:center;gap:12px;flex:none}
77
+ .rk-agency-hint{font-size:13px;color:var(--rk-text-muted);white-space:nowrap}
149
78
  @media(max-width:760px){.rk-agency-head{grid-template-columns:1fr}.rk-agency-head-cta{flex-wrap:wrap}}
150
79
 
151
- .rk-agency-stats{margin-bottom:28px}
152
- .rk-agency-stat{display:grid;gap:6px;align-content:start}
153
- .rk-agency-stat-ic{display:grid;place-items:center;width:24px;height:24px;border-radius:8px;background:var(--rk-gold-tint);font-size:13px}
154
- .rk-agency-stat-sub{font-size:12px;color:var(--rk-text-faint)}
155
- .rk-agency-stat-of{font-size:var(--rk-text-xl);color:var(--rk-text-faint);font-weight:700}
156
-
157
80
  .rk-agency-panel{background:var(--rk-bg-section);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);padding:24px}
158
81
  .rk-agency-toolbar{margin-bottom:20px}
159
82
  .rk-agency-toolbar-copy{display:grid;gap:4px}
@@ -164,6 +87,8 @@ export function renderReskinAgency() {
164
87
  @media(max-width:620px){.rk-agency-search-wrap{width:100%}}
165
88
 
166
89
  .rk-agency-grid{align-items:stretch}
90
+ /* the JS card wrapper is transparent to the grid so cards + invite share tracks */
91
+ .rk-agency-cards{display:contents}
167
92
  .rk-agency-card{padding:22px;display:flex;flex-direction:column;gap:16px}
168
93
  .rk-agency-card-top{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:start}
169
94
  .rk-agency-avatar{font-family:var(--rk-font-display);font-weight:800;font-size:22px}
@@ -171,81 +96,200 @@ export function renderReskinAgency() {
171
96
  .rk-agency-name{font-size:var(--rk-text-lg);font-weight:800;font-family:var(--rk-font-display);color:var(--rk-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
172
97
  .rk-agency-email{font-size:13px;color:var(--rk-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;transition:color var(--rk-dur) var(--rk-ease)}
173
98
  .rk-agency-email:hover{color:var(--rk-gold-700)}
174
- .rk-agency-status{flex:none;align-self:start;white-space:nowrap}
175
- .rk-agency-meta{display:flex;align-items:center;flex-wrap:wrap;gap:8px}
176
- .rk-agency-user{font-family:var(--rk-font-mono);font-size:12px;color:var(--rk-text-faint);cursor:pointer;transition:color var(--rk-dur) var(--rk-ease)}
99
+ .rk-agency-seen-pill{flex:none;align-self:start;white-space:nowrap}
100
+ .rk-agency-idrow{display:flex}
101
+ .rk-agency-user{font-family:var(--rk-font-mono);font-size:12px;color:var(--rk-text-faint);cursor:pointer;transition:color var(--rk-dur) var(--rk-ease);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
177
102
  .rk-agency-user:hover{color:var(--rk-n-600)}
178
- .rk-agency-minis{display:grid;grid-template-columns:repeat(3,1fr);gap:2px;background:var(--rk-n-100);border-radius:var(--rk-r-xl);padding:14px 10px;margin-top:auto}
179
- .rk-agency-mini{display:grid;gap:3px;justify-items:center;text-align:center;position:relative}
180
- .rk-agency-mini+.rk-agency-mini::before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);height:26px;width:1px;background:var(--rk-border)}
181
- .rk-agency-mini-num{font-family:var(--rk-font-display);font-weight:800;font-size:1.1rem;color:var(--rk-ink);letter-spacing:-.02em}
182
- .rk-agency-mini-lbl{font-size:11px;font-weight:600;color:var(--rk-text-muted);text-transform:uppercase;letter-spacing:.03em}
183
- .rk-agency-card-foot{display:flex;align-items:center;flex-wrap:wrap;gap:8px}
103
+ .rk-agency-card-foot{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:auto}
184
104
  .rk-agency-open{font-weight:600}
185
- .rk-agency-seen{margin-left:auto;font-size:12px;color:var(--rk-text-faint);white-space:nowrap}
186
105
 
187
106
  .rk-agency-invite{border-style:dashed;border-color:var(--rk-border-strong);background:linear-gradient(180deg,var(--rk-gold-tint),#fff 70%);display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px;min-height:240px}
188
- .rk-agency-invite-ic{font-size:26px}
107
+ .rk-agency-invite-ic{display:grid;place-items:center}
189
108
  .rk-agency-invite-title{font-size:var(--rk-text-lg);font-family:var(--rk-font-display);font-weight:800;color:var(--rk-ink)}
190
109
  .rk-agency-invite-copy{font-size:13px;max-width:26ch}
191
110
 
192
111
  .rk-agency-empty{display:none;flex-direction:column;align-items:center;gap:12px;padding:56px 20px;text-align:center}
193
112
  .rk-agency-empty[data-rk-shown="1"]{display:flex}
194
113
  `;
114
+ // Client-side: replicate the live localStorage account-registry read, build
115
+ // real farmville client cards, and wire the real actions. Written with string
116
+ // concatenation (not template literals) to avoid nested-`${}` escaping.
195
117
  const script = `
196
118
  (function(){
119
+ var ACCOUNT_REGISTRY_INDEX_KEY = "vidfarm:agency:accounts:index";
120
+ var ACCOUNT_REGISTRY_ENTRY_PREFIX = "vidfarm:agency:account_";
121
+ var TINTS = ["gold","sky","violet","green","coral","ink"];
122
+
197
123
  var root=document;
198
- var grid=root.querySelector('[data-rk-agency-grid]');
124
+ var cardsWrap=root.querySelector('[data-rk-agency-cards]');
125
+ var invite=root.querySelector('[data-rk-agency-invite]');
199
126
  var search=root.querySelector('[data-rk-agency-search]');
200
127
  var empty=root.querySelector('[data-rk-agency-empty]');
201
- // client-side search filter over the workspace cards
202
- function filter(){
203
- if(!grid)return;
204
- var q=(search&&search.value||'').trim().toLowerCase();
205
- var visible=0;
206
- grid.querySelectorAll('[data-rk-client]').forEach(function(card){
207
- var key=card.getAttribute('data-rk-client')||'';
208
- var isInvite=key==='__invite__';
209
- var hit=!q||(!isInvite&&key.indexOf(q)!==-1);
210
- // keep the invite card visible only when not searching
211
- var show=isInvite?!q:hit;
212
- card.hidden=!show;
213
- if(show&&!isInvite)visible++;
214
- });
215
- if(empty){empty.hidden=!(q&&visible===0);empty.setAttribute('data-rk-shown',(q&&visible===0)?'1':'0');}
128
+ var emptyMsg=root.querySelector('[data-rk-agency-empty-msg]');
129
+ var countEl=root.querySelector('[data-rk-agency-count]');
130
+ var pluralEl=root.querySelector('[data-rk-agency-plural]');
131
+ var searchQuery="";
132
+
133
+ function escapeHtml(value){
134
+ return String(value==null?"":value)
135
+ .replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")
136
+ .replace(/"/g,"&quot;").replace(/'/g,"&#39;");
137
+ }
138
+
139
+ function initials(name){
140
+ var letters=String(name||"").replace(/&/g," ").split(/\\s+/)
141
+ .map(function(w){var m=w.match(/[a-z0-9]/i);return m?m[0]:"";})
142
+ .filter(Boolean);
143
+ var out=(letters[0]||"")+(letters[1]||"");
144
+ return (out||"?").toUpperCase();
145
+ }
146
+
147
+ function tintFor(seed){
148
+ var s=String(seed||""),h=0;
149
+ for(var i=0;i<s.length;i++){h=(h*31+s.charCodeAt(i))>>>0;}
150
+ return TINTS[h%TINTS.length];
216
151
  }
217
- if(search)search.addEventListener('input',filter);
218
- // copy helpers (email / user id / login link)
152
+
153
+ function relTime(iso){
154
+ var t=Date.parse(iso||"");
155
+ if(isNaN(t))return "";
156
+ var diff=Date.now()-t; if(diff<0)diff=0;
157
+ var m=Math.floor(diff/60000);
158
+ if(m<1)return "just now";
159
+ if(m<60)return m+"m ago";
160
+ var hr=Math.floor(m/60);
161
+ if(hr<24)return hr+"h ago";
162
+ var d=Math.floor(hr/24);
163
+ if(d<7)return d+"d ago";
164
+ try{return new Date(t).toLocaleDateString("en-US",{month:"short",day:"numeric"});}catch(_){return d+"d ago";}
165
+ }
166
+
167
+ function readAccountIds(){
168
+ try{
169
+ var raw=window.localStorage.getItem(ACCOUNT_REGISTRY_INDEX_KEY);
170
+ var parsed=raw?JSON.parse(raw):[];
171
+ return Array.isArray(parsed)?parsed.filter(function(v){return typeof v==="string"&&v;}):[];
172
+ }catch(_){return [];}
173
+ }
174
+
175
+ function readAccounts(){
176
+ return readAccountIds().map(function(userId){
177
+ try{
178
+ var raw=window.localStorage.getItem(ACCOUNT_REGISTRY_ENTRY_PREFIX+userId);
179
+ if(!raw)return null;
180
+ var parsed=JSON.parse(raw);
181
+ if(!parsed||typeof parsed!=="object"||typeof parsed.apiKey!=="string"||!parsed.apiKey)return null;
182
+ return {
183
+ userId:userId,
184
+ email:typeof parsed.email==="string"?parsed.email:"",
185
+ displayName:typeof parsed.displayName==="string"?parsed.displayName:"",
186
+ lastSeenAt:typeof parsed.lastSeenAt==="string"?parsed.lastSeenAt:"",
187
+ autologinUrl:typeof parsed.autologinUrl==="string"?parsed.autologinUrl:"/auto-login?api_key="+encodeURIComponent(parsed.apiKey)
188
+ };
189
+ }catch(_){return null;}
190
+ }).filter(Boolean).sort(function(a,b){return Date.parse(b.lastSeenAt||"")-Date.parse(a.lastSeenAt||"");});
191
+ }
192
+
193
+ function matchesSearch(account){
194
+ var needle=searchQuery.trim().toLowerCase();
195
+ if(!needle)return true;
196
+ return [account.displayName,account.email,account.userId]
197
+ .some(function(v){return String(v||"").toLowerCase().indexOf(needle)!==-1;});
198
+ }
199
+
200
+ function cardHtml(account){
201
+ var title=account.displayName||account.email||account.userId;
202
+ var tint=tintFor(account.userId);
203
+ var seen=relTime(account.lastSeenAt);
204
+ var seenPill=seen?'<span class="rk-pill rk-agency-seen-pill">Active '+escapeHtml(seen)+'</span>':'';
205
+ var openTarget="/u/"+encodeURIComponent(account.userId)+"/library";
206
+ return ''
207
+ +'<article class="rk-card rk-card-hover rk-agency-card">'
208
+ +'<div class="rk-agency-card-top">'
209
+ +'<span class="rk-tile rk-tile-lg rk-tile-'+tint+' rk-agency-avatar">'+escapeHtml(initials(title))+'</span>'
210
+ +'<div class="rk-agency-card-id">'
211
+ +'<h3 class="rk-agency-name">'+escapeHtml(title)+'</h3>'
212
+ +'<div class="rk-agency-email" role="button" tabindex="0" data-rk-copy-text="'+escapeHtml(account.email||"")+'" title="Copy email">'+escapeHtml(account.email||"No email")+'</div>'
213
+ +'</div>'
214
+ +seenPill
215
+ +'</div>'
216
+ +'<div class="rk-agency-idrow">'
217
+ +'<span class="rk-agency-user" role="button" tabindex="0" data-rk-copy-text="'+escapeHtml(account.userId)+'" title="Copy user id">User '+escapeHtml(account.userId)+'</span>'
218
+ +'</div>'
219
+ +'<div class="rk-agency-card-foot">'
220
+ +'<button class="rk-btn rk-btn-ink rk-btn-sm rk-agency-open" type="button" data-rk-open-tab="'+escapeHtml(openTarget)+'">Open workspace <span class="rk-arrow">→</span></button>'
221
+ +'<button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-copy-login="'+escapeHtml(account.autologinUrl)+'">Copy login</button>'
222
+ +'</div>'
223
+ +'</article>';
224
+ }
225
+
219
226
  function flash(el,label){
220
- var tag=el.getAttribute('data-rk-flash-tag')||el.getAttribute('title')||'';
227
+ if(!el)return;
221
228
  var prev=el.textContent;
222
229
  el.textContent=label;
223
- setTimeout(function(){el.textContent=prev;},1200);
230
+ window.setTimeout(function(){el.textContent=prev;},1300);
224
231
  }
225
232
  function copy(value,el){
226
- if(!value||!navigator.clipboard)return;
227
- navigator.clipboard.writeText(value).then(function(){flash(el,'Copied!');});
233
+ if(!value)return;
234
+ if(navigator.clipboard&&navigator.clipboard.writeText){
235
+ navigator.clipboard.writeText(value).then(function(){flash(el,"Copied");},function(){flash(el,"Clipboard blocked");});
236
+ }else{flash(el,"Clipboard blocked");}
228
237
  }
229
- root.querySelectorAll('[data-rk-copy-text]').forEach(function(el){
230
- var run=function(){copy(el.getAttribute('data-rk-copy-text'),el);};
231
- el.addEventListener('click',run);
232
- el.addEventListener('keydown',function(e){if(e.key==='Enter'||e.key===' '){e.preventDefault();run();}});
233
- });
234
- root.querySelectorAll('[data-rk-copy-login]').forEach(function(btn){
235
- btn.addEventListener('click',function(){
236
- var rel=btn.getAttribute('data-rk-copy-login')||'';
237
- var abs=rel;try{abs=new URL(rel,location.origin).toString();}catch(_){}
238
- copy(abs,btn);
238
+
239
+ function wire(){
240
+ if(!cardsWrap)return;
241
+ cardsWrap.querySelectorAll('[data-rk-copy-text]').forEach(function(el){
242
+ var run=function(){copy(el.getAttribute('data-rk-copy-text'),el);};
243
+ el.addEventListener('click',run);
244
+ el.addEventListener('keydown',function(e){if(e.key==='Enter'||e.key===' '){e.preventDefault();run();}});
245
+ });
246
+ cardsWrap.querySelectorAll('[data-rk-copy-login]').forEach(function(btn){
247
+ btn.addEventListener('click',function(){
248
+ var rel=btn.getAttribute('data-rk-copy-login')||'';
249
+ var abs=rel;try{abs=new URL(rel,window.location.origin).toString();}catch(_){}
250
+ copy(abs,btn);
251
+ });
252
+ });
253
+ cardsWrap.querySelectorAll('[data-rk-open-tab]').forEach(function(btn){
254
+ btn.addEventListener('click',function(){
255
+ var target=btn.getAttribute('data-rk-open-tab');
256
+ if(target)window.open(target,'_blank','noopener,noreferrer');
257
+ });
239
258
  });
240
- });
241
- // "Add client" CTA — reskin surface has no backend; nudge to real flow
242
- root.querySelectorAll('[data-rk-add-client]').forEach(function(btn){
243
- btn.addEventListener('click',function(){window.open('/agency-client-login','_blank','noopener,noreferrer');});
244
- });
259
+ }
260
+
261
+ function render(){
262
+ if(!cardsWrap)return;
263
+ var accounts=readAccounts();
264
+ var total=accounts.length;
265
+ if(countEl)countEl.textContent=String(total);
266
+ if(pluralEl)pluralEl.textContent=total===1?'':'s';
267
+
268
+ var filtered=accounts.filter(matchesSearch);
269
+ cardsWrap.innerHTML=filtered.map(cardHtml).join('');
270
+ wire();
271
+
272
+ // hide the "add client" invite while actively searching (it isn't a result)
273
+ if(invite)invite.hidden=!!searchQuery.trim();
274
+
275
+ if(empty&&emptyMsg){
276
+ var showEmpty=false,msg='';
277
+ if(total===0){showEmpty=true;msg='No cached accounts found in this browser yet.';}
278
+ else if(filtered.length===0){showEmpty=true;msg='No agency clients match that search.';}
279
+ emptyMsg.textContent=msg;
280
+ empty.hidden=!showEmpty;
281
+ empty.setAttribute('data-rk-shown',showEmpty?'1':'0');
282
+ }
283
+ }
284
+
285
+ if(search){
286
+ search.addEventListener('input',function(){searchQuery=search.value||'';render();});
287
+ }
288
+ render();
245
289
  })();`;
246
290
  return reskinDocument({
247
291
  title: "vidfarm reskin — Agency",
248
- description: "Reskinned vidfarm agency page (client workspaces, seats, pooled billing).",
292
+ description: "Reskinned vidfarm agency page — cached client workspaces, open library, copy autologin.",
249
293
  activeSlug: "agency",
250
294
  pageCss,
251
295
  body,