@actuate-media/cms-admin 0.65.0 → 0.67.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 (138) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +7 -4
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/components/inspector-pane.test.d.ts +2 -0
  6. package/dist/__tests__/components/inspector-pane.test.d.ts.map +1 -0
  7. package/dist/__tests__/components/inspector-pane.test.js +31 -0
  8. package/dist/__tests__/components/inspector-pane.test.js.map +1 -0
  9. package/dist/__tests__/lib/pages-tree.test.d.ts +2 -0
  10. package/dist/__tests__/lib/pages-tree.test.d.ts.map +1 -0
  11. package/dist/__tests__/lib/pages-tree.test.js +173 -0
  12. package/dist/__tests__/lib/pages-tree.test.js.map +1 -0
  13. package/dist/__tests__/views/dashboard.test.js +7 -3
  14. package/dist/__tests__/views/dashboard.test.js.map +1 -1
  15. package/dist/__tests__/views/media-seo-audit.render.test.d.ts +2 -0
  16. package/dist/__tests__/views/media-seo-audit.render.test.d.ts.map +1 -0
  17. package/dist/__tests__/views/media-seo-audit.render.test.js +102 -0
  18. package/dist/__tests__/views/media-seo-audit.render.test.js.map +1 -0
  19. package/dist/__tests__/views/onboarding-block.render.test.d.ts +2 -0
  20. package/dist/__tests__/views/onboarding-block.render.test.d.ts.map +1 -0
  21. package/dist/__tests__/views/onboarding-block.render.test.js +146 -0
  22. package/dist/__tests__/views/onboarding-block.render.test.js.map +1 -0
  23. package/dist/__tests__/views/pages-list-view.test.js +77 -1
  24. package/dist/__tests__/views/pages-list-view.test.js.map +1 -1
  25. package/dist/actuate-admin.css +1 -1
  26. package/dist/components/script-tags/ScriptTagEditorPane.d.ts +26 -0
  27. package/dist/components/script-tags/ScriptTagEditorPane.d.ts.map +1 -0
  28. package/dist/components/script-tags/ScriptTagEditorPane.js +160 -0
  29. package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -0
  30. package/dist/components/ui/InspectorPane.d.ts +51 -0
  31. package/dist/components/ui/InspectorPane.d.ts.map +1 -0
  32. package/dist/components/ui/InspectorPane.js +42 -0
  33. package/dist/components/ui/InspectorPane.js.map +1 -0
  34. package/dist/components/ui/RowActions.d.ts +19 -0
  35. package/dist/components/ui/RowActions.d.ts.map +1 -0
  36. package/dist/components/ui/RowActions.js +16 -0
  37. package/dist/components/ui/RowActions.js.map +1 -0
  38. package/dist/components/ui/index.d.ts +4 -0
  39. package/dist/components/ui/index.d.ts.map +1 -1
  40. package/dist/components/ui/index.js +2 -0
  41. package/dist/components/ui/index.js.map +1 -1
  42. package/dist/layout/Header.js +1 -1
  43. package/dist/layout/Header.js.map +1 -1
  44. package/dist/layout/Layout.js +8 -38
  45. package/dist/layout/Layout.js.map +1 -1
  46. package/dist/lib/page-editor-service.d.ts +11 -2
  47. package/dist/lib/page-editor-service.d.ts.map +1 -1
  48. package/dist/lib/page-editor-service.js +8 -2
  49. package/dist/lib/page-editor-service.js.map +1 -1
  50. package/dist/lib/pages-service.d.ts +9 -0
  51. package/dist/lib/pages-service.d.ts.map +1 -1
  52. package/dist/lib/pages-service.js +14 -0
  53. package/dist/lib/pages-service.js.map +1 -1
  54. package/dist/lib/pages-tree.d.ts +44 -0
  55. package/dist/lib/pages-tree.d.ts.map +1 -0
  56. package/dist/lib/pages-tree.js +106 -0
  57. package/dist/lib/pages-tree.js.map +1 -0
  58. package/dist/lib/posts-service.d.ts +11 -0
  59. package/dist/lib/posts-service.d.ts.map +1 -1
  60. package/dist/lib/posts-service.js +13 -0
  61. package/dist/lib/posts-service.js.map +1 -1
  62. package/dist/lib/seo-service.d.ts +3 -0
  63. package/dist/lib/seo-service.d.ts.map +1 -1
  64. package/dist/lib/seo-service.js.map +1 -1
  65. package/dist/views/Dashboard.d.ts.map +1 -1
  66. package/dist/views/Dashboard.js +40 -3
  67. package/dist/views/Dashboard.js.map +1 -1
  68. package/dist/views/MediaBrowser.d.ts.map +1 -1
  69. package/dist/views/MediaBrowser.js +55 -2
  70. package/dist/views/MediaBrowser.js.map +1 -1
  71. package/dist/views/MediaSeoAudit.d.ts +25 -0
  72. package/dist/views/MediaSeoAudit.d.ts.map +1 -0
  73. package/dist/views/MediaSeoAudit.js +97 -0
  74. package/dist/views/MediaSeoAudit.js.map +1 -0
  75. package/dist/views/Pages/PagesListView.d.ts.map +1 -1
  76. package/dist/views/Pages/PagesListView.js +145 -16
  77. package/dist/views/Pages/PagesListView.js.map +1 -1
  78. package/dist/views/Pages/PagesTreeTable.d.ts +35 -0
  79. package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -0
  80. package/dist/views/Pages/PagesTreeTable.js +28 -0
  81. package/dist/views/Pages/PagesTreeTable.js.map +1 -0
  82. package/dist/views/Posts/PostsListView.d.ts.map +1 -1
  83. package/dist/views/Posts/PostsListView.js +66 -9
  84. package/dist/views/Posts/PostsListView.js.map +1 -1
  85. package/dist/views/ScriptTags.d.ts +5 -1
  86. package/dist/views/ScriptTags.d.ts.map +1 -1
  87. package/dist/views/ScriptTags.js +32 -7
  88. package/dist/views/ScriptTags.js.map +1 -1
  89. package/dist/views/dashboard/OnboardingBlock.d.ts +29 -0
  90. package/dist/views/dashboard/OnboardingBlock.d.ts.map +1 -0
  91. package/dist/views/dashboard/OnboardingBlock.js +71 -0
  92. package/dist/views/dashboard/OnboardingBlock.js.map +1 -0
  93. package/dist/views/page-editor/PageSectionEditor.d.ts +7 -1
  94. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  95. package/dist/views/page-editor/PageSectionEditor.js +3 -3
  96. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  97. package/dist/views/page-editor/PageSettingsModal.d.ts +1 -1
  98. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
  99. package/dist/views/page-editor/PageSettingsModal.js +42 -3
  100. package/dist/views/page-editor/PageSettingsModal.js.map +1 -1
  101. package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
  102. package/dist/views/seo/TechnicalTab.js +15 -0
  103. package/dist/views/seo/TechnicalTab.js.map +1 -1
  104. package/package.json +3 -3
  105. package/src/AdminRoot.tsx +7 -3
  106. package/src/__tests__/components/inspector-pane.test.tsx +64 -0
  107. package/src/__tests__/lib/pages-tree.test.ts +197 -0
  108. package/src/__tests__/views/dashboard.test.tsx +7 -3
  109. package/src/__tests__/views/media-seo-audit.render.test.tsx +117 -0
  110. package/src/__tests__/views/onboarding-block.render.test.tsx +193 -0
  111. package/src/__tests__/views/pages-list-view.test.tsx +93 -1
  112. package/src/components/script-tags/ScriptTagEditorPane.tsx +474 -0
  113. package/src/components/ui/InspectorPane.tsx +129 -0
  114. package/src/components/ui/RowActions.tsx +50 -0
  115. package/src/components/ui/index.ts +12 -0
  116. package/src/layout/Header.tsx +2 -2
  117. package/src/layout/Layout.tsx +8 -80
  118. package/src/lib/page-editor-service.ts +16 -2
  119. package/src/lib/pages-service.ts +18 -0
  120. package/src/lib/pages-tree.ts +145 -0
  121. package/src/lib/posts-service.ts +26 -0
  122. package/src/lib/seo-service.ts +3 -0
  123. package/src/views/Dashboard.tsx +286 -214
  124. package/src/views/MediaBrowser.tsx +673 -512
  125. package/src/views/MediaSeoAudit.tsx +320 -0
  126. package/src/views/Pages/PagesListView.tsx +459 -185
  127. package/src/views/Pages/PagesTreeTable.tsx +339 -0
  128. package/src/views/Posts/PostsListView.tsx +135 -10
  129. package/src/views/ScriptTags.tsx +204 -129
  130. package/src/views/dashboard/OnboardingBlock.tsx +233 -0
  131. package/src/views/page-editor/PageSectionEditor.tsx +9 -2
  132. package/src/views/page-editor/PageSettingsModal.tsx +75 -2
  133. package/src/views/seo/TechnicalTab.tsx +15 -0
  134. package/dist/views/ScriptTagEditor.d.ts +0 -6
  135. package/dist/views/ScriptTagEditor.d.ts.map +0 -1
  136. package/dist/views/ScriptTagEditor.js +0 -111
  137. package/dist/views/ScriptTagEditor.js.map +0 -1
  138. package/src/views/ScriptTagEditor.tsx +0 -353
