@actuate-media/cms-admin 0.16.0 → 0.17.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.
@@ -15,7 +15,7 @@
15
15
  * >= 1024 (desktop): stat cards 5-col, main grid is `1fr 320px`
16
16
  */
17
17
 
18
- import { useMemo, useState } from 'react'
18
+ import { useEffect, useMemo, useState } from 'react'
19
19
  import {
20
20
  FileText,
21
21
  File as FileIcon,
@@ -119,6 +119,24 @@ function collectionLabel(col: CollectionMeta, plural = true): string {
119
119
  return col.labels?.singular ?? fallback
120
120
  }
121
121
 
122
+ /**
123
+ * Resolve the correct editor route for a recent/queued document.
124
+ *
125
+ * Post-type documents MUST open in the section-based post editor
126
+ * (`/posts/:type/:id/edit`). Routing them through the generic
127
+ * `/:collection/:id` handler lands them in the legacy `DocumentEdit`
128
+ * view, which has no concept of the section model — opening + saving
129
+ * there silently strips a post's `sections` data. The canonical Pages
130
+ * collection opens in the full page editor (which wires up preview).
131
+ * Everything else falls through to the existing generic collection
132
+ * route, which already resolves correctly.
133
+ */
134
+ function editPathForDoc(col: CollectionMeta | undefined, collection: string, id: string): string {
135
+ if (col?.type === 'post') return `/posts/${collection}/${id}/edit`
136
+ if (collection === 'pages') return `/pages/${id}/edit`
137
+ return `/${collection}/${id}`
138
+ }
139
+
122
140
  function relativeTime(dateStr: string): string {
123
141
  const then = new Date(dateStr).getTime()
124
142
  const diff = Date.now() - then
@@ -199,33 +217,32 @@ function authorAvatar(name: string | null | undefined): { color: string; initial
199
217
  return { color, initials }
200
218
  }
201
219
 
220
+ /**
221
+ * Semantic state styling uses theme tokens (`success` / `warning` / `info`
222
+ * / `brand`) so the dashboard tracks the design system in both light and
223
+ * dark mode. Two deliberate exceptions remain as raw values:
224
+ * - Error states keep `text-red-*` / `bg-red-*`. The `--destructive`
225
+ * token's dark value is intentionally dark (low contrast as TEXT on a
226
+ * dark card), so tokenizing error text here would regress contrast.
227
+ * Promoting error to a token is a separate, admin-wide change.
228
+ * - `AVATAR_COLORS` are dynamic per-author hashes (allowed: truly
229
+ * dynamic values, applied via inline style).
230
+ */
202
231
  function statusBadge(status: string): {
203
232
  label: string
204
233
  cls: string
205
234
  } {
206
235
  switch (status) {
207
236
  case 'PUBLISHED':
208
- return {
209
- label: 'Published',
210
- cls: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300',
211
- }
237
+ return { label: 'Published', cls: 'bg-success/10 text-success' }
212
238
  case 'DRAFT':
213
- return {
214
- label: 'Draft',
215
- cls: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
216
- }
239
+ return { label: 'Draft', cls: 'bg-muted text-muted-foreground' }
217
240
  case 'SCHEDULED':
218
- return {
219
- label: 'Scheduled',
220
- cls: 'bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300',
221
- }
241
+ return { label: 'Scheduled', cls: 'bg-warning/10 text-warning' }
222
242
  case 'IN_REVIEW':
223
- return {
224
- label: 'In Review',
225
- cls: 'bg-blue-100 text-blue-800 dark:bg-blue-950/60 dark:text-blue-300',
226
- }
243
+ return { label: 'In Review', cls: 'bg-info/10 text-info' }
227
244
  default:
228
- return { label: status, cls: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300' }
245
+ return { label: status, cls: 'bg-muted text-muted-foreground' }
229
246
  }
230
247
  }
231
248
 
@@ -245,15 +262,44 @@ interface StatCardData {
245
262
 
246
263
  export function Dashboard({ config, session, onNavigate }: DashboardProps) {
247
264
  const nav = (path: string) => onNavigate?.(path)
248
- const { data: stats, loading, error, exhausted, refetch } = useApiData<DashboardStats>('/stats')
249
- const { data: health } = useApiData<HealthData>('/health')
250
- const { data: delivery, loading: deliveryLoading } =
251
- useApiData<DeliveryStats>('/stats/api-delivery')
252
- const { data: contentHealthData, loading: contentHealthLoading } =
253
- useApiData<ContentHealthData>('/stats/content-health')
254
-
255
- const greeting = useMemo(() => timeOfDayGreeting(), [])
256
- const dateStr = useMemo(() => todayDateString(), [])
265
+ // Cache responses for 30s so navigating away and back doesn't re-fire
266
+ // all four dashboard requests on every visit. First load is unaffected
267
+ // (cold cache) and each request still streams independently; `refetch`
268
+ // (the Retry button) bypasses the cache.
269
+ const DASH_CACHE_MS = 30_000
270
+ const {
271
+ data: stats,
272
+ loading,
273
+ error,
274
+ exhausted,
275
+ refetch,
276
+ } = useApiData<DashboardStats>('/stats', null, {
277
+ cacheMs: DASH_CACHE_MS,
278
+ })
279
+ const { data: health } = useApiData<HealthData>('/health', null, { cacheMs: DASH_CACHE_MS })
280
+ const { data: delivery, loading: deliveryLoading } = useApiData<DeliveryStats>(
281
+ '/stats/api-delivery',
282
+ null,
283
+ { cacheMs: DASH_CACHE_MS },
284
+ )
285
+ const { data: contentHealthData, loading: contentHealthLoading } = useApiData<ContentHealthData>(
286
+ '/stats/content-health',
287
+ null,
288
+ { cacheMs: DASH_CACHE_MS },
289
+ )
290
+
291
+ // A slow ticker (60s) so the greeting, calendar date, and "x minutes
292
+ // ago" relative times stay correct on a dashboard left open for hours
293
+ // — without it, a tab opened at 11:58am would read "Good morning" all
294
+ // afternoon and show stale activity timestamps.
295
+ const [now, setNow] = useState(() => Date.now())
296
+ useEffect(() => {
297
+ const id = setInterval(() => setNow(Date.now()), 60_000)
298
+ return () => clearInterval(id)
299
+ }, [])
300
+
301
+ const greeting = useMemo(() => timeOfDayGreeting(), [now])
302
+ const dateStr = useMemo(() => todayDateString(), [now])
257
303
  const userName = useMemo(() => firstNameForGreeting(session), [session])
258
304
 
259
305
  const collections = useMemo(() => resolveCollections(config), [config])
@@ -283,8 +329,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
283
329
  label: collectionLabel(primary[0]),
284
330
  value: String(counts[primary[0].slug] ?? 0),
285
331
  icon: primary[0].type === 'page' ? FileIcon : FileText,
286
- iconBg: 'bg-violet-100 dark:bg-violet-950/60',
287
- iconColor: 'text-violet-600 dark:text-violet-300',
332
+ iconBg: 'bg-chart-1/15',
333
+ iconColor: 'text-chart-1',
288
334
  href: `/${primary[0].slug}`,
289
335
  })
290
336
  } else {
@@ -292,8 +338,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
292
338
  label: 'Documents',
293
339
  value: String(stats?.totalDocuments ?? 0),
294
340
  icon: FileText,
295
- iconBg: 'bg-violet-100 dark:bg-violet-950/60',
296
- iconColor: 'text-violet-600 dark:text-violet-300',
341
+ iconBg: 'bg-chart-1/15',
342
+ iconColor: 'text-chart-1',
297
343
  })
298
344
  }
