@mevdragon/vidfarm-devcli 0.20.6 → 0.20.9

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 (157) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +15 -1
  2. package/.agents/skills/vidfarm-director/SKILL.md +106 -0
  3. package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
  4. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
  5. package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
  6. package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
  7. package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +117 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +245 -0
  9. package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
  10. package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
  11. package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
  12. package/.agents/skills/vidfarm-director/references/primitives.md +308 -0
  13. package/SKILL.director.md +517 -250
  14. package/dist/src/cli.js +563 -99
  15. package/dist/src/devcli/clips.js +29 -12
  16. package/dist/src/devcli/doctor.js +2 -2
  17. package/dist/src/devcli/local-backend.js +46 -2
  18. package/dist/src/services/clip-curation/cost.js +5 -1
  19. package/dist/src/services/clip-curation/gemini.js +14 -2
  20. package/dist/src/services/clip-curation/hunt.js +56 -0
  21. package/dist/src/services/clip-curation/index.js +1 -1
  22. package/dist/src/services/clip-curation/scan.js +5 -3
  23. package/package.json +30 -10
  24. package/SKILL.platform.md +0 -432
  25. package/demo/README.md +0 -28
  26. package/demo/dist/app.css +0 -1
  27. package/demo/dist/app.js +0 -1850
  28. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  29. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  30. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  31. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  32. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  33. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  34. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  35. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  36. package/demo/dist/favicon.ico +0 -0
  37. package/demo/dist/icons/timeline/audio.svg +0 -7
  38. package/demo/dist/icons/timeline/captions.svg +0 -5
  39. package/demo/dist/icons/timeline/composition.svg +0 -12
  40. package/demo/dist/icons/timeline/image.svg +0 -18
  41. package/demo/dist/icons/timeline/music.svg +0 -10
  42. package/demo/dist/icons/timeline/text.svg +0 -3
  43. package/demo/dist/index.html +0 -15
  44. package/dist/src/account-pages-legacy.js +0 -9396
  45. package/dist/src/account-pages.js +0 -61
  46. package/dist/src/app.js +0 -21603
  47. package/dist/src/composition-runtime.js +0 -1053
  48. package/dist/src/config.js +0 -217
  49. package/dist/src/context.js +0 -447
  50. package/dist/src/dev-app-legacy.js +0 -739
  51. package/dist/src/dev-app.js +0 -6
  52. package/dist/src/devcli/migrate-local.js +0 -140
  53. package/dist/src/devcli/sync.js +0 -368
  54. package/dist/src/domain.js +0 -5
  55. package/dist/src/editor-chat-history.js +0 -82
  56. package/dist/src/editor-chat.js +0 -828
  57. package/dist/src/editor-dark-theme.js +0 -1128
  58. package/dist/src/frontend/debug.js +0 -71
  59. package/dist/src/frontend/discover-client.js +0 -130
  60. package/dist/src/frontend/discover-store.js +0 -23
  61. package/dist/src/frontend/file-directory.js +0 -1018
  62. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  63. package/dist/src/frontend/homepage-client.js +0 -446
  64. package/dist/src/frontend/homepage-shared.js +0 -201
  65. package/dist/src/frontend/homepage-store.js +0 -66
  66. package/dist/src/frontend/homepage-view.js +0 -705
  67. package/dist/src/frontend/page-runtime-client.js +0 -132
  68. package/dist/src/frontend/page-runtime-store.js +0 -9
  69. package/dist/src/frontend/sentry.js +0 -42
  70. package/dist/src/frontend/template-editor-chat.js +0 -4181
  71. package/dist/src/help-page.js +0 -346
  72. package/dist/src/homepage.js +0 -1458
  73. package/dist/src/index.js +0 -16
  74. package/dist/src/instrument.js +0 -30
  75. package/dist/src/landing-page.js +0 -384
  76. package/dist/src/page-runtime.js +0 -2
  77. package/dist/src/page-shell.js +0 -1452
  78. package/dist/src/primitive-context.js +0 -416
  79. package/dist/src/primitive-registry.js +0 -3940
  80. package/dist/src/primitive-sdk.js +0 -4
  81. package/dist/src/primitives/hyperframes-media.js +0 -108
  82. package/dist/src/react-page-shell.js +0 -35
  83. package/dist/src/ready-post-schedule-component.js +0 -1540
  84. package/dist/src/registry.js +0 -296
  85. package/dist/src/reskin/agency-page.js +0 -299
  86. package/dist/src/reskin/calendar-page.js +0 -568
  87. package/dist/src/reskin/chat-page.js +0 -942
  88. package/dist/src/reskin/discover-page.js +0 -1788
  89. package/dist/src/reskin/document.js +0 -1587
  90. package/dist/src/reskin/help-page.js +0 -357
  91. package/dist/src/reskin/index-page.js +0 -62
  92. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  93. package/dist/src/reskin/inpaint-page.js +0 -2554
  94. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  95. package/dist/src/reskin/job-runs-page.js +0 -477
  96. package/dist/src/reskin/library-page.js +0 -1634
  97. package/dist/src/reskin/login-page.js +0 -262
  98. package/dist/src/reskin/portfolio-page.js +0 -687
  99. package/dist/src/reskin/pricing-page.js +0 -390
  100. package/dist/src/reskin/settings-page.js +0 -732
  101. package/dist/src/reskin/theme.js +0 -711
  102. package/dist/src/runtime.js +0 -35
  103. package/dist/src/services/api-call-history.js +0 -249
  104. package/dist/src/services/auth.js +0 -152
  105. package/dist/src/services/billing-pricing.js +0 -39
  106. package/dist/src/services/billing.js +0 -241
  107. package/dist/src/services/cast.js +0 -127
  108. package/dist/src/services/chat-threads.js +0 -92
  109. package/dist/src/services/clip-records.js +0 -250
  110. package/dist/src/services/clip-search.js +0 -77
  111. package/dist/src/services/clip-vectors.js +0 -125
  112. package/dist/src/services/composition-sanitize.js +0 -124
  113. package/dist/src/services/composition-watch.js +0 -79
  114. package/dist/src/services/elevenlabs.js +0 -222
  115. package/dist/src/services/file-directory.js +0 -117
  116. package/dist/src/services/fork-access.js +0 -93
  117. package/dist/src/services/fork-manifest.js +0 -42
  118. package/dist/src/services/ghostcut.js +0 -179
  119. package/dist/src/services/hyperframes.js +0 -3654
  120. package/dist/src/services/job-capacity.js +0 -14
  121. package/dist/src/services/job-logs.js +0 -197
  122. package/dist/src/services/jobs.js +0 -136
  123. package/dist/src/services/local-dynamo.js +0 -0
  124. package/dist/src/services/media-processing.js +0 -766
  125. package/dist/src/services/primitive-media-lambda.js +0 -280
  126. package/dist/src/services/providers.js +0 -2748
  127. package/dist/src/services/rate-limits.js +0 -262
  128. package/dist/src/services/scene-annotations.js +0 -32
  129. package/dist/src/services/serverless-auth.js +0 -382
  130. package/dist/src/services/serverless-jobs.js +0 -1084
  131. package/dist/src/services/serverless-provider-keys.js +0 -409
  132. package/dist/src/services/serverless-records.js +0 -1515
  133. package/dist/src/services/serverless-template-configs.js +0 -75
  134. package/dist/src/services/storage.js +0 -461
  135. package/dist/src/services/swipe-customize.js +0 -437
  136. package/dist/src/services/template-certification.js +0 -413
  137. package/dist/src/services/template-loader.js +0 -99
  138. package/dist/src/services/template-runtime-bundles.js +0 -217
  139. package/dist/src/services/template-sources.js +0 -1017
  140. package/dist/src/services/upstream.js +0 -248
  141. package/dist/src/services/video-normalization.js +0 -2
  142. package/dist/src/services/webhooks.js +0 -62
  143. package/dist/src/template-editor-pages.js +0 -2576
  144. package/dist/src/template-editor-shell.js +0 -2893
  145. package/dist/src/template-sdk.js +0 -4
  146. package/dist/src/worker.js +0 -17
  147. package/public/assets/discover-client-app.js +0 -1
  148. package/public/assets/file-directory-app.js +0 -3
  149. package/public/assets/homepage-app.js +0 -54
  150. package/public/assets/homepage-client-app.js +0 -80
  151. package/public/assets/page-runtime-client-app.js +0 -94
  152. package/public/assets/placeholders/scene-placeholder.png +0 -0
  153. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  154. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  155. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  156. package/src/assets/favicon.ico +0 -0
  157. package/src/assets/logo-vidfarm.png +0 -0
@@ -1,1540 +0,0 @@
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