@actuate-media/cms-admin 0.9.0 → 0.11.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 (294) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +8 -5
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/layout/primitives.test.d.ts +2 -0
  5. package/dist/__tests__/layout/primitives.test.d.ts.map +1 -0
  6. package/dist/__tests__/layout/primitives.test.js +34 -0
  7. package/dist/__tests__/layout/primitives.test.js.map +1 -0
  8. package/dist/__tests__/lib/cv.test.d.ts +2 -0
  9. package/dist/__tests__/lib/cv.test.d.ts.map +1 -0
  10. package/dist/__tests__/lib/cv.test.js +66 -0
  11. package/dist/__tests__/lib/cv.test.js.map +1 -0
  12. package/dist/actuate-admin.css +1 -1
  13. package/dist/assets/actuate-logo.d.ts +36 -0
  14. package/dist/assets/actuate-logo.d.ts.map +1 -0
  15. package/dist/assets/actuate-logo.js +15 -0
  16. package/dist/assets/actuate-logo.js.map +1 -0
  17. package/dist/components/Breadcrumbs.js +2 -2
  18. package/dist/components/CommandPalette.js +10 -10
  19. package/dist/components/ContentOverviewChart.js +3 -3
  20. package/dist/components/ErrorBoundary.js +1 -1
  21. package/dist/components/FocalPointPicker.js +2 -2
  22. package/dist/components/FolderTree.js +20 -20
  23. package/dist/components/LivePreview.js +3 -3
  24. package/dist/components/LocaleSwitcher.js +1 -1
  25. package/dist/components/MediaPickerModal.js +4 -4
  26. package/dist/components/PresenceIndicator.js +1 -1
  27. package/dist/components/SEOConfigPanel.d.ts +2 -0
  28. package/dist/components/SEOConfigPanel.d.ts.map +1 -0
  29. package/dist/components/SEOConfigPanel.js +174 -0
  30. package/dist/components/SEOConfigPanel.js.map +1 -0
  31. package/dist/components/SEOPanel.js +9 -9
  32. package/dist/components/SEOPerformance.js +2 -2
  33. package/dist/components/SchedulePublishDialog.d.ts +18 -0
  34. package/dist/components/SchedulePublishDialog.d.ts.map +1 -0
  35. package/dist/components/SchedulePublishDialog.js +106 -0
  36. package/dist/components/SchedulePublishDialog.js.map +1 -0
  37. package/dist/components/SharePreviewLinkDialog.d.ts +17 -0
  38. package/dist/components/SharePreviewLinkDialog.d.ts.map +1 -0
  39. package/dist/components/SharePreviewLinkDialog.js +83 -0
  40. package/dist/components/SharePreviewLinkDialog.js.map +1 -0
  41. package/dist/components/TipTapEditor.js +5 -5
  42. package/dist/components/VersionHistory.js +2 -2
  43. package/dist/components/ui/Badge.d.ts +33 -3
  44. package/dist/components/ui/Badge.d.ts.map +1 -1
  45. package/dist/components/ui/Badge.js +42 -8
  46. package/dist/components/ui/Badge.js.map +1 -1
  47. package/dist/components/ui/Button.d.ts +19 -8
  48. package/dist/components/ui/Button.d.ts.map +1 -1
  49. package/dist/components/ui/Button.js +35 -14
  50. package/dist/components/ui/Button.js.map +1 -1
  51. package/dist/components/ui/Card.d.ts +26 -0
  52. package/dist/components/ui/Card.d.ts.map +1 -0
  53. package/dist/components/ui/Card.js +45 -0
  54. package/dist/components/ui/Card.js.map +1 -0
  55. package/dist/components/ui/DataTable.js +1 -1
  56. package/dist/components/ui/Input.d.ts +15 -0
  57. package/dist/components/ui/Input.d.ts.map +1 -0
  58. package/dist/components/ui/Input.js +23 -0
  59. package/dist/components/ui/Input.js.map +1 -0
  60. package/dist/components/ui/SearchInput.js +1 -1
  61. package/dist/components/ui/Select.d.ts +16 -0
  62. package/dist/components/ui/Select.d.ts.map +1 -0
  63. package/dist/components/ui/Select.js +25 -0
  64. package/dist/components/ui/Select.js.map +1 -0
  65. package/dist/components/ui/Toast.js +1 -1
  66. package/dist/components/ui/index.d.ts +10 -4
  67. package/dist/components/ui/index.d.ts.map +1 -1
  68. package/dist/components/ui/index.js +5 -2
  69. package/dist/components/ui/index.js.map +1 -1
  70. package/dist/fields/BlockBuilderField.js +3 -3
  71. package/dist/fields/DateField.js +1 -1
  72. package/dist/fields/RelationshipField.js +3 -3
  73. package/dist/fields/TextField.js +1 -1
  74. package/dist/index.d.ts +6 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +5 -0
  77. package/dist/index.js.map +1 -1
  78. package/dist/layout/Header.js +1 -1
  79. package/dist/layout/Layout.d.ts +14 -0
  80. package/dist/layout/Layout.d.ts.map +1 -1
  81. package/dist/layout/Layout.js +17 -11
  82. package/dist/layout/Layout.js.map +1 -1
  83. package/dist/layout/Sidebar.d.ts.map +1 -1
  84. package/dist/layout/Sidebar.js +21 -11
  85. package/dist/layout/Sidebar.js.map +1 -1
  86. package/dist/layout/primitives/AdminShell.d.ts +43 -0
  87. package/dist/layout/primitives/AdminShell.d.ts.map +1 -0
  88. package/dist/layout/primitives/AdminShell.js +51 -0
  89. package/dist/layout/primitives/AdminShell.js.map +1 -0
  90. package/dist/layout/primitives/Box.d.ts +19 -0
  91. package/dist/layout/primitives/Box.d.ts.map +1 -0
  92. package/dist/layout/primitives/Box.js +12 -0
  93. package/dist/layout/primitives/Box.js.map +1 -0
  94. package/dist/layout/primitives/Cluster.d.ts +27 -0
  95. package/dist/layout/primitives/Cluster.d.ts.map +1 -0
  96. package/dist/layout/primitives/Cluster.js +37 -0
  97. package/dist/layout/primitives/Cluster.js.map +1 -0
  98. package/dist/layout/primitives/Grid.d.ts +45 -0
  99. package/dist/layout/primitives/Grid.d.ts.map +1 -0
  100. package/dist/layout/primitives/Grid.js +59 -0
  101. package/dist/layout/primitives/Grid.js.map +1 -0
  102. package/dist/layout/primitives/PageContainer.d.ts +36 -0
  103. package/dist/layout/primitives/PageContainer.d.ts.map +1 -0
  104. package/dist/layout/primitives/PageContainer.js +41 -0
  105. package/dist/layout/primitives/PageContainer.js.map +1 -0
  106. package/dist/layout/primitives/Split.d.ts +34 -0
  107. package/dist/layout/primitives/Split.d.ts.map +1 -0
  108. package/dist/layout/primitives/Split.js +27 -0
  109. package/dist/layout/primitives/Split.js.map +1 -0
  110. package/dist/layout/primitives/Stack.d.ts +23 -0
  111. package/dist/layout/primitives/Stack.d.ts.map +1 -0
  112. package/dist/layout/primitives/Stack.js +34 -0
  113. package/dist/layout/primitives/Stack.js.map +1 -0
  114. package/dist/layout/primitives/index.d.ts +30 -0
  115. package/dist/layout/primitives/index.d.ts.map +1 -0
  116. package/dist/layout/primitives/index.js +22 -0
  117. package/dist/layout/primitives/index.js.map +1 -0
  118. package/dist/layout/primitives/tokens.d.ts +48 -0
  119. package/dist/layout/primitives/tokens.d.ts.map +1 -0
  120. package/dist/layout/primitives/tokens.js +54 -0
  121. package/dist/layout/primitives/tokens.js.map +1 -0
  122. package/dist/lib/cv.d.ts +53 -0
  123. package/dist/lib/cv.d.ts.map +1 -0
  124. package/dist/lib/cv.js +39 -0
  125. package/dist/lib/cv.js.map +1 -0
  126. package/dist/views/ApiKeys.d.ts.map +1 -1
  127. package/dist/views/ApiKeys.js +13 -11
  128. package/dist/views/ApiKeys.js.map +1 -1
  129. package/dist/views/CollectionList.js +8 -8
  130. package/dist/views/Dashboard.d.ts.map +1 -1
  131. package/dist/views/Dashboard.js +333 -78
  132. package/dist/views/Dashboard.js.map +1 -1
  133. package/dist/views/DocumentEdit.d.ts.map +1 -1
  134. package/dist/views/DocumentEdit.js +17 -5
  135. package/dist/views/DocumentEdit.js.map +1 -1
  136. package/dist/views/ForgotPassword.js +2 -2
  137. package/dist/views/FormEditor.js +5 -5
  138. package/dist/views/FormSubmissions.js +6 -6
  139. package/dist/views/Forms.js +2 -2
  140. package/dist/views/Login.d.ts +16 -1
  141. package/dist/views/Login.d.ts.map +1 -1
  142. package/dist/views/Login.js +17 -7
  143. package/dist/views/Login.js.map +1 -1
  144. package/dist/views/MediaBrowser.js +16 -16
  145. package/dist/views/PageEditor.js +2 -2
  146. package/dist/views/Pages.js +10 -10
  147. package/dist/views/PostEditor.js +2 -2
  148. package/dist/views/Posts.js +4 -4
  149. package/dist/views/Redirects.js +4 -4
  150. package/dist/views/ResetPassword.js +2 -2
  151. package/dist/views/SEO.js +6 -6
  152. package/dist/views/ScriptTagEditor.js +4 -4
  153. package/dist/views/ScriptTags.js +2 -2
  154. package/dist/views/Settings.d.ts.map +1 -1
  155. package/dist/views/Settings.js +9 -8
  156. package/dist/views/Settings.js.map +1 -1
  157. package/dist/views/SetupWizard.js +2 -2
  158. package/dist/views/Users.js +4 -4
  159. package/dist/views/page-builder/AIBlockAssist.js +1 -1
  160. package/dist/views/page-builder/AIGenerateDialog.js +10 -10
  161. package/dist/views/page-builder/BlockEditor.js +10 -10
  162. package/dist/views/page-builder/BlockPicker.js +4 -4
  163. package/dist/views/page-builder/BottomBar.js +1 -1
  164. package/dist/views/page-builder/BuilderToolbar.js +2 -2
  165. package/dist/views/page-builder/ContextPanel.js +2 -2
  166. package/dist/views/page-builder/DesignScore.js +9 -9
  167. package/dist/views/page-builder/NodeSettings.js +8 -8
  168. package/dist/views/page-builder/PageBuilder.js +3 -3
  169. package/dist/views/page-builder/PageSettings.js +1 -1
  170. package/dist/views/page-builder/PageTemplates.js +2 -2
  171. package/dist/views/page-builder/SEOPanel.js +13 -13
  172. package/dist/views/page-builder/SavedSections.js +5 -5
  173. package/dist/views/page-builder/TemplatePicker.js +2 -2
  174. package/dist/views/page-builder/block-renderers/CTAPreview.js +5 -5
  175. package/dist/views/page-builder/block-renderers/CardsPreview.js +1 -1
  176. package/dist/views/page-builder/block-renderers/CodePreview.js +1 -1
  177. package/dist/views/page-builder/block-renderers/FAQPreview.js +3 -3
  178. package/dist/views/page-builder/block-renderers/FallbackPreview.js +1 -1
  179. package/dist/views/page-builder/block-renderers/FormPreview.js +3 -3
  180. package/dist/views/page-builder/block-renderers/GalleryPreview.js +5 -5
  181. package/dist/views/page-builder/block-renderers/HeroPreview.js +3 -3
  182. package/dist/views/page-builder/block-renderers/ImagePreview.js +3 -3
  183. package/dist/views/page-builder/block-renderers/TextPreview.js +3 -3
  184. package/dist/views/page-builder/block-renderers/VideoPreview.js +4 -4
  185. package/dist/views/page-builder/canvas/BlockRenderer.js +1 -1
  186. package/dist/views/page-builder/canvas/BuilderCanvas.js +3 -3
  187. package/dist/views/page-builder/canvas/ColumnRenderer.js +2 -2
  188. package/dist/views/page-builder/canvas/ContainerRenderer.js +2 -2
  189. package/dist/views/page-builder/canvas/RowRenderer.js +2 -2
  190. package/dist/views/page-builder/canvas/SectionRenderer.js +2 -2
  191. package/package.json +6 -2
  192. package/src/AdminRoot.tsx +21 -11
  193. package/src/__tests__/layout/primitives.test.ts +37 -0
  194. package/src/__tests__/lib/cv.test.ts +74 -0
  195. package/src/assets/actuate-logo.tsx +72 -0
  196. package/src/components/Breadcrumbs.tsx +6 -6
  197. package/src/components/CommandPalette.tsx +34 -34
  198. package/src/components/ContentOverviewChart.tsx +3 -3
  199. package/src/components/ErrorBoundary.tsx +3 -3
  200. package/src/components/FocalPointPicker.tsx +4 -4
  201. package/src/components/FolderTree.tsx +38 -38
  202. package/src/components/LivePreview.tsx +16 -16
  203. package/src/components/LocaleSwitcher.tsx +7 -7
  204. package/src/components/MediaPickerModal.tsx +21 -21
  205. package/src/components/PresenceIndicator.tsx +2 -2
  206. package/src/components/SEOConfigPanel.tsx +582 -0
  207. package/src/components/SEOPanel.tsx +46 -46
  208. package/src/components/SEOPerformance.tsx +21 -21
  209. package/src/components/SchedulePublishDialog.tsx +241 -0
  210. package/src/components/SharePreviewLinkDialog.tsx +227 -0
  211. package/src/components/TipTapEditor.tsx +33 -33
  212. package/src/components/VersionHistory.tsx +16 -16
  213. package/src/components/ui/Badge.tsx +66 -14
  214. package/src/components/ui/Button.tsx +70 -33
  215. package/src/components/ui/Card.tsx +101 -0
  216. package/src/components/ui/DataTable.tsx +1 -1
  217. package/src/components/ui/Input.tsx +35 -0
  218. package/src/components/ui/SearchInput.tsx +4 -4
  219. package/src/components/ui/Select.tsx +56 -0
  220. package/src/components/ui/Toast.tsx +1 -1
  221. package/src/components/ui/index.ts +18 -4
  222. package/src/fields/BlockBuilderField.tsx +3 -3
  223. package/src/fields/DateField.tsx +1 -1
  224. package/src/fields/RelationshipField.tsx +10 -10
  225. package/src/fields/TextField.tsx +1 -1
  226. package/src/index.ts +32 -0
  227. package/src/layout/Header.tsx +28 -28
  228. package/src/layout/Layout.tsx +39 -46
  229. package/src/layout/Sidebar.tsx +37 -64
  230. package/src/layout/primitives/AdminShell.tsx +118 -0
  231. package/src/layout/primitives/Box.tsx +30 -0
  232. package/src/layout/primitives/Cluster.tsx +74 -0
  233. package/src/layout/primitives/Grid.tsx +120 -0
  234. package/src/layout/primitives/PageContainer.tsx +96 -0
  235. package/src/layout/primitives/Split.tsx +73 -0
  236. package/src/layout/primitives/Stack.tsx +67 -0
  237. package/src/layout/primitives/index.ts +36 -0
  238. package/src/layout/primitives/tokens.ts +76 -0
  239. package/src/lib/cv.ts +96 -0
  240. package/src/styles/build-input.css +1 -1
  241. package/src/views/ApiKeys.tsx +57 -57
  242. package/src/views/CollectionList.tsx +30 -30
  243. package/src/views/Dashboard.tsx +737 -186
  244. package/src/views/DocumentEdit.tsx +90 -10
  245. package/src/views/ForgotPassword.tsx +18 -18
  246. package/src/views/FormEditor.tsx +75 -75
  247. package/src/views/FormSubmissions.tsx +76 -76
  248. package/src/views/Forms.tsx +27 -27
  249. package/src/views/Login.tsx +65 -25
  250. package/src/views/MediaBrowser.tsx +127 -127
  251. package/src/views/PageEditor.tsx +25 -25
  252. package/src/views/Pages.tsx +59 -59
  253. package/src/views/PostEditor.tsx +37 -37
  254. package/src/views/Posts.tsx +48 -48
  255. package/src/views/Redirects.tsx +21 -21
  256. package/src/views/ResetPassword.tsx +28 -28
  257. package/src/views/SEO.tsx +144 -144
  258. package/src/views/ScriptTagEditor.tsx +24 -24
  259. package/src/views/ScriptTags.tsx +10 -10
  260. package/src/views/Settings.tsx +88 -80
  261. package/src/views/SetupWizard.tsx +28 -28
  262. package/src/views/Users.tsx +20 -20
  263. package/src/views/page-builder/AIBlockAssist.tsx +1 -1
  264. package/src/views/page-builder/AIGenerateDialog.tsx +63 -63
  265. package/src/views/page-builder/BlockEditor.tsx +26 -26
  266. package/src/views/page-builder/BlockPicker.tsx +22 -22
  267. package/src/views/page-builder/BottomBar.tsx +8 -8
  268. package/src/views/page-builder/BuilderToolbar.tsx +17 -17
  269. package/src/views/page-builder/ContextPanel.tsx +3 -3
  270. package/src/views/page-builder/DesignScore.tsx +21 -21
  271. package/src/views/page-builder/NodeSettings.tsx +27 -27
  272. package/src/views/page-builder/PageBuilder.tsx +11 -11
  273. package/src/views/page-builder/PageSettings.tsx +4 -4
  274. package/src/views/page-builder/PageTemplates.tsx +18 -18
  275. package/src/views/page-builder/SEOPanel.tsx +53 -53
  276. package/src/views/page-builder/SavedSections.tsx +37 -37
  277. package/src/views/page-builder/TemplatePicker.tsx +17 -17
  278. package/src/views/page-builder/block-renderers/CTAPreview.tsx +13 -13
  279. package/src/views/page-builder/block-renderers/CardsPreview.tsx +5 -5
  280. package/src/views/page-builder/block-renderers/CodePreview.tsx +6 -6
  281. package/src/views/page-builder/block-renderers/FAQPreview.tsx +13 -13
  282. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +3 -3
  283. package/src/views/page-builder/block-renderers/FormPreview.tsx +20 -20
  284. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +8 -8
  285. package/src/views/page-builder/block-renderers/HeroPreview.tsx +16 -16
  286. package/src/views/page-builder/block-renderers/ImagePreview.tsx +4 -4
  287. package/src/views/page-builder/block-renderers/TextPreview.tsx +14 -14
  288. package/src/views/page-builder/block-renderers/VideoPreview.tsx +12 -12
  289. package/src/views/page-builder/canvas/BlockRenderer.tsx +4 -4
  290. package/src/views/page-builder/canvas/BuilderCanvas.tsx +6 -6
  291. package/src/views/page-builder/canvas/ColumnRenderer.tsx +3 -3
  292. package/src/views/page-builder/canvas/ContainerRenderer.tsx +2 -2
  293. package/src/views/page-builder/canvas/RowRenderer.tsx +2 -2
  294. package/src/views/page-builder/canvas/SectionRenderer.tsx +2 -2