299
345
 
@@ -302,8 +348,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
302
348
  label: collectionLabel(primary[1]),
303
349
  value: String(counts[primary[1].slug] ?? 0),
304
350
  icon: primary[1].type === 'page' ? FileIcon : FileText,
305
- iconBg: 'bg-cyan-100 dark:bg-cyan-950/60',
306
- iconColor: 'text-cyan-600 dark:text-cyan-300',
351
+ iconBg: 'bg-chart-2/15',
352
+ iconColor: 'text-chart-2',
307
353
  href: `/${primary[1].slug}`,
308
354
  })
309
355
  }
@@ -316,8 +362,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
316
362
  ? `${stats.totalMedia} file${stats.totalMedia === 1 ? '' : 's'}`
317
363
  : undefined,
318
364
  icon: ImageIcon,
319
- iconBg: 'bg-emerald-100 dark:bg-emerald-950/60',
320
- iconColor: 'text-emerald-600 dark:text-emerald-300',
365
+ iconBg: 'bg-chart-3/15',
366
+ iconColor: 'text-chart-3',
321
367
  href: '/media',
322
368
  })
323
369
 
@@ -330,8 +376,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
330
376
  value: String(formsCount),
331
377
  hint: submissions > 0 ? `${submissions} response${submissions === 1 ? '' : 's'}` : undefined,
