@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,29 +1,48 @@
1
- // /reskin/calendar — the scheduled-posts calendar ported into the farmville
2
- // design system. A calm, full-width month board where each day holds simple
3
- // post chips (platform swatch + title + time) with a small status dot, today
4
- // highlighted, and a compact status legend beneath the grid.
1
+ // /reskin/calendar — the scheduled-posts calendar, MIGRATED to real production data.
5
2
  //
6
- // SIMPLIFIED layout: the earlier version paired the month grid with a right-hand
7
- // side rail (Channels card + "Up next" agenda) plus a legend and a Month/Week/Day
8
- // toggle. That was too busy, so the board is now the full-width hero of the page
9
- // and the rail is gone more whitespace, fewer competing elements per cell.
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.
10
7
  //
11
- // Renders with representative SAMPLE data (a full month of scheduled short-form
12
- // posts) so it can be viewed without auth or a FlockPoster connection. Uses ONLY
13
- // shared rk-* primitives + a small page-scoped `rk-calendar-` CSS block.
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/…).
14
28
  import { reskinDocument, rkEscape } from "./document.js";
15
- // ── The month we're rendering. today = Jul 7, 2026 (deterministic snapshot). ──
16
- const YEAR = 2026;
17
- const MONTH = 6; // 0-indexed July
18
- const TODAY_DOM = 7;
19
- const MONTH_LABEL = "July 2026";
20
- const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
21
- const PLATFORM = {
22
- TikTok: { initial: "T", swatch: "tiktok", label: "TikTok" },
23
- Instagram: { initial: "I", swatch: "instagram", label: "Instagram" },
24
- YouTube: { initial: "Y", swatch: "youtube", label: "YouTube" },
25
- Email: { initial: "E", swatch: "email", label: "Email" }
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: []
26
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.
27
46
  const STATUS = {
28
47
  draft: { label: "Draft", pill: "rk-pill" },
29
48
  review: { label: "Review", pill: "rk-pill-gold" },
@@ -32,221 +51,517 @@ const STATUS = {
32
51
  sent: { label: "Sent", pill: "rk-pill-violet" },
33
52
  error: { label: "Failed", pill: "rk-pill-red" }
34
53
  };
35
- // day-of-month scheduled posts (in-month only).
36
- const POSTS_BY_DOM = {
37
- 1: [
38
- { title: "Cold open hook A/B", platform: "TikTok", status: "scheduled", time: "9:00 AM" },
39
- { title: "Behind the build", platform: "Instagram", status: "draft", time: "12:30 PM" }
40
- ],
41
- 2: [{ title: "3 mistakes founders make", platform: "YouTube", status: "review", time: "2:00 PM" }],
42
- 6: [{ title: "Monday momentum", platform: "TikTok", status: "scheduled", time: "8:30 AM" }],
43
- 7: [
44
- { title: "Feature reveal teaser", platform: "Instagram", status: "ready", time: "11:00 AM" },
45
- { title: "Newsletter: ship week", platform: "Email", status: "scheduled", time: "4:00 PM" },
46
- { title: "POV: launch day", platform: "TikTok", status: "scheduled", time: "6:00 PM" }
47
- ],
48
- 8: [{ title: "Testimonial cut", platform: "YouTube", status: "scheduled", time: "1:00 PM" }],
49
- 9: [{ title: "Duet reply to @acme", platform: "TikTok", status: "draft", time: "5:15 PM" }],
50
- 10: [
51
- { title: "Weekly recap", platform: "Email", status: "scheduled", time: "9:00 AM" },
52
- { title: "Founder Q&A reel", platform: "Instagram", status: "review", time: "3:00 PM" }
53
- ],
54
- 13: [{ title: "Hook test v4", platform: "TikTok", status: "scheduled", time: "8:00 AM" }],
55
- 14: [
56
- { title: "Case study reel", platform: "Instagram", status: "scheduled", time: "12:00 PM" },
57
- { title: "Shorts: 60s explainer", platform: "YouTube", status: "ready", time: "5:00 PM" }
58
- ],
59
- 15: [{ title: "Mid-month drop", platform: "Email", status: "sent", time: "9:00 AM" }],
60
- 16: [{ title: "Trend jack: sound #4127", platform: "TikTok", status: "error", time: "7:00 PM" }],
61
- 17: [{ title: "Behind the numbers", platform: "YouTube", status: "scheduled", time: "2:00 PM" }],
62
- 20: [
63
- { title: "Monday reset", platform: "TikTok", status: "scheduled", time: "8:30 AM" },
64
- { title: "Carousel: 5 tips", platform: "Instagram", status: "draft", time: "1:00 PM" }
65
- ],
66
- 21: [{ title: "Founder story pt.2", platform: "Instagram", status: "scheduled", time: "11:00 AM" }],
67
- 23: [{ title: "Live recap", platform: "YouTube", status: "review", time: "4:00 PM" }],
68
- 24: [
69
- { title: "Weekly drop", platform: "Email", status: "scheduled", time: "9:00 AM" },
70
- { title: "Friday feature", platform: "TikTok", status: "scheduled", time: "5:00 PM" }
71
- ],
72
- 27: [{ title: "Hook test v5", platform: "TikTok", status: "scheduled", time: "8:00 AM" }],
73
- 28: [
74
- { title: "Product deep-dive", platform: "YouTube", status: "scheduled", time: "1:00 PM" },
75
- { title: "Reel: real results", platform: "Instagram", status: "ready", time: "6:00 PM" }
76
- ],
77
- 30: [
78
- { title: "Month-end recap", platform: "Email", status: "scheduled", time: "10:00 AM" },
79
- { title: "TikTok trend remix", platform: "TikTok", status: "draft", time: "3:00 PM" },
80
- { title: "Teaser: August drop", platform: "Instagram", status: "scheduled", time: "7:00 PM" }
81
- ],
82
- 31: [{ title: "Final Friday push", platform: "TikTok", status: "scheduled", time: "5:00 PM" }]
83
- };
84
- const CHANNELS = [
85
- { name: "Vidfarm Daily", handle: "@vidfarm", platform: "TikTok", active: true },
86
- { name: "Founder Reels", handle: "@dylan.builds", platform: "Instagram" },
87
- { name: "Studio Shorts", handle: "@vidfarm.studio", platform: "YouTube" },
88
- { name: "Weekly Drop", handle: "dylan@studio.co", platform: "Email" }
89
- ];
90
- function allPosts() {
91
- const out = [];
92
- for (const [dom, posts] of Object.entries(POSTS_BY_DOM)) {
93
- for (const p of posts)
94
- out.push({ ...p, dom: Number(dom) });
95
- }
96
- return out;
97
- }
98
- function platformSwatch(platform) {
99
- const meta = PLATFORM[platform];
100
- return `<span class="rk-calendar-swatch rk-calendar-swatch-${meta.swatch}" aria-hidden="true">${meta.initial}</span>`;
101
- }
102
- // One calm chip: platform swatch + title + a small status dot beside the time.
103
- // No colored borders — status lives in the dot (mapped by the legend below).
104
- function chip(p) {
105
- const st = STATUS[p.status];
106
- const meta = PLATFORM[p.platform];
107
- return `<button type="button" class="rk-calendar-chip" title="${rkEscape(`${p.title} · ${meta.label} · ${st.label} · ${p.time}`)}">
108
- ${platformSwatch(p.platform)}
109
- <span class="rk-calendar-chip-body">
110
- <span class="rk-calendar-chip-title">${rkEscape(p.title)}</span>
111
- <span class="rk-calendar-chip-foot">
112
- <span class="rk-calendar-dot" data-status="${p.status}" aria-hidden="true"></span>
113
- <span class="rk-calendar-chip-time">${rkEscape(p.time)}</span>
114
- </span>
115
- </span>
116
- </button>`;
117
- }
118
- function monthCells() {
119
- const firstWeekday = new Date(Date.UTC(YEAR, MONTH, 1)).getUTCDay();
120
- const daysInMonth = new Date(Date.UTC(YEAR, MONTH + 1, 0)).getUTCDate();
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();
121
62
  const rows = Math.ceil((firstWeekday + daysInMonth) / 7);
122
63
  const total = rows * 7;
64
+ const todayDom = now.getUTCDate();
123
65
  const cells = [];
124
66
  for (let i = 0; i < total; i++) {
125
- const offset = i - firstWeekday; // 0 → the 1st
126
- const d = new Date(Date.UTC(YEAR, MONTH, 1 + offset));
127
- const inMonth = d.getUTCMonth() === MONTH;
67
+ const offset = i - firstWeekday;
68
+ const d = new Date(Date.UTC(year, month, 1 + offset));
69
+ const inMonth = d.getUTCMonth() === month;
128
70
  const dom = d.getUTCDate();
129
- const isToday = inMonth && dom === TODAY_DOM;
130
- const posts = inMonth ? POSTS_BY_DOM[dom] ?? [] : [];
131
- const shown = posts.slice(0, 3);
132
- const overflow = posts.length - shown.length;
71
+ const isToday = inMonth && dom === todayDom;
133
72
  const cls = ["rk-calendar-cell"];
134
73
  if (!inMonth)
135
74
  cls.push("is-out");
136
75
  if (isToday)
137
76
  cls.push("is-today");
138
- const badge = isToday ? `<span class="rk-calendar-today">Today</span>` : "";
139
- const chips = shown.map(chip).join("");
140
- const more = overflow > 0 ? `<span class="rk-calendar-more">+${overflow} more</span>` : "";
141
77
  cells.push(`<div class="${cls.join(" ")}">
142
78
  <div class="rk-calendar-cell-head">
143
79
  <span class="rk-calendar-daynum">${dom}</span>
144
- ${badge}
145
80
  </div>
146
- <div class="rk-calendar-cell-posts">${chips}${more}</div>
81
+ <div class="rk-calendar-cell-posts"></div>
147
82
  </div>`);
148
83
  }
149
84
  return cells.join("");
150
85
  }
151
- export function renderReskinCalendar() {
152
- const posts = allPosts();
153
- const totalPosts = posts.length;
154
- // weekday header
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";
155
94
  const weekdayHead = WEEKDAYS.map((w) => `<div class="rk-calendar-weekday">${w}</div>`).join("");
156
- // compact one-line status legend under the grid (explains the chip dots)
157
95
  const legendOrder = ["draft", "review", "ready", "scheduled", "sent", "error"];
158
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");
159
106
  const body = `
160
- <main class="rk-container-wide rk-page">
161
- <div class="rk-page-head">
162
- <span class="rk-badge">Publishing calendar</span>
163
- <h1 class="rk-h1">Calendar</h1>
164
- <p class="rk-sub">${totalPosts} scheduled short-form posts across ${CHANNELS.length} channels this July.</p>
165
- </div>
166
-
167
- <div class="rk-calendar-toolbar">
168
- <div class="rk-calendar-monthnav">
169
- <button type="button" class="rk-btn rk-btn-ghost rk-calendar-navbtn" aria-label="Previous month">←</button>
170
- <span class="rk-calendar-month">${MONTH_LABEL}</span>
171
- <button type="button" class="rk-btn rk-btn-ghost rk-calendar-navbtn" aria-label="Next month">→</button>
172
- <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-calendar-today-btn">Today</button>
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>
173
112
  </div>
174
- <button type="button" class="rk-btn rk-btn-gold">+ Schedule post</button>
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>
175
114
  </div>
176
115
 
177
- <div class="rk-card rk-calendar-grid-card">
178
- <div class="rk-calendar-scroll">
179
- <div class="rk-calendar-weekdays">${weekdayHead}</div>
180
- <div class="rk-calendar-grid">${monthCells()}</div>
181
- </div>
182
- </div>
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>
183
140
 
184
- <div class="rk-calendar-legend">${legend}</div>
185
- </main>`;
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>`;
186
149
  const pageCss = `
187
- .rk-calendar-toolbar{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:24px}
188
- .rk-calendar-monthnav{display:flex;align-items:center;gap:10px}
189
- .rk-calendar-month{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-2xl);letter-spacing:-.02em;color:var(--rk-ink);min-width:8.6rem}
190
- .rk-calendar-navbtn{width:40px;height:40px;padding:0;font-size:16px;line-height:1}
191
- .rk-calendar-today-btn{margin-left:4px}
192
-
193
- /* month board — the full-width hero */
194
- .rk-calendar-grid-card{padding:0;overflow:hidden}
195
- .rk-calendar-scroll{overflow-x:auto}
196
- .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)}
197
- .rk-calendar-weekday{padding:14px 10px;text-align:center;font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--rk-text-muted)}
198
- .rk-calendar-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:1px;background:var(--rk-border)}
199
- .rk-calendar-cell{background:#fff;min-height:152px;padding:12px 11px;display:flex;flex-direction:column;gap:9px;transition:background var(--rk-dur) var(--rk-ease)}
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}}
200
233
  .rk-calendar-cell:hover{background:var(--rk-n-50)}