@@ -41,6 +41,8 @@ import {
41
41
  } from 'lucide-react'
42
42
  import { useApiData } from '../lib/useApiData.js'
43
43
  import { editPathForDoc } from '../lib/collection-routes.js'
44
+ import { OnboardingBlock, shouldShowOnboarding } from './dashboard/OnboardingBlock.js'
45
+ import type { OnboardingSignals } from './dashboard/OnboardingBlock.js'
44
46
 
45
47
  interface DashboardStats {
46
48
  totalDocuments: number
@@ -287,6 +289,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
287
289
 
288
290
  const collections = useMemo(() => resolveCollections(config), [config])
289
291
 
292
+ // Hero CTA targets (mirrors the quick-action "New Post" resolution).
293
+ const heroNewPostPath = useMemo(() => {
294
+ const postTypes = collections.filter((c) => c.type === 'post')
295
+ if (postTypes.length === 1) return `/${postTypes[0]!.slug}/new`
296
+ return postTypes.length > 1 ? '/posts/new' : '/posts/types/new'
297
+ }, [collections])
298
+ const siteUrl: string | null = config?.site?.url ?? config?.seo?.siteUrl ?? null
299
+
290
300
  // ── Stat cards ──────────────────────────────────────────────────────────
291
301
  const statCards: StatCardData[] = useMemo(() => {
292
302
  const counts = stats?.collectionCounts ?? {}
@@ -513,6 +523,40 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
513
523
 
514
524
  const totalIssues = contentHealth.issues.reduce((s, i) => s + i.count, 0)
515
525
 
526
+ // ── Onboarding (empty-site getting-started block) ───────────────────────
527
+ // Signals come exclusively from data already fetched above (/stats +
528
+ // /stats/api-delivery) — see OnboardingBlock.tsx for the per-step
529
+ // derivation rationale. When the site is effectively empty (zero
530
+ // posts+pages AND zero media) the onboarding block REPLACES the
531
+ // Recent Activity / Publishing Queue / Content Health grid.
532
+ const onboardingSignals: OnboardingSignals = useMemo(() => {
533
+ const counts = stats?.collectionCounts ?? {}
534
+ const contentCollections = collections.filter(
535
+ (c) => c.type === 'post' || c.type === 'page' || c.slug === 'posts' || c.slug === 'pages',
536
+ )
537
+ // No post/page collections configured → fall back to the total document
538
+ // count so a populated site never regresses to the onboarding view.
539
+ const postsAndPagesCount =
540
+ contentCollections.length > 0
541
+ ? contentCollections.reduce((sum, c) => sum + (counts[c.slug] ?? 0), 0)
542
+ : (stats?.totalDocuments ?? 0)
543
+ return {
544
+ postsAndPagesCount,
545
+ mediaCount: stats?.totalMedia ?? 0,
546
+ seoScore: stats?.avgSeoScore ?? 0,
547
+ userCount: stats?.totalUsers ?? 0,
548
+ hasApiTraffic: delivery?.hasData ?? false,
549
+ }
550
+ }, [stats, collections, delivery])
551
+
552
+ // Belt-and-braces: any recent activity means the site is not empty even
553
+ // if collection counts are missing/stale, so never hide it behind
554
+ // onboarding.
555
+ const showOnboarding =
556
+ stats !== null &&
557
+ (stats.recentDocuments?.length ?? 0) === 0 &&
558
+ shouldShowOnboarding(onboardingSignals)
559
+
516
560
  // ── Render ──────────────────────────────────────────────────────────────
517
561
 
518
562
  if (loading && !stats) {
@@ -561,24 +605,46 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
561
605
  )}
562
606
 
563
607
  {/* Hero greeting ─────────────────────────────────────────────────
564
- Primary "+ New Post" and "View Site" actions live in the top bar
565
- (Header.tsx page-action slot), not here, so the hero is purely
566
- informational and the dashboard's chrome-level affordances stay
567
- consistent across navigation. The on-page secondary action row
568
- below covers the same creation paths plus authoring shortcuts. */}
569
- <div className="flex flex-col gap-1 sm:gap-0.5">
570
- <h1 className="text-foreground">
571
- {greeting}, {userName} <span aria-hidden>👋</span>
572
- </h1>
573
- <p className="text-muted-foreground text-sm">
574
- {dateStr}
575
- {totalIssues > 0 && (
576
- <span className="hidden sm:inline">
577
- {' · '}
578
- {totalIssues} content item{totalIssues === 1 ? '' : 's'} need attention
579
- </span>
580
- )}
581
- </p>
608
+ Per the design, the primary "+ New Post" and "View Site" actions
609
+ sit on the right of the hero (`.dash-hdr` → `.dh-r`), not in the
610
+ top bar. The quick-action row below keeps the wider authoring
611
+ shortcuts. */}
612
+ <div className="flex flex-wrap items-start justify-between gap-3">
613
+ <div className="flex flex-col gap-1 sm:gap-0.5">
614
+ <h1 className="text-foreground">
615
+ {greeting}, {userName} <span aria-hidden>👋</span>
616
+ </h1>
617
+ <p className="text-muted-foreground text-sm">
618
+ {dateStr}
619
+ {totalIssues > 0 && (
620
+ <span className="hidden sm:inline">
621
+ {' · '}
622
+ {totalIssues} content item{totalIssues === 1 ? '' : 's'} need attention
623
+ </span>
624
+ )}
625
+ </p>
626
+ </div>
627
+ <div className="flex items-center gap-2">
628
+ <button
629
+ type="button"
630
+ onClick={() => nav(heroNewPostPath)}
631
+ className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-sm font-medium shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
632
+ >
633
+ <Plus className="h-3.5 w-3.5" aria-hidden />
634
+ New Post
635
+ </button>
636
+ {siteUrl ? (
637
+ <a
638
+ href={siteUrl}
639
+ target="_blank"
640
+ rel="noopener noreferrer"
641
+ className="border-border bg-card text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors"
642
+ >
643
+ <ExternalLink className="h-3.5 w-3.5" aria-hidden />
644
+ View Site
645
+ </a>
646
+ ) : null}
647
+ </div>
582
648
  </div>
583
649
 
584
650
  {/* Quick actions ──────────────────────────────────────────────── */}
@@ -641,124 +707,71 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
641
707
  })}
