@aphexcms/cms-core 9.5.2 → 9.7.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 (301) hide show
  1. package/dist/api/documents.d.ts +28 -0
  2. package/dist/api/documents.d.ts.map +1 -1
  3. package/dist/api/documents.js +18 -0
  4. package/dist/api/schemas/documents.d.ts +19 -0
  5. package/dist/api/schemas/documents.d.ts.map +1 -1
  6. package/dist/api/schemas/documents.js +18 -0
  7. package/dist/client/api.d.ts +2 -0
  8. package/dist/client/api.d.ts.map +1 -0
  9. package/dist/client/api.js +11 -0
  10. package/dist/client/ui.d.ts +25 -0
  11. package/dist/client/ui.d.ts.map +1 -0
  12. package/dist/client/ui.js +53 -0
  13. package/dist/components/admin/ActivityView.svelte +227 -0
  14. package/dist/components/admin/ActivityView.svelte.d.ts +4 -0
  15. package/dist/components/admin/ActivityView.svelte.d.ts.map +1 -0
  16. package/dist/components/admin/DocumentEditor.svelte +197 -11
  17. package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
  18. package/dist/components/admin/ScheduleDialog.svelte +174 -0
  19. package/dist/components/admin/ScheduleDialog.svelte.d.ts +20 -0
  20. package/dist/components/admin/ScheduleDialog.svelte.d.ts.map +1 -0
  21. package/dist/components/admin/fields/ArrayField.svelte +10 -2
  22. package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -1
  23. package/dist/components/layout/OrganizationSwitcher.svelte +3 -3
  24. package/dist/components/layout/sidebar/NavUser.svelte +5 -5
  25. package/dist/db/interfaces/events.d.ts +55 -0
  26. package/dist/db/interfaces/events.d.ts.map +1 -0
  27. package/dist/db/interfaces/events.js +1 -0
  28. package/dist/db/interfaces/index.d.ts +10 -2
  29. package/dist/db/interfaces/index.d.ts.map +1 -1
  30. package/dist/db/interfaces/plugin-storage.d.ts +39 -0
  31. package/dist/db/interfaces/plugin-storage.d.ts.map +1 -0
  32. package/dist/db/interfaces/plugin-storage.js +1 -0
  33. package/dist/events/catalog.d.ts +8 -0
  34. package/dist/events/catalog.d.ts.map +1 -0
  35. package/dist/events/catalog.js +11 -0
  36. package/dist/events/consumer.d.ts +79 -0
  37. package/dist/events/consumer.d.ts.map +1 -0
  38. package/dist/events/consumer.js +76 -0
  39. package/dist/events/define-event.d.ts +17 -0
  40. package/dist/events/define-event.d.ts.map +1 -0
  41. package/dist/events/define-event.js +7 -0
  42. package/dist/events/emit.d.ts +11 -0
  43. package/dist/events/emit.d.ts.map +1 -0
  44. package/dist/events/emit.js +20 -0
  45. package/dist/events/index.d.ts +4 -0
  46. package/dist/events/index.d.ts.map +1 -0
  47. package/dist/events/index.js +3 -0
  48. package/dist/field-validation/rule.d.ts.map +1 -1
  49. package/dist/field-validation/rule.js +15 -0
  50. package/dist/field-validation/utils.d.ts.map +1 -1
  51. package/dist/field-validation/utils.js +7 -1
  52. package/dist/forms/define-form.d.ts +25 -0
  53. package/dist/forms/define-form.d.ts.map +1 -0
  54. package/dist/forms/define-form.js +22 -0
  55. package/dist/forms/index.d.ts +4 -0
  56. package/dist/forms/index.d.ts.map +1 -0
  57. package/dist/forms/index.js +5 -0
  58. package/dist/forms/types.d.ts +49 -0
  59. package/dist/forms/types.d.ts.map +1 -0
  60. package/dist/forms/types.js +1 -0
  61. package/dist/forms/validate.d.ts +9 -0
  62. package/dist/forms/validate.d.ts.map +1 -0
  63. package/dist/forms/validate.js +18 -0
  64. package/dist/hooks.d.ts.map +1 -1
  65. package/dist/hooks.js +15 -0
  66. package/dist/index.d.ts +2 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +6 -0
  69. package/dist/jobs/document-jobs.d.ts +27 -0
  70. package/dist/jobs/document-jobs.d.ts.map +1 -0
  71. package/dist/jobs/document-jobs.js +42 -0
  72. package/dist/jobs/embedded-runner.d.ts +25 -0
  73. package/dist/jobs/embedded-runner.d.ts.map +1 -0
  74. package/dist/jobs/embedded-runner.js +46 -0
  75. package/dist/jobs/index.d.ts +7 -0
  76. package/dist/jobs/index.d.ts.map +1 -0
  77. package/dist/jobs/index.js +8 -0
  78. package/dist/jobs/relay.d.ts +27 -0
  79. package/dist/jobs/relay.d.ts.map +1 -0
  80. package/dist/jobs/relay.js +60 -0
  81. package/dist/jobs/run-batch.d.ts +52 -0
  82. package/dist/jobs/run-batch.d.ts.map +1 -0
  83. package/dist/jobs/run-batch.js +61 -0
  84. package/dist/jobs/run-due-jobs.d.ts +30 -0
  85. package/dist/jobs/run-due-jobs.d.ts.map +1 -0
  86. package/dist/jobs/run-due-jobs.js +69 -0
  87. package/dist/jobs/types.d.ts +18 -0
  88. package/dist/jobs/types.d.ts.map +1 -0
  89. package/dist/jobs/types.js +1 -0
  90. package/dist/lib/api/documents.d.ts +28 -0
  91. package/dist/lib/api/documents.d.ts.map +1 -1
  92. package/dist/lib/api/documents.js +18 -0
  93. package/dist/lib/api/documents.js.map +1 -1
  94. package/dist/lib/api/schemas/documents.d.ts +19 -0
  95. package/dist/lib/api/schemas/documents.d.ts.map +1 -1
  96. package/dist/lib/api/schemas/documents.js +18 -0
  97. package/dist/lib/api/schemas/documents.js.map +1 -1
  98. package/dist/lib/client/api.d.ts +2 -0
  99. package/dist/lib/client/api.d.ts.map +1 -0
  100. package/dist/lib/client/api.js +12 -0
  101. package/dist/lib/client/api.js.map +1 -0
  102. package/dist/lib/client/ui.d.ts +25 -0
  103. package/dist/lib/client/ui.d.ts.map +1 -0
  104. package/dist/lib/client/ui.js +54 -0
  105. package/dist/lib/client/ui.js.map +1 -0
  106. package/dist/lib/db/interfaces/events.d.ts +55 -0
  107. package/dist/lib/db/interfaces/events.d.ts.map +1 -0
  108. package/dist/lib/db/interfaces/events.js +2 -0
  109. package/dist/lib/db/interfaces/events.js.map +1 -0
  110. package/dist/lib/db/interfaces/index.d.ts +10 -2
  111. package/dist/lib/db/interfaces/index.d.ts.map +1 -1
  112. package/dist/lib/db/interfaces/plugin-storage.d.ts +39 -0
  113. package/dist/lib/db/interfaces/plugin-storage.d.ts.map +1 -0
  114. package/dist/lib/db/interfaces/plugin-storage.js +2 -0
  115. package/dist/lib/db/interfaces/plugin-storage.js.map +1 -0
  116. package/dist/lib/events/catalog.d.ts +8 -0
  117. package/dist/lib/events/catalog.d.ts.map +1 -0
  118. package/dist/lib/events/catalog.js +12 -0
  119. package/dist/lib/events/catalog.js.map +1 -0
  120. package/dist/lib/events/consumer.d.ts +79 -0
  121. package/dist/lib/events/consumer.d.ts.map +1 -0
  122. package/dist/lib/events/consumer.js +77 -0
  123. package/dist/lib/events/consumer.js.map +1 -0
  124. package/dist/lib/events/define-event.d.ts +17 -0
  125. package/dist/lib/events/define-event.d.ts.map +1 -0
  126. package/dist/lib/events/define-event.js +8 -0
  127. package/dist/lib/events/define-event.js.map +1 -0
  128. package/dist/lib/events/emit.d.ts +11 -0
  129. package/dist/lib/events/emit.d.ts.map +1 -0
  130. package/dist/lib/events/emit.js +21 -0
  131. package/dist/lib/events/emit.js.map +1 -0
  132. package/dist/lib/events/index.d.ts +4 -0
  133. package/dist/lib/events/index.d.ts.map +1 -0
  134. package/dist/lib/events/index.js +4 -0
  135. package/dist/lib/events/index.js.map +1 -0
  136. package/dist/lib/field-validation/rule.d.ts.map +1 -1
  137. package/dist/lib/field-validation/rule.js +15 -0
  138. package/dist/lib/field-validation/rule.js.map +1 -1
  139. package/dist/lib/field-validation/utils.d.ts.map +1 -1
  140. package/dist/lib/field-validation/utils.js +7 -1
  141. package/dist/lib/field-validation/utils.js.map +1 -1
  142. package/dist/lib/forms/define-form.d.ts +25 -0
  143. package/dist/lib/forms/define-form.d.ts.map +1 -0
  144. package/dist/lib/forms/define-form.js +23 -0
  145. package/dist/lib/forms/define-form.js.map +1 -0
  146. package/dist/lib/forms/index.d.ts +4 -0
  147. package/dist/lib/forms/index.d.ts.map +1 -0
  148. package/dist/lib/forms/index.js +6 -0
  149. package/dist/lib/forms/index.js.map +1 -0
  150. package/dist/lib/forms/types.d.ts +49 -0
  151. package/dist/lib/forms/types.d.ts.map +1 -0
  152. package/dist/lib/forms/types.js +2 -0
  153. package/dist/lib/forms/types.js.map +1 -0
  154. package/dist/lib/forms/validate.d.ts +9 -0
  155. package/dist/lib/forms/validate.d.ts.map +1 -0
  156. package/dist/lib/forms/validate.js +19 -0
  157. package/dist/lib/forms/validate.js.map +1 -0
  158. package/dist/lib/hooks.d.ts.map +1 -1
  159. package/dist/lib/hooks.js +15 -0
  160. package/dist/lib/hooks.js.map +1 -1
  161. package/dist/lib/index.d.ts +2 -0
  162. package/dist/lib/index.d.ts.map +1 -1
  163. package/dist/lib/index.js +6 -0
  164. package/dist/lib/index.js.map +1 -1
  165. package/dist/lib/jobs/document-jobs.d.ts +27 -0
  166. package/dist/lib/jobs/document-jobs.d.ts.map +1 -0
  167. package/dist/lib/jobs/document-jobs.js +43 -0
  168. package/dist/lib/jobs/document-jobs.js.map +1 -0
  169. package/dist/lib/jobs/embedded-runner.d.ts +25 -0
  170. package/dist/lib/jobs/embedded-runner.d.ts.map +1 -0
  171. package/dist/lib/jobs/embedded-runner.js +47 -0
  172. package/dist/lib/jobs/embedded-runner.js.map +1 -0
  173. package/dist/lib/jobs/index.d.ts +7 -0
  174. package/dist/lib/jobs/index.d.ts.map +1 -0
  175. package/dist/lib/jobs/index.js +9 -0
  176. package/dist/lib/jobs/index.js.map +1 -0
  177. package/dist/lib/jobs/relay.d.ts +27 -0
  178. package/dist/lib/jobs/relay.d.ts.map +1 -0
  179. package/dist/lib/jobs/relay.js +61 -0
  180. package/dist/lib/jobs/relay.js.map +1 -0
  181. package/dist/lib/jobs/run-batch.d.ts +52 -0
  182. package/dist/lib/jobs/run-batch.d.ts.map +1 -0
  183. package/dist/lib/jobs/run-batch.js +62 -0
  184. package/dist/lib/jobs/run-batch.js.map +1 -0
  185. package/dist/lib/jobs/run-due-jobs.d.ts +30 -0
  186. package/dist/lib/jobs/run-due-jobs.d.ts.map +1 -0
  187. package/dist/lib/jobs/run-due-jobs.js +70 -0
  188. package/dist/lib/jobs/run-due-jobs.js.map +1 -0
  189. package/dist/lib/jobs/types.d.ts +18 -0
  190. package/dist/lib/jobs/types.d.ts.map +1 -0
  191. package/dist/lib/jobs/types.js +2 -0
  192. package/dist/lib/jobs/types.js.map +1 -0
  193. package/dist/lib/local-api/collection-api.d.ts +42 -0
  194. package/dist/lib/local-api/collection-api.d.ts.map +1 -1
  195. package/dist/lib/local-api/collection-api.js +200 -25
  196. package/dist/lib/local-api/collection-api.js.map +1 -1
  197. package/dist/lib/local-api/hooks.d.ts +7 -0
  198. package/dist/lib/local-api/hooks.d.ts.map +1 -0
  199. package/dist/lib/local-api/hooks.js +20 -0
  200. package/dist/lib/local-api/hooks.js.map +1 -0
  201. package/dist/lib/mcp/tools.js +1 -1
  202. package/dist/lib/mcp/tools.js.map +1 -1
  203. package/dist/lib/plugins/resolver.d.ts +12 -1
  204. package/dist/lib/plugins/resolver.d.ts.map +1 -1
  205. package/dist/lib/plugins/resolver.js +4 -1
  206. package/dist/lib/plugins/resolver.js.map +1 -1
  207. package/dist/lib/plugins/types.d.ts +47 -2
  208. package/dist/lib/plugins/types.d.ts.map +1 -1
  209. package/dist/lib/plugins/types.js.map +1 -1
  210. package/dist/lib/schema-utils/define-type.d.ts +32 -0
  211. package/dist/lib/schema-utils/define-type.d.ts.map +1 -0
  212. package/dist/lib/schema-utils/define-type.js +41 -0
  213. package/dist/lib/schema-utils/define-type.js.map +1 -0
  214. package/dist/lib/schema-utils/index.d.ts +1 -0
  215. package/dist/lib/schema-utils/index.d.ts.map +1 -1
  216. package/dist/lib/schema-utils/index.js +1 -0
  217. package/dist/lib/schema-utils/index.js.map +1 -1
  218. package/dist/lib/server/api/index.d.ts.map +1 -1
  219. package/dist/lib/server/api/index.js +8 -0
  220. package/dist/lib/server/api/index.js.map +1 -1
  221. package/dist/lib/server/api/routes/documents-publish.d.ts.map +1 -1
  222. package/dist/lib/server/api/routes/documents-publish.js +110 -0
  223. package/dist/lib/server/api/routes/documents-publish.js.map +1 -1
  224. package/dist/lib/server/api/routes/jobs.d.ts +4 -0
  225. package/dist/lib/server/api/routes/jobs.d.ts.map +1 -0
  226. package/dist/lib/server/api/routes/jobs.js +93 -0
  227. package/dist/lib/server/api/routes/jobs.js.map +1 -0
  228. package/dist/lib/server/api/routes/workers-run.d.ts +4 -0
  229. package/dist/lib/server/api/routes/workers-run.d.ts.map +1 -0
  230. package/dist/lib/server/api/routes/workers-run.js +41 -0
  231. package/dist/lib/server/api/routes/workers-run.js.map +1 -0
  232. package/dist/lib/server/index.d.ts +1 -0
  233. package/dist/lib/server/index.d.ts.map +1 -1
  234. package/dist/lib/server/index.js +2 -0
  235. package/dist/lib/server/index.js.map +1 -1
  236. package/dist/lib/services/version-service.d.ts +16 -0
  237. package/dist/lib/services/version-service.d.ts.map +1 -1
  238. package/dist/lib/services/version-service.js +72 -48
  239. package/dist/lib/services/version-service.js.map +1 -1
  240. package/dist/lib/types/config.d.ts +43 -0
  241. package/dist/lib/types/config.d.ts.map +1 -1
  242. package/dist/lib/types/events.d.ts +134 -0
  243. package/dist/lib/types/events.d.ts.map +1 -0
  244. package/dist/lib/types/events.js +4 -0
  245. package/dist/lib/types/events.js.map +1 -0
  246. package/dist/lib/types/index.d.ts +1 -0
  247. package/dist/lib/types/index.d.ts.map +1 -1
  248. package/dist/lib/types/index.js +1 -0
  249. package/dist/lib/types/index.js.map +1 -1
  250. package/dist/lib/types/schemas.d.ts +71 -0
  251. package/dist/lib/types/schemas.d.ts.map +1 -1
  252. package/dist/lib/utils/index.d.ts +1 -1
  253. package/dist/lib/utils/index.d.ts.map +1 -1
  254. package/dist/lib/utils/index.js.map +1 -1
  255. package/dist/local-api/collection-api.d.ts +42 -0
  256. package/dist/local-api/collection-api.d.ts.map +1 -1
  257. package/dist/local-api/collection-api.js +200 -25
  258. package/dist/local-api/hooks.d.ts +7 -0
  259. package/dist/local-api/hooks.d.ts.map +1 -0
  260. package/dist/local-api/hooks.js +19 -0
  261. package/dist/mcp/tools.js +1 -1
  262. package/dist/plugins/resolver.d.ts +12 -1
  263. package/dist/plugins/resolver.d.ts.map +1 -1
  264. package/dist/plugins/resolver.js +4 -1
  265. package/dist/plugins/types.d.ts +47 -2
  266. package/dist/plugins/types.d.ts.map +1 -1
  267. package/dist/schema-utils/define-type.d.ts +32 -0
  268. package/dist/schema-utils/define-type.d.ts.map +1 -0
  269. package/dist/schema-utils/define-type.js +40 -0
  270. package/dist/schema-utils/index.d.ts +1 -0
  271. package/dist/schema-utils/index.d.ts.map +1 -1
  272. package/dist/schema-utils/index.js +1 -0
  273. package/dist/server/api/index.d.ts.map +1 -1
  274. package/dist/server/api/index.js +8 -0
  275. package/dist/server/api/routes/documents-publish.d.ts.map +1 -1
  276. package/dist/server/api/routes/documents-publish.js +110 -0
  277. package/dist/server/api/routes/jobs.d.ts +4 -0
  278. package/dist/server/api/routes/jobs.d.ts.map +1 -0
  279. package/dist/server/api/routes/jobs.js +92 -0
  280. package/dist/server/api/routes/workers-run.d.ts +4 -0
  281. package/dist/server/api/routes/workers-run.d.ts.map +1 -0
  282. package/dist/server/api/routes/workers-run.js +40 -0
  283. package/dist/server/index.d.ts +1 -0
  284. package/dist/server/index.d.ts.map +1 -1
  285. package/dist/server/index.js +2 -0
  286. package/dist/services/version-service.d.ts +16 -0
  287. package/dist/services/version-service.d.ts.map +1 -1
  288. package/dist/services/version-service.js +72 -48
  289. package/dist/types/config.d.ts +43 -0
  290. package/dist/types/config.d.ts.map +1 -1
  291. package/dist/types/events.d.ts +134 -0
  292. package/dist/types/events.d.ts.map +1 -0
  293. package/dist/types/events.js +3 -0
  294. package/dist/types/index.d.ts +1 -0
  295. package/dist/types/index.d.ts.map +1 -1
  296. package/dist/types/index.js +1 -0
  297. package/dist/types/schemas.d.ts +71 -0
  298. package/dist/types/schemas.d.ts.map +1 -1
  299. package/dist/utils/index.d.ts +1 -1
  300. package/dist/utils/index.d.ts.map +1 -1
  301. package/package.json +2 -1
