@actuate-media/cms-admin 0.16.1 → 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.
- package/dist/__tests__/lib/useApiData.test.js +28 -1
- package/dist/__tests__/lib/useApiData.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/lib/useApiData.d.ts +11 -0
- package/dist/lib/useApiData.d.ts.map +1 -1
- package/dist/lib/useApiData.js +41 -7
- package/dist/lib/useApiData.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +54 -48
- package/dist/views/Dashboard.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/lib/useApiData.test.ts +36 -1
- package/src/lib/useApiData.ts +52 -7
- package/src/styles/theme.css +37 -0
- package/src/views/Dashboard.tsx +77 -61
package/src/lib/useApiData.ts
CHANGED
|
@@ -6,6 +6,34 @@ export interface UseApiDataOptions {
|
|
|
6
6
|
maxRetries?: number
|
|
7
7
|
/** Base delay in ms for exponential backoff (default 1000). Actual delay = base * 2^attempt. */
|
|
8
8
|
retryBaseMs?: number
|
|
9
|
+
/**
|
|
10
|
+
* When set, successful responses are cached in-memory keyed by endpoint
|
|
11
|
+
* for this many ms and reused on the next mount, skipping the network
|
|
12
|
+
* (cache-first). Opt-in — omit to keep the default always-fetch
|
|
13
|
+
* behaviour. `refetch()` always bypasses the cache. Useful for views
|
|
14
|
+
* that remount frequently (e.g. dashboard navigation) so they don't
|
|
15
|
+
* re-issue the same requests on every visit.
|
|
16
|
+
*/
|
|
17
|
+
cacheMs?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Process-wide response cache for `cacheMs` consumers. Keyed by endpoint
|
|
22
|
+
* (note: the locale query param is appended downstream by `cmsApi`, so a
|
|
23
|
+
* locale switch is not reflected here — only enable `cacheMs` for
|
|
24
|
+
* locale-invariant endpoints like `/stats`).
|
|
25
|
+
*/
|
|
26
|
+
const responseCache = new Map<string, { data: unknown; at: number }>()
|
|
27
|
+
|
|
28
|
+
function readCache<T>(endpoint: string, cacheMs: number): { value: T | null } | null {
|
|
29
|
+
const hit = responseCache.get(endpoint)
|
|
30
|
+
if (hit && Date.now() - hit.at < cacheMs) return { value: hit.data as T | null }
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Test-only: drop all cached responses so cases don't bleed into each other. */
|
|
35
|
+
export function __clearApiDataCache(): void {
|
|
36
|
+
responseCache.clear()
|
|
9
37
|
}
|
|
10
38
|
|
|
11
39
|
export interface UseApiDataResult<T> {
|
|
@@ -22,9 +50,12 @@ export function useApiData<T>(
|
|
|
22
50
|
defaultValue: T | null = null,
|
|
23
51
|
options: UseApiDataOptions = {},
|
|
24
52
|
): UseApiDataResult<T> {
|
|
25
|
-
const { maxRetries = 3, retryBaseMs = 1000 } = options
|
|
26
|
-
|
|
27
|
-
|
|
53
|
+
const { maxRetries = 3, retryBaseMs = 1000, cacheMs } = options
|
|
54
|
+
// Read the cache synchronously for the initial state so a warm cache
|
|
55
|
+
// hit paints immediately instead of flashing a loading spinner.
|
|
56
|
+
const cachedInitial = endpoint && cacheMs ? readCache<T>(endpoint, cacheMs) : null
|
|
57
|
+
const [data, setData] = useState<T | null>(cachedInitial ? cachedInitial.value : defaultValue)
|
|
58
|
+
const [loading, setLoading] = useState(endpoint !== null && !cachedInitial)
|
|
28
59
|
const [error, setError] = useState<string | null>(null)
|
|
29
60
|
const [exhausted, setExhausted] = useState(false)
|
|
30
61
|
const abortRef = useRef<AbortController | null>(null)
|
|
@@ -32,12 +63,24 @@ export function useApiData<T>(
|
|
|
32
63
|
const retryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
33
64
|
|
|
34
65
|
const fetchData = useCallback(
|
|
35
|
-
async (isRetry = false) => {
|
|
66
|
+
async (isRetry = false, forceRefresh = false) => {
|
|
36
67
|
if (!endpoint) {
|
|
37
68
|
setLoading(false)
|
|
38
69
|
return
|
|
39
70
|
}
|
|
40
71
|
|
|
72
|
+
// Cache-first: a fresh cached response short-circuits the network.
|
|
73
|
+
// `refetch()` passes `forceRefresh` to bypass this.
|
|
74
|
+
if (!isRetry && !forceRefresh && cacheMs) {
|
|
75
|
+
const hit = readCache<T>(endpoint, cacheMs)
|
|
76
|
+
if (hit) {
|
|
77
|
+
setData(hit.value)
|
|
78
|
+
setError(null)
|
|
79
|
+
setLoading(false)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
41
84
|
abortRef.current?.abort()
|
|
42
85
|
const controller = new AbortController()
|
|
43
86
|
abortRef.current = controller
|
|
@@ -65,12 +108,14 @@ export function useApiData<T>(
|
|
|
65
108
|
setExhausted(true)
|
|
66
109
|
setLoading(false)
|
|
67
110
|
} else {
|
|
68
|
-
|
|
111
|
+
const value = res.data ?? null
|
|
112
|
+
setData(value)
|
|
69
113
|
setLoading(false)
|
|
70
114
|
retryCountRef.current = 0
|
|
115
|
+
if (cacheMs) responseCache.set(endpoint, { data: value, at: Date.now() })
|
|
71
116
|
}
|
|
72
117
|
},
|
|
73
|
-
[endpoint, maxRetries, retryBaseMs],
|
|
118
|
+
[endpoint, maxRetries, retryBaseMs, cacheMs],
|
|
74
119
|
)
|
|
75
120
|
|
|
76
121
|
const manualRefetch = useCallback(() => {
|
|
@@ -80,7 +125,7 @@ export function useApiData<T>(
|
|
|
80
125
|
}
|
|
81
126
|
retryCountRef.current = 0
|
|
82
127
|
setExhausted(false)
|
|
83
|
-
fetchData(false)
|
|
128
|
+
fetchData(false, true)
|
|
84
129
|
}, [fetchData])
|
|
85
130
|
|
|
86
131
|
useEffect(() => {
|
package/src/styles/theme.css
CHANGED
|
@@ -18,6 +18,27 @@
|
|
|
18
18
|
--accent-foreground: #030213;
|
|
19
19
|
--destructive: #d4183d;
|
|
20
20
|
--destructive-foreground: #ffffff;
|
|
21
|
+
/*
|
|
22
|
+
* Semantic status + brand tokens.
|
|
23
|
+
* ----------------------------------------------------------------
|
|
24
|
+
* `destructive` already covers the "error/danger" state; these add
|
|
25
|
+
* the remaining semantic states the admin needs (success / warning /
|
|
26
|
+
* info) plus the violet brand accent used for links and primary
|
|
27
|
+
* affordances. Values are tuned to read as TEXT on both the card
|
|
28
|
+
* surface and their own `/10` tint (badge backgrounds), and as a
|
|
29
|
+
* solid fill (dots, progress bars). The `*-foreground` variants are
|
|
30
|
+
* for text placed on a SOLID fill. Light values match the previous
|
|
31
|
+
* emerald/amber/blue/violet-600 palette so tokenizing is a no-op
|
|
32
|
+
* visually; dark values track the *-300/*-400 tints used before.
|
|
33
|
+
*/
|
|
34
|
+
--success: oklch(0.5 0.13 155);
|
|
35
|
+
--success-foreground: #ffffff;
|
|
36
|
+
--warning: oklch(0.52 0.13 73);
|
|
37
|
+
--warning-foreground: oklch(0.205 0 0);
|
|
38
|
+
--info: oklch(0.5 0.17 254);
|
|
39
|
+
--info-foreground: #ffffff;
|
|
40
|
+
--brand: oklch(0.54 0.23 293);
|
|
41
|
+
--brand-foreground: #ffffff;
|
|
21
42
|
--border: rgba(0, 0, 0, 0.1);
|
|
22
43
|
--input: transparent;
|
|
23
44
|
--input-background: #f3f3f5;
|
|
@@ -59,6 +80,14 @@
|
|
|
59
80
|
--accent-foreground: oklch(0.985 0 0);
|
|
60
81
|
--destructive: oklch(0.396 0.141 25.723);
|
|
61
82
|
--destructive-foreground: oklch(0.637 0.237 25.331);
|
|
83
|
+
--success: oklch(0.72 0.15 155);
|
|
84
|
+
--success-foreground: oklch(0.205 0 0);
|
|
85
|
+
--warning: oklch(0.8 0.13 80);
|
|
86
|
+
--warning-foreground: oklch(0.205 0 0);
|
|
87
|
+
--info: oklch(0.72 0.14 252);
|
|
88
|
+
--info-foreground: oklch(0.205 0 0);
|
|
89
|
+
--brand: oklch(0.72 0.18 293);
|
|
90
|
+
--brand-foreground: oklch(0.205 0 0);
|
|
62
91
|
--border: oklch(0.269 0 0);
|
|
63
92
|
--input: oklch(0.269 0 0);
|
|
64
93
|
--input-background: oklch(0.205 0 0);
|
|
@@ -98,6 +127,14 @@
|
|
|
98
127
|
--color-accent-foreground: var(--accent-foreground);
|
|
99
128
|
--color-destructive: var(--destructive);
|
|
100
129
|
--color-destructive-foreground: var(--destructive-foreground);
|
|
130
|
+
--color-success: var(--success);
|
|
131
|
+
--color-success-foreground: var(--success-foreground);
|
|
132
|
+
--color-warning: var(--warning);
|
|
133
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
134
|
+
--color-info: var(--info);
|
|
135
|
+
--color-info-foreground: var(--info-foreground);
|
|
136
|
+
--color-brand: var(--brand);
|
|
137
|
+
--color-brand-foreground: var(--brand-foreground);
|
|
101
138
|
--color-border: var(--border);
|
|
102
139
|
--color-input: var(--input);
|
|
103
140
|
--color-input-background: var(--input-background);
|
package/src/views/Dashboard.tsx
CHANGED
|
@@ -217,33 +217,32 @@ function authorAvatar(name: string | null | undefined): { color: string; initial
|
|
|
217
217
|
return { color, initials }
|
|
218
218
|
}
|
|
219
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
|
+
*/
|
|
220
231
|
function statusBadge(status: string): {
|
|
221
232
|
label: string
|
|
222
233
|
cls: string
|
|
223
234
|
} {
|
|
224
235
|
switch (status) {
|
|
225
236
|
case 'PUBLISHED':
|
|
226
|
-
return {
|
|
227
|
-
label: 'Published',
|
|
228
|
-
cls: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300',
|
|
229
|
-
}
|
|
237
|
+
return { label: 'Published', cls: 'bg-success/10 text-success' }
|
|
230
238
|
case 'DRAFT':
|
|
231
|
-
return {
|
|
232
|
-
label: 'Draft',
|
|
233
|
-
cls: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
|
|
234
|
-
}
|
|
239
|
+
return { label: 'Draft', cls: 'bg-muted text-muted-foreground' }
|
|
235
240
|
case 'SCHEDULED':
|
|
236
|
-
return {
|
|
237
|
-
label: 'Scheduled',
|
|
238
|
-
cls: 'bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300',
|
|
239
|
-
}
|
|
241
|
+
return { label: 'Scheduled', cls: 'bg-warning/10 text-warning' }
|
|
240
242
|
case 'IN_REVIEW':
|
|
241
|
-
return {
|
|
242
|
-
label: 'In Review',
|
|
243
|
-
cls: 'bg-blue-100 text-blue-800 dark:bg-blue-950/60 dark:text-blue-300',
|
|
244
|
-
}
|
|
243
|
+
return { label: 'In Review', cls: 'bg-info/10 text-info' }
|
|
245
244
|
default:
|
|
246
|
-
return { label: status, cls: 'bg-
|
|
245
|
+
return { label: status, cls: 'bg-muted text-muted-foreground' }
|
|
247
246
|
}
|
|
248
247
|
}
|
|
249
248
|
|
|
@@ -263,12 +262,31 @@ interface StatCardData {
|
|
|
263
262
|
|
|
264
263
|
export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
265
264
|
const nav = (path: string) => onNavigate?.(path)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
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
|
+
)
|
|
272
290
|
|
|
273
291
|
// A slow ticker (60s) so the greeting, calendar date, and "x minutes
|
|
274
292
|
// ago" relative times stay correct on a dashboard left open for hours
|
|
@@ -311,8 +329,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
311
329
|
label: collectionLabel(primary[0]),
|
|
312
330
|
value: String(counts[primary[0].slug] ?? 0),
|
|
313
331
|
icon: primary[0].type === 'page' ? FileIcon : FileText,
|
|
314
|
-
iconBg: 'bg-
|
|
315
|
-
iconColor: 'text-
|
|
332
|
+
iconBg: 'bg-chart-1/15',
|
|
333
|
+
iconColor: 'text-chart-1',
|
|
316
334
|
href: `/${primary[0].slug}`,
|
|
317
335
|
})
|
|
318
336
|
} else {
|
|
@@ -320,8 +338,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
320
338
|
label: 'Documents',
|
|
321
339
|
value: String(stats?.totalDocuments ?? 0),
|
|
322
340
|
icon: FileText,
|
|
323
|
-
iconBg: 'bg-
|
|
324
|
-
iconColor: 'text-
|
|
341
|
+
iconBg: 'bg-chart-1/15',
|
|
342
|
+
iconColor: 'text-chart-1',
|
|
325
343
|
})
|
|
326
344
|
}
|
|
327
345
|
|
|
@@ -330,8 +348,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
330
348
|
label: collectionLabel(primary[1]),
|
|
331
349
|
value: String(counts[primary[1].slug] ?? 0),
|
|
332
350
|
icon: primary[1].type === 'page' ? FileIcon : FileText,
|
|
333
|
-
iconBg: 'bg-
|
|
334
|
-
iconColor: 'text-
|
|
351
|
+
iconBg: 'bg-chart-2/15',
|
|
352
|
+
iconColor: 'text-chart-2',
|
|
335
353
|
href: `/${primary[1].slug}`,
|
|
336
354
|
})
|
|
337
355
|
}
|
|
@@ -344,8 +362,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
344
362
|
? `${stats.totalMedia} file${stats.totalMedia === 1 ? '' : 's'}`
|
|
345
363
|
: undefined,
|
|
346
364
|
icon: ImageIcon,
|
|
347
|
-
iconBg: 'bg-
|
|
348
|
-
iconColor: 'text-
|
|
365
|
+
iconBg: 'bg-chart-3/15',
|
|
366
|
+
iconColor: 'text-chart-3',
|
|
349
367
|
href: '/media',
|
|
350
368
|
})
|
|
351
369
|
|
|
@@ -358,8 +376,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
358
376
|
value: String(formsCount),
|
|
359
377
|
hint: submissions > 0 ? `${submissions} response${submissions === 1 ? '' : 's'}` : undefined,
|
|
360
378
|
icon: ClipboardList,
|
|
361
|
-
iconBg: 'bg-
|
|
362
|
-
iconColor: 'text-
|
|
379
|
+
iconBg: 'bg-chart-4/15',
|
|
380
|
+
iconColor: 'text-chart-4',
|
|
363
381
|
href: '/forms',
|
|
364
382
|
})
|
|
365
383
|
|
|
@@ -371,8 +389,8 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
371
389
|
hint: seo > 0 ? (seo >= 70 ? 'Good' : seo >= 40 ? 'Fair' : 'Needs work') : 'No content yet',
|
|
372
390
|
hintUp: seo >= 70,
|
|
373
391
|
icon: Search,
|
|
374
|
-
iconBg: 'bg-
|
|
375
|
-
iconColor: 'text-
|
|
392
|
+
iconBg: 'bg-chart-5/15',
|
|
393
|
+
iconColor: 'text-chart-5',
|
|
376
394
|
href: '/seo',
|
|
377
395
|
})
|
|
378
396
|
|
|
@@ -517,7 +535,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
517
535
|
if (loading && !stats) {
|
|
518
536
|
return (
|
|
519
537
|
<div className="flex h-64 items-center justify-center p-4 sm:p-6">
|
|
520
|
-
<Loader2 className="h-6 w-6 animate-spin
|
|
538
|
+
<Loader2 className="text-brand h-6 w-6 animate-spin" />
|
|
521
539
|
</div>
|
|
522
540
|
)
|
|
523
541
|
}
|
|
@@ -526,13 +544,11 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
526
544
|
<div className="w-full space-y-5 p-4 sm:p-6 lg:px-8">
|
|
527
545
|
{/* Health banners ────────────────────────────────────────────── */}
|
|
528
546
|
{health && health.status === 'degraded' && (
|
|
529
|
-
<div className="flex items-start gap-3 rounded-lg border
|
|
530
|
-
<Database className="mt-0.5 h-5 w-5 shrink-0
|
|
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" />
|
|
531
549
|
<div className="min-w-0 flex-1">
|
|
532
|
-
<span className="text-sm font-medium
|
|
533
|
-
|
|
534
|
-
</span>
|
|
535
|
-
<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">
|
|
536
552
|
{!health.databaseConnected
|
|
537
553
|
? 'Cannot connect to the database. Check your DATABASE_URL environment variable.'
|
|
538
554
|
: !health.secretConfigured
|
|
@@ -547,14 +563,14 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
547
563
|
)}
|
|
548
564
|
|
|
549
565
|
{error && exhausted && (
|
|
550
|
-
<div className="flex items-center gap-3 rounded-lg border
|
|
551
|
-
<AlertTriangle className="h-5 w-5 shrink-0
|
|
552
|
-
<span className="flex-1 text-sm
|
|
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">
|
|
553
569
|
Some dashboard data may be unavailable.
|
|
554
570
|
</span>
|
|
555
571
|
<button
|
|
556
572
|
onClick={refetch}
|
|
557
|
-
className="rounded-lg border
|
|
573
|
+
className="border-warning/40 text-warning hover:bg-warning/10 rounded-lg border px-3 py-1 text-sm transition-colors"
|
|
558
574
|
>
|
|
559
575
|
Retry
|
|
560
576
|
</button>
|
|
@@ -588,7 +604,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
588
604
|
<button
|
|
589
605
|
key={a.label}
|
|
590
606
|
onClick={a.onClick}
|
|
591
|
-
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
|
|
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"
|
|
592
608
|
>
|
|
593
609
|
<a.icon className="h-3.5 w-3.5" />
|
|
594
610
|
{a.label}
|
|
@@ -617,7 +633,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
617
633
|
<p className="text-muted-foreground mt-1 text-xs">{card.label}</p>
|
|
618
634
|
{card.hint && (
|
|
619
635
|
<p
|
|
620
|
-
className={`mt-1.5 text-[11px] ${card.hintUp ? 'text-
|
|
636
|
+
className={`mt-1.5 text-[11px] ${card.hintUp ? 'text-success' : 'text-muted-foreground'}`}
|
|
621
637
|
>
|
|
622
638
|
{card.hint}
|
|
623
639
|
</p>
|
|
@@ -653,7 +669,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
653
669
|
</div>
|
|
654
670
|
{(stats?.recentDocuments?.length ?? 0) > 8 && (
|
|
655
671
|
<button
|
|
656
|
-
className="text-xs font-medium
|
|
672
|
+
className="text-brand text-xs font-medium hover:underline"
|
|
657
673
|
onClick={() => setActivityLimit((n) => (n >= 20 ? 8 : 20))}
|
|
658
674
|
>
|
|
659
675
|
{activityLimit >= 20 ? 'Show less' : 'Show more'}
|
|
@@ -721,7 +737,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
721
737
|
</p>
|
|
722
738
|
</div>
|
|
723
739
|
<button
|
|
724
|
-
className="text-xs font-medium
|
|
740
|
+
className="text-brand text-xs font-medium hover:underline disabled:cursor-default disabled:no-underline disabled:opacity-50"
|
|
725
741
|
disabled={publishQueue.length === 0}
|
|
726
742
|
onClick={() => {
|
|
727
743
|
// We don't have a dedicated "scheduled" admin page, so deep-
|
|
@@ -750,7 +766,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
750
766
|
onClick={() => nav(q.editPath)}
|
|
751
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"
|
|
752
768
|
>
|
|
753
|
-
<span className="h-2 w-2 shrink-0 rounded-full
|
|
769
|
+
<span className="bg-warning h-2 w-2 shrink-0 rounded-full" aria-hidden />
|
|
754
770
|
<div className="min-w-0 flex-1">
|
|
755
771
|
<p className="text-foreground truncate text-sm font-medium">{q.title}</p>
|
|
756
772
|
<p className="text-muted-foreground truncate text-[11px]">
|
|
@@ -775,7 +791,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
775
791
|
<p className="text-muted-foreground mt-0.5 text-xs">SEO & quality issues</p>
|
|
776
792
|
</div>
|
|
777
793
|
<button
|
|
778
|
-
className="text-xs font-medium
|
|
794
|
+
className="text-brand text-xs font-medium hover:underline"
|
|
779
795
|
onClick={() => nav('/seo')}
|
|
780
796
|
>
|
|
781
797
|
Fix issues
|
|
@@ -792,9 +808,9 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
792
808
|
<span
|
|
793
809
|
className={`text-xs font-medium ${
|
|
794
810
|
contentHealth.tone === 'ok'
|
|
795
|
-
? 'text-
|
|
811
|
+
? 'text-success'
|
|
796
812
|
: contentHealth.tone === 'warn'
|
|
797
|
-
? 'text-
|
|
813
|
+
? 'text-warning'
|
|
798
814
|
: contentHealth.tone === 'err'
|
|
799
815
|
? 'text-red-600 dark:text-red-400'
|
|
800
816
|
: 'text-muted-foreground'
|
|
@@ -807,9 +823,9 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
807
823
|
<div
|
|
808
824
|
className={`h-full rounded-full transition-all ${
|
|
809
825
|
contentHealth.tone === 'ok'
|
|
810
|
-
? 'bg-
|
|
826
|
+
? 'bg-success'
|
|
811
827
|
: contentHealth.tone === 'warn'
|
|
812
|
-
? 'bg-
|
|
828
|
+
? 'bg-warning'
|
|
813
829
|
: contentHealth.tone === 'err'
|
|
814
830
|
? 'bg-red-500'
|
|
815
831
|
: 'bg-muted-foreground/40'
|
|
@@ -836,7 +852,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
836
852
|
iss.tone === 'err'
|
|
837
853
|
? 'bg-red-500'
|
|
838
854
|
: iss.tone === 'warn'
|
|
839
|
-
? 'bg-
|
|
855
|
+
? 'bg-warning'
|
|
840
856
|
: 'bg-muted-foreground'
|
|
841
857
|
}`}
|
|
842
858
|
aria-hidden
|
|
@@ -849,7 +865,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
849
865
|
iss.tone === 'err'
|
|
850
866
|
? 'text-red-600 dark:text-red-400'
|
|
851
867
|
: iss.tone === 'warn'
|
|
852
|
-
? 'text-
|
|
868
|
+
? 'text-warning'
|
|
853
869
|
: 'text-muted-foreground'
|
|
854
870
|
}`}
|
|
855
871
|
>
|
|
@@ -882,7 +898,7 @@ export function Dashboard({ config, session, onNavigate }: DashboardProps) {
|
|
|
882
898
|
</p>
|
|
883
899
|
</div>
|
|
884
900
|
<button
|
|
885
|
-
className="inline-flex items-center gap-1 text-xs font-medium
|
|
901
|
+
className="text-brand inline-flex items-center gap-1 text-xs font-medium hover:underline"
|
|
886
902
|
onClick={() => nav('/api-keys')}
|
|
887
903
|
>
|
|
888
904
|
API docs <ExternalLink className="h-3 w-3" />
|
|
@@ -1037,9 +1053,9 @@ function DeliveryTile({
|
|
|
1037
1053
|
}) {
|
|
1038
1054
|
const subTone =
|
|
1039
1055
|
tone === 'ok'
|
|
1040
|
-
? 'text-
|
|
1056
|
+
? 'text-success'
|
|
1041
1057
|
: tone === 'warn'
|
|
1042
|
-
? 'text-
|
|
1058
|
+
? 'text-warning'
|
|
1043
1059
|
: tone === 'err'
|
|
1044
1060
|
? 'text-red-600 dark:text-red-400'
|
|
1045
1061
|
: 'text-muted-foreground'
|