332
378
  icon: ClipboardList,
333
- iconBg: 'bg-amber-100 dark:bg-amber-950/60',
334
- iconColor: 'text-amber-600 dark:text-amber-300',
379
+ iconBg: 'bg-chart-4/15',
380
+ iconColor: 'text-chart-4',
335
381
  href: '/forms',
336
382
  })
337
383
 
@@ -343,8 +389,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
343
389
  hint: seo > 0 ? (seo >= 70 ? 'Good' : seo >= 40 ? 'Fair' : 'Needs work') : 'No content yet',
344
390
  hintUp: seo >= 70,
345
391
  icon: Search,
346
- iconBg: 'bg-indigo-100 dark:bg-indigo-950/60',
347
- iconColor: 'text-indigo-600 dark:text-indigo-300',
392
+ iconBg: 'bg-chart-5/15',
393
+ iconColor: 'text-chart-5',
348
394
  href: '/seo',
349
395
  })
350
396
 
@@ -396,9 +442,12 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
396
442
  relTime: relativeTime(d.updatedAt),
397
443
  avatar: authorAvatar(d.author),
398
444
  statusInfo: statusBadge(d.status),
445
+ editPath: editPathForDoc(col, d.collection, d.id),
399
446
  }
400
447
  })
401
- }, [stats, collections, activityLimit])
448
+ // `now` keeps the relative timestamps fresh on a long-open tab.
449
+ // eslint-disable-next-line react-hooks/exhaustive-deps
450
+ }, [stats, collections, activityLimit, now])
402
451
 
403
452
  // ── Publishing queue (scheduled docs only) ──────────────────────────────
404
453
  const publishQueue = useMemo(() => {
@@ -415,9 +464,11 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
415
464
  type: col ? collectionLabel(col, false) : d.collection,
416
465
  date: relativeTime(d.updatedAt),
417
466
  author: d.author,
467
+ editPath: editPathForDoc(col, d.collection, d.id),
418
468
  }
419
469
  })
420
- }, [stats, collections])
470
+ // eslint-disable-next-line react-hooks/exhaustive-deps
471
+ }, [stats, collections, now])
421
472
 
422
473
  // ── Content health (sourced from /stats/content-health) ─────────────────
423
474
  // Persisted by the nightly content-health cron. The four issue rows
@@ -484,7 +535,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
484
535
  if (loading && !stats) {
485
536
  return (
486
537
  <div className="flex h-64 items-center justify-center p-4 sm:p-6">
487
- <Loader2 className="h-6 w-6 animate-spin text-violet-600" />
538
+ <Loader2 className="text-brand h-6 w-6 animate-spin" />
488
539
  </div>
489
540
  )
490
541
  }
@@ -493,13 +544,11 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
493
544
  <div className="w-full space-y-5 p-4 sm:p-6 lg:px-8">
494
545
  {/* Health banners ────────────────────────────────────────────── */}