@@ -14,6 +14,7 @@
14
14
  import { setSaveStateContext } from '../../save-state-context.svelte';
15
15
  import { usePermissions } from '../../permissions-context.svelte';
16
16
  import AdminSlot from './AdminSlot.svelte';
17
+ import ScheduleDialog from './ScheduleDialog.svelte';
17
18
  import { createPartResolver } from '../../plugins/resolver';
18
19
  import type { CMSPlugin, DocumentActionProps } from '../../plugins/types';
19
20
  import { getDefaultValueForFieldType } from '../../utils/field-defaults';
@@ -37,7 +38,10 @@
37
38
  Smartphone,
38
39
  ZoomIn,
39
40
  ZoomOut,
40
- ArrowLeft
41
+ ArrowLeft,
42
+ CalendarClock,
43
+ X,
44
+ Lock
41
45
  } from '@lucide/svelte';
42
46
  import { stegaEncodeDocument } from '../../preview/stega.js';
43
47
  import { collectAssetRefs, injectAssetData, type ResolvedAsset } from '../../preview/assets.js';
@@ -160,6 +164,14 @@
160
164
  let publishedData = $state<Record<string, any> | null>(null);
161
165
  const isViewingPublished = $derived(perspective === 'published');
162
166
 
167
+ // The scheduled action follows the active perspective — the tab IS the intent:
168
+ // Draft tab → publish (send this draft live later); Published tab → unpublish (take it
169
+ // down later). No toggle in the dialog. The control is gated on the matching permission.
170
+ const scheduleAction = $derived<'publish' | 'unpublish'>(
171
+ perspective === 'published' ? 'unpublish' : 'publish'
172
+ );
173
+ const canScheduleNow = $derived(scheduleAction === 'publish' ? canPublishDoc : canUnpublishDoc);
174
+
163
175
  // Plugin document actions applicable to this type. Capability-gated, with