642
708
  </div>
643
709
 
644
- {/* Main grid ──────────────────────────────────────────────────── */}
645
- <div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_320px]">
646
- {/* Recent Activity ───────────────────────────────────────── */}
647
- <section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
648
- <header className="border-border flex items-center justify-between border-b px-4 py-3">
649
- <div className="min-w-0">
650
- <h2 className="text-foreground text-sm font-medium">Recent Activity</h2>
651
- <p className="text-muted-foreground mt-0.5 text-xs">Last 7 days across all content</p>
652
- </div>
653
- {(stats?.recentDocuments?.length ?? 0) > 8 && (
654
- <button
655
- className="text-brand text-xs font-medium hover:underline"
656
- onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
657
- >
658
- {activityLimit >= 20 ? 'Show less' : 'Show more'}
659
- </button>
660
- )}
661
- </header>
662
- {activity.length === 0 ? (
663
- <EmptyState
664
- icon={Activity}
665
- title="No activity yet"
666
- subtitle="Content changes will appear here as your team works."
667
- />
668
- ) : (
669
- <ul role="list" className="divide-border divide-y">
670
- {activity.map((it) => (
671
- <li key={it.id}>
672
- <button
673
- type="button"
674
- onClick={() => nav(it.editPath)}
675
- className="hover:bg-accent/50 focus-visible:bg-accent/50 w-full cursor-pointer px-4 py-3 text-left transition-colors"
676
- >
677
- <div className="flex min-w-0 items-start gap-3">
678
- <div
679
- className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-medium text-white"
680
- style={{ background: it.avatar.color }}
681
- aria-hidden
682
- >
683
- {it.avatar.initials}
684
- </div>
685
- <div className="min-w-0 flex-1">
686
- <p className="text-foreground truncate text-sm leading-snug">
687
- <span className="font-medium">
688
- &ldquo;{it.title || 'Untitled'}&rdquo;
689
- </span>{' '}
690
- <span className="text-muted-foreground">— {it.typeLabel}</span>
691
- </p>
692
- <div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
693
- <span
694
- className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
695
- >
696
- {it.statusInfo.label}
697
- </span>
698
- <span className="max-w-[120px] truncate">{it.author}</span>
699
- <span aria-hidden>·</span>
700
- <span>{it.relTime}</span>
701
- </div>
702
- </div>
703
- </div>
704
- </button>
705
- </li>
706
- ))}
707
- </ul>
708
- )}
709
- </section>
710
-
711
- {/* Right column ─────────────────────────────────────────── */}
712
- <aside className="flex min-w-0 flex-col gap-4">
713
- {/* Publishing Queue */}
710
+ {/* Onboarding (empty site) or the main grid ──────────────────── */}
711
+ {showOnboarding ? (
712
+ <OnboardingBlock signals={onboardingSignals} onNavigate={onNavigate} />
713
+ ) : (
714
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_320px]">
715
+ {/* Recent Activity ───────────────────────────────────────── */}
714
716
  <section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
715
717
  <header className="border-border flex items-center justify-between border-b px-4 py-3">
716
718
  <div className="min-w-0">
717
- <h2 className="text-foreground text-sm font-medium">Publishing Queue</h2>
719
+ <h2 className="text-foreground text-sm font-medium">Recent Activity</h2>
718
720
  <p className="text-muted-foreground mt-0.5 text-xs">
719
- {publishQueue.length} item{publishQueue.length === 1 ? '' : 's'} scheduled
721
+ Last 7 days across all content
720
722
  </p>
721
723
  </div>
722
- <button
723
- className="text-brand text-xs font-medium hover:underline disabled:cursor-default disabled:no-underline disabled:opacity-50"
724
- disabled={publishQueue.length === 0}
725
- onClick={() => {
726
- // We don't have a dedicated "scheduled" admin page, so deep-
727
- // link to the first scheduled item that's where authors
728
- // typically need to land to reschedule or cancel.
729
- const first = publishQueue[0]
730
- if (first) nav(first.editPath)
731
- }}
732
- >
733
- Manage
734
- </button>
724
+ {(stats?.recentDocuments?.length ?? 0) > 8 && (
725
+ <button
726
+ className="text-brand text-xs font-medium hover:underline"
727
+ onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
728
+ >
729
+ {activityLimit >= 20 ? 'Show less' : 'Show more'}
730
+ </button>
731
+ )}
735
732
  </header>
736
- {publishQueue.length === 0 ? (
733
+ {activity.length === 0 ? (
737
734
  <EmptyState
738
- icon={Clock}
739
- title="No scheduled content"
740
- subtitle="Scheduled posts and pages appear here."
741
- compact
735
+ icon={Activity}
736
+ title="No activity yet"
737
+ subtitle="Content changes will appear here as your team works."
742
738
  />
743
739
  ) : (
744
740
  <ul role="list" className="divide-border divide-y">
745
- {publishQueue.map((q) => (
746
- <li key={q.id}>
741
+ {activity.map((it) => (
742
+ <li key={it.id}>
747
743
  <button
748
744
  type="button"
749
- onClick={() => nav(q.editPath)}
750
- className="hover:bg-accent/50 focus-visible:bg-accent/50 flex w-full min-w-0 cursor-pointer items-center gap-2.5 px-4 py-2.5 text-left transition-colors"
745
+ onClick={() => nav(it.editPath)}
746
+ className="hover:bg-accent/50 focus-visible:bg-accent/50 w-full cursor-pointer px-4 py-3 text-left transition-colors"
751
747
  >
752
- <span className="bg-warning h-2 w-2 shrink-0 rounded-full" aria-hidden />
753
- <div className="min-w-0 flex-1">
754
- <p className="text-foreground truncate text-sm font-medium">{q.title}</p>
755
- <p className="text-muted-foreground truncate text-[11px]">
756
- {q.date} · {q.author}
757
- </p>
748
+ <div className="flex min-w-0 items-start gap-3">
749
+ <div
750
+ className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-medium text-white"
751
+ style={{ background: it.avatar.color }}
752
+ aria-hidden
753
+ >
754
+ {it.avatar.initials}
755
+ </div>
756
+ <div className="min-w-0 flex-1">
757
+ <p className="text-foreground truncate text-sm leading-snug">
758
+ <span className="font-medium">
759
+ &ldquo;{it.title || 'Untitled'}&rdquo;
760
+ </span>{' '}
761
+ <span className="text-muted-foreground">— {it.typeLabel}</span>
762
+ </p>
763
+ <div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
764
+ <span
765
+ className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
766
+ >
767
+ {it.statusInfo.label}
768
+ </span>
769
+ <span className="max-w-[120px] truncate">{it.author}</span>
770
+ <span aria-hidden>·</span>
771
+ <span>{it.relTime}</span>
772
+ </div>
773
+ </div>
758
774
  </div>
759
- <span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
760
- {q.type}
761
- </span>
762
775
  </button>
763
776
  </li>
764
777
  ))}
@@ -766,103 +779,162 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
766
779
  )}
767
780
  </section>
768
781
 
769
- {/* Content Health */}
770
- <section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
771
- <header className="border-border flex items-center justify-between border-b px-4 py-3">
772
- <div className="min-w-0">
773
- <h2 className="text-foreground text-sm font-medium">Content Health</h2>
774
- <p className="text-muted-foreground mt-0.5 text-xs">SEO &amp; quality issues</p>
775
- </div>
776
- <button
777
- className="text-brand text-xs font-medium hover:underline"
778
- onClick={() => nav('/seo')}
779
- >
780
- Fix issues
781
- </button>
782
- </header>
783
- <div className="px-4 pt-3.5 pb-2.5">
784
- <div className="mb-1.5 flex items-baseline justify-between">
785
- <p className="text-foreground text-2xl leading-none font-medium tracking-tight">
786
- {contentHealth.score > 0 ? contentHealth.score : '—'}
787
- {contentHealth.score > 0 && (
788
- <span className="text-muted-foreground ml-0.5 text-sm font-normal">/100</span>
789
- )}
790
- </p>
791
- <span
792
- className={`text-xs font-medium ${
793
- contentHealth.tone === 'ok'
794
- ? 'text-success'
795
- : contentHealth.tone === 'warn'
796
- ? 'text-warning'
797
- : contentHealth.tone === 'err'
798
- ? 'text-destructive dark:text-red-400'
799
- : 'text-muted-foreground'
800
- }`}
782
+ {/* Right column ─────────────────────────────────────────── */}
783
+ <aside className="flex min-w-0 flex-col gap-4">
784
+ {/* Publishing Queue */}
785
+ <section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
786
+ <header className="border-border flex items-center justify-between border-b px-4 py-3">
787
+ <div className="min-w-0">
788
+ <h2 className="text-foreground text-sm font-medium">Publishing Queue</h2>
789
+ <p className="text-muted-foreground mt-0.5 text-xs">
790
+ {publishQueue.length} item{publishQueue.length === 1 ? '' : 's'} scheduled
791
+ </p>
792
+ </div>
793
+ <button
794
+ className="text-brand text-xs font-medium hover:underline disabled:cursor-default disabled:no-underline disabled:opacity-50"
795
+ disabled={publishQueue.length === 0}
796
+ onClick={() => {
797
+ // We don't have a dedicated "scheduled" admin page, so deep-
798
+ // link to the first scheduled item — that's where authors
799
+ // typically need to land to reschedule or cancel.
800
+ const first = publishQueue[0]
801
+ if (first) nav(first.editPath)
802
+ }}
801
803
  >
802
- {contentHealth.label}
803
- </span>
804
- </div>
805
- <div className="bg-muted h-1.5 overflow-hidden rounded-full">
806
- <div
807
- className={`h-full rounded-full transition-all ${
808
- contentHealth.tone === 'ok'
809
- ? 'bg-success'
810
- : contentHealth.tone === 'warn'
811
- ? 'bg-warning'
812
- : contentHealth.tone === 'err'
813
- ? 'bg-destructive'
814
- : 'bg-muted-foreground/40'
815
- }`}
816
- style={{ width: `${Math.max(0, Math.min(100, contentHealth.score))}%` }}
804
+ Manage
805
+ </button>
806
+ </header>
807
+ {publishQueue.length === 0 ? (
808
+ <EmptyState
809
+ icon={Clock}
810
+ title="No scheduled content"
811
+ subtitle="Scheduled posts and pages appear here."
812
+ compact
817
813
  />
818
- </div>
819
- </div>
820
- {contentHealth.issues.length === 0 ? (
821
- <div className="text-muted-foreground px-4 pt-1 pb-4 text-xs">
822
- No outstanding issues. Nice work.
823
- </div>
824
- ) : (
825
- <ul role="list" className="divide-border divide-y">
826
- {contentHealth.issues.map((iss, i) => (
827
- <li key={i}>
828
- <button
829
- type="button"
830
- onClick={() => nav('/seo')}
831
- className="hover:bg-accent/50 focus-visible:bg-accent/50 flex w-full items-center gap-2.5 px-4 py-2 text-left transition-colors"
832
- >
833
- <span
834
- className={`h-1.5 w-1.5 shrink-0 rounded-full ${
835
- iss.tone === 'err'
836
- ? 'bg-destructive'
837
- : iss.tone === 'warn'
838
- ? 'bg-warning'
839
- : 'bg-muted-foreground'
840
- }`}
841
- aria-hidden
842
- />
843
- <span className="text-muted-foreground flex-1 truncate text-xs">
844
- {iss.label}
845
- </span>
846
- <span
847
- className={`shrink-0 text-sm font-medium ${
848
- iss.tone === 'err'
814
+ ) : (
815
+ <ul role="list" className="divide-border divide-y">
816
+ {publishQueue.map((q) => (
817
+ <li key={q.id}>
818
+ <button
819
+ type="button"
820
+ onClick={() => nav(q.editPath)}
821
+ className="hover:bg-accent/50 focus-visible:bg-accent/50 flex w-full min-w-0 cursor-pointer items-center gap-2.5 px-4 py-2.5 text-left transition-colors"
822
+ >
823
+ <span className="bg-warning h-2 w-2 shrink-0 rounded-full" aria-hidden />
824
+ <div className="min-w-0 flex-1">
825
+ <p className="text-foreground truncate text-sm font-medium">{q.title}</p>
826
+ <p className="text-muted-foreground truncate text-[11px]">
827
+ {q.date} · {q.author}
828
+ </p>
829
+ </div>
830
+ <span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
831
+ {q.type}
832
+ </span>
833
+ </button>
834
+ </li>
835
+ ))}
836
+ </ul>
837
+ )}
838
+ </section>
839
+
840
+ {/* Content Health */}
841
+ <section className="bg-card border-border overflow-hidden rounded-xl border shadow-sm">
842
+ <header className="border-border flex items-center justify-between border-b px-4 py-3">
843
+ <div className="min-w-0">
844
+ <h2 className="text-foreground text-sm font-medium">Content Health</h2>
845
+ <p className="text-muted-foreground mt-0.5 text-xs">SEO &amp; quality issues</p>
846
+ </div>
847
+ <button
848
+ className="text-brand text-xs font-medium hover:underline"
849
+ onClick={() => nav('/seo')}
850
+ >
851
+ Fix issues
852
+ </button>
853
+ </header>
854
+ <div className="px-4 pt-3.5 pb-2.5">
855
+ <div className="mb-1.5 flex items-baseline justify-between">
856
+ <p className="text-foreground text-2xl leading-none font-medium tracking-tight">
857
+ {contentHealth.score > 0 ? contentHealth.score : '—'}
858
+ {contentHealth.score > 0 && (
859
+ <span className="text-muted-foreground ml-0.5 text-sm font-normal">/100</span>
860
+ )}
861
+ </p>
862
+ <span
863
+ className={`text-xs font-medium ${
864
+ contentHealth.tone === 'ok'
865
+ ? 'text-success'
866
+ : contentHealth.tone === 'warn'
867
+ ? 'text-warning'
868
+ : contentHealth.tone === 'err'
849
869
  ? 'text-destructive dark:text-red-400'
850
- : iss.tone === 'warn'
851
- ? 'text-warning'
852
- : 'text-muted-foreground'
853
- }`}
870
+ : 'text-muted-foreground'
871
+ }`}
872
+ >
873
+ {contentHealth.label}
874
+ </span>
875
+ </div>
876
+ <div className="bg-muted h-1.5 overflow-hidden rounded-full">
877
+ <div
878
+ className={`h-full rounded-full transition-all ${
879
+ contentHealth.tone === 'ok'
880
+ ? 'bg-success'
881
+ : contentHealth.tone === 'warn'
882
+ ? 'bg-warning'
883
+ : contentHealth.tone === 'err'
884
+ ? 'bg-destructive'
885
+ : 'bg-muted-foreground/40'
886
+ }`}
887
+ style={{ width: `${Math.max(0, Math.min(100, contentHealth.score))}%` }}
888
+ />
889
+ </div>
890
+ </div>
891
+ {contentHealth.issues.length === 0 ? (
892
+ <div className="text-muted-foreground px-4 pt-1 pb-4 text-xs">
893
+ No outstanding issues. Nice work.
894
+ </div>
895
+ ) : (
896
+ <ul role="list" className="divide-border divide-y">
897
+ {contentHealth.issues.map((iss, i) => (
898
+ <li key={i}>
899
+ <button
900
+ type="button"
901
+ onClick={() => nav('/seo')}
902
+ className="hover:bg-accent/50 focus-visible:bg-accent/50 flex w-full items-center gap-2.5 px-4 py-2 text-left transition-colors"
854
903
  >
855
- {iss.count}
856
- </span>
857
- <ChevronRight className="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
858
- </button>
859
- </li>
860
- ))}
861
- </ul>
862
- )}
863
- </section>
864
- </aside>
865
- </div>
904
+ <span
905
+ className={`h-1.5 w-1.5 shrink-0 rounded-full ${
906
+ iss.tone === 'err'
907
+ ? 'bg-destructive'
908
+ : iss.tone === 'warn'
909
+ ? 'bg-warning'
910
+ : 'bg-muted-foreground'
911
+ }`}
912
+ aria-hidden
913
+ />
914
+ <span className="text-muted-foreground flex-1 truncate text-xs">
915
+ {iss.label}
916
+ </span>
917
+ <span
918
+ className={`shrink-0 text-sm font-medium ${
919
+ iss.tone === 'err'
920
+ ? 'text-destructive dark:text-red-400'
921
+ : iss.tone === 'warn'
922
+ ? 'text-warning'
923
+ : 'text-muted-foreground'
924
+ }`}
925
+ >
926
+ {iss.count}
927
+ </span>
928
+ <ChevronRight className="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
929
+ </button>
930
+ </li>
931
+ ))}
932
+ </ul>
933
+ )}
934
+ </section>
935
+ </aside>
936
+ </div>
937
+ )}
866
938
 
867
939
  {/* Delivery API ──────────────────────────────────────────────────
868
940
  Headless content delivery metrics aggregated over the last