201
234
  .rk-calendar-cell.is-out{background:var(--rk-n-50)}
202
235
  .rk-calendar-cell.is-out:hover{background:var(--rk-n-100)}
203
236
  .rk-calendar-cell.is-today{background:var(--rk-gold-tint);box-shadow:inset 0 0 0 2px var(--rk-gold-500)}
204
- .rk-calendar-cell-head{display:flex;align-items:center;justify-content:space-between;gap:6px;min-height:28px}
205
- .rk-calendar-daynum{width:28px;height:28px;border-radius:var(--rk-r-full);display:grid;place-items:center;font-family:var(--rk-font-display);font-weight:700;font-size:14px;color:var(--rk-n-700)}
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)}
206
240
  .rk-calendar-cell.is-out .rk-calendar-daynum{color:var(--rk-n-400);font-weight:600}
207
241
  .rk-calendar-cell.is-today .rk-calendar-daynum{background:var(--rk-gold-500);color:var(--rk-ink);box-shadow:var(--rk-shadow-xs)}
208
- .rk-calendar-today{display:inline-flex;align-items:center;padding:3px 9px;border-radius:var(--rk-r-full);background:var(--rk-gold-500);color:var(--rk-ink);font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}
209
- .rk-calendar-cell-posts{display:flex;flex-direction:column;gap:7px}
242
+ .rk-calendar-cell-posts{display:flex;flex-direction:column;gap:3px;min-height:0;overflow:hidden}
210
243
 