164
176
  // built-in privileged roles bypassing. Cheap to rebuild from the plugin list.
165
177
  const pluginDocumentActions = $derived(
@@ -706,6 +718,53 @@
706
718
 
707
719
  // Header options dropdown
708
720
  let showHeaderMenu = $state(false);
721
+ let showScheduleDialog = $state(false);
722
+
723
+ // Pending scheduled publish/unpublish for this document (replace semantics → at most one).
724
+ type ScheduledJob = { jobId: string; type: string; runAt: string; status: string };
725
+ let scheduledJobs = $state<ScheduledJob[]>([]);
726
+ // The soonest pending schedule — what the header indicator shows.
727
+ const nextSchedule = $derived(
728
+ [...scheduledJobs].sort(
729
+ (a, b) => new Date(a.runAt).getTime() - new Date(b.runAt).getTime()
730
+ )[0] ?? null
731
+ );
732
+
733
+ async function loadSchedule() {
734
+ if (!documentId) {
735
+ scheduledJobs = [];
736
+ return;
737
+ }
738
+ try {
739
+ const res = await documents.getSchedule(documentId);
740
+ scheduledJobs = res.success ? (res.data ?? []) : [];
741
+ } catch {
742
+ scheduledJobs = [];
743
+ }
744
+ }
745
+
746
+ // Optimistic: the schedule call returns the created job, so show the badge immediately.
747
+ // Replace semantics on the server mean this new job supersedes any prior one. Reconcile
748
+ // with a background fetch so we converge on server truth (id, exact runAt).
749
+ function onScheduleCreated(job: ScheduledJob) {
750
+ scheduledJobs = [job];
751
+ loadSchedule();
752
+ }
753
+
754
+ async function cancelSchedule() {
755
+ if (!documentId) return;
756
+ // Optimistically clear the badge; restore from the server if the cancel fails.
757
+ const previous = scheduledJobs;
758
+ scheduledJobs = [];
759
+ const res = await documents.cancelSchedule(documentId);
760
+ if (res.success) {
761
+ toast.success('Schedule cancelled');
762
+ loadSchedule();
763
+ } else {
764
+ scheduledJobs = previous;
765
+ toast.error(res.error || 'Failed to cancel schedule');
766
+ }
767
+ }
709
768
 
710
769
  // Version history
711
770
  let showVersionHistory = $state(false);
@@ -775,6 +834,33 @@
775
834
 
776
835
  const savedAgoText = $derived(lastSaved ? `Saved ${timeAgo(lastSaved)}` : null);
777
836
 
837
+ // Relative future time for the scheduled-publish indicator ("in 3h"). Reads `now` so it ticks.
838
+ function timeUntil(date: Date): string {
839
+ const seconds = Math.floor((date.getTime() - now) / 1000);
840
+ if (seconds <= 0) return 'now';
841
+ if (seconds < 60) return `in ${seconds}s`;
842
+ const minutes = Math.floor(seconds / 60);
843
+ if (minutes < 60) return `in ${minutes}m`;
844
+ const hours = Math.floor(minutes / 60);
845
+ if (hours < 24) return `in ${hours}h`;
846
+ const days = Math.floor(hours / 24);
847
+ return `in ${days}d`;
848
+ }
849
+
850
+ // Human phrasing for the schedule banner: "today/tomorrow at 8:00 AM", "on Monday at …",
851
+ // or "on Nov 10 at …" further out. Reads `now` so it stays current.
852
+ function humanizeSchedule(date: Date): string {
853
+ const time = date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
854
+ const startOfDay = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
855
+ const days = Math.round((startOfDay(date) - startOfDay(new Date(now))) / 86_400_000);
856
+ let day: string;
857
+ if (days <= 0) day = 'today';
858
+ else if (days === 1) day = 'tomorrow';
859
+ else if (days < 7) day = `on ${date.toLocaleDateString(undefined, { weekday: 'long' })}`;
860
+ else day = `on ${date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`;
861
+ return `${day} at ${time}`;
862
+ }
863
+
778
864
  // Auto-save functionality (every 2 seconds when there are changes)
779
865
  let hasUnsavedChanges = $state(false);
780
866
  let autoSaveTimer: ReturnType<typeof setTimeout> | null = null;
@@ -817,6 +903,14 @@
817
903
  (hasUnpublishedContent || isUnpublished) && !saving && documentId && !hasValidationErrors
818
904
  );
