@mevdragon/vidfarm-devcli 0.5.3 → 0.6.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 (113) hide show
  1. package/README.md +3 -3
  2. package/demo/README.md +28 -0
  3. package/demo/dist/app.css +1 -0
  4. package/demo/dist/app.js +1184 -0
  5. package/demo/dist/chunks/chunk-DXB73IDG.js +1 -0
  6. package/demo/dist/chunks/chunk-S7OWAJDS.js +36 -0
  7. package/demo/dist/chunks/chunk-VTIBZ6AN.js +1 -0
  8. package/demo/dist/chunks/dist-ADSJKBVE.js +332 -0
  9. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +1 -0
  10. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +425 -0
  11. package/demo/dist/chunks/lib-XAQ37YOE.js +1 -0
  12. package/demo/dist/chunks/src-TJ2QYA4U.js +207 -0
  13. package/demo/dist/favicon.ico +0 -0
  14. package/demo/dist/icons/timeline/audio.svg +7 -0
  15. package/demo/dist/icons/timeline/captions.svg +5 -0
  16. package/demo/dist/icons/timeline/composition.svg +12 -0
  17. package/demo/dist/icons/timeline/image.svg +18 -0
  18. package/demo/dist/icons/timeline/music.svg +10 -0
  19. package/demo/dist/icons/timeline/text.svg +3 -0
  20. package/demo/dist/index.html +15 -0
  21. package/dist/src/account-pages-legacy.js +9396 -0
  22. package/dist/src/account-pages.js +61 -0
  23. package/dist/src/app.js +14378 -0
  24. package/dist/src/cli.js +1 -1
  25. package/dist/src/composition-runtime.js +613 -0
  26. package/dist/src/config.js +166 -0
  27. package/dist/src/context.js +447 -0
  28. package/dist/src/dev-app-legacy.js +739 -0
  29. package/dist/src/dev-app.js +6 -0
  30. package/dist/src/domain.js +2 -0
  31. package/dist/src/editor-chat-history.js +82 -0
  32. package/dist/src/editor-chat.js +449 -0
  33. package/dist/src/editor-dark-theme.js +1128 -0
  34. package/dist/src/frontend/debug.js +71 -0
  35. package/dist/src/frontend/flockposter-cache-store.js +124 -0
  36. package/dist/src/frontend/homepage-client.js +182 -0
  37. package/dist/src/frontend/homepage-shared.js +4 -0
  38. package/dist/src/frontend/homepage-store.js +28 -0
  39. package/dist/src/frontend/homepage-view.js +547 -0
  40. package/dist/src/frontend/page-runtime-client.js +132 -0
  41. package/dist/src/frontend/page-runtime-store.js +9 -0
  42. package/dist/src/frontend/sentry.js +42 -0
  43. package/dist/src/frontend/template-editor-chat.js +3960 -0
  44. package/dist/src/help-page.js +346 -0
  45. package/dist/src/homepage.js +1235 -0
  46. package/dist/src/hyperframes/composition.js +180 -0
  47. package/dist/src/index.js +16 -0
  48. package/dist/src/instrument.js +30 -0
  49. package/dist/src/lib/crypto.js +45 -0
  50. package/dist/src/lib/dev-log.js +54 -0
  51. package/dist/src/lib/display-name.js +11 -0
  52. package/dist/src/lib/ids.js +24 -0
  53. package/dist/src/lib/images.js +19 -0
  54. package/dist/src/lib/json.js +15 -0
  55. package/dist/src/lib/package-root.js +47 -0
  56. package/dist/src/lib/template-paths.js +28 -0
  57. package/dist/src/lib/time.js +7 -0
  58. package/dist/src/lib/url-clean.js +85 -0
  59. package/dist/src/page-runtime.js +2 -0
  60. package/dist/src/page-shell.js +1381 -0
  61. package/dist/src/primitive-context.js +357 -0
  62. package/dist/src/primitive-registry.js +2436 -0
  63. package/dist/src/primitive-sdk.js +4 -0
  64. package/dist/src/primitives/hyperframes-media.js +108 -0
  65. package/dist/src/react-page-shell.js +35 -0
  66. package/dist/src/ready-post-schedule-component.js +1540 -0
  67. package/dist/src/registry.js +296 -0
  68. package/dist/src/runtime.js +35 -0
  69. package/dist/src/services/api-call-history.js +249 -0
  70. package/dist/src/services/auth.js +152 -0
  71. package/dist/src/services/billing-pricing.js +39 -0
  72. package/dist/src/services/billing.js +228 -0
  73. package/dist/src/services/cast.js +127 -0
  74. package/dist/src/services/chat-threads.js +92 -0
  75. package/dist/src/services/composition-sanitize.js +124 -0
  76. package/dist/src/services/composition-watch.js +79 -0
  77. package/dist/src/services/fork-access.js +93 -0
  78. package/dist/src/services/fork-manifest.js +42 -0
  79. package/dist/src/services/ghostcut.js +179 -0
  80. package/dist/src/services/hyperframes.js +2307 -0
  81. package/dist/src/services/job-capacity.js +14 -0
  82. package/dist/src/services/job-logs.js +197 -0
  83. package/dist/src/services/jobs.js +136 -0
  84. package/dist/src/services/local-dynamo.js +0 -0
  85. package/dist/src/services/media-processing.js +766 -0
  86. package/dist/src/services/primitive-media-lambda.js +280 -0
  87. package/dist/src/services/providers.js +2926 -0
  88. package/dist/src/services/rate-limits.js +262 -0
  89. package/dist/src/services/serverless-auth.js +382 -0
  90. package/dist/src/services/serverless-jobs.js +1082 -0
  91. package/dist/src/services/serverless-provider-keys.js +409 -0
  92. package/dist/src/services/serverless-records.js +1385 -0
  93. package/dist/src/services/serverless-template-configs.js +75 -0
  94. package/dist/src/services/storage.js +383 -0
  95. package/dist/src/services/template-certification.js +413 -0
  96. package/dist/src/services/template-loader.js +99 -0
  97. package/dist/src/services/template-runtime-bundles.js +217 -0
  98. package/dist/src/services/template-sources.js +1017 -0
  99. package/dist/src/services/video-normalization.js +2 -0
  100. package/dist/src/services/webhooks.js +62 -0
  101. package/dist/src/template-editor-pages.js +2576 -0
  102. package/dist/src/template-editor-shell.js +2840 -0
  103. package/dist/src/template-sdk.js +4 -0
  104. package/dist/src/worker.js +17 -0
  105. package/package.json +6 -4
  106. package/public/assets/homepage-app.js +54 -0
  107. package/public/assets/homepage-client-app.js +80 -0
  108. package/public/assets/page-runtime-client-app.js +94 -0
  109. package/src/assets/SELLING_AWARENESS_STAGES.md +579 -0
  110. package/src/assets/SELLING_WITH_HOOKS.md +377 -0
  111. package/src/assets/SELLING_WITH_VSLS.md +606 -0
  112. package/src/assets/favicon.ico +0 -0
  113. package/src/assets/logo-vidfarm.png +0 -0