495
546
  {health && health.status === 'degraded' && (
496
- <div className="flex items-start gap-3 rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-900 dark:bg-blue-950/40">
497
- <Database className="mt-0.5 h-5 w-5 shrink-0 text-blue-600 dark:text-blue-400" />
547
+ <div className="border-info/30 bg-info/10 flex items-start gap-3 rounded-lg border p-3">
548
+ <Database className="text-info mt-0.5 h-5 w-5 shrink-0" />
498
549
  <div className="min-w-0 flex-1">
499
- <span className="text-sm font-medium text-blue-900 dark:text-blue-200">
500
- Database setup required
501
- </span>
502
- <p className="mt-0.5 text-xs text-blue-700 dark:text-blue-300">
550
+ <span className="text-info text-sm font-medium">Database setup required</span>
551
+ <p className="text-muted-foreground mt-0.5 text-xs">
503
552
  {!health.databaseConnected
504
553
  ? 'Cannot connect to the database. Check your DATABASE_URL environment variable.'
505
554
  : !health.secretConfigured
@@ -514,14 +563,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
514
563
  )}
515
564
 
516
565
  {error && exhausted && (
517
- <div className="flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-900 dark:bg-amber-950/40">
518
- <AlertTriangle className="h-5 w-5 shrink-0 text-amber-600 dark:text-amber-400" />
519
- <span className="flex-1 text-sm text-amber-800 dark:text-amber-200">
566
+ <div className="border-warning/30 bg-warning/10 flex items-center gap-3 rounded-lg border p-3">
567
+ <AlertTriangle className="text-warning h-5 w-5 shrink-0" />
568
+ <span className="text-warning flex-1 text-sm">
520
569
  Some dashboard data may be unavailable.
521
570
  </span>
522
571
  <button
523
572
  onClick={refetch}
524
- className="rounded-lg border border-amber-300 px-3 py-1 text-sm text-amber-700 transition-colors hover:bg-amber-100 dark:border-amber-700 dark:text-amber-200 dark:hover:bg-amber-900/40"
573
+ className="border-warning/40 text-warning hover:bg-warning/10 rounded-lg border px-3 py-1 text-sm transition-colors"
525
574
  >
526
575
  Retry
527
576
  </button>
@@ -555,7 +604,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
555
604
  <button
556
605
  key={a.label}
557
606
  onClick={a.onClick}
558
- className="border-border bg-card inline-flex shrink-0 items-center gap-1.5 rounded-lg border px-3.5 py-2 text-sm shadow-sm transition-colors hover:border-violet-400 hover:bg-violet-50 hover:text-violet-700 dark:hover:bg-violet-950/40 dark:hover:text-violet-300"
607
+ className="border-border bg-card hover:border-brand/50 hover:bg-brand/10 hover:text-brand inline-flex shrink-0 items-center gap-1.5 rounded-lg border px-3.5 py-2 text-sm shadow-sm transition-colors"
559
608
  >
560
609
  <a.icon className="h-3.5 w-3.5" />
561
610
  {a.label}
@@ -584,7 +633,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
584
633
  <p className="text-muted-foreground mt-1 text-xs">{card.label}</p>
585
634
  {card.hint && (
586
635
  <p
587
- className={`mt-1.5 text-[11px] ${card.hintUp ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground'}`}
636
+ className={`mt-1.5 text-[11px] ${card.hintUp ? 'text-success' : 'text-muted-foreground'}`}
588
637
  >
589
638
  {card.hint}
590
639
  </p>
@@ -618,12 +667,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
618
667
  <h2 className="text-foreground text-sm font-semibold">Recent Activity</h2>
619
668
  <p className="text-muted-foreground mt-0.5 text-xs">Last 7 days across all content</p>
620
669
  </div>
621
- <button
622
- className="text-xs font-medium text-violet-600 hover:underline dark:text-violet-400"
623
- onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
624
- >
625
- {activity.length >= 20 ? 'Show less' : 'View all'}
626
- </button>
670
+ {(stats?.recentDocuments?.length ?? 0) > 8 && (
671
+ <button
672
+ className="text-brand text-xs font-medium hover:underline"
673
+ onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
674
+ >
675
+ {activityLimit >= 20 ? 'Show less' : 'Show more'}
676
+ </button>
677
+ )}
627
678
  </header>
628
679
  {activity.length === 0 ? (
629
680
  <EmptyState
@@ -634,38 +685,40 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
634
685
  ) : (
635
686
  <ul role="list" className="divide-border divide-y">
636
687
  {activity.map((it) => (
637
- <li
638
- key={it.id}
639
- className="hover:bg-accent/50 cursor-pointer px-4 py-3 transition-colors"
640
- onClick={() => nav(`/${it.collection}/${it.id}`)}
641
- >
642
- <div className="flex min-w-0 items-start gap-3">
643
- <div
644
- className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white"
645
- style={{ background: it.avatar.color }}
646
- aria-hidden
647
- >
648
- {it.avatar.initials}
649
- </div>
650
- <div className="min-w-0 flex-1">
651
- <p className="text-foreground truncate text-sm leading-snug">
652
- <span className="font-semibold">
653
- &ldquo;{it.title || 'Untitled'}&rdquo;
654
- </span>{' '}
655
- <span className="text-muted-foreground">— {it.typeLabel}</span>
656
- </p>
657
- <div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
658
- <span
659
- className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
660
- >
661
- {it.statusInfo.label}
662
- </span>
663
- <span className="max-w-[120px] truncate">{it.author}</span>
664
- <span aria-hidden>·</span>
665
- <span>{it.relTime}</span>
688
+ <li key={it.id}>
689
+ <button
690
+ type="button"
691
+ onClick={() => nav(it.editPath)}
692
+ className="hover:bg-accent/50 focus-visible:bg-accent/50 w-full cursor-pointer px-4 py-3 text-left transition-colors"
693
+ >
694
+ <div className="flex min-w-0 items-start gap-3">
695
+ <div
696
+ className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-bold text-white"
697
+ style={{ background: it.avatar.color }}
698
+ aria-hidden
699
+ >
700
+ {it.avatar.initials}
701
+ </div>
702
+ <div className="min-w-0 flex-1">
703
+ <p className="text-foreground truncate text-sm leading-snug">
704
+ <span className="font-semibold">
705
+ &ldquo;{it.title || 'Untitled'}&rdquo;
706
+ </span>{' '}
707
+ <span className="text-muted-foreground">— {it.typeLabel}</span>
708
+ </p>
709
+ <div className="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-[11px]">
710
+ <span
711
+ className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${it.statusInfo.cls}`}
712
+ >
713
+ {it.statusInfo.label}
714
+ </span>
715
+ <span className="max-w-[120px] truncate">{it.author}</span>
716
+ <span aria-hidden>·</span>
717
+ <span>{it.relTime}</span>
718
+ </div>
666
719
  </div>
667
720
  </div>
668
- </div>
721
+ </button>
669
722
  </li>
670
723
  ))}
671
724
  </ul>
@@ -684,14 +737,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
684
737
  </p>
685
738
  </div>
686
739
  <button
687
- className="text-xs font-medium text-violet-600 hover:underline disabled:cursor-default disabled:no-underline disabled:opacity-50 dark:text-violet-400"
740
+ className="text-brand text-xs font-medium hover:underline disabled:cursor-default disabled:no-underline disabled:opacity-50"
688
741
  disabled={publishQueue.length === 0}
689
742
  onClick={() => {
690
743
  // We don't have a dedicated "scheduled" admin page, so deep-
691
744
  // link to the first scheduled item — that's where authors
692
745
  // typically need to land to reschedule or cancel.
693
746
  const first = publishQueue[0]
694
- if (first) nav(`/${first.collection}/${first.id}`)
747
+ if (first) nav(first.editPath)
695
748
  }}
696
749
  >
697
750
  Manage
@@ -707,21 +760,23 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
707
760
  ) : (
708
761
  <ul role="list" className="divide-border divide-y">
709
762
  {publishQueue.map((q) => (
710
- <li
711
- key={q.id}
712
- className="hover:bg-accent/50 flex min-w-0 cursor-pointer items-center gap-2.5 px-4 py-2.5 transition-colors"
713
- onClick={() => nav(`/${q.collection}/${q.id}`)}
714
- >
715
- <span className="h-2 w-2 shrink-0 rounded-full bg-amber-500" aria-hidden />
716
- <div className="min-w-0 flex-1">
717
- <p className="text-foreground truncate text-sm font-medium">{q.title}</p>
718
- <p className="text-muted-foreground truncate text-[11px]">
719
- {q.date} · {q.author}
720
- </p>
721
- </div>
722
- <span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
723
- {q.type}
724
- </span>
763
+ <li key={q.id}>
764
+ <button
765
+ type="button"
766
+ onClick={() => nav(q.editPath)}
767
+ 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"
768
+ >
769
+ <span className="bg-warning h-2 w-2 shrink-0 rounded-full" aria-hidden />
770
+ <div className="min-w-0 flex-1">
771
+ <p className="text-foreground truncate text-sm font-medium">{q.title}</p>
772
+ <p className="text-muted-foreground truncate text-[11px]">
773
+ {q.date} · {q.author}
774
+ </p>
775
+ </div>
776
+ <span className="border-border bg-background text-muted-foreground shrink-0 rounded border px-1.5 py-0.5 text-[10px] capitalize">
777
+ {q.type}
778
+ </span>
779
+ </button>
725
780
  </li>
726
781
  ))}
727
782
  </ul>
@@ -736,7 +791,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
736
791
  <p className="text-muted-foreground mt-0.5 text-xs">SEO &amp; quality issues</p>
737
792
  </div>
738
793
  <button
739
- className="text-xs font-medium text-violet-600 hover:underline dark:text-violet-400"
794
+ className="text-brand text-xs font-medium hover:underline"
740
795
  onClick={() => nav('/seo')}
741
796
  >
742
797
  Fix issues
@@ -753,9 +808,9 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
753
808
  <span
754
809
  className={`text-xs font-medium ${
755
810
  contentHealth.tone === 'ok'
756
- ? 'text-emerald-600 dark:text-emerald-400'
811
+ ? 'text-success'
757
812
  : contentHealth.tone === 'warn'
758
- ? 'text-amber-600 dark:text-amber-400'
813
+ ? 'text-warning'
759
814
  : contentHealth.tone === 'err'
760
815
  ? 'text-red-600 dark:text-red-400'
761
816
  : 'text-muted-foreground'
@@ -768,9 +823,9 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
768
823
  <div
769
824
  className={`h-full rounded-full transition-all ${
770
825
  contentHealth.tone === 'ok'
771
- ? 'bg-emerald-500'
826
+ ? 'bg-success'
772
827
  : contentHealth.tone === 'warn'
773
- ? 'bg-amber-500'
828
+ ? 'bg-warning'
774
829
  : contentHealth.tone === 'err'
775
830
  ? 'bg-red-500'
776
831
  : 'bg-muted-foreground/40'
@@ -786,36 +841,38 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
786
841
  ) : (
787
842
  <ul role="list" className="divide-border divide-y">
788
843
  {contentHealth.issues.map((iss, i) => (
789
- <li
790
- key={i}
791
- className="hover:bg-accent/50 flex cursor-pointer items-center gap-2.5 px-4 py-2 transition-colors"
792
- onClick={() => nav('/seo')}
793
- >
794
- <span
795
- className={`h-1.5 w-1.5 shrink-0 rounded-full ${
796
- iss.tone === 'err'
797
- ? 'bg-red-500'
798
- : iss.tone === 'warn'
799
- ? 'bg-amber-500'
800
- : 'bg-muted-foreground'
801
- }`}
802
- aria-hidden
803
- />
804
- <span className="text-muted-foreground flex-1 truncate text-xs">
805
- {iss.label}
806
- </span>
807
- <span
808
- className={`shrink-0 text-sm font-semibold ${
809
- iss.tone === 'err'
810
- ? 'text-red-600 dark:text-red-400'
811
- : iss.tone === 'warn'
812
- ? 'text-amber-600 dark:text-amber-400'
813
- : 'text-muted-foreground'
814
- }`}
844
+ <li key={i}>
845
+ <button
846
+ type="button"
847
+ onClick={() => nav('/seo')}
848
+ 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"
815
849
  >
816
- {iss.count}
817
- </span>
818
- <ChevronRight className="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
850
+ <span
851
+ className={`h-1.5 w-1.5 shrink-0 rounded-full ${
852
+ iss.tone === 'err'
853
+ ? 'bg-red-500'
854
+ : iss.tone === 'warn'
855
+ ? 'bg-warning'
856
+ : 'bg-muted-foreground'
857
+ }`}
858
+ aria-hidden
859
+ />
860
+ <span className="text-muted-foreground flex-1 truncate text-xs">
861
+ {iss.label}
862
+ </span>
863
+ <span
864
+ className={`shrink-0 text-sm font-semibold ${
865
+ iss.tone === 'err'
866
+ ? 'text-red-600 dark:text-red-400'
867
+ : iss.tone === 'warn'
868
+ ? 'text-warning'
869
+ : 'text-muted-foreground'
870
+ }`}
871
+ >
872
+ {iss.count}
873
+ </span>
874
+ <ChevronRight className="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
875
+ </button>
819
876
  </li>
820
877
  ))}
821
878
  </ul>
@@ -841,7 +898,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
841
898
  </p>
842
899
  </div>
843
900
  <button
844
- className="inline-flex items-center gap-1 text-xs font-medium text-violet-600 hover:underline dark:text-violet-400"
901
+ className="text-brand inline-flex items-center gap-1 text-xs font-medium hover:underline"
845
902
  onClick={() => nav('/api-keys')}
846
903
  >
847
904
  API docs <ExternalLink className="h-3 w-3" />
@@ -996,9 +1053,9 @@ function DeliveryTile({
996
1053
  }) {
997
1054
  const subTone =
998
1055
  tone === 'ok'
999
- ? 'text-emerald-600 dark:text-emerald-400'
1056
+ ? 'text-success'
1000
1057
  : tone === 'warn'
1001
- ? 'text-amber-600 dark:text-amber-400'
1058
+ ? 'text-warning'
1002
1059
  : tone === 'err'
1003
1060
  ? 'text-red-600 dark:text-red-400'
1004
1061
  : 'text-muted-foreground'