@@ -438,10 +438,10 @@ function Section({
438
438
  badge?: React.ReactNode
439
439
  }) {
440
440
  return (
441
- <div className="border border-[var(--border)] rounded-lg overflow-hidden">
441
+ <div className="overflow-hidden rounded-lg border border-[var(--border)]">
442
442
  <button
443
443
  onClick={() => onToggle(id)}
444
- className="flex items-center justify-between w-full px-4 py-2.5 text-left text-sm font-medium text-[var(--foreground)] hover:bg-[var(--muted)] transition-colors"
444
+ className="flex w-full items-center justify-between px-4 py-2.5 text-left text-sm font-medium text-[var(--foreground)] transition-colors hover:bg-[var(--muted)]"
445
445
  >
446
446
  <span className="flex items-center gap-2">
447
447
  {icon}
@@ -454,7 +454,7 @@ function Section({
454
454
  <ChevronDown className="h-4 w-4 text-[var(--muted-foreground)]" />
455
455
  )}
456
456
  </button>
457
- {expanded && <div className="px-4 pb-4 pt-1">{children}</div>}
457
+ {expanded && <div className="px-4 pt-1 pb-4">{children}</div>}
458
458
  </div>
459
459
  )
460
460
  }
@@ -472,9 +472,9 @@ function ToggleSwitch({
472
472
  }) {
473
473
  return (
474
474
  <div className="flex items-center justify-between gap-3">
475
- <div className="flex-1 min-w-0">
475
+ <div className="min-w-0 flex-1">
476
476
  <label className="text-sm font-medium text-[var(--foreground)]">{label}</label>
477
- <p className="text-xs text-[var(--muted-foreground)] mt-0.5">{description}</p>
477
+ <p className="mt-0.5 text-xs text-[var(--muted-foreground)]">{description}</p>
478
478
  </div>
479
479
  <button
480
480
  type="button"
@@ -484,7 +484,7 @@ function ToggleSwitch({
484
484
  className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full transition-colors ${checked ? 'bg-[var(--primary)]' : 'bg-[var(--muted)]'}`}
485
485
  >
486
486
  <span
487
- className={`pointer-events-none block h-5 w-5 rounded-full bg-white shadow-sm transition-transform mt-0.5 ${checked ? 'translate-x-[22px]' : 'translate-x-0.5'}`}
487
+ className={`pointer-events-none mt-0.5 block h-5 w-5 rounded-full bg-white shadow-sm transition-transform ${checked ? 'translate-x-[22px]' : 'translate-x-0.5'}`}
488
488
  />
489
489
  </button>
490
490
  </div>
@@ -510,18 +510,18 @@ function InputField({
510
510
  }) {
511
511
  return (
512
512
  <div>
513
- <label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">
513
+ <label className="mb-1 block text-xs font-medium text-[var(--muted-foreground)]">
514
514
  {label}
515
515
  </label>
516
516
  <input
517
517
  type={type}
518
518
  value={value}
519
519
  onChange={(e) => onChange(e.target.value)}
520
- className="w-full px-3 py-1.5 text-sm border border-[var(--border)] rounded-lg bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] placeholder:text-[var(--muted-foreground)]"
520
+ className="w-full rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-1.5 text-sm text-[var(--foreground)] placeholder:text-[var(--muted-foreground)] focus:ring-2 focus:ring-[var(--primary)] focus:outline-none"
521
521
  placeholder={placeholder}
522
522
  />
523
523
  {charCount !== undefined && charTarget && (
524
- <p className="text-xs mt-1 text-[var(--muted-foreground)]">
524
+ <p className="mt-1 text-xs text-[var(--muted-foreground)]">
525
525
  {charCount} chars {charTarget}
526
526
  </p>
527
527
  )}
@@ -548,18 +548,18 @@ function TextareaField({
548
548
  }) {
549
549
  return (
550
550
  <div>
551
- <label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">
551
+ <label className="mb-1 block text-xs font-medium text-[var(--muted-foreground)]">
552
552
  {label}
553
553
  </label>
554
554
  <textarea
555
555
  value={value}
556
556
  onChange={(e) => onChange(e.target.value)}
557
557
  rows={rows}
558
- className="w-full px-3 py-1.5 text-sm border border-[var(--border)] rounded-lg bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] resize-none placeholder:text-[var(--muted-foreground)]"
558
+ className="w-full resize-none rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-1.5 text-sm text-[var(--foreground)] placeholder:text-[var(--muted-foreground)] focus:ring-2 focus:ring-[var(--primary)] focus:outline-none"
559
559
  placeholder={placeholder}
560
560
  />
561
561
  {charCount !== undefined && charTarget && (
562
- <p className="text-xs mt-1 text-[var(--muted-foreground)]">
562
+ <p className="mt-1 text-xs text-[var(--muted-foreground)]">
563
563
  {charCount} chars {charTarget}
564
564
  </p>
565
565
  )}
@@ -622,24 +622,24 @@ export function SEOPanel({
622
622
  return (
623
623
  <div className="rounded-lg border border-[var(--border)] bg-[var(--card)]">
624
624
  {/* Header */}
625
- <div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)]">
626
- <h3 className="font-semibold text-[var(--foreground)] flex items-center gap-2 text-sm">
627
- <Search className="w-4 h-4" />
625
+ <div className="flex items-center justify-between border-b border-[var(--border)] px-4 py-3">
626
+ <h3 className="flex items-center gap-2 text-sm font-semibold text-[var(--foreground)]">
627
+ <Search className="h-4 w-4" />
628
628
  SEO
629
629
  </h3>
630
630
  <button
631
631
  onClick={() => update({ metaTitle: title, ogTitle: '', ogDescription: '' })}
632
- className="text-xs text-[var(--primary)] hover:opacity-80 flex items-center gap-1"
632
+ className="flex items-center gap-1 text-xs text-[var(--primary)] hover:opacity-80"
633
633
  >
634
- <RefreshCw className="w-3 h-3" />
634
+ <RefreshCw className="h-3 w-3" />
635
635
  Reset
636
636
  </button>
637
637
  </div>
638
638
 
639
639
  {/* Score */}
640
- <div className="flex items-center gap-4 px-4 py-3 border-b border-[var(--border)]">
640
+ <div className="flex items-center gap-4 border-b border-[var(--border)] px-4 py-3">
641
641
  <ScoreRing score={score} />
642
- <div className="text-xs text-[var(--muted-foreground)] space-y-0.5">
642
+ <div className="space-y-0.5 text-xs text-[var(--muted-foreground)]">
643
643
  <div className="flex items-center gap-1.5">
644
644
  <CheckCircle2 className="h-3 w-3 text-green-500" /> {goodCount} passed
645
645
  </div>
@@ -653,7 +653,7 @@ export function SEOPanel({
653
653
  </div>
654
654
 
655
655
  {/* Sections */}
656
- <div className="p-3 space-y-2">
656
+ <div className="space-y-2 p-3">
657
657
  {/* SEO Analysis */}
658
658
  <Section
659
659
  id="analysis"
@@ -672,7 +672,7 @@ export function SEOPanel({
672
672
  <div key={c.id} className="flex items-start gap-2 py-1">
673
673
  <StatusDot status={c.status} />
674
674
  <div className="min-w-0">
675
- <p className="text-sm text-[var(--foreground)] leading-snug">{c.label}</p>
675
+ <p className="text-sm leading-snug text-[var(--foreground)]">{c.label}</p>
676
676
  <p className="text-xs text-[var(--muted-foreground)]">{c.detail}</p>
677
677
  </div>
678
678
  </div>
@@ -719,8 +719,8 @@ export function SEOPanel({
719
719
  <span
720
720
  className={
721
721
  readability.passiveEstimate > 15
722
- ? 'text-amber-500 font-medium'
723
- : 'text-green-500 font-medium'
722
+ ? 'font-medium text-amber-500'
723
+ : 'font-medium text-green-500'
724
724
  }
725
725
  >
726
726
  {readability.passiveEstimate}%
@@ -781,7 +781,7 @@ export function SEOPanel({
781
781
  <div>
782
782
  <label
783
783
  id="robots-policy-label"
784
- className="mb-1 block text-xs font-medium text-muted-foreground"
784
+ className="text-muted-foreground mb-1 block text-xs font-medium"
785
785
  >
786
786
  Robots Policy
787
787
  </label>
@@ -794,21 +794,21 @@ export function SEOPanel({
794
794
  >
795
795
  <Select.Trigger
796
796
  aria-labelledby="robots-policy-label"
797
- className="flex w-full items-center justify-between rounded-lg border border-border bg-background px-3 py-1.5 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
797
+ className="border-border bg-background text-foreground focus:ring-primary flex w-full items-center justify-between rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
798
798
  >
799
799
  <Select.Value />
800
800
  <Select.Icon>
801
- <ChevronDown className="h-4 w-4 text-muted-foreground" />
801
+ <ChevronDown className="text-muted-foreground h-4 w-4" />
802
802
  </Select.Icon>
803
803
  </Select.Trigger>
804
804
  <Select.Portal>
805
- <Select.Content className="z-50 overflow-hidden rounded-lg border border-border bg-card shadow-md">
805
+ <Select.Content className="border-border bg-card z-50 overflow-hidden rounded-lg border shadow-md">
806
806
  <Select.Viewport className="p-1">
807
807
  {ROBOTS_POLICY_OPTIONS.map((option) => (
808
808
  <Select.Item
809
809
  key={option.value}
810
810
  value={option.value}
811
- className="relative flex cursor-pointer select-none items-center rounded-md py-1.5 pl-8 pr-3 text-sm text-foreground outline-none hover:bg-muted focus:bg-muted"
811
+ className="text-foreground hover:bg-muted focus:bg-muted relative flex cursor-pointer items-center rounded-md py-1.5 pr-3 pl-8 text-sm outline-none select-none"
812
812
  >
813
813
  <Select.ItemIndicator className="absolute left-2 inline-flex items-center">
814
814
  <Check className="h-4 w-4" />
@@ -820,7 +820,7 @@ export function SEOPanel({
820
820
  </Select.Content>
821
821
  </Select.Portal>
822
822
  </Select.Root>
823
- <p className="mt-1 text-xs text-muted-foreground">
823
+ <p className="text-muted-foreground mt-1 text-xs">
824
824
  Use inheritance for most pages. Override only when a page needs different
825
825
  index/follow behavior.
826
826
  </p>
@@ -836,14 +836,14 @@ export function SEOPanel({
836
836
  expanded={expandedSections.includes('preview')}
837
837
  onToggle={toggleSection}
838
838
  >
839
- <div className="rounded-lg border border-[var(--border)] p-3 bg-[var(--background)]">
840
- <div className="text-sm text-blue-600 hover:underline cursor-pointer line-clamp-1">
839
+ <div className="rounded-lg border border-[var(--border)] bg-[var(--background)] p-3">
840
+ <div className="line-clamp-1 cursor-pointer text-sm text-blue-600 hover:underline">
841
841
  {metaTitle || title || 'Page Title'}
842
842
  </div>
843
- <div className="text-xs text-green-700 mt-1 truncate">
843
+ <div className="mt-1 truncate text-xs text-green-700">
844
844
  {siteUrl}/{slug}
845
845
  </div>
846
- <div className="text-sm text-[var(--muted-foreground)] mt-1 line-clamp-2">
846
+ <div className="mt-1 line-clamp-2 text-sm text-[var(--muted-foreground)]">
847
847
  {metaDesc ||
848
848
  'Add a meta description to see how this page will appear in search results.'}
849
849
  </div>
@@ -880,8 +880,8 @@ export function SEOPanel({
880
880
  type="url"
881
881
  />
882
882
 
883
- <div className="border-t border-[var(--border)] pt-3 mt-3">
884
- <p className="text-xs font-medium text-[var(--muted-foreground)] mb-2">
883
+ <div className="mt-3 border-t border-[var(--border)] pt-3">
884
+ <p className="mb-2 text-xs font-medium text-[var(--muted-foreground)]">
885
885
  Twitter / X Overrides
886
886
  </p>
887
887
  <div className="space-y-3">
@@ -910,31 +910,31 @@ export function SEOPanel({
910
910
 
911
911
  {/* Social preview card */}
912
912
  <div className="mt-3">
913
- <p className="text-xs font-medium text-[var(--muted-foreground)] mb-2">
913
+ <p className="mb-2 text-xs font-medium text-[var(--muted-foreground)]">
914
914
  Social Preview
915
915
  </p>
916
- <div className="rounded-lg border border-[var(--border)] overflow-hidden bg-[var(--muted)]">
916
+ <div className="overflow-hidden rounded-lg border border-[var(--border)] bg-[var(--muted)]">
917
917
  {seoData.ogImage ? (
918
- <div className="aspect-video bg-[var(--muted)] flex items-center justify-center overflow-hidden">
918
+ <div className="flex aspect-video items-center justify-center overflow-hidden bg-[var(--muted)]">
919
919
  <img
920
920
  src={seoData.ogImage}
921
921
  alt="OG preview"
922
- className="w-full h-full object-cover"
922
+ className="h-full w-full object-cover"
923
923
  />
924
924
  </div>
925
925
  ) : (
926
- <div className="aspect-video bg-[var(--muted)] flex items-center justify-center text-[var(--muted-foreground)] text-sm">
926
+ <div className="flex aspect-video items-center justify-center bg-[var(--muted)] text-sm text-[var(--muted-foreground)]">
927
927
  No OG image set
928
928
  </div>
929
929
  )}
930
930
  <div className="p-3">
931
- <div className="text-sm font-medium text-[var(--foreground)] line-clamp-1">
931
+ <div className="line-clamp-1 text-sm font-medium text-[var(--foreground)]">
932
932
  {displayTitle}
933
933
  </div>
934
- <div className="text-xs text-[var(--muted-foreground)] mt-1 line-clamp-2">
934
+ <div className="mt-1 line-clamp-2 text-xs text-[var(--muted-foreground)]">
935
935
  {displayDesc.slice(0, 100)}
936
936
  </div>
937
- <div className="text-xs text-[var(--muted-foreground)] mt-1 truncate">
937
+ <div className="mt-1 truncate text-xs text-[var(--muted-foreground)]">
938
938
  {siteUrl}
939
939
  </div>
940
940
  </div>
@@ -959,19 +959,19 @@ export function SEOPanel({
959
959
  onChange={(v) => update({ isCornerstone: v })}
960
960
  />
961
961
  {seoData.isCornerstone && (
962
- <div className="flex items-center gap-1.5 text-xs text-amber-600 bg-amber-50 rounded-md px-2.5 py-1.5">
962
+ <div className="flex items-center gap-1.5 rounded-md bg-amber-50 px-2.5 py-1.5 text-xs text-amber-600">
963
963
  <Star className="h-3.5 w-3.5" />
964
964
  Cornerstone content is held to stricter SEO standards
965
965
  </div>
966
966
  )}
967
967
  <div>
968
- <label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">
968
+ <label className="mb-1 block text-xs font-medium text-[var(--muted-foreground)]">
969
969
  Schema Type
970
970
  </label>
971
971
  <select
972
972
  value={seoData.schemaType ?? 'Article'}
973
973
  onChange={(e) => update({ schemaType: e.target.value })}
974
- className="w-full px-3 py-1.5 text-sm border border-[var(--border)] rounded-lg bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
974
+ className="w-full rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-1.5 text-sm text-[var(--foreground)] focus:ring-2 focus:ring-[var(--primary)] focus:outline-none"
975
975
  >
976
976
  {SCHEMA_TYPES.map((t) => (
977
977
  <option key={t} value={t}>
@@ -27,7 +27,7 @@ function ScoreBadge({ score }: { score: number }) {
27
27
  const bg = score >= 80 ? 'bg-green-50' : score >= 60 ? 'bg-amber-50' : 'bg-red-50'
28
28
  return (
29
29
  <span
30
- className={`inline-flex items-center justify-center w-9 h-9 rounded-full text-sm font-semibold ${color} ${bg}`}
30
+ className={`inline-flex h-9 w-9 items-center justify-center rounded-full text-sm font-semibold ${color} ${bg}`}
31
31
  >
32
32
  {score}
33
33
  </span>
@@ -53,10 +53,10 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
53
53
  const visible = topContent.slice(page * perPage, (page + 1) * perPage)
54
54
 
55
55
  return (
56
- <div className="bg-white rounded-lg border border-gray-200">
57
- <div className="p-4 border-b border-gray-200 flex items-center justify-between">
56
+ <div className="rounded-lg border border-gray-200 bg-white">
57
+ <div className="flex items-center justify-between border-b border-gray-200 p-4">
58
58
  <div className="flex items-center gap-2">
59
- <Search className="w-4 h-4 text-gray-500" />
59
+ <Search className="h-4 w-4 text-gray-500" />
60
60
  <h2 className="text-sm font-semibold text-gray-900">SEO Performance</h2>
61
61
  </div>
62
62
  {totalIssues > 0 && (
@@ -66,25 +66,25 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
66
66
  )}
67
67
  </div>
68
68
 
69
- <div className="grid grid-cols-1 lg:grid-cols-12 divide-y lg:divide-y-0 lg:divide-x divide-gray-200">
70
- <div className="lg:col-span-7 p-4">
71
- <h3 className="text-sm font-medium text-gray-700 mb-3">Top Performing Content</h3>
69
+ <div className="grid grid-cols-1 divide-y divide-gray-200 lg:grid-cols-12 lg:divide-x lg:divide-y-0">
70
+ <div className="p-4 lg:col-span-7">
71
+ <h3 className="mb-3 text-sm font-medium text-gray-700">Top Performing Content</h3>
72
72
  <div className="space-y-2">
73
73
  {visible.map((item) => (
74
74
  <div key={item.id} className="flex items-center justify-between py-2">
75
- <div className="flex-1 min-w-0 mr-3">
76
- <p className="text-sm font-medium text-gray-900 truncate">{item.title}</p>
75
+ <div className="mr-3 min-w-0 flex-1">
76
+ <p className="truncate text-sm font-medium text-gray-900">{item.title}</p>
77
77
  <p className="text-xs text-gray-500 capitalize">{item.collection}</p>
78
78
  </div>
79
79
  <ScoreBadge score={item.score} />
80
80
  </div>
81
81
  ))}
82
82
  {visible.length === 0 && (
83
- <p className="text-sm text-gray-400 py-4 text-center">No published content yet</p>
83
+ <p className="py-4 text-center text-sm text-gray-400">No published content yet</p>
84
84
  )}
85
85
  </div>
86
86
  {topContent.length > perPage && (
87
- <div className="flex items-center justify-between mt-3 pt-3 border-t border-gray-100 text-xs text-gray-500">
87
+ <div className="mt-3 flex items-center justify-between border-t border-gray-100 pt-3 text-xs text-gray-500">
88
88
  <span>
89
89
  Showing {page * perPage + 1}-{Math.min((page + 1) * perPage, topContent.length)} of{' '}
90
90
  {topContent.length}
@@ -93,9 +93,9 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
93
93
  <button
94
94
  onClick={() => setPage(Math.max(0, page - 1))}
95
95
  disabled={page === 0}
96
- className="p-1 rounded hover:bg-gray-100 disabled:opacity-30"
96
+ className="rounded p-1 hover:bg-gray-100 disabled:opacity-30"
97
97
  >
98
- <ChevronLeft className="w-3.5 h-3.5" />
98
+ <ChevronLeft className="h-3.5 w-3.5" />
99
99
  </button>
100
100
  <span>
101
101
  Page {page + 1} of {totalPages}
@@ -103,21 +103,21 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
103
103
  <button
104
104
  onClick={() => setPage(Math.min(totalPages - 1, page + 1))}
105
105
  disabled={page >= totalPages - 1}
106
- className="p-1 rounded hover:bg-gray-100 disabled:opacity-30"
106
+ className="rounded p-1 hover:bg-gray-100 disabled:opacity-30"
107
107
  >
108
- <ChevronRight className="w-3.5 h-3.5" />
108
+ <ChevronRight className="h-3.5 w-3.5" />
109
109
  </button>
110
110
  </div>
111
111
  </div>
112
112
  )}
113
113
  </div>
114
114
 
115
- <div className="lg:col-span-5 p-4">
116
- <h3 className="text-sm font-medium text-gray-700 mb-3">SEO Issues</h3>
115
+ <div className="p-4 lg:col-span-5">
116
+ <h3 className="mb-3 text-sm font-medium text-gray-700">SEO Issues</h3>
117
117
  <div className="space-y-3">
118
118
  <div className="flex items-center justify-between">
119
119
  <div className="flex items-center gap-2">
120
- <FileWarning className="w-4 h-4 text-red-400" />
120
+ <FileWarning className="h-4 w-4 text-red-400" />
121
121
  <span className="text-sm text-gray-700">Missing meta descriptions</span>
122
122
  </div>
123
123
  <span className="text-sm font-semibold text-gray-900">
@@ -126,7 +126,7 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
126
126
  </div>
127
127
  <div className="flex items-center justify-between">
128
128
  <div className="flex items-center gap-2">
129
- <LinkIcon className="w-4 h-4 text-red-400" />
129
+ <LinkIcon className="h-4 w-4 text-red-400" />
130
130
  <span className="text-sm text-gray-700">Broken internal links</span>
131
131
  </div>
132
132
  <span className="text-sm font-semibold text-gray-900">
@@ -135,7 +135,7 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
135
135
  </div>
136
136
  <div className="flex items-center justify-between">
137
137
  <div className="flex items-center gap-2">
138
- <ImageOff className="w-4 h-4 text-red-400" />
138
+ <ImageOff className="h-4 w-4 text-red-400" />
139
139
  <span className="text-sm text-gray-700">Missing alt text</span>
140
140
  </div>
141
141
  <span className="text-sm font-semibold text-gray-900">{issues.missingAltText}</span>
@@ -144,7 +144,7 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
144
144
  {totalIssues > 0 && (
145
145
  <button
146
146
  onClick={() => onNavigate?.('/seo')}
147
- className="mt-4 text-sm text-blue-600 hover:text-blue-700 font-medium"
147
+ className="mt-4 text-sm font-medium text-blue-600 hover:text-blue-700"
148
148
  >
149
149
  View All Issues
150
150
  </button>
@@ -0,0 +1,241 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Calendar, Clock, Loader2, X } from 'lucide-react'
5
+ import { toast } from 'sonner'
6
+ import { cmsApi } from '../lib/api.js'
7
+
8
+ export interface SchedulePublishDialogProps {
9
+ collectionSlug: string
10
+ documentId: string
11
+ /** Existing scheduled publish time, if any. */
12
+ scheduledAt?: string | null
13
+ /** Existing scheduled unpublish time, if any. */
14
+ scheduledUnpublishAt?: string | null
15
+ open: boolean
16
+ onClose: () => void
17
+ /** Called with the updated schedule fields after a successful save. */
18
+ onScheduled?: (next: {
19
+ status: string
20
+ scheduledAt: string | null
21
+ scheduledUnpublishAt: string | null
22
+ }) => void
23
+ }
24
+
25
+ /**
26
+ * Convert a Date or ISO string to the `YYYY-MM-DDTHH:mm` shape that the
27
+ * `<input type="datetime-local">` element expects, in the user's local
28
+ * timezone (the input is timezone-naive but we present + parse it as local).
29
+ */
30
+ function toLocalDateTimeInput(value: string | Date | null | undefined): string {
31
+ if (!value) return ''
32
+ const d = typeof value === 'string' ? new Date(value) : value
33
+ if (Number.isNaN(d.getTime())) return ''
34
+ const pad = (n: number) => String(n).padStart(2, '0')
35
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
36
+ }
37
+
38
+ /** Round "now + 1 hour" to the next quarter-hour for a friendly default. */
39
+ function defaultScheduleTime(): string {
40
+ const d = new Date(Date.now() + 60 * 60 * 1000)
41
+ d.setMinutes(Math.ceil(d.getMinutes() / 15) * 15, 0, 0)
42
+ return toLocalDateTimeInput(d)
43
+ }
44
+
45
+ export function SchedulePublishDialog({
46
+ collectionSlug,
47
+ documentId,
48
+ scheduledAt,
49
+ scheduledUnpublishAt,
50
+ open,
51
+ onClose,
52
+ onScheduled,
53
+ }: SchedulePublishDialogProps) {
54
+ const [publishAt, setPublishAt] = useState('')
55
+ const [unpublishAt, setUnpublishAt] = useState('')
56
+ const [includeUnpublish, setIncludeUnpublish] = useState(false)
57
+ const [saving, setSaving] = useState(false)
58
+ const [cancelling, setCancelling] = useState(false)
59
+
60
+ const hasExistingSchedule = Boolean(scheduledAt || scheduledUnpublishAt)
61
+
62
+ useEffect(() => {
63
+ if (!open) return
64
+ setPublishAt(scheduledAt ? toLocalDateTimeInput(scheduledAt) : defaultScheduleTime())
65
+ setUnpublishAt(scheduledUnpublishAt ? toLocalDateTimeInput(scheduledUnpublishAt) : '')
66
+ setIncludeUnpublish(Boolean(scheduledUnpublishAt))
67
+ }, [open, scheduledAt, scheduledUnpublishAt])
68
+
69
+ async function handleSave() {
70
+ if (!publishAt && !(includeUnpublish && unpublishAt)) {
71
+ toast.error('Pick a publish or unpublish time')
72
+ return
73
+ }
74
+
75
+ // datetime-local is parsed by the browser as local time; new Date() on
76
+ // that string respects the user's timezone and serialises to a UTC
77
+ // ISO string the server can compare against `Date.now()` cleanly.
78
+ const publishDate = publishAt ? new Date(publishAt) : null
79
+ const unpublishDate = includeUnpublish && unpublishAt ? new Date(unpublishAt) : null
80
+
81
+ if (publishDate && publishDate.getTime() <= Date.now()) {
82
+ toast.error('Publish time must be in the future')
83
+ return
84
+ }
85
+ if (unpublishDate && unpublishDate.getTime() <= Date.now()) {
86
+ toast.error('Unpublish time must be in the future')
87
+ return
88
+ }
89
+ if (publishDate && unpublishDate && unpublishDate <= publishDate) {
90
+ toast.error('Unpublish time must be after publish time')
91
+ return
92
+ }
93
+
94
+ setSaving(true)
95
+ const res = await cmsApi<any>(`/collections/${collectionSlug}/${documentId}/schedule`, {
96
+ method: 'POST',
97
+ body: JSON.stringify({
98
+ scheduledAt: publishDate ? publishDate.toISOString() : null,
99
+ scheduledUnpublishAt: unpublishDate ? unpublishDate.toISOString() : null,
100
+ }),
101
+ })
102
+ setSaving(false)
103
+ if (res.error) {
104
+ toast.error(res.error)
105
+ return
106
+ }
107
+ toast.success(publishDate ? 'Publish scheduled' : 'Unpublish scheduled')
108
+ onScheduled?.({
109
+ status: res.data?.status ?? 'SCHEDULED',
110
+ scheduledAt: res.data?.scheduledAt ?? (publishDate ? publishDate.toISOString() : null),
111
+ scheduledUnpublishAt:
112
+ res.data?.scheduledUnpublishAt ?? (unpublishDate ? unpublishDate.toISOString() : null),
113
+ })
114
+ onClose()
115
+ }
116
+
117
+ async function handleCancelSchedule() {
118
+ setCancelling(true)
119
+ const res = await cmsApi<any>(`/collections/${collectionSlug}/${documentId}/schedule`, {
120
+ method: 'DELETE',
121
+ })
122
+ setCancelling(false)
123
+ if (res.error) {
124
+ toast.error(res.error)
125
+ return
126
+ }
127
+ toast.success('Schedule cancelled')
128
+ onScheduled?.({
129
+ status: res.data?.status ?? 'DRAFT',
130
+ scheduledAt: null,
131
+ scheduledUnpublishAt: null,
132
+ })
133
+ onClose()
134
+ }
135
+
136
+ if (!open) return null
137
+
138
+ return (
139
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
140
+ <div className="fixed inset-0 bg-black/40" onClick={onClose} />
141
+ <div className="relative w-full max-w-md rounded-lg bg-white shadow-xl">
142
+ <div className="flex items-center justify-between border-b border-gray-200 px-4 py-3">
143
+ <div className="flex items-center gap-2">
144
+ <Calendar className="h-5 w-5 text-gray-600" />
145
+ <h2 className="text-lg font-semibold text-gray-900">Schedule publishing</h2>
146
+ </div>
147
+ <button
148
+ onClick={onClose}
149
+ className="rounded-lg p-1.5 transition-colors hover:bg-gray-100"
150
+ >
151
+ <X className="h-5 w-5 text-gray-500" />
152
+ </button>
153
+ </div>
154
+
155
+ <div className="space-y-4 px-4 py-4">
156
+ <div className="space-y-1.5">
157
+ <label className="text-sm font-medium text-gray-700">Publish at</label>
158
+ <div className="relative">
159
+ <Clock className="pointer-events-none absolute top-2.5 left-2.5 h-4 w-4 text-gray-400" />
160
+ <input
161
+ type="datetime-local"
162
+ value={publishAt}
163
+ onChange={(e) => setPublishAt(e.target.value)}
164
+ min={toLocalDateTimeInput(new Date(Date.now() + 60 * 1000))}
165
+ className="w-full rounded-md border border-gray-300 bg-white py-2 pr-3 pl-9 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
166
+ />
167
+ </div>
168
+ <p className="text-xs text-gray-500">
169
+ The document will move from DRAFT to PUBLISHED at this time (your local timezone).
170
+ </p>
171
+ </div>
172
+
173
+ <label className="flex items-center gap-2 text-sm text-gray-700">
174
+ <input
175
+ type="checkbox"
176
+ checked={includeUnpublish}
177
+ onChange={(e) => setIncludeUnpublish(e.target.checked)}
178
+ className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
179
+ />
180
+ Also schedule an unpublish
181
+ </label>
182
+
183
+ {includeUnpublish && (
184
+ <div className="space-y-1.5">
185
+ <label className="text-sm font-medium text-gray-700">Unpublish at</label>
186
+ <div className="relative">
187
+ <Clock className="pointer-events-none absolute top-2.5 left-2.5 h-4 w-4 text-gray-400" />
188
+ <input
189
+ type="datetime-local"
190
+ value={unpublishAt}
191
+ onChange={(e) => setUnpublishAt(e.target.value)}
192
+ min={publishAt || toLocalDateTimeInput(new Date(Date.now() + 60 * 1000))}
193
+ className="w-full rounded-md border border-gray-300 bg-white py-2 pr-3 pl-9 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
194
+ />
195
+ </div>
196
+ </div>
197
+ )}
198
+ </div>
199
+
200
+ <div className="flex items-center justify-between gap-2 border-t border-gray-200 px-4 py-3">
201
+ {hasExistingSchedule ? (
202
+ <button
203
+ onClick={handleCancelSchedule}
204
+ disabled={cancelling || saving}
205
+ className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-red-700 hover:bg-red-50 disabled:opacity-50"
206
+ >
207
+ {cancelling ? (
208
+ <Loader2 className="h-4 w-4 animate-spin" />
209
+ ) : (
210
+ <X className="h-4 w-4" />
211
+ )}
212
+ Cancel schedule
213
+ </button>
214
+ ) : (
215
+ <span />
216
+ )}
217
+ <div className="flex items-center gap-2">
218
+ <button
219
+ onClick={onClose}
220
+ className="rounded-md px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-100"
221
+ >
222
+ Close
223
+ </button>
224
+ <button
225
+ onClick={handleSave}
226
+ disabled={saving || cancelling}
227
+ className="inline-flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
228
+ >
229
+ {saving ? (
230
+ <Loader2 className="h-4 w-4 animate-spin" />
231
+ ) : (
232
+ <Calendar className="h-4 w-4" />
233
+ )}
234
+ {hasExistingSchedule ? 'Reschedule' : 'Schedule'}
235
+ </button>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ )
241
+ }