819
905
 
906
+ // The schedule button mirrors the inline action button's enabled state (permission is checked
907
+ // separately, for visibility): scheduling a PUBLISH is enabled exactly when the Publish button
908
+ // is (`canPublish` — there are unpublished, valid changes), so you can't queue a no-op publish;
909
+ // scheduling an UNPUBLISH mirrors the Unpublish button (enabled whenever not mid-save).
910
+ const canScheduleActionNow = $derived(
911
+ scheduleAction === 'publish' ? Boolean(canPublish) : !saving
912
+ );
913
+
820
914
  // Get preview title based on schema config. When viewing in published
821
915
  // perspective, read from the loaded published snapshot — otherwise the
822
916
  // header shows the draft's title even though the body shows published
@@ -884,6 +978,14 @@
884
978
  }
885
979
  });
886
980
 
981
+ // Keep the schedule indicator in sync with the current document.
982
+ $effect(() => {
983
+ // Reference documentId so this re-runs when the editor switches documents.
984
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions -- reactive dependency
985
+ documentId;
986
+ loadSchedule();
987
+ });
988
+
887
989
  // Load existing document data when editing
888
990
  $effect(() => {
889
991
  if (!isCreating && documentId) {
@@ -1775,6 +1877,41 @@
1775
1877
  {/if}
1776
1878
 
1777
1879
  <div class="relative flex h-full w-full min-w-0 flex-col overflow-hidden">
1880
+ <!-- Scheduled publish/unpublish banner — a full-width strip pinned to the very top edge of
1881
+ the editor, above the hero header (breadcrumb + actions), so a pending schedule is the
1882
+ first thing seen. Inner padding matches the hero's (px-4 lg:px-6) so its text lines up
1883
+ with the title below. -->
1884
+ {#if !presentationMode && nextSchedule}
1885
+ {@const isPub = nextSchedule.type === 'document.publish'}
1886
+ {@const runAtDate = new Date(nextSchedule.runAt)}
1887
+ <div
1888
+ class="border-primary/15 bg-primary/5 text-primary flex w-full flex-wrap items-center gap-x-2 gap-y-1 border-b px-4 py-2.5 lg:px-6"
1889
+ >
1890
+ <Lock class="h-4 w-4 shrink-0" />
1891
+ <span class="min-w-0 text-[0.9375rem] leading-tight font-medium">
1892
+ Scheduled to be {isPub ? 'published' : 'unpublished'}
1893
+ {humanizeSchedule(runAtDate)}
1894
+ <span class="text-primary/50 font-normal">· {timeUntil(runAtDate)}</span>
1895
+ </span>
1896
+ <div class="ml-auto flex items-center gap-1">
1897
+ <button
1898
+ type="button"
1899
+ class="hover:bg-primary/10 rounded px-2 py-1 text-xs font-medium transition-colors"
1900
+ onclick={() => (showScheduleDialog = true)}
1901
+ >
1902
+ Reschedule
1903
+ </button>
1904
+ <button
1905
+ type="button"
1906
+ class="hover:bg-primary/10 flex items-center gap-1 rounded px-2 py-1 text-xs font-medium transition-colors"
1907
+ onclick={cancelSchedule}
1908
+ >
1909
+ <X class="h-3 w-3" /> Cancel
1910
+ </button>
1911
+ </div>
1912
+ </div>
1913
+ {/if}
1914
+
1778
1915
  <!-- Hero Header -->
1779
1916
  {#if !presentationMode}
1780
1917
  <div class="bg-background w-full min-w-0 overflow-x-clip px-4 pt-4 pb-5 lg:px-6 lg:pt-5">
@@ -1795,7 +1932,7 @@
1795
1932
  <!-- Always one line (`truncate`); font shrinks where space is tight (narrow
1796
1933
  editor / reference panel) so more of the title fits before the ellipsis,
1797
1934
  and stays large on wide screens. -->
1798
- <h1 class="block w-full min-w-0 truncate text-xl font-semibold tracking-tight lg:text-2xl">
1935
+ <h1 class="block w-full min-w-0 truncate text-2xl font-semibold tracking-tight lg:text-4xl">
1799
1936
  {getPreviewTitle()}
1800
1937
  </h1>
1801
1938
 
@@ -1858,7 +1995,7 @@
1858
1995
  {#if schema && schema.groups && schema.groups.length > 0}
1859
1996
  {@const visibleGroups = schema.groups.filter((g) => !g.hidden)}
1860
1997
  <div
1861
- class="border-rule/60 flex shrink-0 items-center gap-1 overflow-x-auto border-b px-4 py-2 [scrollbar-width:none] lg:px-6 [&::-webkit-scrollbar]:hidden"
1998
+ class="border-rule/60 flex shrink-0 [scrollbar-width:none] items-center gap-1 overflow-x-auto border-b px-4 py-2 lg:px-6 [&::-webkit-scrollbar]:hidden"
1862
1999
  >
1863
2000
  <button
1864
2001
  type="button"
@@ -2272,15 +2409,37 @@
2272
2409
  : 'Unknown'}
2273
2410
  {/if}
2274
2411
  </p>
2275
- {#if fullDocument?._meta?.status === 'unpublished'}
2276
- {#if canPublishDoc}
2277
- <Button size="sm" onclick={publishDocument} disabled={saving}>Publish</Button>
2412
+ <div class="flex items-center gap-2">
2413
+ {#if documentId && canScheduleNow}
2414
+ <Button
2415
+ variant="ghost"
2416
+ size="icon"
2417
+ disabled={!canScheduleActionNow}
2418
+ onclick={() => (showScheduleDialog = true)}
2419
+ class="h-8 w-8 {canScheduleActionNow ? 'cursor-pointer' : ''} {nextSchedule
2420
+ ? 'text-primary'
2421
+ : ''}"
2422
+ title={!canScheduleActionNow
2423
+ ? 'Nothing to publish — make a change first'
2424
+ : nextSchedule
2425
+ ? 'Reschedule'
2426
+ : scheduleAction === 'publish'
2427
+ ? 'Schedule publish'
2428
+ : 'Schedule unpublish'}
2429
+ >
2430
+ <CalendarClock class="h-4 w-4" />
2431
+ </Button>
2278
2432
  {/if}
2279
- {:else if canUnpublishDoc}
2280
- <Button size="sm" variant="secondary" onclick={unpublishDocument} disabled={saving}>
2281
- Unpublish
2282
- </Button>
2283
- {/if}
2433
+ {#if fullDocument?._meta?.status === 'unpublished'}
2434
+ {#if canPublishDoc}
2435
+ <Button size="sm" onclick={publishDocument} disabled={saving}>Publish</Button>
2436
+ {/if}
2437
+ {:else if canUnpublishDoc}
2438
+ <Button size="sm" variant="secondary" onclick={unpublishDocument} disabled={saving}>
2439
+ Unpublish
2440
+ </Button>
2441
+ {/if}
2442
+ </div>
2284
2443
  </div>
2285
2444
  {:else}
2286
2445
  <div class="flex items-center justify-between">
@@ -2293,6 +2452,24 @@
2293
2452
 
2294
2453
  <!-- Right: Publish button + horizontal three dots menu -->
2295
2454
  <div class="flex items-center gap-2">
2455
+ {#if documentId && canScheduleNow}
2456
+ <Button
2457
+ variant="ghost"
2458
+ size="icon"
2459
+ disabled={!canScheduleActionNow}
2460
+ onclick={() => (showScheduleDialog = true)}
2461
+ class="h-8 w-8 {canScheduleActionNow ? 'cursor-pointer' : ''} {nextSchedule
2462
+ ? 'text-primary'
2463
+ : ''}"
2464
+ title={!canScheduleActionNow
2465
+ ? 'Nothing to publish — make a change first'
2466
+ : nextSchedule
2467
+ ? 'Reschedule'
2468
+ : 'Schedule publish'}
2469
+ >
2470
+ <CalendarClock class="h-4 w-4" />
2471
+ </Button>
2472
+ {/if}
2296
2473
  {#if canPublishDoc && !isViewingPublished}
2297
2474
  <Button
2298
2475
  onclick={publishDocument}
@@ -2428,6 +2605,15 @@
2428
2605
  {/if}
2429
2606
 
2430
2607
  <!-- Inspect Modal -->
2608
+ {#if documentId}
2609
+ <ScheduleDialog
2610
+ bind:open={showScheduleDialog}
2611
+ {documentId}
2612
+ action={scheduleAction}
2613
+ onScheduled={onScheduleCreated}
2614
+ />
2615
+ {/if}
2616
+
2431
2617
  {#if showInspect}
2432
2618
  <div class="absolute inset-0 z-50 flex items-center justify-center bg-black/50">
2433
2619
  <div
@@ -1 +1 @@
1
- {"version":3,"file":"DocumentEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/admin/DocumentEditor.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAQzD,OAAO,KAAK,EAAE,SAAS,EAAuB,MAAM,qBAAqB,CAAC;AA6BzE,UAAU,KAAK;IACd,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB;;kDAE8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,oBAAoB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,sBAAsB,CAAC,EAAE;QACxB,aAAa,EAAE,MAAM,CAAC;QACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC1B,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,IAAI,CAAC;IACT,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,yGAAyG;IACzG,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,6EAA6E;IAC7E,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC;CACtB;AA6rEF,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"DocumentEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/admin/DocumentEditor.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AASzD,OAAO,KAAK,EAAE,SAAS,EAAuB,MAAM,qBAAqB,CAAC;AAgCzE,UAAU,KAAK;IACd,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB;;kDAE8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,oBAAoB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,sBAAsB,CAAC,EAAE;QACxB,aAAa,EAAE,MAAM,CAAC;QACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC1B,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,IAAI,CAAC;IACT,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,yGAAyG;IACzG,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,6EAA6E;IAC7E,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC;CACtB;AAq1EF,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -0,0 +1,174 @@
1
+ <script lang="ts">
2
+ // Schedule a future publish/unpublish. Layout mirrors Sanity's scheduled-drafts dialog
3
+ // (title · description · "Schedule on" field → calendar+time popover · Cancel/Schedule),
4
+ // in Aphex's shadcn skin. Posts to /documents/:id/schedule; the worker runs it at `runAt`
5
+ // (re-validating then), and `runAt` is floored to the minute so 1:30 means 1:30:00.
6
+ import * as Dialog from '@aphexcms/ui/shadcn/dialog';
7
+ import * as Popover from '@aphexcms/ui/shadcn/popover';
8
+ import { Calendar } from '@aphexcms/ui/shadcn/calendar';
9
+ import { Button } from '@aphexcms/ui/shadcn/button';
10
+ import { Label } from '@aphexcms/ui/shadcn/label';
11
+ import { Calendar as CalendarIcon, Globe } from '@lucide/svelte';
12
+ import { CalendarDate, today, getLocalTimeZone, type DateValue } from '@internationalized/date';
13
+ import { documents } from '../../api/documents';
14
+ import { toast } from 'svelte-sonner';
15
+
16
+ interface Props {
17
+ open: boolean;
18
+ documentId: string;
19
+ /**
20
+ * The action to schedule. Derived from the editor's active perspective by the caller:
21
+ * viewing the Draft tab → 'publish' (send this draft live later); viewing the Published
22
+ * tab → 'unpublish' (take it down later). No in-dialog toggle — the tab is the intent.
23
+ */
24
+ action: 'publish' | 'unpublish';
25
+ onScheduled?: (job: { jobId: string; type: string; runAt: string; status: string }) => void;
26
+ }
27
+ let { open = $bindable(), documentId, action, onScheduled }: Props = $props();
28
+
29
+ let dateValue = $state<DateValue | undefined>(undefined);
30
+ let timeStr = $state('09:00'); // "HH:MM", 24h internally
31
+ let pickerOpen = $state(false);
32
+ let submitting = $state(false);
33
+
34
+ const actionVerb = $derived(action === 'publish' ? 'Publish' : 'Unpublish');
35
+
36
+ const minDate = today(getLocalTimeZone());
37
+ // Short local timezone label, e.g. "MST" — shown so the user knows what zone they're setting.
38
+ const tzLabel =
39
+ new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' })
40
+ .formatToParts(new Date())
41
+ .find((p) => p.type === 'timeZoneName')?.value ?? '';
42
+
43
+ const pad = (n: number) => String(n).padStart(2, '0');
44
+
45
+ // Reset the date/time each time the dialog opens: default to one hour out, floored to the minute.
46
+ $effect(() => {
47
+ if (open) {
48
+ const d = new Date(Date.now() + 60 * 60 * 1000);
49
+ dateValue = new CalendarDate(d.getFullYear(), d.getMonth() + 1, d.getDate());
50
+ timeStr = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
51
+ }
52
+ });
53
+
54
+ /** Combine the picked calendar date + time into a local Date floored to the minute. */
55
+ function buildRunAt(): Date | null {
56
+ if (!dateValue) return null;
57
+ const [h, m] = timeStr.split(':').map(Number);
58
+ const d = new Date(dateValue.year, dateValue.month - 1, dateValue.day, h ?? 0, m ?? 0, 0, 0);
59
+ return Number.isNaN(d.getTime()) ? null : d;
60
+ }
61
+
62
+ const runAtPreview = $derived(buildRunAt());
63
+ const fieldLabel = $derived(
64
+ runAtPreview
65
+ ? runAtPreview.toLocaleString(undefined, {
66
+ month: 'short',
67
+ day: 'numeric',
68
+ year: 'numeric',
69
+ hour: 'numeric',
70
+ minute: '2-digit'
71
+ })
72
+ : 'Select date and time'
73
+ );
74
+
75
+ function setToCurrentTime() {
76
+ const now = new Date();
77
+ dateValue = new CalendarDate(now.getFullYear(), now.getMonth() + 1, now.getDate());
78
+ timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
79
+ }
80
+
81
+ async function submit() {
82
+ const runAt = buildRunAt();
83
+ if (!runAt) {
84
+ toast.error('Pick a date and time');
85
+ return;
86
+ }
87
+ runAt.setSeconds(0, 0);
88
+ // Reject genuinely past minutes; the current minute is allowed (runs on the next tick).
89
+ const currentMinute = new Date();
90
+ currentMinute.setSeconds(0, 0);
91
+ if (runAt.getTime() < currentMinute.getTime()) {
92
+ toast.error('Pick a time in the future');
93
+ return;
94
+ }
95
+
96
+ submitting = true;
97
+ try {
98
+ const res = await documents.schedule(documentId, { action, runAt: runAt.toISOString() });
99
+ if (res.success) {
100
+ toast.success(
101
+ `${action === 'publish' ? 'Publish' : 'Unpublish'} scheduled for ${runAt.toLocaleString()}`
102
+ );
103
+ open = false;
104
+ if (res.data) onScheduled?.(res.data);
105
+ } else {
106
+ toast.error(res.error || res.message || 'Failed to schedule');
107
+ }
108
+ } catch (err) {
109
+ toast.error(err instanceof Error ? err.message : 'Failed to schedule');
110
+ } finally {
111
+ submitting = false;
112
+ }
113
+ }
114
+ </script>
115
+
116
+ <Dialog.Root bind:open>
117
+ <Dialog.Content class="max-w-md">
118
+ <Dialog.Header>
119
+ <Dialog.Title>Schedule {actionVerb}</Dialog.Title>
120
+ <Dialog.Description>
121
+ Select when this document should be {action === 'publish' ? 'published' : 'unpublished'}.
122
+ </Dialog.Description>
123
+ </Dialog.Header>
124
+
125
+ <div class="space-y-4 py-2">
126
+ <div class="space-y-1.5">
127
+ <Label>Schedule on</Label>
128
+ <Popover.Root bind:open={pickerOpen}>
129
+ <Popover.Trigger
130
+ class="border-input bg-background hover:bg-muted/40 focus-visible:ring-ring flex w-full items-center justify-between rounded-md border px-3 py-2 text-left text-sm focus-visible:ring-1 focus-visible:outline-none"
131
+ >
132
+ <span>{fieldLabel}</span>
133
+ <CalendarIcon class="text-muted-foreground h-4 w-4" />
134
+ </Popover.Trigger>
135
+ <Popover.Content class="z-[70] w-[19rem] p-0" align="start">
136
+ <Calendar
137
+ type="single"
138
+ bind:value={dateValue}
139
+ minValue={minDate}
140
+ class="w-full rounded-b-none [--cell-size:2.4rem]"
141
+ />
142
+ <div class="border-rule flex flex-wrap items-center gap-x-2 gap-y-1 border-t p-3">
143
+ <input
144
+ type="time"
145
+ bind:value={timeStr}
146
+ class="border-input bg-background focus-visible:ring-ring rounded-md border px-2 py-1 text-sm focus-visible:ring-1 focus-visible:outline-none"
147
+ />
148
+ <button
149
+ type="button"
150
+ class="text-muted-foreground hover:text-foreground text-xs underline-offset-2 hover:underline"
151
+ onclick={setToCurrentTime}
152
+ >
153
+ Set to current time
154
+ </button>
155
+ <span class="text-muted-foreground ml-auto flex items-center gap-1 text-xs">
156
+ <Globe class="h-3 w-3" />
157
+ {tzLabel}
158
+ </span>
159
+ </div>
160
+ </Popover.Content>
161
+ </Popover.Root>
162
+ </div>
163
+ </div>
164
+
165
+ <Dialog.Footer>
166
+ <Button variant="ghost" size="sm" onclick={() => (open = false)} disabled={submitting}>
167
+ Cancel
168
+ </Button>
169
+ <Button size="sm" onclick={submit} disabled={submitting}>
170
+ {submitting ? 'Scheduling…' : 'Schedule'}
171
+ </Button>
172
+ </Dialog.Footer>
173
+ </Dialog.Content>
174
+ </Dialog.Root>
@@ -0,0 +1,20 @@
1
+ interface Props {
2
+ open: boolean;
3
+ documentId: string;
4
+ /**
5
+ * The action to schedule. Derived from the editor's active perspective by the caller:
6
+ * viewing the Draft tab → 'publish' (send this draft live later); viewing the Published
7
+ * tab → 'unpublish' (take it down later). No in-dialog toggle — the tab is the intent.
8
+ */
9
+ action: 'publish' | 'unpublish';
10
+ onScheduled?: (job: {
11
+ jobId: string;
12
+ type: string;
13
+ runAt: string;
14
+ status: string;
15
+ }) => void;
16
+ }
17
+ declare const ScheduleDialog: import("svelte").Component<Props, {}, "open">;
18
+ type ScheduleDialog = ReturnType<typeof ScheduleDialog>;
19
+ export default ScheduleDialog;
20
+ //# sourceMappingURL=ScheduleDialog.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScheduleDialog.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/admin/ScheduleDialog.svelte.ts"],"names":[],"mappings":"AAkBC,UAAU,KAAK;IACd,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,MAAM,EAAE,SAAS,GAAG,WAAW,CAAC;IAChC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC5F;AAwJF,QAAA,MAAM,cAAc,+CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -34,7 +34,6 @@
34
34
  import { documents } from '../../../api/documents';
35
35
  import { SvelteMap } from 'svelte/reactivity';
36
36
  import { getDocumentVersion } from '../../../document-refresh.svelte';
37
- import RichtextField from './richtext/RichtextField.svelte';
38
37
 
39
38
  interface Props {
40
39
  field: ArrayFieldType;
@@ -76,6 +75,11 @@
76
75
  }
77
76
 
78
77
  const isBlockArray = $derived(field.of.some((ref) => ref.type === 'block'));
78
+ // Rich text (TipTap) is by far the largest field dependency. Load it only when
79
+ // the array actually holds block content, so document editors without a
80
+ // rich-text field — and every admin page that never renders one — stay off
81
+ // that chunk. The import is cached, so a stable isBlockArray never refetches.
82
+ const richtextModule = $derived(isBlockArray ? import('./richtext/RichtextField.svelte') : null);
79
83
  const isGridLayout = $derived(field.options?.layout === 'grid');
80
84
  const isReferenceArray = $derived(
81
85
  field.of && field.of.length > 0 && field.of[0]?.type === 'reference'
@@ -512,7 +516,11 @@
512
516
  </script>
513
517
 
514
518
  {#if isBlockArray}
515
- <RichtextField {field} {value} {onUpdate} {readonly} {onOpenReference} {organizationId} />
519
+ {#if richtextModule}
520
+ {#await richtextModule then { default: RichtextField }}
521
+ <RichtextField {field} {value} {onUpdate} {readonly} {onOpenReference} {organizationId} />
522
+ {/await}
523
+ {/if}
516
524
  {:else if isReferenceArray && referenceFieldShape}
517
525
  <!-- Reference array — each row reuses ReferenceField. Drag handle on the
518
526
  left, ReferenceField fills the row, an X button on empty rows lets the
@@ -1 +1 @@
1
- {"version":3,"file":"ArrayField.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/admin/fields/ArrayField.svelte.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAqB,MAAM,wBAAwB,CAAC;AAgC7F,UAAU,KAAK;IACd,KAAK,EAAE,cAAc,CAAC;IACtB,KAAK,EAAE,GAAG,CAAC;IACX,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;IAC/B,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AA86BF,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"ArrayField.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/admin/fields/ArrayField.svelte.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAqB,MAAM,wBAAwB,CAAC;AA+B7F,UAAU,KAAK;IACd,KAAK,EAAE,cAAc,CAAC;IACtB,KAAK,EAAE,GAAG,CAAC;IACX,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;IAC/B,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAs7BF,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -111,7 +111,7 @@
111
111
  <SidebarMenuButton
112
112
  {...props}
113
113
  size="lg"
114
- class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
114
+ class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground cursor-pointer"
115
115
  disabled={isSwitching}
116
116
  >
117
117
  {#if activeOrganization}
@@ -119,7 +119,7 @@
119
119
  <img
120
120
  src={getOrganizationLogo(activeOrganization)!}
121
121
  alt={activeOrganization.name}
122
- class="aspect-square size-8 rounded-lg object-cover"
122
+ class="aspect-square size-8 object-cover"
123
123
  />
124
124
  {:else}
125
125
  <div
@@ -275,7 +275,7 @@
275
275
  {#if canCreateOrganization}
276
276
  <DropdownMenuSeparator />
277
277
  <DropdownMenuItem
278
- class="gap-2 p-2"
278
+ class="cursor-pointer gap-2 p-2"
279
279
  onclick={() => goto('/admin/organizations?action=create')}
280
280
  >
281
281
  <div class="flex size-6 items-center justify-center rounded-md border bg-transparent">
@@ -40,7 +40,7 @@
40
40
  <SidebarMenuButton
41
41
  {...props}
42
42
  size="lg"
43
- class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
43
+ class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground cursor-pointer"
44
44
  >
45
45
  {#if user.image}
46
46
  <img
@@ -76,22 +76,22 @@
76
76
  <p class="text-muted-foreground text-xs">{user.email}</p>
77
77
  </div>
78
78
  <DropdownMenuSeparator />
79
- <DropdownMenuItem onclick={() => goto('/admin/settings/account')}>
79
+ <DropdownMenuItem class="cursor-pointer" onclick={() => goto('/admin/settings/account')}>
80
80
  <Settings class="mr-2 h-4 w-4" />
81
81
  <span>Account Settings</span>
82
82
  </DropdownMenuItem>
83
- <DropdownMenuItem onclick={() => goto('/invitations')}>
83
+ <DropdownMenuItem class="cursor-pointer" onclick={() => goto('/invitations')}>
84
84
  <Mail class="mr-2 h-4 w-4" />
85
85
  <span>Invitations</span>
86
86
  </DropdownMenuItem>
87
87
  {#if user.role === 'super_admin'}
88
- <DropdownMenuItem onclick={() => goto('/god-mode')}>
88
+ <DropdownMenuItem class="cursor-pointer" onclick={() => goto('/god-mode')}>
89
89
  <Shield class="mr-2 h-4 w-4" />
90
90
  <span>God Mode</span>
91
91
  </DropdownMenuItem>
92
92
  {/if}
93
93
  <DropdownMenuSeparator />
94
- <DropdownMenuItem class="text-destructive" onclick={handleSignOut}>
94
+ <DropdownMenuItem class="text-destructive cursor-pointer" onclick={handleSignOut}>
95
95
  <LogOut class="mr-2 h-4 w-4" />
96
96
  <span>Sign Out</span>
97
97
  </DropdownMenuItem>
@@ -0,0 +1,55 @@
1
+ import type { DomainEvent, AppendEventInput, Job, ScheduleJobInput, ClaimJobsOptions, ListEventsOptions, ListJobsOptions, OutboxRow, ListUnprocessedOutboxOptions, Page } from '../../types/events.js';
2
+ /**
3
+ * The durable spine: an append-only domain-event log plus a DB-backed job queue.
4
+ * All rows are organization-scoped. (The event-log read API — `listEvents` — lands
5
+ * with the admin history view in a later slice; there's no reader for it yet.)
6
+ */
7
+ export interface EventJobAdapter {
8
+ /**
9
+ * Append an immutable domain event AND its outbox worklist row, in one insert path.
10
+ * Call on the tx handle (inside `withTransaction`) so both commit atomically with the
11
+ * state change that caused them — the transactional-outbox guarantee. The outbox row is
12
+ * always written (even with no subscribers today): the relay decides fan-out at run time,
13
+ * so emission can't know whether anyone is listening.
14
+ */
15
+ appendEvent(input: AppendEventInput): Promise<DomainEvent>;
16
+ /** Read a single event by id (org-scoped). */
17
+ getEvent(organizationId: string, id: string): Promise<DomainEvent | null>;
18
+ /** List domain events for the org, newest first (read-only history / observability). */
19
+ listEvents(options: ListEventsOptions): Promise<Page<DomainEvent>>;
20
+ /**
21
+ * Read pending outbox rows (`processedAt IS NULL`), oldest first — the relay's input.
22
+ * No claim/lease: fan-out is idempotent (per event×consumer job key), so concurrent
23
+ * relays are safe. Omit `organizationId` to read across all orgs (worker context).
24
+ */
25
+ listUnprocessedOutbox(options: ListUnprocessedOutboxOptions): Promise<OutboxRow[]>;
26
+ /** Stamp `processedAt` once an outbox row has been fanned out to every subscriber. */
27
+ markOutboxProcessed(organizationId: string, id: string): Promise<void>;
28
+ /** Schedule a job. Idempotent when `idempotencyKey` is set: a duplicate key returns the existing job. */
29
+ scheduleJob(input: ScheduleJobInput): Promise<Job>;
30
+ /**
31
+ * Atomically claim up to `limit` due jobs with a lease, incrementing `attempts`.
32
+ * Claims jobs that are `pending` and due, or `leased` with an expired lease (crash recovery).
33
+ */
34
+ claimDueJobs(options: ClaimJobsOptions): Promise<Job[]>;
35
+ /** Mark a claimed job completed. */
36
+ completeJob(organizationId: string, id: string): Promise<void>;
37
+ /**
38
+ * Reschedule a claimed job for a later retry (backoff). Status → `pending`, `runAt`
39
+ * moves forward, the error is recorded, the lease is cleared. The backoff/max-attempts
40
+ * *policy* lives in the core runner; this is just the state transition.
41
+ */
42
+ retryJob(organizationId: string, id: string, options: {
43
+ runAt: Date;
44
+ error: string;
45
+ }): Promise<void>;
46
+ /** Dead-letter a claimed job: status → `failed`, record the error, clear the lease. */
47
+ failJob(organizationId: string, id: string, options: {
48
+ error: string;
49
+ }): Promise<void>;
50
+ /** Cancel a job (e.g. a scheduled publish the user called off): status → `cancelled`, clear the lease. */
51
+ cancelJob(organizationId: string, id: string): Promise<void>;
52
+ /** List jobs for the org, newest first (read-only history / observability). */
53
+ listJobs(options: ListJobsOptions): Promise<Page<Job>>;
54
+ }
55
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../src/lib/db/interfaces/events.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,WAAW,EACX,gBAAgB,EAChB,GAAG,EACH,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,4BAA4B,EAC5B,IAAI,EACJ,MAAM,oBAAoB,CAAC;AAE5B;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAE/B;;;;;;OAMG;IACH,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC3D,8CAA8C;IAC9C,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC1E,wFAAwF;IACxF,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IAGnE;;;;OAIG;IACH,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACnF,sFAAsF;IACtF,mBAAmB,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAGvE,yGAAyG;IACzG,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACnD;;;OAGG;IACH,YAAY,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACxD,oCAAoC;IACpC,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CACP,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;QAAE,KAAK,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GACrC,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,uFAAuF;IACvF,OAAO,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvF,0GAA0G;IAC1G,SAAS,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;CACvD"}