@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,567 @@
1
+ // /reskin/calendar — the scheduled-posts calendar, MIGRATED to real production data.
2
+ //
3
+ // Mirrors the live /calendar page's two-column layout: a LEFT rail listing the
4
+ // connected channels (searchable, click to filter the board to one channel) and
5
+ // a compact month board on the RIGHT sized to the viewport so the whole month
6
+ // is visible without scrolling.
7
+ //
8
+ // Like the live page, this renders the shell server-side, then CLIENT-FETCHES
9
+ // the real posts from `input.loadEndpoint` (`/calendar/feed`, same-origin,
10
+ // credentials included) and drops them onto the correct day cells. It accepts
11
+ // the SAME input object the live route already builds (via
12
+ // `Parameters<typeof renderCalendarPage>[0]`).
13
+ //
14
+ // /calendar/feed response contract (see serializeScheduleCalendarPost in app.ts):
15
+ // {
16
+ // channels: [{ id, name, handle, platform, avatarUrl, platformIconUrl? }],
17
+ // posts: [{ id, channelId, scheduledAt (ISO), status (label),
18
+ // statusTone: "draft"|"review"|"ready"|"scheduled"|"sent"|"error",
19
+ // title, summary, kind, url }],
20
+ // startDate, endDate, syncStatus, error?
21
+ // }
22
+ // The client sends `?start_date=&end_date=` for the visible month range; posts map
23
+ // onto days by the local date of `scheduledAt`. Email channel ids are `email:<id>`
24
+ // and match `post.channelId`, so platform is resolved via the channels lookup.
25
+ //
26
+ // Design stays farmville (shared rk-* primitives + a page-scoped rk-calendar- block);
27
+ // NO decorative emoji — platform swatches use letter initials (T/I/Y/E/…).
28
+ import { reskinDocument, rkEscape } from "./document.js";
29
+ // A safe default so calling with no/partial args never throws. The live route
30
+ // always passes a fully-populated object (posts seeded empty — they arrive from
31
+ // the feed), so this default only matters for previews / direct calls.
32
+ const DEFAULT_INPUT = {
33
+ userId: "",
34
+ currentAccountId: "",
35
+ name: null,
36
+ email: "",
37
+ loadEndpoint: "/calendar/feed",
38
+ hasFlockPosterKey: false,
39
+ connectChannelsHref: "/settings?tab=channels&connect=flockposter",
40
+ channels: [],
41
+ posts: []
42
+ };
43
+ const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
44
+ 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>`;
45
+ // statusTone → { label, pill } for the chip tooltip + the legend dots.
46
+ const STATUS = {
47
+ draft: { label: "Draft", pill: "rk-pill" },
48
+ review: { label: "Review", pill: "rk-pill-gold" },
49
+ ready: { label: "Ready", pill: "rk-pill-green" },
50
+ scheduled: { label: "Scheduled", pill: "rk-pill-sky" },
51
+ sent: { label: "Sent", pill: "rk-pill-violet" },
52
+ error: { label: "Failed", pill: "rk-pill-red" }
53
+ };
54
+ // Render a bare month-grid skeleton server-side (based on `today`) so the board
55
+ // paints immediately; the client rebuilds it and fills in the real posts.
56
+ function skeletonCells() {
57
+ const now = new Date();
58
+ const year = now.getUTCFullYear();
59
+ const month = now.getUTCMonth();
60
+ const firstWeekday = new Date(Date.UTC(year, month, 1)).getUTCDay();
61
+ const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
62
+ const rows = Math.ceil((firstWeekday + daysInMonth) / 7);
63
+ const total = rows * 7;
64
+ const todayDom = now.getUTCDate();
65
+ const cells = [];
66
+ for (let i = 0; i < total; i++) {
67
+ const offset = i - firstWeekday;
68
+ const d = new Date(Date.UTC(year, month, 1 + offset));
69
+ const inMonth = d.getUTCMonth() === month;
70
+ const dom = d.getUTCDate();
71
+ const isToday = inMonth && dom === todayDom;
72
+ const cls = ["rk-calendar-cell"];
73
+ if (!inMonth)
74
+ cls.push("is-out");
75
+ if (isToday)
76
+ cls.push("is-today");
77
+ cells.push(`<div class="${cls.join(" ")}">
78
+ <div class="rk-calendar-cell-head">
79
+ <span class="rk-calendar-daynum">${dom}</span>
80
+ </div>
81
+ <div class="rk-calendar-cell-posts"></div>
82
+ </div>`);
83
+ }
84
+ return cells.join("");
85
+ }
86
+ export function renderReskinCalendar(input = DEFAULT_INPUT) {
87
+ const cfg = { ...DEFAULT_INPUT, ...input };
88
+ // "Schedule post" → the real destination the live app uses. Scheduling a post
89
+ // happens from the Library's schedule modal (POST /approved/posts/:id/schedules);
90
+ // if no channels are connected yet, the prerequisite is to connect them first.
91
+ const acctPrefix = /^\/u\/[^/]+/.exec(cfg.connectChannelsHref)?.[0] ?? "";
92
+ const scheduleHref = cfg.hasFlockPosterKey ? `${acctPrefix}/library` : cfg.connectChannelsHref;
93
+ const scheduleLabel = cfg.hasFlockPosterKey ? "Schedule post" : "Connect a channel";
94
+ const weekdayHead = WEEKDAYS.map((w) => `<div class="rk-calendar-weekday">${w}</div>`).join("");
95
+ const legendOrder = ["draft", "review", "ready", "scheduled", "sent", "error"];
96
+ const legend = legendOrder.map((s) => `<span class="rk-calendar-legend-item"><span class="rk-calendar-dot" data-status="${s}"></span>${STATUS[s].label}</span>`).join("");
97
+ // seed + config handed to the client (posts arrive from the feed).
98
+ const boot = {
99
+ loadEndpoint: cfg.loadEndpoint,
100
+ hasChannels: cfg.hasFlockPosterKey,
101
+ connectChannelsHref: cfg.connectChannelsHref,
102
+ seedChannels: cfg.channels,
103
+ seedPosts: cfg.posts
104
+ };
105
+ const bootJson = JSON.stringify(boot).replace(/</g, "\\u003c");
106
+ const body = `
107
+ <main class="rk-container-wide rk-page rk-calendar-page">
108
+ <div class="rk-spread rk-page-head rk-calendar-head">
109
+ <div class="rk-stack rk-calendar-head-copy">
110
+ <h1 class="rk-h1">Calendar</h1>
111
+ <p class="rk-sub" data-rk-cal-sub>Your scheduled short-form posts across every connected channel.</p>
112
+ </div>
113
+ <a class="rk-btn rk-btn-gold rk-btn-sm rk-calendar-cta" href="${rkEscape(scheduleHref)}">${rkEscape(scheduleLabel)} <span class="rk-arrow">&rarr;</span></a>
114
+ </div>
115
+
116
+ <div class="rk-calendar-notice" data-rk-cal-notice hidden></div>
117
+
118
+ <div class="rk-calendar-shell">
119
+ <aside class="rk-calendar-rail" aria-label="Connected channels">
120
+ <label class="rk-calendar-chsearch">
121
+ <span class="rk-calendar-chsearch-ic" aria-hidden="true">${SEARCH_ICON}</span>
122
+ <input class="rk-input rk-calendar-chsearch-input" data-rk-cal-chsearch type="search"
123
+ placeholder="Search channels" autocomplete="off" aria-label="Search channels" />
124
+ </label>
125
+ <div class="rk-calendar-channels" data-rk-cal-channels></div>
126
+ <a class="rk-calendar-connect" href="${rkEscape(cfg.connectChannelsHref)}">+ Connect a channel</a>
127
+ </aside>
128
+
129
+ <section class="rk-calendar-stage">
130
+ <div class="rk-calendar-toolbar">
131
+ <div class="rk-calendar-monthnav">
132
+ <button type="button" class="rk-btn rk-btn-ghost rk-calendar-navbtn" data-rk-cal-prev aria-label="Previous month">&larr;</button>
133
+ <span class="rk-calendar-month" data-rk-cal-month>&nbsp;</span>
134
+ <button type="button" class="rk-btn rk-btn-ghost rk-calendar-navbtn" data-rk-cal-next aria-label="Next month">&rarr;</button>
135
+ <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-calendar-today-btn" data-rk-cal-today>Today</button>
136
+ <span class="rk-calendar-sync" data-rk-cal-sync aria-live="polite"></span>
137
+ </div>
138
+ <div class="rk-calendar-legend">${legend}</div>
139
+ </div>
140
+
141
+ <div class="rk-card rk-calendar-grid-card">
142
+ <div class="rk-calendar-weekdays">${weekdayHead}</div>
143
+ <div class="rk-calendar-grid" data-rk-cal-grid>${skeletonCells()}</div>
144
+ </div>
145
+ </section>
146
+ </div>
147
+ </main>
148
+ <script type="application/json" data-rk-cal-boot>${bootJson}</script>`;
149
+ const pageCss = `
150
+ /* .rk-page zeroes horizontal padding; the wide container == viewport at 1280 so
151
+ margin:auto adds no gutter. Restore side gutters (longhand beats the shorthand),
152
+ and trim the bottom so the whole month fits in one viewport. */
153
+ .rk-calendar-page{padding-left:var(--rk-gutter);padding-right:var(--rk-gutter);padding-bottom:28px}
154
+ .rk-calendar-head{align-items:flex-start;margin-bottom:14px}
155
+ .rk-calendar-head-copy{gap:4px;margin:0;flex:1 1 auto;min-width:0}
156
+ .rk-calendar-cta{flex:none;margin-top:2px}
157
+
158
+ /* notice banner (errors / empty warnings) */
159
+ .rk-calendar-notice{margin-bottom:12px}
160
+ .rk-calendar-notice[hidden]{display:none}
161
+
162
+ /* two-column shell: channel rail left, compact month board right */
163
+ .rk-calendar-shell{display:grid;grid-template-columns:280px minmax(0,1fr);gap:16px;align-items:start}
164
+ @media(max-width:960px){.rk-calendar-shell{grid-template-columns:1fr}}
165
+
166
+ /* ── channel rail ── */
167
+ .rk-calendar-rail{display:grid;gap:10px;background:#fff;border:1px solid var(--rk-border);
168
+ border-radius:var(--rk-r-2xl);padding:12px;box-shadow:var(--rk-shadow-card)}
169
+ .rk-calendar-chsearch{position:relative;display:block}
170
+ .rk-calendar-chsearch-ic{position:absolute;left:13px;top:50%;transform:translateY(-50%);color:var(--rk-n-400);pointer-events:none;display:flex}
171
+ .rk-calendar-chsearch-input{padding:9px 12px 9px 37px;border-radius:var(--rk-r-full);font-size:13.5px}
172
+ .rk-calendar-channels{display:grid;gap:6px;max-height:calc(100vh - 320px);min-height:120px;overflow:auto}
173
+ .rk-calendar-channel{display:flex;align-items:center;gap:10px;width:100%;padding:9px 10px;border:1px solid transparent;
174
+ border-radius:var(--rk-r-xl);background:transparent;cursor:pointer;text-align:left;font-family:var(--rk-font-body);
175
+ transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
176
+ .rk-calendar-channel:hover{background:var(--rk-n-100)}
177
+ .rk-calendar-channel.is-selected{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
178
+ .rk-calendar-channel-avatar{position:relative;width:34px;height:34px;border-radius:var(--rk-r-full);flex:none;overflow:visible}
179
+ .rk-calendar-channel-avatar img{width:100%;height:100%;border-radius:var(--rk-r-full);object-fit:cover;display:block;
180
+ border:1px solid var(--rk-border)}
181
+ .rk-calendar-channel-initials{width:100%;height:100%;border-radius:var(--rk-r-full);display:grid;place-items:center;
182
+ background:var(--rk-n-800);color:#fff;font-family:var(--rk-font-display);font-weight:800;font-size:12px;letter-spacing:.03em}
183
+ .rk-calendar-channel-main{display:grid;gap:1px;min-width:0;flex:1}
184
+ .rk-calendar-channel-name{font-size:13.5px;font-weight:700;color:var(--rk-ink);letter-spacing:-.01em;
185
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
186
+ .rk-calendar-channel-sub{font-size:11.5px;font-weight:500;color:var(--rk-text-muted);
187
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
188
+ .rk-calendar-channel-count{flex:none;font-size:11px;font-weight:700;color:var(--rk-text-muted);
189
+ background:var(--rk-n-100);border-radius:var(--rk-r-full);padding:2px 8px}
190
+ .rk-calendar-channel.is-selected .rk-calendar-channel-count{background:#fff}
191
+ .rk-calendar-channels-empty{padding:14px 10px;font-size:12.5px;color:var(--rk-text-muted);line-height:1.5}
192
+ .rk-calendar-connect{display:block;text-align:center;padding:9px 12px;border:1.5px dashed var(--rk-border-strong);
193
+ border-radius:var(--rk-r-xl);font-size:13px;font-weight:600;color:var(--rk-text-muted);
194
+ transition:border-color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
195
+ .rk-calendar-connect:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink)}
196
+
197
+ /* platform swatch badge on the avatar — letter initial, no emoji */
198
+ .rk-calendar-swatch{position:absolute;right:-3px;bottom:-3px;width:16px;height:16px;border-radius:6px;
199
+ display:grid;place-items:center;font-family:var(--rk-font-display);font-size:9px;font-weight:800;line-height:1;
200
+ box-shadow:var(--rk-shadow-xs);border:1.5px solid #fff}
201
+ .rk-calendar-swatch-tiktok{background:var(--rk-ink);color:#fff}
202
+ .rk-calendar-swatch-instagram{background:linear-gradient(135deg,#f9ce34,#ee2a7b 55%,#6228d7);color:#fff}
203
+ .rk-calendar-swatch-youtube{background:#ff3b30;color:#fff}
204
+ .rk-calendar-swatch-email{background:var(--rk-sky-tint);color:#1d6fb8}
205
+ .rk-calendar-swatch-generic{background:var(--rk-gold-tint2);color:var(--rk-gold-700)}
206
+
207
+ /* ── stage: toolbar + board ── */
208
+ .rk-calendar-stage{display:grid;gap:10px;min-width:0}
209
+ .rk-calendar-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap}
210
+ .rk-calendar-monthnav{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
211
+ .rk-calendar-month{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-lg);letter-spacing:-.02em;color:var(--rk-ink);min-width:7.4rem;text-align:center}
212
+ .rk-calendar-navbtn{width:32px;height:32px;padding:0;font-size:14px;line-height:1}
213
+ .rk-calendar-today-btn{margin-left:2px;padding:5px 11px;font-size:12.5px}
214
+ .rk-calendar-sync{font-size:12px;font-weight:600;color:var(--rk-text-muted);margin-left:4px}
215
+ .rk-calendar-sync.is-error{color:var(--rk-red)}
216
+
217
+ /* compact one-line legend, lives in the toolbar */
218
+ .rk-calendar-legend{display:flex;flex-wrap:wrap;gap:12px}
219
+ .rk-calendar-legend-item{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:600;color:var(--rk-text-muted)}
220
+
221
+ /* month board — sized to the viewport so the whole month is visible */
222
+ .rk-calendar-grid-card{padding:0;overflow:hidden;position:relative;display:flex;flex-direction:column;
223
+ height:calc(100vh - 218px);min-height:430px}
224
+ @media(max-width:960px){.rk-calendar-grid-card{height:auto;min-height:0}}
225
+ .rk-calendar-weekdays{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));background:var(--rk-n-50);border-bottom:1px solid var(--rk-border);flex:none}
226
+ .rk-calendar-weekday{padding:8px 8px;text-align:center;font-size:10px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--rk-text-muted)}
227
+ .rk-calendar-grid{flex:1;min-height:0;display:grid;grid-template-columns:repeat(7,minmax(0,1fr));grid-auto-rows:1fr;
228
+ gap:1px;background:var(--rk-border);transition:opacity var(--rk-dur) var(--rk-ease)}
229
+ .rk-calendar-grid.is-loading{opacity:.55}
230
+ .rk-calendar-cell{background:#fff;min-height:0;padding:6px 7px;display:flex;flex-direction:column;gap:4px;overflow:hidden;
231
+ transition:background var(--rk-dur) var(--rk-ease)}
232
+ @media(max-width:960px){.rk-calendar-cell{min-height:92px}}
233
+ .rk-calendar-cell:hover{background:var(--rk-n-50)}
234
+ .rk-calendar-cell.is-out{background:var(--rk-n-50)}
235
+ .rk-calendar-cell.is-out:hover{background:var(--rk-n-100)}
236
+ .rk-calendar-cell.is-today{background:var(--rk-gold-tint);box-shadow:inset 0 0 0 2px var(--rk-gold-500)}
237
+ .rk-calendar-cell-head{display:flex;align-items:center;justify-content:space-between;gap:4px;flex:none}
238
+ .rk-calendar-daynum{width:22px;height:22px;border-radius:var(--rk-r-full);display:grid;place-items:center;
239
+ font-family:var(--rk-font-display);font-weight:700;font-size:12px;color:var(--rk-n-700)}
240
+ .rk-calendar-cell.is-out .rk-calendar-daynum{color:var(--rk-n-400);font-weight:600}
241
+ .rk-calendar-cell.is-today .rk-calendar-daynum{background:var(--rk-gold-500);color:var(--rk-ink);box-shadow:var(--rk-shadow-xs)}
242
+ .rk-calendar-cell-posts{display:flex;flex-direction:column;gap:3px;min-height:0;overflow:hidden}
243
+
244
+ /* post chip — one compact line: status dot · time · title */
245
+ .rk-calendar-chip{display:flex;gap:6px;align-items:center;width:100%;text-align:left;padding:3px 7px;border-radius:8px;
246
+ background:#fff;border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-xs);cursor:pointer;min-width:0;flex:none;
247
+ transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease);text-decoration:none;color:inherit}
248
+ .rk-calendar-chip:hover{transform:translateY(-1px);box-shadow:var(--rk-shadow-sm)}
249
+ .rk-calendar-chip-time{flex:none;font-size:10px;font-weight:700;color:var(--rk-text-muted);font-variant-numeric:tabular-nums}
250
+ .rk-calendar-chip-title{flex:1;min-width:0;font-size:11px;font-weight:600;color:var(--rk-ink);letter-spacing:-.01em;
251
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
252
+ .rk-calendar-more{font-size:10px;font-weight:700;color:var(--rk-text-muted);padding:0 2px;background:none;border:0;cursor:default}
253
+
254
+ /* status dot — chips + legend */
255
+ .rk-calendar-dot{width:7px;height:7px;border-radius:var(--rk-r-full);background:var(--rk-n-400);flex:none}
256
+ .rk-calendar-dot[data-status="review"]{background:var(--rk-gold-700)}
257
+ .rk-calendar-dot[data-status="ready"]{background:var(--rk-green)}
258
+ .rk-calendar-dot[data-status="scheduled"]{background:var(--rk-sky)}
259
+ .rk-calendar-dot[data-status="sent"]{background:var(--rk-violet)}
260
+ .rk-calendar-dot[data-status="error"]{background:var(--rk-red)}
261
+
262
+ @media(max-width:760px){
263
+ .rk-calendar-weekdays,.rk-calendar-grid{min-width:660px}
264
+ .rk-calendar-grid-card{overflow-x:auto}
265
+ .rk-calendar-toolbar{gap:10px}
266
+ }
267
+ `;
268
+ const script = `
269
+ (function(){
270
+ var root=document;
271
+ var bootEl=root.querySelector('[data-rk-cal-boot]');
272
+ var boot={};
273
+ try{ boot=JSON.parse(bootEl&&bootEl.textContent||'{}'); }catch(e){ boot={}; }
274
+ var loadEndpoint=boot.loadEndpoint||'/calendar/feed';
275
+ var gridEl=root.querySelector('[data-rk-cal-grid]');
276
+ var monthEl=root.querySelector('[data-rk-cal-month]');
277
+ var subEl=root.querySelector('[data-rk-cal-sub]');
278
+ var syncEl=root.querySelector('[data-rk-cal-sync]');
279
+ var noticeEl=root.querySelector('[data-rk-cal-notice]');
280
+ var channelsEl=root.querySelector('[data-rk-cal-channels]');
281
+ var chSearchEl=root.querySelector('[data-rk-cal-chsearch]');
282
+ if(!gridEl){return;}
283
+
284
+ var STATUS=${JSON.stringify(STATUS)};
285
+ var monthFmt=new Intl.DateTimeFormat(undefined,{month:'long',year:'numeric'});
286
+ var timeFmt=new Intl.DateTimeFormat(undefined,{hour:'numeric',minute:'2-digit'});
287
+
288
+ var today=startOfDay(new Date());
289
+ var cursor=startOfMonth(today);
290
+ // channelId -> channel record (resolved from the feed's channels list, seeded first)
291
+ var channelsById={};
292
+ var channelOrder=[];
293
+ ingestChannels(boot.seedChannels||[]);
294
+ // dateKey -> [posts]; seed with any server-provided posts
295
+ var postsByDay={};
296
+ ingestPosts(boot.seedPosts||[]);
297
+ var loading=false;
298
+ var selectedChannelId='';
299
+ var channelQuery='';
300
+
301
+ function esc(s){var d=root.createElement('div');d.textContent=s==null?'':String(s);return d.innerHTML;}
302
+ function startOfDay(d){return new Date(d.getFullYear(),d.getMonth(),d.getDate());}
303
+ function startOfMonth(d){return new Date(d.getFullYear(),d.getMonth(),1);}
304
+ function startOfWeek(d){var n=startOfDay(d);n.setDate(n.getDate()-n.getDay());return n;}
305
+ function addDays(d,n){var x=new Date(d);x.setDate(x.getDate()+n);return startOfDay(x);}
306
+ function addMonths(d,n){var x=new Date(d.getFullYear(),d.getMonth()+n,1);return x;}
307
+ function toKey(d){return [d.getFullYear(),String(d.getMonth()+1).padStart(2,'0'),String(d.getDate()).padStart(2,'0')].join('-');}
308
+ function sameDay(a,b){return a.getFullYear()===b.getFullYear()&&a.getMonth()===b.getMonth()&&a.getDate()===b.getDate();}
309
+ function sameMonth(a,b){return a.getFullYear()===b.getFullYear()&&a.getMonth()===b.getMonth();}
310
+
311
+ function platformMeta(platform,channelId,kind){
312
+ var p=String(platform||'').toLowerCase();
313
+ if(p.indexOf('tiktok')>-1)return {initial:'T',swatch:'tiktok'};
314
+ if(p.indexOf('instagram')>-1)return {initial:'I',swatch:'instagram'};
315
+ if(p.indexOf('youtube')>-1)return {initial:'Y',swatch:'youtube'};
316
+ if(p.indexOf('email')>-1||/^email:/.test(String(channelId||''))||/email/i.test(String(kind||'')))return {initial:'E',swatch:'email'};
317
+ if(p.indexOf('facebook')>-1)return {initial:'F',swatch:'generic'};
318
+ if(p.indexOf('linkedin')>-1)return {initial:'L',swatch:'generic'};
319
+ if(p.indexOf('twitter')>-1||p==='x')return {initial:'X',swatch:'generic'};
320
+ var src=String(platform||channelId||'').replace(/^email:/,'').trim();
321
+ return {initial:(src.charAt(0)||'C').toUpperCase(),swatch:'generic'};
322
+ }
323
+
324
+ function ingestChannels(list){
325
+ (Array.isArray(list)?list:[]).forEach(function(ch){
326
+ if(!ch||ch.id==null)return;
327
+ var id=String(ch.id);
328
+ if(!channelsById[id])channelOrder.push(id);
329
+ channelsById[id]={
330
+ id:id,
331
+ name:ch.name||ch.title||id,
332
+ handle:ch.handle||null,
333
+ platform:ch.platform||null,
334
+ avatarUrl:ch.avatarUrl||null
335
+ };
336
+ });
337
+ }
338
+
339
+ function ingestPosts(list){
340
+ (Array.isArray(list)?list:[]).forEach(function(post){
341
+ if(!post)return;
342
+ var t=Date.parse(post.scheduledAt);
343
+ if(isNaN(t))return;
344
+ var d=startOfDay(new Date(t));
345
+ var key=toKey(d);
346
+ if(!postsByDay[key])postsByDay[key]=[];
347
+ // de-dupe by id within the day
348
+ var id=String(post.id||post.scheduleId||'');
349
+ if(id&&postsByDay[key].some(function(p){return String(p.id||p.scheduleId||'')===id;}))return;
350
+ postsByDay[key].push(post);
351
+ postsByDay[key].sort(function(a,b){return Date.parse(a.scheduledAt)-Date.parse(b.scheduledAt);});
352
+ });
353
+ }
354
+
355
+ function postCountFor(channelId){
356
+ var n=0;
357
+ Object.keys(postsByDay).forEach(function(key){
358
+ postsByDay[key].forEach(function(p){ if(String(p.channelId)===channelId)n++; });
359
+ });
360
+ return n;
361
+ }
362
+
363
+ function channelInitials(name){
364
+ return String(name||'').split(/\\s+/).filter(Boolean).slice(0,2)
365
+ .map(function(part){return (part[0]||'').toUpperCase();}).join('')||'CH';
366
+ }
367
+
368
+ function renderChannels(){
369
+ if(!channelsEl)return;
370
+ var q=channelQuery.trim().toLowerCase();
371
+ var ids=channelOrder.filter(function(id){
372
+ if(!q)return true;
373
+ var ch=channelsById[id];
374
+ return [ch.name,ch.handle,ch.platform,ch.id].filter(Boolean).join(' ').toLowerCase().indexOf(q)>-1;
375
+ });
376
+ if(!channelOrder.length){
377
+ channelsEl.innerHTML='<div class="rk-calendar-channels-empty">No channels connected yet. Connect FlockPoster or add an email channel to start scheduling.</div>';
378
+ return;
379
+ }
380
+ if(!ids.length){
381
+ channelsEl.innerHTML='<div class="rk-calendar-channels-empty">No channels match \\u201c'+esc(channelQuery)+'\\u201d.</div>';
382
+ return;
383
+ }
384
+ var allBtn='<button type="button" class="rk-calendar-channel'+(selectedChannelId===''?' is-selected':'')+'" data-rk-channel="">'
385
+ +'<span class="rk-calendar-channel-avatar"><span class="rk-calendar-channel-initials">All</span></span>'
386
+ +'<span class="rk-calendar-channel-main">'
387
+ +'<span class="rk-calendar-channel-name">All channels</span>'
388
+ +'<span class="rk-calendar-channel-sub">'+channelOrder.length+' connected</span>'
389
+ +'</span>'
390
+ +'</button>';
391
+ var items=ids.map(function(id){
392
+ var ch=channelsById[id];
393
+ var pm=platformMeta(ch.platform,ch.id,null);
394
+ var avatar=ch.avatarUrl
395
+ ? '<img src="'+esc(ch.avatarUrl)+'" alt="" loading="lazy" />'
396
+ : '<span class="rk-calendar-channel-initials">'+esc(channelInitials(ch.name))+'</span>';
397
+ var count=postCountFor(id);
398
+ return '<button type="button" class="rk-calendar-channel'+(selectedChannelId===id?' is-selected':'')+'" data-rk-channel="'+esc(id)+'">'
399
+ +'<span class="rk-calendar-channel-avatar">'+avatar
400
+ +'<span class="rk-calendar-swatch rk-calendar-swatch-'+pm.swatch+'" aria-hidden="true">'+esc(pm.initial)+'</span>'
401
+ +'</span>'
402
+ +'<span class="rk-calendar-channel-main">'
403
+ +'<span class="rk-calendar-channel-name">'+esc(ch.name)+'</span>'
404
+ +'<span class="rk-calendar-channel-sub">'+esc([ch.platform,ch.handle].filter(Boolean).join(' \\u00b7 ')||'Channel')+'</span>'
405
+ +'</span>'
406
+ +(count?'<span class="rk-calendar-channel-count">'+count+'</span>':'')
407
+ +'</button>';
408
+ }).join('');
409
+ channelsEl.innerHTML=allBtn+items;
410
+ }
411
+
412
+ function chip(post){
413
+ var tone=String(post.statusTone||'draft');
414
+ var meta=STATUS[tone]||{label:tone,pill:'rk-pill'};
415
+ var time='';
416
+ var t=Date.parse(post.scheduledAt);
417
+ if(!isNaN(t))time=timeFmt.format(new Date(t));
418
+ var label=post.status?String(post.status):meta.label;
419
+ var title=post.title||'Scheduled post';
420
+ var ch=channelsById[String(post.channelId)];
421
+ var chName=ch?ch.name:'';
422
+ var tag='a';var attrs='';
423
+ if(post.url){attrs=' href="'+esc(post.url)+'" target="_blank" rel="noreferrer"';}else{tag='button';attrs=' type="button"';}
424
+ return '<'+tag+' class="rk-calendar-chip"'+attrs+' title="'+esc(title+' \\u00b7 '+label+(time?' \\u00b7 '+time:'')+(chName?' \\u00b7 '+chName:''))+'">'
425
+ +'<span class="rk-calendar-dot" data-status="'+esc(tone)+'" aria-hidden="true"></span>'
426
+ +(time?'<span class="rk-calendar-chip-time">'+esc(time)+'</span>':'')
427
+ +'<span class="rk-calendar-chip-title">'+esc(title)+'</span>'
428
+ +'</'+tag+'>';
429
+ }
430
+
431
+ function visiblePosts(key){
432
+ var dayPosts=postsByDay[key]||[];
433
+ if(!selectedChannelId)return dayPosts;
434
+ return dayPosts.filter(function(p){return String(p.channelId)===selectedChannelId;});
435
+ }
436
+
437
+ function renderGrid(){
438
+ monthEl.textContent=monthFmt.format(cursor);
439
+ var firstCell=startOfWeek(startOfMonth(cursor));
440
+ var lastOfMonth=new Date(cursor.getFullYear(),cursor.getMonth()+1,0);
441
+ var rows=Math.ceil((addDays(lastOfMonth,0).getTime()-firstCell.getTime())/(7*86400000))||5;
442
+ rows=Math.max(rows,Math.ceil(((startOfMonth(cursor).getDay())+lastOfMonth.getDate())/7));
443
+ var total=rows*7;
444
+ var cells=[];
445
+ var monthPostCount=0;
446
+ for(var i=0;i<total;i++){
447
+ var d=addDays(firstCell,i);
448
+ var key=toKey(d);
449
+ var dayPosts=visiblePosts(key);
450
+ var inMonth=sameMonth(d,cursor);
451
+ if(inMonth)monthPostCount+=dayPosts.length;
452
+ var isToday=sameDay(d,today);
453
+ var cls='rk-calendar-cell';
454
+ if(!inMonth)cls+=' is-out';
455
+ if(isToday)cls+=' is-today';
456
+ var shown=dayPosts.slice(0,2);
457
+ var overflow=dayPosts.length-shown.length;
458
+ var chips=shown.map(chip).join('');
459
+ var more=overflow>0?'<span class="rk-calendar-more">+'+overflow+' more</span>':'';
460
+ cells.push('<div class="'+cls+'">'
461
+ +'<div class="rk-calendar-cell-head"><span class="rk-calendar-daynum">'+d.getDate()+'</span></div>'
462
+ +'<div class="rk-calendar-cell-posts">'+chips+more+'</div>'
463
+ +'</div>');
464
+ }
465
+ gridEl.innerHTML=cells.join('');
466
+ if(subEl){
467
+ var scope=selectedChannelId&&channelsById[selectedChannelId]?(' on '+channelsById[selectedChannelId].name):'';
468
+ subEl.textContent=monthPostCount
469
+ ?(monthPostCount+' scheduled post'+(monthPostCount===1?'':'s')+scope+' in '+monthFmt.format(cursor)+'.')
470
+ :('Nothing scheduled'+scope+' in '+monthFmt.format(cursor)+' yet.');
471
+ }
472
+ }
473
+
474
+ function setNotice(msg,isError){
475
+ if(!noticeEl)return;
476
+ if(!msg){noticeEl.hidden=true;noticeEl.textContent='';noticeEl.className='rk-calendar-notice';return;}
477
+ noticeEl.hidden=false;
478
+ noticeEl.className='rk-calendar-notice rk-notice '+(isError?'rk-notice-err':'rk-notice-ok');
479
+ noticeEl.textContent=msg;
480
+ }
481
+
482
+ function setSync(msg,isError){
483
+ if(!syncEl)return;
484
+ syncEl.textContent=msg||'';
485
+ syncEl.className='rk-calendar-sync'+(isError?' is-error':'');
486
+ }
487
+
488
+ function load(){
489
+ if(loading)return;
490
+ loading=true;
491
+ gridEl.classList.add('is-loading');
492
+ setSync('Loading\\u2026',false);
493
+ var first=startOfWeek(startOfMonth(cursor));
494
+ var last=addDays(first,41);
495
+ var params=new URLSearchParams({start_date:toKey(first),end_date:toKey(last)});
496
+ var url;
497
+ try{
498
+ var u=new URL(loadEndpoint,window.location.origin);
499
+ params.forEach(function(v,k){u.searchParams.set(k,v);});
500
+ url=u.pathname+u.search;
501
+ }catch(e){ url=loadEndpoint+(loadEndpoint.indexOf('?')>-1?'&':'?')+params.toString(); }
502
+ fetch(url,{credentials:'same-origin',headers:{Accept:'application/json'}})
503
+ .then(function(r){return r.json().catch(function(){return {};}).then(function(j){return {ok:r.ok,json:j};});})
504
+ .then(function(res){
505
+ var payload=res.json||{};
506
+ if(!res.ok){
507
+ var em=(payload&&typeof payload.error==='string'&&payload.error)?payload.error:'Unable to load calendar.';
508
+ throw new Error(em);
509
+ }
510
+ ingestChannels(payload.channels||[]);
511
+ ingestPosts(payload.posts||[]);
512
+ if(payload&&typeof payload.error==='string'&&payload.error){
513
+ setNotice(payload.error,false);
514
+ }else{
515
+ setNotice('',false);
516
+ }
517
+ setSync(payload&&payload.syncStatus==='error'?'Synced with warnings':'Up to date',payload&&payload.syncStatus==='error');
518
+ renderChannels();
519
+ renderGrid();
520
+ })
521
+ .catch(function(err){
522
+ var msg=(err&&err.message)?err.message:'Unable to load calendar.';
523
+ setNotice(msg,true);
524
+ setSync('Offline',true);
525
+ renderChannels();
526
+ renderGrid();
527
+ })
528
+ .finally(function(){
529
+ loading=false;
530
+ gridEl.classList.remove('is-loading');
531
+ });
532
+ }
533
+
534
+ if(channelsEl){
535
+ channelsEl.addEventListener('click',function(e){
536
+ var btn=e.target.closest('[data-rk-channel]');
537
+ if(!btn)return;
538
+ selectedChannelId=btn.getAttribute('data-rk-channel')||'';
539
+ renderChannels();
540
+ renderGrid();
541
+ });
542
+ }
543
+ if(chSearchEl){
544
+ chSearchEl.addEventListener('input',function(){
545
+ channelQuery=chSearchEl.value||'';
546
+ renderChannels();
547
+ });
548
+ }
549
+
550
+ root.querySelector('[data-rk-cal-prev]')&&root.querySelector('[data-rk-cal-prev]').addEventListener('click',function(){cursor=addMonths(cursor,-1);renderGrid();load();});
551
+ root.querySelector('[data-rk-cal-next]')&&root.querySelector('[data-rk-cal-next]').addEventListener('click',function(){cursor=addMonths(cursor,1);renderGrid();load();});
552
+ root.querySelector('[data-rk-cal-today]')&&root.querySelector('[data-rk-cal-today]').addEventListener('click',function(){cursor=startOfMonth(today);renderGrid();load();});
553
+
554
+ renderChannels();
555
+ renderGrid();
556
+ load();
557
+ })();`;
558
+ return reskinDocument({
559
+ title: "vidfarm reskin — Calendar",
560
+ description: "Reskinned vidfarm calendar — connected channels on the left, a compact viewport-fit month of scheduled posts on the right, live from /calendar/feed.",
561
+ activeSlug: "calendar",
562
+ pageCss,
563
+ body,
564
+ script
565
+ });
566
+ }
567
+ //# sourceMappingURL=calendar-page.js.map