@@ -0,0 +1,1540 @@
1
+ import { escapeJsonForHtml } from "./page-shell.js";
2
+ export function renderReadyPostScheduleStyles() {
3
+ return `.ready-post-schedule-modal {
4
+ width: min(880px, calc(100vw - 28px));
5
+ padding: 0;
6
+ border: 1px solid rgba(191, 164, 109, 0.36);
7
+ border-radius: var(--radius-md);
8
+ background: var(--paper-strong);
9
+ color: var(--ink);
10
+ box-shadow: 0 28px 72px rgba(15, 23, 42, 0.18);
11
+ }
12
+
13
+ .ready-post-schedule-modal::backdrop {
14
+ background: rgba(36, 48, 65, 0.26);
15
+ backdrop-filter: blur(8px);
16
+ }
17
+
18
+ .ready-post-schedule-head {
19
+ display: grid;
20
+ gap: 8px;
21
+ padding: 22px 22px 18px;
22
+ border-bottom: 1px solid var(--line);
23
+ background: rgba(222, 192, 129, 0.1);
24
+ }
25
+
26
+ .ready-post-schedule-head h2 {
27
+ font-size: clamp(1.8rem, 4vw, 2.5rem);
28
+ }
29
+
30
+ .ready-post-schedule-body {
31
+ display: grid;
32
+ gap: 14px;
33
+ padding: 20px 22px 22px;
34
+ }
35
+
36
+ .ready-post-schedule-grid {
37
+ display: grid;
38
+ grid-template-columns: minmax(320px, 1fr) minmax(280px, 0.74fr);
39
+ gap: 16px;
40
+ align-items: start;
41
+ }
42
+
43
+ .ready-post-calendar {
44
+ display: grid;
45
+ gap: 12px;
46
+ padding: 14px;
47
+ border: 1px solid rgba(209, 219, 233, 0.96);
48
+ border-radius: 18px;
49
+ background: rgba(255, 255, 255, 0.62);
50
+ }
51
+
52
+ .ready-post-calendar-head {
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: space-between;
56
+ gap: 10px;
57
+ }
58
+
59
+ .ready-post-calendar-title {
60
+ color: #2f3748;
61
+ font-weight: 800;
62
+ letter-spacing: -0.02em;
63
+ }
64
+
65
+ .ready-post-calendar-nav {
66
+ display: flex;
67
+ gap: 6px;
68
+ }
69
+
70
+ .ready-post-calendar-nav button {
71
+ width: 38px;
72
+ min-width: 38px;
73
+ min-height: 38px;
74
+ padding: 0;
75
+ }
76
+
77
+ .ready-post-calendar-weekdays,
78
+ .ready-post-calendar-days {
79
+ display: grid;
80
+ grid-template-columns: repeat(7, minmax(0, 1fr));
81
+ gap: 6px;
82
+ }
83
+
84
+ .ready-post-calendar-weekdays span {
85
+ color: var(--muted);
86
+ font-size: 0.72rem;
87
+ font-weight: 800;
88
+ text-align: center;
89
+ text-transform: uppercase;
90
+ }
91
+
92
+ .ready-post-calendar-day {
93
+ position: relative;
94
+ display: grid;
95
+ place-items: center;
96
+ min-width: 0;
97
+ min-height: 52px;
98
+ padding: 0;
99
+ border-color: rgba(209, 219, 233, 0.78);
100
+ background: rgba(255, 255, 255, 0.76);
101
+ border-radius: 14px;
102
+ color: #2f3748;
103
+ font-size: 0.9rem;
104
+ font-weight: 700;
105
+ }
106
+
107
+ .ready-post-calendar-day-number {
108
+ line-height: 1;
109
+ }
110
+
111
+ .ready-post-calendar-day-preview {
112
+ position: absolute;
113
+ right: 7px;
114
+ bottom: 6px;
115
+ min-width: 18px;
116
+ padding: 2px 5px;
117
+ border-radius: 999px;
118
+ background: rgba(255, 252, 247, 0.92);
119
+ color: #536176;
120
+ font-size: 0.58rem;
121
+ font-weight: 900;
122
+ line-height: 1.1;
123
+ }
124
+
125
+ .ready-post-calendar-day.has-posts {
126
+ border-color: rgba(83, 114, 89, 0.34);
127
+ background: rgba(83, 114, 89, 0.11);
128
+ }
129
+
130
+ .ready-post-calendar-day.has-posts::after {
131
+ content: "";
132
+ position: absolute;
133
+ left: 50%;
134
+ bottom: 8px;
135
+ width: 18px;
136
+ height: 3px;
137
+ border-radius: 999px;
138
+ background: rgba(83, 114, 89, 0.58);
139
+ transform: translateX(-50%);
140
+ }
141
+
142
+ .ready-post-calendar-day.has-many-posts::after {
143
+ display: none;
144
+ }
145
+
146
+ .ready-post-calendar-day.has-many-posts {
147
+ background: rgba(191, 164, 109, 0.2);
148
+ border-color: rgba(191, 164, 109, 0.44);
149
+ }
150
+
151
+ .ready-post-calendar-day.is-muted {
152
+ opacity: 0.42;
153
+ }
154
+
155
+ .ready-post-calendar-day.is-today {
156
+ border-color: rgba(191, 164, 109, 0.85);
157
+ }
158
+
159
+ .ready-post-calendar-day.is-selected {
160
+ background: #243041;
161
+ color: #fffaf2;
162
+ border-color: #243041;
163
+ }
164
+
165
+ .ready-post-calendar-day.is-selected::after {
166
+ background: rgba(255, 250, 242, 0.78);
167
+ }
168
+
169
+ .ready-post-calendar-day.is-selected .ready-post-calendar-day-preview {
170
+ background: rgba(255, 250, 242, 0.18);
171
+ color: rgba(255, 250, 242, 0.9);
172
+ }
173
+
174
+ .ready-post-calendar-popover {
175
+ position: fixed;
176
+ z-index: 999;
177
+ display: grid;
178
+ width: min(390px, calc(100vw - 32px));
179
+ gap: 12px;
180
+ padding: 14px;
181
+ border: 1px solid rgba(191, 164, 109, 0.28);
182
+ border-radius: 18px;
183
+ background: rgba(255, 252, 247, 0.98);
184
+ box-shadow: 0 18px 44px rgba(37, 45, 61, 0.18);
185
+ }
186
+
187
+ .ready-post-calendar-popover[hidden] {
188
+ display: none;
189
+ }
190
+
191
+ .ready-post-calendar-popover-title {
192
+ padding-right: 30px;
193
+ color: #2f3748;
194
+ font-size: 0.95rem;
195
+ font-weight: 800;
196
+ }
197
+
198
+ .ready-post-calendar-popover-close {
199
+ position: absolute;
200
+ top: 10px;
201
+ right: 10px;
202
+ display: inline-flex;
203
+ align-items: center;
204
+ justify-content: center;
205
+ width: 26px;
206
+ min-width: 26px;
207
+ height: 26px;
208
+ min-height: 26px;
209
+ padding: 0;
210
+ border: 1px solid rgba(209, 219, 233, 0.8);
211
+ border-radius: 999px;
212
+ background: rgba(255, 255, 255, 0.72);
213
+ color: var(--muted);
214
+ box-shadow: none;
215
+ font-size: 0.95rem;
216
+ line-height: 1;
217
+ }
218
+
219
+ .ready-post-calendar-popover-close:hover {
220
+ background: rgba(255, 255, 255, 0.95);
221
+ color: #2f3748;
222
+ transform: none;
223
+ box-shadow: none;
224
+ }
225
+
226
+ .ready-post-calendar-popover-list {
227
+ display: grid;
228
+ gap: 10px;
229
+ max-height: min(360px, calc(100vh - 180px));
230
+ overflow: auto;
231
+ padding-right: 2px;
232
+ }
233
+
234
+ .ready-post-calendar-popover-item {
235
+ display: grid;
236
+ gap: 6px;
237
+ padding: 10px 11px;
238
+ border: 1px solid rgba(209, 219, 233, 0.72);
239
+ border-radius: 14px;
240
+ background: rgba(255, 255, 255, 0.72);
241
+ }
242
+
243
+ .ready-post-calendar-popover-time {
244
+ color: var(--muted);
245
+ font-size: 0.72rem;
246
+ font-weight: 800;
247
+ letter-spacing: 0.08em;
248
+ text-transform: uppercase;
249
+ }
250
+
251
+ .ready-post-calendar-popover-item a {
252
+ color: #2f3748;
253
+ font-size: 0.95rem;
254
+ font-weight: 700;
255
+ line-height: 1.18;
256
+ text-decoration: none;
257
+ }
258
+
259
+ .ready-post-calendar-popover-item a:hover {
260
+ text-decoration: underline;
261
+ }
262
+
263
+ .ready-post-calendar-popover-linkless {
264
+ color: #2f3748;
265
+ font-size: 0.95rem;
266
+ font-weight: 700;
267
+ line-height: 1.18;
268
+ }
269
+
270
+ .ready-post-calendar-popover-summary,
271
+ .ready-post-calendar-popover-channel {
272
+ color: var(--muted);
273
+ font-size: 0.78rem;
274
+ line-height: 1.35;
275
+ }
276
+
277
+ .ready-post-calendar-popover-channel {
278
+ font-weight: 800;
279
+ }
280
+
281
+ .ready-post-schedule-side {
282
+ display: grid;
283
+ gap: 14px;
284
+ }
285
+
286
+ .ready-post-datetime-row {
287
+ display: grid;
288
+ grid-template-columns: minmax(0, 1fr) minmax(118px, 0.46fr);
289
+ gap: 10px;
290
+ align-items: end;
291
+ }
292
+
293
+ .ready-post-datetime-row .label-group {
294
+ min-width: 0;
295
+ }
296
+
297
+ .ready-post-datetime-shell {
298
+ position: relative;
299
+ min-width: 0;
300
+ cursor: pointer;
301
+ }
302
+
303
+ .ready-post-datetime-shell input {
304
+ min-width: 0;
305
+ padding-right: 44px;
306
+ cursor: pointer;
307
+ }
308
+
309
+ .ready-post-datetime-icon {
310
+ pointer-events: none;
311
+ position: absolute;
312
+ right: 16px;
313
+ top: 50%;
314
+ width: 18px;
315
+ height: 18px;
316
+ transform: translateY(-50%);
317
+ color: #243041;
318
+ }
319
+
320
+ .ready-post-timezone-select {
321
+ min-width: 0;
322
+ padding-inline: 14px;
323
+ text-overflow: ellipsis;
324
+ }
325
+
326
+ .ready-post-channel-picker {
327
+ position: relative;
328
+ display: grid;
329
+ gap: 8px;
330
+ }
331
+
332
+ .ready-post-channel-trigger {
333
+ display: flex;
334
+ align-items: center;
335
+ justify-content: flex-start;
336
+ width: 100%;
337
+ min-height: 54px;
338
+ gap: 10px;
339
+ padding: 8px 14px;
340
+ border: 1px solid var(--line);
341
+ border-radius: 999px;
342
+ background: rgba(255, 255, 255, 0.92);
343
+ color: var(--ink);
344
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
345
+ }
346
+
347
+ .ready-post-channel-trigger.is-placeholder {
348
+ color: var(--muted);
349
+ }
350
+
351
+ .ready-post-channel-trigger:hover {
352
+ transform: none;
353
+ }
354
+
355
+ .ready-post-channel-avatar {
356
+ display: inline-flex;
357
+ align-items: center;
358
+ justify-content: center;
359
+ width: 34px;
360
+ height: 34px;
361
+ flex: 0 0 34px;
362
+ overflow: hidden;
363
+ border: 1px solid rgba(209, 219, 233, 0.96);
364
+ border-radius: 999px;
365
+ background: #f3efe5;
366
+ color: #243041;
367
+ font-size: 0.72rem;
368
+ font-weight: 800;
369
+ }
370
+
371
+ .ready-post-channel-avatar img {
372
+ width: 100%;
373
+ height: 100%;
374
+ object-fit: cover;
375
+ }
376
+
377
+ .ready-post-channel-copy {
378
+ display: grid;
379
+ min-width: 0;
380
+ text-align: left;
381
+ line-height: 1.2;
382
+ }
383
+
384
+ .ready-post-channel-title {
385
+ overflow: hidden;
386
+ color: #2f3748;
387
+ font-weight: 800;
388
+ text-overflow: ellipsis;
389
+ white-space: nowrap;
390
+ }
391
+
392
+ .ready-post-channel-meta {
393
+ overflow: hidden;
394
+ color: var(--muted);
395
+ font-size: 0.82rem;
396
+ text-overflow: ellipsis;
397
+ white-space: nowrap;
398
+ }
399
+
400
+ .ready-post-channel-menu {
401
+ position: absolute;
402
+ top: calc(100% + 8px);
403
+ right: 0;
404
+ left: 0;
405
+ z-index: 80;
406
+ display: grid;
407
+ gap: 8px;
408
+ max-height: 330px;
409
+ padding: 10px;
410
+ border: 1px solid rgba(191, 164, 109, 0.28);
411
+ border-radius: 18px;
412
+ background: rgba(255, 252, 247, 0.98);
413
+ box-shadow: 0 18px 44px rgba(37, 45, 61, 0.18);
414
+ }
415
+
416
+ .ready-post-email-instructions {
417
+ display: grid;
418
+ gap: 8px;
419
+ }
420
+
421
+ .ready-post-email-instructions[hidden] {
422
+ display: none;
423
+ }
424
+
425
+ .ready-post-email-instructions textarea {
426
+ min-height: 118px;
427
+ resize: vertical;
428
+ border-radius: 16px;
429
+ padding: 12px 14px;
430
+ font-size: 1rem;
431
+ font-weight: 400;
432
+ line-height: 1.45;
433
+ background: rgba(255, 255, 255, 0.88);
434
+ }
435
+
436
+ .ready-post-channel-menu[hidden] {
437
+ display: none;
438
+ }
439
+
440
+ .ready-post-channel-search {
441
+ min-height: 42px;
442
+ padding: 10px 13px;
443
+ border-radius: 14px;
444
+ }
445
+
446
+ .ready-post-channel-list {
447
+ display: grid;
448
+ gap: 6px;
449
+ overflow: auto;
450
+ max-height: 244px;
451
+ }
452
+
453
+ .ready-post-channel-option {
454
+ display: flex;
455
+ justify-content: flex-start;
456
+ width: 100%;
457
+ min-height: 48px;
458
+ gap: 10px;
459
+ padding: 7px 9px;
460
+ border: 0;
461
+ border-radius: 12px;
462
+ background: transparent;
463
+ box-shadow: none;
464
+ }
465
+
466
+ .ready-post-channel-option:hover,
467
+ .ready-post-channel-option.is-selected {
468
+ background: rgba(222, 192, 129, 0.18);
469
+ transform: none;
470
+ box-shadow: none;
471
+ }
472
+
473
+ .ready-post-channel-empty {
474
+ padding: 12px;
475
+ color: var(--muted);
476
+ font-size: 0.9rem;
477
+ }
478
+
479
+ .ready-post-schedule-note {
480
+ display: none;
481
+ padding: 12px 14px;
482
+ border: 1px solid rgba(120, 138, 166, 0.24);
483
+ border-radius: 18px;
484
+ background: rgba(255, 255, 255, 0.72);
485
+ color: #5b6d8f;
486
+ font-size: 0.92rem;
487
+ line-height: 1.5;
488
+ }
489
+
490
+ .ready-post-schedule-note[data-visible="true"] {
491
+ display: block;
492
+ }
493
+
494
+ .ready-post-schedule-note[data-tone="info"] {
495
+ border-color: rgba(84, 120, 184, 0.28);
496
+ background: rgba(238, 245, 255, 0.82);
497
+ color: #4d6591;
498
+ }
499
+
500
+ .ready-post-schedule-note[data-tone="success"] {
501
+ border-color: rgba(64, 139, 92, 0.28);
502
+ background: rgba(237, 248, 240, 0.82);
503
+ color: #326744;
504
+ }
505
+
506
+ .ready-post-schedule-note[data-tone="error"] {
507
+ border-color: rgba(190, 83, 73, 0.32);
508
+ background: rgba(255, 241, 239, 0.88);
509
+ color: #a34239;
510
+ }
511
+
512
+ .ready-post-schedule-toast {
513
+ position: absolute;
514
+ right: 28px;
515
+ bottom: 86px;
516
+ z-index: 4;
517
+ max-width: min(420px, calc(100% - 56px));
518
+ padding: 10px 13px;
519
+ border: 1px solid rgba(191, 164, 109, 0.38);
520
+ border-radius: 14px;
521
+ background: rgba(35, 43, 58, 0.94);
522
+ color: #fff;
523
+ font-size: 0.9rem;
524
+ line-height: 1.35;
525
+ box-shadow: 0 18px 42px rgba(20, 24, 34, 0.22);
526
+ opacity: 0;
527
+ pointer-events: none;
528
+ transform: translateY(8px);
529
+ transition: opacity 160ms ease, transform 160ms ease;
530
+ }
531
+
532
+ .ready-post-schedule-toast[data-visible="true"] {
533
+ opacity: 1;
534
+ transform: translateY(0);
535
+ }
536
+
537
+ .ready-post-schedule-actions {
538
+ justify-content: flex-end;
539
+ }
540
+
541
+ @media (max-width: 920px) {
542
+ .ready-post-actions {
543
+ justify-content: flex-start;
544
+ }
545
+
546
+ .ready-post-grid {
547
+ grid-template-columns: 1fr;
548
+ height: auto;
549
+ overflow: visible;
550
+ }
551
+
552
+ .ready-post-carousel,
553
+ .ready-post-slide {
554
+ min-height: 0;
555
+ }
556
+
557
+ .ready-post-phone-stage {
558
+ height: auto;
559
+ min-height: 560px;
560
+ }
561
+
562
+ .ready-post-schedule-toast {
563
+ right: 18px;
564
+ bottom: 74px;
565
+ max-width: calc(100% - 36px);
566
+ }
567
+
568
+ .ready-post-phone {
569
+ width: min(340px, 100%);
570
+ height: auto;
571
+ }
572
+
573
+ .ready-post-detail-column {
574
+ overflow: visible;
575
+ padding-right: 0;
576
+ }
577
+
578
+ .ready-post-asset {
579
+ align-items: flex-start;
580
+ flex-direction: column;
581
+ }
582
+
583
+ .ready-post-asset-actions,
584
+ .ready-post-asset-actions .button {
585
+ width: 100%;
586
+ }
587
+
588
+ .ready-post-title-row,
589
+ .ready-post-schedule-grid {
590
+ grid-template-columns: 1fr;
591
+ display: grid;
592
+ }
593
+
594
+ .ready-post-actions {
595
+ max-width: 100%;
596
+ min-width: 0;
597
+ flex-wrap: wrap;
598
+ white-space: normal;
599
+ padding-bottom: 2px;
600
+ }
601
+
602
+ .ready-post-calendar-day {
603
+ min-height: 44px;
604
+ }
605
+
606
+ .ready-post-calendar-nav button {
607
+ width: 44px;
608
+ min-width: 44px;
609
+ min-height: 44px;
610
+ }
611
+
612
+ .ready-post-calendar-popover-close {
613
+ width: 40px;
614
+ min-width: 40px;
615
+ height: 40px;
616
+ min-height: 40px;
617
+ }
618
+
619
+ .ready-post-calendar-popover-title {
620
+ padding-right: 46px;
621
+ }
622
+ }
623
+
624
+ @media (max-width: 560px) {
625
+ .ready-post-datetime-row {
626
+ grid-template-columns: 1fr;
627
+ }
628
+ }
629
+ `;
630
+ }
631
+ export function renderReadyPostScheduleModal() {
632
+ return `<dialog class="ready-post-schedule-modal" data-schedule-modal>
633
+ <div class="ready-post-schedule-head">
634
+ <div class="label">Schedule post</div>
635
+ <h2>Choose a slot</h2>
636
+ <p>This is a UI preview only. It does not create a scheduled post yet.</p>
637
+ </div>
638
+ <form method="dialog" class="ready-post-schedule-body">
639
+ <div class="ready-post-schedule-grid">
640
+ <section class="ready-post-calendar" aria-label="Schedule calendar">
641
+ <div class="ready-post-calendar-head">
642
+ <div class="ready-post-calendar-title" data-schedule-calendar-title></div>
643
+ <div class="ready-post-calendar-nav">
644
+ <button type="button" class="secondary" data-schedule-prev-month aria-label="Previous month">‹</button>
645
+ <button type="button" class="secondary" data-schedule-next-month aria-label="Next month">›</button>
646
+ </div>
647
+ </div>
648
+ <div class="ready-post-calendar-weekdays" aria-hidden="true">
649
+ <span>Sun</span><span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span><span>Sat</span>
650
+ </div>
651
+ <div class="ready-post-calendar-days" data-schedule-calendar-days></div>
652
+ <input type="hidden" name="schedule_date" data-schedule-date>
653
+ </section>
654
+ <div class="ready-post-schedule-side">
655
+ <div class="ready-post-datetime-row">
656
+ <label class="label-group">
657
+ <span class="label">Date & time</span>
658
+ <span class="ready-post-datetime-shell" data-schedule-datetime-shell>
659
+ <input type="datetime-local" name="schedule_datetime" data-schedule-datetime>
660
+ <svg class="ready-post-datetime-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="10" cy="10" r="7.2"></circle><path d="M10 6.4v4l2.7 1.6"></path></svg>
661
+ </span>
662
+ </label>
663
+ <label class="label-group">
664
+ <span class="label">Timezone</span>
665
+ <select class="ready-post-timezone-select" name="schedule_timezone" data-schedule-timezone></select>
666
+ </label>
667
+ </div>
668
+ <label class="label-group">
669
+ <span class="label">Channel</span>
670
+ <span class="ready-post-channel-picker" data-schedule-channel-picker>
671
+ <button type="button" class="ready-post-channel-trigger" data-schedule-channel-trigger aria-haspopup="listbox" aria-expanded="false"></button>
672
+ <span class="ready-post-channel-menu" data-schedule-channel-menu hidden>
673
+ <input class="ready-post-channel-search" type="search" data-schedule-channel-search placeholder="Search channels" autocomplete="off">
674
+ <span class="ready-post-channel-list" data-schedule-channel-list role="listbox"></span>
675
+ </span>
676
+ <input type="hidden" name="schedule_channel" data-schedule-channel-value>
677
+ </span>
678
+ </label>
679
+ <label class="label-group ready-post-email-instructions" data-schedule-email-instructions hidden>
680
+ <span class="label">Additional instructions</span>
681
+ <textarea name="additional_notes" data-schedule-additional-notes maxlength="4000" placeholder="Example: publish this to LinkedIn after approval, adapt the caption for Reddit, or send to the Douyin team for localization."></textarea>
682
+ </label>
683
+ </div>
684
+ </div>
685
+ <div class="ready-post-schedule-note" data-schedule-note role="status" aria-live="polite"></div>
686
+ <div class="toolbar ready-post-schedule-actions">
687
+ <button type="button" class="secondary" data-schedule-close>Close</button>
688
+ <button type="button" data-schedule-submit disabled>Schedule</button>
689
+ </div>
690
+ </form>
691
+ <div class="ready-post-schedule-toast" data-schedule-toast role="status" aria-live="polite"></div>
692
+ <div class="ready-post-calendar-popover" data-schedule-post-popover hidden></div>
693
+ </dialog>`;
694
+ }
695
+ export function renderReadyPostScheduleData(boot) {
696
+ return `<script id="ready-post-schedule-data" type="application/json">${escapeJsonForHtml(boot)}</script>`;
697
+ }
698
+ export function renderReadyPostScheduleScript() {
699
+ return `<script>
700
+ (() => {
701
+ const scheduleBoot = (() => {
702
+ try {
703
+ return JSON.parse(document.getElementById("ready-post-schedule-data")?.textContent || "{}");
704
+ } catch {
705
+ return {};
706
+ }
707
+ })();
708
+ document.addEventListener("click", (event) => {
709
+ const scheduleTrigger = event.target.closest("[data-schedule]");
710
+ if (scheduleTrigger) {
711
+ scheduleBoot.scheduleEndpoint = scheduleTrigger.dataset.scheduleEndpoint || scheduleBoot.scheduleEndpoint || null;
712
+ const modal = document.querySelector("[data-schedule-modal]");
713
+ if (modal?.showModal) {
714
+ modal.showModal();
715
+ } else if (modal) {
716
+ modal.setAttribute("open", "");
717
+ }
718
+ void refreshScheduleCalendar();
719
+ }
720
+ if (event.target.closest("[data-schedule-close]")) {
721
+ const modal = document.querySelector("[data-schedule-modal]");
722
+ if (modal?.close) {
723
+ modal.close();
724
+ } else if (modal) {
725
+ modal.removeAttribute("open");
726
+ }
727
+ }
728
+ if (event.target.closest("[data-schedule-post-popover-close]")) {
729
+ closePostPopover();
730
+ }
731
+ });
732
+ document.querySelector("[data-schedule-modal]")?.addEventListener("click", (event) => {
733
+ if (event.target === event.currentTarget) {
734
+ event.currentTarget.close?.();
735
+ }
736
+ });
737
+ const channelPicker = document.querySelector("[data-schedule-channel-picker]");
738
+ const channelTrigger = document.querySelector("[data-schedule-channel-trigger]");
739
+ const channelMenu = document.querySelector("[data-schedule-channel-menu]");
740
+ const channelSearch = document.querySelector("[data-schedule-channel-search]");
741
+ const channelList = document.querySelector("[data-schedule-channel-list]");
742
+ const channelValueInput = document.querySelector("[data-schedule-channel-value]");
743
+ const emailInstructions = document.querySelector("[data-schedule-email-instructions]");
744
+ const additionalNotesInput = document.querySelector("[data-schedule-additional-notes]");
745
+ const scheduleDateInput = document.querySelector("[data-schedule-date]");
746
+ const scheduleDateTimeInput = document.querySelector("[data-schedule-datetime]");
747
+ const scheduleDateTimeShell = document.querySelector("[data-schedule-datetime-shell]");
748
+ const timezoneSelect = document.querySelector("[data-schedule-timezone]");
749
+ const calendarTitle = document.querySelector("[data-schedule-calendar-title]");
750
+ const calendarDays = document.querySelector("[data-schedule-calendar-days]");
751
+ const postPopover = document.querySelector("[data-schedule-post-popover]");
752
+ const scheduleNote = document.querySelector("[data-schedule-note]");
753
+ const scheduleSubmit = document.querySelector("[data-schedule-submit]");
754
+ const scheduleToast = document.querySelector("[data-schedule-toast]");
755
+ const monthFormatter = new Intl.DateTimeFormat(undefined, { month: "long", year: "numeric" });
756
+ const popoverDateFormatter = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" });
757
+ const popoverTimeFormatter = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" });
758
+ const toastDateFormatter = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
759
+ const minScheduleLeadMs = 10 * 60 * 1000;
760
+ const cacheKey = "vidfarm:flockposter-cache:v1_" + (scheduleBoot.userId || "public");
761
+ const isoDate = (date) => {
762
+ const year = date.getFullYear();
763
+ const month = String(date.getMonth() + 1).padStart(2, "0");
764
+ const day = String(date.getDate()).padStart(2, "0");
765
+ return year + "-" + month + "-" + day;
766
+ };
767
+ const pad2 = (value) => String(value).padStart(2, "0");
768
+ const isoDateTimeLocal = (date, time) => isoDate(date) + "T" + time;
769
+ const startOfDay = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
770
+ const today = startOfDay(new Date());
771
+ let selectedDate = today;
772
+ let calendarCursor = new Date(today.getFullYear(), today.getMonth(), 1);
773
+ let selectedTime = (() => {
774
+ const next = new Date();
775
+ next.setMinutes(next.getMinutes() >= 30 ? 0 : 30, 0, 0);
776
+ if (next.getTime() < Date.now() + minScheduleLeadMs) next.setHours(next.getHours() + 1);
777
+ return pad2(next.getHours()) + ":" + pad2(next.getMinutes());
778
+ })();
779
+ let scheduleChannels = Array.isArray(scheduleBoot.channels) ? scheduleBoot.channels : [];
780
+ let selectedChannelId = "";
781
+ let channelSearchQuery = "";
782
+ let isScheduleSubmitting = false;
783
+ let scheduleSubmitMode = "schedule";
784
+ let hasInteractedWithScheduleDateTime = false;
785
+ let scheduledPosts = [];
786
+ let loadedRangeStart = null;
787
+ let loadedRangeEnd = null;
788
+ let scheduleFetchPromise = null;
789
+ let hasLoadedScheduleCalendar = false;
790
+ let activePostPopoverDateKey = "";
791
+
792
+ const createOption = (label, value) => {
793
+ const option = document.createElement("option");
794
+ option.textContent = label;
795
+ option.value = value || label;
796
+ return option;
797
+ };
798
+
799
+ const describeChannel = (channel) => {
800
+ const name = channel?.name || channel?.title || channel?.id || "Untitled channel";
801
+ const handle = channel?.handle ? " @" + channel.handle : "";
802
+ const platform = channel?.platform ? " · " + String(channel.platform).toUpperCase() : "";
803
+ return name + handle + platform;
804
+ };
805
+
806
+ const normalizeChannelsForSchedule = (entries) => (Array.isArray(entries) ? entries : []).map((channel) => ({
807
+ id: String(channel.id || ""),
808
+ name: String(channel.name || channel.title || channel.id || "Channel"),
809
+ title: String(channel.title || channel.name || channel.id || "Channel"),
810
+ handle: channel.handle ? String(channel.handle) : null,
811
+ platform: channel.platform ? String(channel.platform) : null,
812
+ avatarUrl: channel.avatarUrl ? String(channel.avatarUrl) : null,
813
+ platformIconUrl: channel.platformIconUrl ? String(channel.platformIconUrl) : null
814
+ })).filter((channel) => channel.id);
815
+
816
+ const dedupeById = (entries) => [...new Map(entries.map((entry) => [entry.id, entry])).values()];
817
+
818
+ const dedupeSchedulePosts = (entries) => {
819
+ const byId = dedupeById(entries);
820
+ const managedSlots = new Set(byId
821
+ .filter((post) => post.managedBy === "vidfarm")
822
+ .map((post) => post.channelId + "|" + post.scheduledAt));
823
+ return byId.filter((post) => post.managedBy !== "external" || !managedSlots.has(post.channelId + "|" + post.scheduledAt));
824
+ };
825
+
826
+ const normalizePostsForSchedule = (entries) => dedupeSchedulePosts((Array.isArray(entries) ? entries : []).map((post) => {
827
+ const scheduledAt = typeof post?.scheduledAt === "string" ? post.scheduledAt : "";
828
+ return {
829
+ id: String(post?.id || ""),
830
+ scheduleId: String(post?.scheduleId || post?.schedule_id || post?.id || ""),
831
+ postId: String(post?.postId || post?.post_id || ""),
832
+ managedBy: String(post?.managedBy || post?.managed_by || ""),
833
+ channelId: String(post?.channelId || ""),
834
+ scheduledAt,
835
+ status: String(post?.status || "Scheduled"),
836
+ title: String(post?.title || "Scheduled post"),
837
+ summary: String(post?.summary || ""),
838
+ url: typeof post?.url === "string" && post.url ? post.url : null
839
+ };
840
+ }).filter((post) => post.id && post.channelId && !Number.isNaN(Date.parse(post.scheduledAt))));
841
+
842
+ const readScheduleCache = () => {
843
+ try {
844
+ const runtime = window.__vidfarmFlockPosterCache;
845
+ if (runtime && typeof runtime.readSnapshot === "function") {
846
+ return runtime.readSnapshot();
847
+ }
848
+ const raw = window.localStorage.getItem(cacheKey);
849
+ return raw ? JSON.parse(raw) : {};
850
+ } catch {
851
+ return {};
852
+ }
853
+ };
854
+
855
+ const applyScheduleCache = () => {
856
+ const snapshot = readScheduleCache();
857
+ if (Array.isArray(snapshot.channels) && snapshot.channels.length) {
858
+ scheduleChannels = normalizeChannelsForSchedule(dedupeById([...scheduleChannels, ...snapshot.channels]));
859
+ }
860
+ if (Array.isArray(snapshot.posts) && snapshot.posts.length) {
861
+ scheduledPosts = normalizePostsForSchedule([...scheduledPosts, ...snapshot.posts]);
862
+ }
863
+ loadedRangeStart = typeof snapshot.calendarRangeStart === "string" ? snapshot.calendarRangeStart : loadedRangeStart;
864
+ loadedRangeEnd = typeof snapshot.calendarRangeEnd === "string" ? snapshot.calendarRangeEnd : loadedRangeEnd;
865
+ };
866
+
867
+ const writeScheduleCache = (channels, posts, rangeStart, rangeEnd) => {
868
+ const runtime = window.__vidfarmFlockPosterCache;
869
+ if (runtime && typeof runtime.setCalendarData === "function") {
870
+ runtime.setCalendarData({ channels, posts, rangeStart, rangeEnd, replaceRange: true });
871
+ return;
872
+ }
873
+ try {
874
+ const current = readScheduleCache();
875
+ window.localStorage.setItem(cacheKey, JSON.stringify({
876
+ channels: dedupeById([...(Array.isArray(current.channels) ? current.channels : []), ...channels]),
877
+ posts: normalizePostsForSchedule([...(Array.isArray(current.posts) ? current.posts : []), ...posts]),
878
+ channelsUpdatedAt: new Date().toISOString(),
879
+ calendarUpdatedAt: new Date().toISOString(),
880
+ calendarRangeStart: rangeStart,
881
+ calendarRangeEnd: rangeEnd
882
+ }));
883
+ } catch {
884
+ // Ignore storage failures.
885
+ }
886
+ };
887
+
888
+ const clearScheduleNote = () => {
889
+ if (!scheduleNote) return;
890
+ scheduleNote.textContent = "";
891
+ scheduleNote.dataset.visible = "false";
892
+ delete scheduleNote.dataset.tone;
893
+ };
894
+
895
+ const setScheduleNote = (message, options = {}) => {
896
+ if (!scheduleNote) return;
897
+ if (!message) {
898
+ clearScheduleNote();
899
+ return;
900
+ }
901
+ scheduleNote.textContent = message;
902
+ scheduleNote.dataset.visible = "true";
903
+ scheduleNote.dataset.tone = options.tone || "info";
904
+ if (options.link) {
905
+ scheduleNote.append(" ");
906
+ const anchor = document.createElement("a");
907
+ anchor.href = options.link.href;
908
+ anchor.textContent = options.link.label;
909
+ scheduleNote.append(anchor);
910
+ }
911
+ };
912
+
913
+ let scheduleToastTimer = 0;
914
+ const showScheduleToast = (message) => {
915
+ if (!scheduleToast) return;
916
+ window.clearTimeout(scheduleToastTimer);
917
+ scheduleToast.textContent = message;
918
+ scheduleToast.dataset.visible = "true";
919
+ scheduleToastTimer = window.setTimeout(() => {
920
+ scheduleToast.dataset.visible = "false";
921
+ }, 3600);
922
+ };
923
+
924
+ const syncDateTimeInput = () => {
925
+ if (!scheduleDateInput || !scheduleDateTimeInput) return;
926
+ scheduleDateInput.value = isoDate(selectedDate);
927
+ scheduleDateTimeInput.value = isoDateTimeLocal(selectedDate, selectedTime);
928
+ updateScheduleSubmitState();
929
+ };
930
+
931
+ const syncDateFromDateTimeInput = () => {
932
+ hasInteractedWithScheduleDateTime = true;
933
+ if (!scheduleDateTimeInput?.value) {
934
+ updateScheduleSubmitState();
935
+ return;
936
+ }
937
+ const [datePart, timePart] = scheduleDateTimeInput.value.split("T");
938
+ if (datePart) {
939
+ const parsed = new Date(datePart + "T00:00:00");
940
+ if (!Number.isNaN(parsed.getTime())) {
941
+ selectedDate = startOfDay(parsed);
942
+ calendarCursor = new Date(parsed.getFullYear(), parsed.getMonth(), 1);
943
+ }
944
+ }
945
+ if (/^\\d{2}:\\d{2}/.test(timePart || "")) {
946
+ selectedTime = timePart.slice(0, 5);
947
+ }
948
+ renderCalendar();
949
+ updateScheduleSubmitState();
950
+ };
951
+
952
+ const showDateTimePicker = () => {
953
+ if (!scheduleDateTimeInput) return;
954
+ scheduleDateTimeInput.focus();
955
+ if (typeof scheduleDateTimeInput.showPicker === "function") {
956
+ try { scheduleDateTimeInput.showPicker(); } catch {}
957
+ }
958
+ };
959
+
960
+ const rangeContainsDate = (rangeStart, rangeEnd, date) => {
961
+ if (!rangeStart || !rangeEnd) return false;
962
+ const time = startOfDay(date).getTime();
963
+ return time >= startOfDay(new Date(rangeStart)).getTime() && time <= startOfDay(new Date(rangeEnd)).getTime();
964
+ };
965
+
966
+ const calendarFetchWindow = () => {
967
+ const first = new Date(calendarCursor.getFullYear(), calendarCursor.getMonth(), 1);
968
+ const gridStart = new Date(first);
969
+ gridStart.setDate(first.getDate() - first.getDay());
970
+ const gridEnd = new Date(gridStart);
971
+ gridEnd.setDate(gridStart.getDate() + 41);
972
+ gridEnd.setHours(23, 59, 59, 999);
973
+ return { startDate: gridStart.toISOString(), endDate: gridEnd.toISOString() };
974
+ };
975
+
976
+ const postsForDate = (date) => {
977
+ const dateKey = isoDate(date);
978
+ const channelId = selectedChannelId;
979
+ return scheduledPosts
980
+ .filter((post) => isoDate(new Date(post.scheduledAt)) === dateKey)
981
+ .filter((post) => !channelId || post.channelId === channelId)
982
+ .sort((a, b) => Date.parse(a.scheduledAt) - Date.parse(b.scheduledAt));
983
+ };
984
+
985
+ const closePostPopover = () => {
986
+ activePostPopoverDateKey = "";
987
+ if (postPopover) postPopover.hidden = true;
988
+ };
989
+
990
+ const hidePostPopover = () => {
991
+ if (postPopover) postPopover.hidden = true;
992
+ };
993
+
994
+ const openPostPopover = (button, date, posts, options = {}) => {
995
+ if (!postPopover || !posts.length) {
996
+ closePostPopover();
997
+ return;
998
+ }
999
+ if (options.persist) {
1000
+ activePostPopoverDateKey = isoDate(date);
1001
+ }
1002
+ const title = popoverDateFormatter.format(date);
1003
+ postPopover.innerHTML = "";
1004
+ const closeButton = document.createElement("button");
1005
+ closeButton.type = "button";
1006
+ closeButton.className = "ready-post-calendar-popover-close";
1007
+ closeButton.setAttribute("aria-label", "Close post preview");
1008
+ closeButton.dataset.schedulePostPopoverClose = "true";
1009
+ closeButton.textContent = "×";
1010
+ const heading = document.createElement("div");
1011
+ heading.className = "ready-post-calendar-popover-title";
1012
+ heading.textContent = title + " · " + posts.length + " post" + (posts.length === 1 ? "" : "s");
1013
+ const list = document.createElement("div");
1014
+ list.className = "ready-post-calendar-popover-list";
1015
+ posts.slice(0, 8).forEach((post) => {
1016
+ const channel = scheduleChannels.find((entry) => entry.id === post.channelId) || null;
1017
+ const item = document.createElement("div");
1018
+ item.className = "ready-post-calendar-popover-item";
1019
+ const time = document.createElement("div");
1020
+ time.className = "ready-post-calendar-popover-time";
1021
+ time.textContent = popoverTimeFormatter.format(new Date(post.scheduledAt));
1022
+ const channelNode = document.createElement("div");
1023
+ channelNode.className = "ready-post-calendar-popover-channel";
1024
+ channelNode.textContent = channel ? describeChannel(channel) : post.channelId;
1025
+ const titleNode = post.url ? document.createElement("a") : document.createElement("span");
1026
+ if (post.url) {
1027
+ titleNode.href = post.url;
1028
+ titleNode.target = "_blank";
1029
+ titleNode.rel = "noreferrer";
1030
+ } else {
1031
+ titleNode.className = "ready-post-calendar-popover-linkless";
1032
+ }
1033
+ titleNode.textContent = post.title || "Scheduled post";
1034
+ item.append(time, titleNode, channelNode);
1035
+ if (post.summary && post.summary !== post.title) {
1036
+ const summary = document.createElement("div");
1037
+ summary.className = "ready-post-calendar-popover-summary";
1038
+ summary.textContent = post.summary;
1039
+ item.append(summary);
1040
+ }
1041
+ list.append(item);
1042
+ });
1043
+ postPopover.append(closeButton, heading, list);
1044
+ const rect = button.getBoundingClientRect();
1045
+ postPopover.hidden = false;
1046
+ const width = postPopover.offsetWidth || 390;
1047
+ const height = postPopover.offsetHeight || 260;
1048
+ const preferRight = rect.right + width + 12 <= window.innerWidth - 16;
1049
+ const left = preferRight
1050
+ ? rect.right + 12
1051
+ : Math.max(16, Math.min(window.innerWidth - width - 16, rect.left));
1052
+ const top = Math.max(16, Math.min(window.innerHeight - height - 16, rect.top));
1053
+ postPopover.style.left = left + "px";
1054
+ postPopover.style.top = top + "px";
1055
+ };
1056
+
1057
+ const channelInitials = (channel) => String(channel?.name || channel?.title || channel?.id || "CH")
1058
+ .split(/\s+/)
1059
+ .filter(Boolean)
1060
+ .slice(0, 2)
1061
+ .map((part) => part[0]?.toUpperCase() || "")
1062
+ .join("") || "CH";
1063
+
1064
+ const channelMeta = (channel) => {
1065
+ const handle = channel?.handle ? (String(channel.platform || "").toLowerCase() === "email" ? channel.handle : "@" + channel.handle) : channel?.id || "";
1066
+ const platform = channel?.platform ? String(channel.platform).toUpperCase() : "CHANNEL";
1067
+ return [handle, platform].filter(Boolean).join(" · ");
1068
+ };
1069
+
1070
+ const isEmailScheduleChannel = (channel) => String(channel?.platform || "").toLowerCase() === "email" || String(channel?.id || "").startsWith("email:");
1071
+
1072
+ const updateEmailInstructionsVisibility = () => {
1073
+ if (!emailInstructions) return;
1074
+ const visible = isEmailScheduleChannel(selectedChannel());
1075
+ emailInstructions.hidden = !visible;
1076
+ if (!visible && additionalNotesInput) {
1077
+ additionalNotesInput.value = "";
1078
+ }
1079
+ };
1080
+
1081
+ const renderChannelAvatar = (channel) => {
1082
+ const avatar = document.createElement("span");
1083
+ avatar.className = "ready-post-channel-avatar";
1084
+ if (channel?.avatarUrl) {
1085
+ const image = document.createElement("img");
1086
+ image.src = channel.avatarUrl;
1087
+ image.alt = "";
1088
+ avatar.append(image);
1089
+ } else {
1090
+ avatar.textContent = channelInitials(channel);
1091
+ }
1092
+ return avatar;
1093
+ };
1094
+
1095
+ const selectedChannel = () => scheduleChannels.find((channel) => channel.id === selectedChannelId) || null;
1096
+
1097
+ const setSelectedChannel = (channelId) => {
1098
+ if (!channelId || !scheduleChannels.some((channel) => channel.id === channelId)) {
1099
+ selectedChannelId = "";
1100
+ } else {
1101
+ selectedChannelId = channelId;
1102
+ }
1103
+ if (channelValueInput) channelValueInput.value = selectedChannelId;
1104
+ updateScheduleSubmitState();
1105
+ };
1106
+
1107
+ const renderChannelTrigger = () => {
1108
+ if (!channelTrigger) return;
1109
+ channelTrigger.innerHTML = "";
1110
+ channelTrigger.classList.remove("is-placeholder");
1111
+ const channel = selectedChannel();
1112
+ if (!channel) {
1113
+ updateEmailInstructionsVisibility();
1114
+ channelTrigger.classList.add("is-placeholder");
1115
+ channelTrigger.textContent = scheduleChannels.length ? "Select a channel" : (scheduleBoot.error || "No channels connected");
1116
+ if (scheduleChannels.length) {
1117
+ setScheduleNote("Choose a channel to schedule this post.", { tone: "info" });
1118
+ } else {
1119
+ setScheduleNote("Add an email channel or connect FlockPoster to load channels.", {
1120
+ tone: "info",
1121
+ link: {
1122
+ href: scheduleBoot.connectHref || "/settings?tab=channels",
1123
+ label: "Open Settings"
1124
+ }
1125
+ });
1126
+ }
1127
+ return;
1128
+ }
1129
+ channelTrigger.append(renderChannelAvatar(channel));
1130
+ const copy = document.createElement("span");
1131
+ copy.className = "ready-post-channel-copy";
1132
+ const title = document.createElement("span");
1133
+ title.className = "ready-post-channel-title";
1134
+ title.textContent = channel.name || channel.title || channel.id;
1135
+ const meta = document.createElement("span");
1136
+ meta.className = "ready-post-channel-meta";
1137
+ meta.textContent = channelMeta(channel);
1138
+ copy.append(title, meta);
1139
+ channelTrigger.append(copy);
1140
+ updateEmailInstructionsVisibility();
1141
+ clearScheduleNote();
1142
+ updateScheduleSubmitState();
1143
+ };
1144
+
1145
+ const renderChannelList = () => {
1146
+ if (!channelList) return;
1147
+ channelList.innerHTML = "";
1148
+ const query = channelSearchQuery.trim().toLowerCase();
1149
+ const visible = scheduleChannels.filter((channel) => {
1150
+ const haystack = [channel.name, channel.title, channel.handle, channel.platform, channel.id].filter(Boolean).join(" ").toLowerCase();
1151
+ return !query || haystack.includes(query);
1152
+ });
1153
+ if (!visible.length) {
1154
+ const empty = document.createElement("span");
1155
+ empty.className = "ready-post-channel-empty";
1156
+ empty.textContent = scheduleChannels.length ? "No channels match that search." : "No channels connected yet.";
1157
+ channelList.append(empty);
1158
+ return;
1159
+ }
1160
+ visible.forEach((channel) => {
1161
+ const button = document.createElement("button");
1162
+ button.type = "button";
1163
+ button.className = "ready-post-channel-option" + (channel.id === selectedChannelId ? " is-selected" : "");
1164
+ button.setAttribute("role", "option");
1165
+ button.setAttribute("aria-selected", channel.id === selectedChannelId ? "true" : "false");
1166
+ button.append(renderChannelAvatar(channel));
1167
+ const copy = document.createElement("span");
1168
+ copy.className = "ready-post-channel-copy";
1169
+ const title = document.createElement("span");
1170
+ title.className = "ready-post-channel-title";
1171
+ title.textContent = channel.name || channel.title || channel.id;
1172
+ const meta = document.createElement("span");
1173
+ meta.className = "ready-post-channel-meta";
1174
+ meta.textContent = channelMeta(channel);
1175
+ copy.append(title, meta);
1176
+ button.append(copy);
1177
+ button.addEventListener("click", () => {
1178
+ setSelectedChannel(channel.id);
1179
+ closeChannelMenu();
1180
+ renderChannelPicker();
1181
+ closePostPopover();
1182
+ renderCalendar();
1183
+ });
1184
+ channelList.append(button);
1185
+ });
1186
+ };
1187
+
1188
+ const renderChannelPicker = () => {
1189
+ const previous = selectedChannelId;
1190
+ setSelectedChannel(previous);
1191
+ renderChannelTrigger();
1192
+ renderChannelList();
1193
+ };
1194
+
1195
+ const openChannelMenu = () => {
1196
+ if (!channelMenu || !channelTrigger) return;
1197
+ channelMenu.hidden = false;
1198
+ channelTrigger.setAttribute("aria-expanded", "true");
1199
+ renderChannelList();
1200
+ setTimeout(() => channelSearch?.focus(), 0);
1201
+ };
1202
+
1203
+ const closeChannelMenu = () => {
1204
+ if (!channelMenu || !channelTrigger) return;
1205
+ channelMenu.hidden = true;
1206
+ channelTrigger.setAttribute("aria-expanded", "false");
1207
+ };
1208
+
1209
+ const refreshScheduleCalendar = async (options = {}) => {
1210
+ applyScheduleCache();
1211
+ if (!scheduleBoot.feedEndpoint || !scheduleBoot.hasFlockPosterKey) {
1212
+ renderChannelPicker();
1213
+ renderCalendar();
1214
+ return;
1215
+ }
1216
+ const range = calendarFetchWindow();
1217
+ if (!options.force && hasLoadedScheduleCalendar && rangeContainsDate(loadedRangeStart, loadedRangeEnd, calendarCursor)) {
1218
+ renderChannelPicker();
1219
+ renderCalendar();
1220
+ return;
1221
+ }
1222
+ if (scheduleFetchPromise) {
1223
+ await scheduleFetchPromise;
1224
+ return;
1225
+ }
1226
+ hasLoadedScheduleCalendar = true;
1227
+ scheduleFetchPromise = (async () => {
1228
+ try {
1229
+ const url = new URL(scheduleBoot.feedEndpoint, window.location.origin);
1230
+ url.searchParams.set("start_date", range.startDate);
1231
+ url.searchParams.set("end_date", range.endDate);
1232
+ const response = await fetch(url.pathname + url.search, { credentials: "same-origin", headers: { accept: "application/json" } });
1233
+ const payload = await response.json();
1234
+ if (!response.ok) {
1235
+ throw new Error(typeof payload?.error === "string" ? payload.error : "Unable to load FlockPoster calendar.");
1236
+ }
1237
+ const nextChannels = normalizeChannelsForSchedule(payload.channels);
1238
+ const nextPosts = normalizePostsForSchedule(payload.posts);
1239
+ scheduleChannels = normalizeChannelsForSchedule(dedupeById([...scheduleChannels, ...nextChannels]));
1240
+ scheduledPosts = normalizePostsForSchedule(dedupeById([...scheduledPosts, ...nextPosts]));
1241
+ loadedRangeStart = typeof payload.startDate === "string" ? payload.startDate : range.startDate;
1242
+ loadedRangeEnd = typeof payload.endDate === "string" ? payload.endDate : range.endDate;
1243
+ scheduleBoot.error = typeof payload.error === "string" ? payload.error : "";
1244
+ writeScheduleCache(nextChannels, nextPosts, loadedRangeStart, loadedRangeEnd);
1245
+ } catch (error) {
1246
+ scheduleBoot.error = error instanceof Error ? error.message : "Unable to load FlockPoster calendar.";
1247
+ } finally {
1248
+ scheduleFetchPromise = null;
1249
+ }
1250
+ })();
1251
+ await scheduleFetchPromise;
1252
+ renderChannelPicker();
1253
+ renderCalendar();
1254
+ };
1255
+
1256
+ const hydrateScheduleChannelsOnPageLoad = async () => {
1257
+ const runtime = window.__vidfarmFlockPosterCache;
1258
+ if (runtime && typeof runtime.ensureHydrated === "function") {
1259
+ await runtime.ensureHydrated({ force: true });
1260
+ }
1261
+ await refreshScheduleCalendar({ force: true });
1262
+ };
1263
+
1264
+ const renderCalendar = () => {
1265
+ if (!calendarTitle || !calendarDays || !scheduleDateInput) return;
1266
+ calendarTitle.textContent = monthFormatter.format(calendarCursor);
1267
+ syncDateTimeInput();
1268
+ calendarDays.innerHTML = "";
1269
+ const first = new Date(calendarCursor.getFullYear(), calendarCursor.getMonth(), 1);
1270
+ const gridStart = new Date(first);
1271
+ gridStart.setDate(first.getDate() - first.getDay());
1272
+ for (let index = 0; index < 42; index += 1) {
1273
+ const date = new Date(gridStart);
1274
+ date.setDate(gridStart.getDate() + index);
1275
+ const button = document.createElement("button");
1276
+ button.type = "button";
1277
+ button.className = "ready-post-calendar-day";
1278
+ const number = document.createElement("span");
1279
+ number.className = "ready-post-calendar-day-number";
1280
+ number.textContent = String(date.getDate());
1281
+ button.append(number);
1282
+ button.dataset.scheduleDate = isoDate(date);
1283
+ if (date.getMonth() !== calendarCursor.getMonth()) button.classList.add("is-muted");
1284
+ if (isoDate(date) === isoDate(today)) button.classList.add("is-today");
1285
+ if (isoDate(date) === isoDate(selectedDate)) button.classList.add("is-selected");
1286
+ const dayPosts = postsForDate(date);
1287
+ if (dayPosts.length) {
1288
+ button.classList.add("has-posts");
1289
+ if (dayPosts.length > 2) button.classList.add("has-many-posts");
1290
+ button.setAttribute("aria-label", number.textContent + ", " + dayPosts.length + " scheduled post" + (dayPosts.length === 1 ? "" : "s"));
1291
+ const preview = document.createElement("span");
1292
+ preview.className = "ready-post-calendar-day-preview";
1293
+ preview.textContent = String(dayPosts.length);
1294
+ button.append(preview);
1295
+ button.addEventListener("mouseenter", () => openPostPopover(button, date, dayPosts));
1296
+ button.addEventListener("focus", () => openPostPopover(button, date, dayPosts));
1297
+ if (activePostPopoverDateKey === isoDate(date)) {
1298
+ setTimeout(() => {
1299
+ openPostPopover(button, date, dayPosts, { persist: true });
1300
+ }, 0);
1301
+ }
1302
+ }
1303
+ button.addEventListener("click", () => {
1304
+ selectedDate = startOfDay(date);
1305
+ calendarCursor = new Date(date.getFullYear(), date.getMonth(), 1);
1306
+ activePostPopoverDateKey = dayPosts.length ? isoDate(date) : "";
1307
+ hidePostPopover();
1308
+ renderCalendar();
1309
+ void refreshScheduleCalendar();
1310
+ });
1311
+ calendarDays.append(button);
1312
+ }
1313
+ };
1314
+
1315
+ const selectedScheduleIso = () => {
1316
+ if (!scheduleDateTimeInput?.value) return "";
1317
+ const timezone = timezoneSelect?.value || "";
1318
+ const offset = /^GMT([+-]\\d{2}:\\d{2})$/.exec(timezone)?.[1];
1319
+ const raw = scheduleDateTimeInput.value.length === 16
1320
+ ? scheduleDateTimeInput.value + ":00"
1321
+ : scheduleDateTimeInput.value;
1322
+ const parsed = new Date(offset ? raw + offset : raw);
1323
+ return Number.isNaN(parsed.getTime()) ? "" : parsed.toISOString();
1324
+ };
1325
+
1326
+ const canSubmitSchedule = () => {
1327
+ const scheduledAt = selectedScheduleIso();
1328
+ return Boolean(
1329
+ scheduleBoot.scheduleEndpoint
1330
+ && selectedChannel()
1331
+ && scheduledAt
1332
+ && Date.parse(scheduledAt) >= Date.now() + minScheduleLeadMs
1333
+ );
1334
+ };
1335
+
1336
+ const validateScheduleDateTime = () => {
1337
+ if (!hasInteractedWithScheduleDateTime) return;
1338
+ const scheduledAt = selectedScheduleIso();
1339
+ if (!scheduledAt) {
1340
+ setScheduleNote("Choose a valid date and time.", { tone: "error" });
1341
+ return;
1342
+ }
1343
+ if (Date.parse(scheduledAt) < Date.now() + minScheduleLeadMs) {
1344
+ setScheduleNote("Choose a time at least 10 minutes from now.", { tone: "error" });
1345
+ return;
1346
+ }
1347
+ if (selectedChannel()) {
1348
+ clearScheduleNote();
1349
+ }
1350
+ };
1351
+
1352
+ const setScheduleSubmitMode = (mode) => {
1353
+ scheduleSubmitMode = mode;
1354
+ if (!scheduleSubmit) return;
1355
+ scheduleSubmit.textContent = mode === "another" ? "Schedule Another?" : "Schedule";
1356
+ scheduleSubmit.classList.toggle("secondary", mode === "another");
1357
+ };
1358
+
1359
+ const updateScheduleSubmitState = () => {
1360
+ if (!scheduleSubmit) return;
1361
+ if (scheduleSubmitMode === "another") {
1362
+ scheduleSubmit.disabled = false;
1363
+ return;
1364
+ }
1365
+ scheduleSubmit.disabled = isScheduleSubmitting || !canSubmitSchedule();
1366
+ validateScheduleDateTime();
1367
+ };
1368
+
1369
+ const submitSchedule = async () => {
1370
+ if (!scheduleSubmit) return;
1371
+ if (scheduleSubmitMode === "another") {
1372
+ setScheduleSubmitMode("schedule");
1373
+ updateScheduleSubmitState();
1374
+ return;
1375
+ }
1376
+ if (!canSubmitSchedule()) {
1377
+ updateScheduleSubmitState();
1378
+ return;
1379
+ }
1380
+ if (!scheduleBoot.scheduleEndpoint) {
1381
+ setScheduleNote("Sign in as the post owner before scheduling.", { tone: "error" });
1382
+ return;
1383
+ }
1384
+ const channel = selectedChannel();
1385
+ if (!channel) {
1386
+ setScheduleNote("Choose a channel before scheduling.", { tone: "error" });
1387
+ return;
1388
+ }
1389
+ const scheduledAt = selectedScheduleIso();
1390
+ if (!scheduledAt) {
1391
+ setScheduleNote("Choose a valid date and time.", { tone: "error" });
1392
+ return;
1393
+ }
1394
+ if (Date.parse(scheduledAt) < Date.now() + minScheduleLeadMs) {
1395
+ setScheduleNote("Choose a time at least 10 minutes from now.", { tone: "error" });
1396
+ return;
1397
+ }
1398
+ const destinationType = isEmailScheduleChannel(channel) ? "email" : "flockposter";
1399
+ const additionalNotes = destinationType === "email" ? String(additionalNotesInput?.value || "").trim() : "";
1400
+ isScheduleSubmitting = true;
1401
+ updateScheduleSubmitState();
1402
+ scheduleSubmit.textContent = "Scheduling...";
1403
+ try {
1404
+ const response = await fetch(scheduleBoot.scheduleEndpoint, {
1405
+ method: "POST",
1406
+ credentials: "same-origin",
1407
+ headers: {
1408
+ "Content-Type": "application/json",
1409
+ accept: "application/json"
1410
+ },
1411
+ body: JSON.stringify({
1412
+ destination_type: destinationType,
1413
+ destination_id: channel.id,
1414
+ scheduled_at: scheduledAt,
1415
+ timezone: timezoneSelect?.value || null,
1416
+ additional_notes: additionalNotes || undefined
1417
+ })
1418
+ });
1419
+ const payload = await response.json().catch(() => ({}));
1420
+ if (!response.ok) {
1421
+ throw new Error(typeof payload?.error === "string" ? payload.error : "Unable to schedule post.");
1422
+ }
1423
+ const channelName = channel.name || channel.title || channel.id || "channel";
1424
+ showScheduleToast("Scheduled to " + channelName + " · " + toastDateFormatter.format(new Date(scheduledAt)));
1425
+ clearScheduleNote();
1426
+ setScheduleSubmitMode("another");
1427
+ await refreshScheduleCalendar({ force: true });
1428
+ } catch (error) {
1429
+ setScheduleNote(error instanceof Error ? error.message : "Unable to schedule post.", { tone: "error" });
1430
+ } finally {
1431
+ isScheduleSubmitting = false;
1432
+ updateScheduleSubmitState();
1433
+ if (scheduleSubmitMode !== "another") {
1434
+ setScheduleSubmitMode("schedule");
1435
+ }
1436
+ }
1437
+ };
1438
+
1439
+ channelTrigger?.addEventListener("click", (event) => {
1440
+ event.preventDefault();
1441
+ closePostPopover();
1442
+ if (channelMenu?.hidden === false) {
1443
+ closeChannelMenu();
1444
+ } else {
1445
+ openChannelMenu();
1446
+ }
1447
+ });
1448
+ channelSearch?.addEventListener("input", () => {
1449
+ channelSearchQuery = String(channelSearch.value || "");
1450
+ renderChannelList();
1451
+ });
1452
+ scheduleDateTimeShell?.addEventListener("click", showDateTimePicker);
1453
+ scheduleDateTimeInput?.addEventListener("focus", showDateTimePicker);
1454
+ scheduleDateTimeInput?.addEventListener("input", () => {
1455
+ hasInteractedWithScheduleDateTime = true;
1456
+ updateScheduleSubmitState();
1457
+ });
1458
+ scheduleDateTimeInput?.addEventListener("change", syncDateFromDateTimeInput);
1459
+ scheduleSubmit?.addEventListener("click", () => {
1460
+ void submitSchedule();
1461
+ });
1462
+ if (timezoneSelect) {
1463
+ const timezoneCities = {
1464
+ "-12": "Baker Island",
1465
+ "-11": "Pago Pago",
1466
+ "-10": "Honolulu",
1467
+ "-9": "Anchorage",
1468
+ "-8": "Los Angeles",
1469
+ "-7": "Denver",
1470
+ "-6": "Chicago",
1471
+ "-5": "New York",
1472
+ "-4": "Toronto",
1473
+ "-3": "Sao Paulo",
1474
+ "-2": "South Georgia",
1475
+ "-1": "Azores",
1476
+ "0": "London",
1477
+ "1": "Paris",
1478
+ "2": "Cairo",
1479
+ "3": "Istanbul",
1480
+ "4": "Dubai",
1481
+ "5": "Karachi",
1482
+ "6": "Dhaka",
1483
+ "7": "Bangkok",
1484
+ "8": "Singapore",
1485
+ "9": "Tokyo",
1486
+ "10": "Sydney",
1487
+ "11": "Noumea",
1488
+ "12": "Auckland",
1489
+ "13": "Apia",
1490
+ "14": "Kiritimati"
1491
+ };
1492
+ timezoneSelect.innerHTML = "";
1493
+ for (let offset = -12; offset <= 14; offset += 1) {
1494
+ const sign = offset >= 0 ? "+" : "-";
1495
+ const value = "GMT" + sign + String(Math.abs(offset)).padStart(2, "0") + ":00";
1496
+ timezoneSelect.append(createOption(value + " · " + timezoneCities[String(offset)], value));
1497
+ }
1498
+ const localOffset = -new Date().getTimezoneOffset() / 60;
1499
+ const roundedOffset = Math.max(-12, Math.min(14, Math.round(localOffset)));
1500
+ const localSign = roundedOffset >= 0 ? "+" : "-";
1501
+ timezoneSelect.value = "GMT" + localSign + String(Math.abs(roundedOffset)).padStart(2, "0") + ":00";
1502
+ timezoneSelect.addEventListener("change", () => {
1503
+ hasInteractedWithScheduleDateTime = true;
1504
+ updateScheduleSubmitState();
1505
+ });
1506
+ }
1507
+ document.querySelector("[data-schedule-prev-month]")?.addEventListener("click", () => {
1508
+ calendarCursor = new Date(calendarCursor.getFullYear(), calendarCursor.getMonth() - 1, 1);
1509
+ closePostPopover();
1510
+ renderCalendar();
1511
+ void refreshScheduleCalendar();
1512
+ });
1513
+ document.querySelector("[data-schedule-next-month]")?.addEventListener("click", () => {
1514
+ calendarCursor = new Date(calendarCursor.getFullYear(), calendarCursor.getMonth() + 1, 1);
1515
+ closePostPopover();
1516
+ renderCalendar();
1517
+ void refreshScheduleCalendar();
1518
+ });
1519
+ document.addEventListener("pointerdown", (event) => {
1520
+ if (channelPicker && !channelPicker.contains(event.target)) {
1521
+ closeChannelMenu();
1522
+ }
1523
+ if (!event.target.closest("[data-schedule-post-popover]") && !event.target.closest(".ready-post-calendar-day")) {
1524
+ closePostPopover();
1525
+ }
1526
+ });
1527
+ window.addEventListener("resize", closePostPopover);
1528
+ window.addEventListener("vidfarm:flockposter-cache-updated", () => {
1529
+ applyScheduleCache();
1530
+ renderChannelPicker();
1531
+ renderCalendar();
1532
+ });
1533
+ applyScheduleCache();
1534
+ renderChannelPicker();
1535
+ renderCalendar();
1536
+ void hydrateScheduleChannelsOnPageLoad();
1537
+ })();
1538
+ </script>`;
1539
+ }
1540
+ //# sourceMappingURL=ready-post-schedule-component.js.map