211
- /* post chip — uniform hairline, airy, status shown by the dot (not a border) */
212
- .rk-calendar-chip{display:flex;gap:9px;align-items:center;width:100%;text-align:left;padding:8px 10px;border-radius:12px;background:#fff;border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-xs);cursor:pointer;transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
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}
213
248
  .rk-calendar-chip:hover{transform:translateY(-1px);box-shadow:var(--rk-shadow-sm)}
214
- .rk-calendar-chip-body{min-width:0;display:flex;flex-direction:column;gap:3px;flex:1}
215
- .rk-calendar-chip-title{font-size:12.5px;font-weight:700;color:var(--rk-ink);line-height:1.25;letter-spacing:-.01em;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
216
- .rk-calendar-chip-foot{display:flex;align-items:center;gap:6px}
217
- .rk-calendar-chip-time{font-size:11px;font-weight:600;color:var(--rk-text-muted)}
218
- .rk-calendar-more{font-size:11px;font-weight:700;color:var(--rk-text-muted);padding:1px 2px}
219
-
220
- /* platform swatch */
221
- .rk-calendar-swatch{width:26px;height:26px;border-radius:9px;display:grid;place-items:center;font-family:var(--rk-font-display);font-size:13px;font-weight:800;line-height:1;flex:none;box-shadow:var(--rk-shadow-xs)}
222
- .rk-calendar-swatch-tiktok{background:var(--rk-ink);color:#fff}
223
- .rk-calendar-swatch-instagram{background:linear-gradient(135deg,#f9ce34,#ee2a7b 55%,#6228d7);color:#fff}
224
- .rk-calendar-swatch-youtube{background:#ff3b30;color:#fff}
225
- .rk-calendar-swatch-email{background:var(--rk-sky-tint);color:#1d6fb8}
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}
226
253
 
227
- /* status dot — used inside chips AND in the legend */
228
- .rk-calendar-dot{width:8px;height:8px;border-radius:var(--rk-r-full);background:var(--rk-n-400);flex:none}
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}
229
256
  .rk-calendar-dot[data-status="review"]{background:var(--rk-gold-700)}
230
257
  .rk-calendar-dot[data-status="ready"]{background:var(--rk-green)}
231
258
  .rk-calendar-dot[data-status="scheduled"]{background:var(--rk-sky)}
232
259
  .rk-calendar-dot[data-status="sent"]{background:var(--rk-violet)}
233
260
  .rk-calendar-dot[data-status="error"]{background:var(--rk-red)}
234
261
 
235
- /* compact one-line legend */
236
- .rk-calendar-legend{display:flex;flex-wrap:wrap;gap:18px;margin-top:18px;padding:0 6px}
237
- .rk-calendar-legend-item{display:inline-flex;align-items:center;gap:7px;font-size:12px;font-weight:600;color:var(--rk-text-muted)}
238
-
239
262
  @media(max-width:760px){
240
263
  .rk-calendar-weekdays,.rk-calendar-grid{min-width:660px}
241
- .rk-calendar-toolbar{gap:12px}
264
+ .rk-calendar-grid-card{overflow-x:auto}
265
+ .rk-calendar-toolbar{gap:10px}
242
266
  }
243
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
+ })();`;
244
558
  return reskinDocument({
245
559
  title: "vidfarm reskin — Calendar",
246
- description: "Reskinned vidfarm calendar — a calm, full-width month of scheduled short-form posts.",
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.",
247
561
  activeSlug: "calendar",
248
562
  pageCss,
249
- body
563
+ body,
564
+ script
250
565
  });
251
566
  }
252
567
  //# sourceMappingURL=calendar-page.js.map