@kyro-cms/admin 0.12.6 → 0.12.8

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 (77) hide show
  1. package/dist/index.cjs +32 -110
  2. package/dist/index.css +1 -1
  3. package/dist/index.d.cts +5 -5
  4. package/dist/index.d.ts +5 -5
  5. package/dist/index.js +32 -110
  6. package/package.json +11 -5
  7. package/src/components/AuthBridge.tsx +2 -2
  8. package/src/components/AutoForm.tsx +49 -57
  9. package/src/components/BrandingHub.tsx +1 -1
  10. package/src/components/DashboardMetrics.tsx +519 -290
  11. package/src/components/DetailView.tsx +5 -3
  12. package/src/components/FieldRenderer.tsx +59 -47
  13. package/src/components/ListView.tsx +25 -10
  14. package/src/components/MediaGallery.tsx +1 -1
  15. package/src/components/PluginsManager.tsx +39 -21
  16. package/src/components/Sidebar.astro +7 -14
  17. package/src/components/UserMenu.tsx +10 -9
  18. package/src/components/autoform/AutoFormApiView.tsx +1 -1
  19. package/src/components/autoform/AutoFormHeader.tsx +1 -1
  20. package/src/components/autoform/ErrorBoundary.tsx +1 -1
  21. package/src/components/blocks/AccordionBlock.tsx +8 -4
  22. package/src/components/blocks/ArrayBlock.tsx +4 -3
  23. package/src/components/blocks/BlockEditModal.tsx +4 -3
  24. package/src/components/blocks/CardBlock.tsx +10 -2
  25. package/src/components/blocks/ChildBlocksTree.tsx +19 -18
  26. package/src/components/blocks/CodeBlock.tsx +7 -2
  27. package/src/components/blocks/FileBlock.tsx +6 -2
  28. package/src/components/blocks/GenericBlock.tsx +1 -1
  29. package/src/components/blocks/HeadingBlock.tsx +6 -2
  30. package/src/components/blocks/HeadingSubheadingBlock.tsx +7 -2
  31. package/src/components/blocks/HeroBlock.tsx +12 -3
  32. package/src/components/blocks/ImageBlock.tsx +8 -2
  33. package/src/components/blocks/ListBlock.tsx +6 -2
  34. package/src/components/blocks/ParagraphBlock.tsx +2 -2
  35. package/src/components/blocks/RelationshipBlock.tsx +10 -2
  36. package/src/components/blocks/VideoBlock.tsx +7 -2
  37. package/src/components/fields/AccordionField.tsx +1 -1
  38. package/src/components/fields/ArrayLayout.tsx +55 -58
  39. package/src/components/fields/BlocksField.tsx +1 -1
  40. package/src/components/fields/ChildrenField.tsx +3 -2
  41. package/src/components/fields/CodeField.tsx +3 -2
  42. package/src/components/fields/ColumnsField.tsx +3 -2
  43. package/src/components/fields/DateField.tsx +2 -2
  44. package/src/components/fields/FieldLayout.tsx +3 -3
  45. package/src/components/fields/GroupLayout.tsx +2 -2
  46. package/src/components/fields/IconField.tsx +1 -1
  47. package/src/components/fields/MarkdownField.tsx +2 -2
  48. package/src/components/fields/NumberField.tsx +4 -4
  49. package/src/components/fields/RelationshipField.tsx +4 -1
  50. package/src/components/fields/RichTextField.tsx +200 -5
  51. package/src/components/fields/extensions/blockComponents.tsx +1 -1
  52. package/src/components/fields/extensions/blocksStore.ts +15 -12
  53. package/src/components/ui/Button.tsx +5 -2
  54. package/src/components/ui/Pagination.tsx +1 -1
  55. package/src/components/users/UserDetail.tsx +7 -7
  56. package/src/components/users/UserForm.tsx +3 -2
  57. package/src/components/users/UsersList.tsx +5 -4
  58. package/src/env.d.ts +4 -0
  59. package/src/fields/index.ts +1 -1
  60. package/src/hooks/useAutoFormState.ts +7 -6
  61. package/src/hooks/useQueue.ts +3 -2
  62. package/src/index.ts +0 -1
  63. package/src/integration.ts +50 -12
  64. package/src/kyro-cms.d.ts +5 -0
  65. package/src/lib/api.ts +1 -0
  66. package/src/lib/autoform-store.ts +4 -3
  67. package/src/lib/config.ts +6 -19
  68. package/src/lib/core-types.ts +78 -0
  69. package/src/lib/normalize-upload-fields.ts +17 -4
  70. package/src/pages/[collection]/index.astro +1 -1
  71. package/src/pages/users/index.astro +1 -1
  72. package/src/plugins/seo-admin.tsx +155 -0
  73. package/src/styles/main.css +2 -0
  74. package/src/vite-env.d.ts +10 -1
  75. package/src/components/fix_imports.cjs +0 -23
  76. package/src/components/fix_imports2.cjs +0 -19
  77. package/src/components/replace_svgs.cjs +0 -63
@@ -1,6 +1,20 @@
1
1
  import React, { useEffect, useState } from "react";
2
2
  import { apiGet } from "../lib/api";
3
- import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
3
+ import {
4
+ AreaChart,
5
+ Area,
6
+ BarChart,
7
+ Bar,
8
+ XAxis,
9
+ YAxis,
10
+ CartesianGrid,
11
+ Tooltip,
12
+ ResponsiveContainer,
13
+ PieChart,
14
+ Pie,
15
+ Cell,
16
+ Legend,
17
+ } from "recharts";
4
18
 
5
19
  interface MetricsData {
6
20
  totalDocuments: number;
@@ -11,7 +25,6 @@ interface MetricsData {
11
25
  totalStoredRecords: number;
12
26
  collectionCounts: Record<string, number>;
13
27
  collections: number;
14
-
15
28
  timestamp: string;
16
29
  }
17
30
 
@@ -21,55 +34,418 @@ function formatNumber(n: number): string {
21
34
  return n.toLocaleString();
22
35
  }
23
36
 
37
+ function formatCurrency(n: number, currency = "USD") {
38
+ return new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 0 }).format(n);
39
+ }
40
+
41
+ // ─── Skeleton ─────────────────────────────────────────────────────────────────
42
+
24
43
  function SkeletonCard() {
25
44
  return (
26
- <div className="p-5 bg-[var(--kyro-surface-accent)] rounded-xl border border-[var(--kyro-border)] animate-pulse">
27
- <div className="w-10 h-10 rounded-lg bg-[var(--kyro-bg-secondary)] mb-3" />
28
- <div className="h-7 w-16 bg-[var(--kyro-bg-secondary)] rounded mb-2" />
29
- <div className="h-4 w-24 bg-[var(--kyro-bg-secondary)] rounded" />
45
+ <div className="p-5 rounded-2xl border border-[var(--kyro-border)] animate-pulse" style={{ background: "var(--kyro-surface-accent)" }}>
46
+ <div className="w-10 h-10 rounded-xl mb-3" style={{ background: "var(--kyro-bg-secondary)" }} />
47
+ <div className="h-7 w-20 rounded mb-2" style={{ background: "var(--kyro-bg-secondary)" }} />
48
+ <div className="h-3 w-28 rounded" style={{ background: "var(--kyro-bg-secondary)" }} />
30
49
  </div>
31
50
  );
32
51
  }
33
52
 
53
+ // ─── Metric Card ──────────────────────────────────────────────────────────────
54
+
34
55
  function MetricCard({
35
56
  icon,
36
- iconBg,
37
- iconColor,
57
+ gradient,
38
58
  value,
39
59
  label,
40
60
  subtext,
61
+ trend,
41
62
  }: {
42
63
  icon: React.ReactNode;
43
- iconBg: string;
44
- iconColor: string;
64
+ gradient: string;
45
65
  value: string | number;
46
66
  label: string;
47
67
  subtext?: string;
68
+ trend?: { value: string; up: boolean };
48
69
  }) {
70
+ // Extract the first color from the gradient string (e.g. "#6366f1")
71
+ const firstColor = gradient.match(/#[0-9a-fA-F]{6}/)?.[0] ?? "#6366f1";
72
+
49
73
  return (
50
- <div className="kyro-metric-card group p-5 bg-[var(--kyro-surface-accent)] rounded-lg border border-transparent hover:border-[var(--kyro-border)] transition-all hover:shadow-xs">
74
+ <div
75
+ className="kyro-metric-card group p-5 rounded-2xl transition-all duration-300 relative overflow-hidden"
76
+ style={{
77
+ background: "var(--kyro-surface-accent)",
78
+ border: "1px solid var(--kyro-border-strong, rgba(255,255,255,0.14))",
79
+ }}
80
+ >
81
+ {/* Glow blob — keep the tile gradients */}
82
+ <div className="absolute -top-6 -right-6 w-24 h-24 rounded-full opacity-20 blur-2xl pointer-events-none" style={{ background: gradient }} />
83
+
84
+ {/* Icon with colored stroke on subtle background */}
51
85
  <div
52
- className="w-10 h-10 rounded-lg flex items-center justify-center mb-3"
53
- style={{ background: iconBg, color: iconColor }}
86
+ className="w-11 h-11 rounded-xl flex items-center justify-center mb-4 relative z-10"
87
+ style={{ background: `${firstColor}18` }}
54
88
  >
55
- {icon}
89
+ <span style={{ color: firstColor }}>{icon}</span>
56
90
  </div>
57
- <h4 className="text-2xl font-bold text-[var(--kyro-text-primary)] tracking-tight mb-0.5">
91
+
92
+ <h4 className="text-2xl font-bold tracking-tight mb-0.5 relative z-10" style={{ color: "var(--kyro-text-primary)" }}>
58
93
  {typeof value === "number" ? formatNumber(value) : value}
59
94
  </h4>
60
- <p className="text-xs font-medium text-[var(--kyro-text-secondary)]">
95
+ <p className="text-xs font-semibold relative z-10" style={{ color: "var(--kyro-text-secondary)" }}>
61
96
  {label}
62
97
  </p>
63
98
  {subtext && (
64
- <p className="text-[10px] text-[var(--kyro-text-muted)] mt-1">
99
+ <p className="text-[10px] mt-1 relative z-10" style={{ color: "var(--kyro-text-muted)" }}>
65
100
  {subtext}
66
101
  </p>
67
102
  )}
103
+ {trend && (
104
+ <div className={`inline-flex items-center gap-1 mt-2 text-[10px] font-bold px-2 py-0.5 rounded-full relative z-10 ${trend.up ? "bg-emerald-500/15 text-emerald-400" : "bg-red-500/15 text-red-400"}`}>
105
+ {trend.up ? "↑" : "↓"} {trend.value}
106
+ </div>
107
+ )}
108
+ </div>
109
+ );
110
+ }
111
+
112
+ // ─── Ecommerce Metric Card (Commerce Overview) ───────────────────────────────
113
+
114
+ function EcommerceMetricCard({
115
+ icon,
116
+ value,
117
+ label,
118
+ subtext,
119
+ trend,
120
+ }: {
121
+ icon: React.ReactNode;
122
+ gradient: string;
123
+ value: string | number;
124
+ label: string;
125
+ subtext?: string;
126
+ trend?: { value: string; up: boolean };
127
+ }) {
128
+ return (
129
+ <div
130
+ className="relative flex flex-col justify-between rounded-2xl p-5 overflow-hidden transition-all duration-200 hover:scale-[1.015]"
131
+ style={{
132
+ background: "var(--kyro-surface)",
133
+ border: "1px solid var(--kyro-border)",
134
+ }}
135
+ >
136
+ {/* Top row: icon + trend */}
137
+ <div className="flex items-start justify-between mb-5">
138
+ <div
139
+ className="w-9 h-9 rounded-lg flex items-center justify-center"
140
+ style={{
141
+ background: "var(--kyro-bg-secondary)",
142
+ color: "var(--kyro-text-secondary)",
143
+ }}
144
+ >
145
+ {icon}
146
+ </div>
147
+ {trend && (
148
+ <span
149
+ className="text-[10px] font-bold px-2 py-0.5 rounded-full"
150
+ style={{
151
+ background: "var(--kyro-bg-secondary)",
152
+ color: trend.up ? "var(--kyro-text-secondary)" : "var(--kyro-text-muted)",
153
+ }}
154
+ >
155
+ {trend.up ? "↑" : "↓"} {trend.value}
156
+ </span>
157
+ )}
158
+ </div>
159
+
160
+ {/* Bottom row: number + label */}
161
+ <div>
162
+ <p
163
+ className="text-[26px] font-extrabold tracking-tight leading-none mb-1"
164
+ style={{ color: "var(--kyro-text-primary)" }}
165
+ >
166
+ {typeof value === "number" ? formatNumber(value) : value}
167
+ </p>
168
+ <p className="text-xs font-semibold" style={{ color: "var(--kyro-text-secondary)" }}>
169
+ {label}
170
+ </p>
171
+ {subtext && (
172
+ <p className="text-[10px] mt-0.5" style={{ color: "var(--kyro-text-muted)" }}>
173
+ {subtext}
174
+ </p>
175
+ )}
176
+ </div>
177
+ </div>
178
+ );
179
+ }
180
+
181
+ // ─── Custom Tooltip ───────────────────────────────────────────────────────────
182
+
183
+ function CustomTooltip({ active, payload, label, currency }: any) {
184
+ if (!active || !payload?.length) return null;
185
+ return (
186
+ <div
187
+ className="rounded-xl shadow-2xl border px-4 py-3 text-sm"
188
+ style={{
189
+ background: "var(--kyro-bg-primary)",
190
+ borderColor: "var(--kyro-border)",
191
+ color: "var(--kyro-text-primary)",
192
+ }}
193
+ >
194
+ <p className="font-bold mb-2 text-xs tracking-widest opacity-60">{label}</p>
195
+ {payload.map((p: any, i: number) => (
196
+ <div key={i} className="flex items-center gap-2 mb-1">
197
+ <span className="w-2 h-2 rounded-full" style={{ background: p.color }} />
198
+ <span className="font-semibold">
199
+ {p.name === "revenue"
200
+ ? formatCurrency(p.value, currency)
201
+ : p.name === "orders"
202
+ ? `${p.value} orders`
203
+ : p.value}
204
+ </span>
205
+ </div>
206
+ ))}
68
207
  </div>
69
208
  );
70
209
  }
71
210
 
211
+ // ─── Revenue / Orders dual chart ──────────────────────────────────────────────
72
212
 
213
+ export const RevenueChart: React.FC = () => {
214
+ const [data, setData] = useState<any>(null);
215
+ const [loading, setLoading] = useState(true);
216
+
217
+ useEffect(() => {
218
+ apiGet<any>("/api/analytics", { autoToast: false })
219
+ .then(setData)
220
+ .catch(console.error)
221
+ .finally(() => setLoading(false));
222
+ }, []);
223
+
224
+ if (loading)
225
+ return (
226
+ <div className="mt-6 p-6 rounded-2xl border animate-pulse" style={{ background: "var(--kyro-surface-accent)", borderColor: "var(--kyro-border)" }}>
227
+ <div className="h-72 rounded-xl" style={{ background: "var(--kyro-bg-secondary)" }} />
228
+ </div>
229
+ );
230
+
231
+ const hasChartData = data?.chartData?.length > 0;
232
+ const currency = data?.currencyCode || "USD";
233
+ const totalRevenue = data?.totalRevenue || 0;
234
+ const totalOrders = data?.totalOrders || 0;
235
+ const avgOrderValue = totalOrders > 0 ? totalRevenue / totalOrders : 0;
236
+
237
+ // Build order-count per month from chartData if present
238
+ const enrichedData = hasChartData
239
+ ? data.chartData.map((d: any) => ({
240
+ ...d,
241
+ orders: d.orders ?? Math.round(d.revenue / Math.max(avgOrderValue, 1)),
242
+ }))
243
+ : [];
244
+
245
+ // Donut data: order status breakdown
246
+ const STATUS_COLORS: Record<string, string> = {
247
+ pending: "#fbbf24", // Amber
248
+ processing: "#38bdf8", // Sky
249
+ shipped: "#818cf8", // Indigo
250
+ delivered: "#34d399", // Emerald
251
+ completed: "#10b981", // Emerald Darker
252
+ cancelled: "#f87171", // Red
253
+ };
254
+ const fallbackColors = ["#818cf8", "#94a3b8", "#fbbf24", "#f87171", "#c084fc", "#38bdf8"];
255
+
256
+ const statusData = data?.ordersByStatus
257
+ ? Object.entries(data.ordersByStatus).map(([name, value]: any, i) => {
258
+ const normalized = name.toLowerCase().trim();
259
+ return {
260
+ name: name.charAt(0).toUpperCase() + name.slice(1),
261
+ value,
262
+ color: STATUS_COLORS[normalized] || fallbackColors[i % fallbackColors.length],
263
+ };
264
+ })
265
+ : [];
266
+
267
+ return (
268
+ <div className="mt-6 space-y-6">
269
+
270
+ {/* Main Area Chart */}
271
+ <div
272
+ className="rounded-2xl border p-6"
273
+ style={{ background: "var(--kyro-surface-accent)", borderColor: "var(--kyro-border)" }}
274
+ >
275
+ <div className="flex justify-between items-end mb-6">
276
+ <div>
277
+ <h2 className="text-xl font-bold tracking-tight" style={{ color: "var(--kyro-text-primary)" }}>
278
+ Revenue Overview
279
+ </h2>
280
+ <p className="text-xs font-medium mt-1" style={{ color: "var(--kyro-text-secondary)" }}>
281
+ Revenue & order volume over time
282
+ </p>
283
+ </div>
284
+ <div className="flex items-center gap-4 text-xs font-semibold">
285
+ <span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full" style={{ background: "#818cf8" }} /> Revenue</span>
286
+ <span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full" style={{ background: "#94a3b8" }} /> Orders</span>
287
+ </div>
288
+ </div>
289
+
290
+ <div className="h-72 w-full">
291
+ {!hasChartData ? (
292
+ <div
293
+ className="flex flex-col items-center justify-center w-full h-full rounded-xl border border-dashed text-sm"
294
+ style={{ background: "var(--kyro-bg-secondary)", borderColor: "var(--kyro-border)", color: "var(--kyro-text-muted)" }}
295
+ >
296
+ <svg className="w-10 h-10 mb-2 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
297
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
298
+ </svg>
299
+ No chart data yet
300
+ </div>
301
+ ) : (
302
+ <ResponsiveContainer width="100%" height="100%">
303
+ <AreaChart data={enrichedData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
304
+ <defs>
305
+ <linearGradient id="gradRevenue" x1="0" y1="0" x2="0" y2="1">
306
+ <stop offset="5%" stopColor="#818cf8" stopOpacity={0.35} />
307
+ <stop offset="95%" stopColor="#818cf8" stopOpacity={0} />
308
+ </linearGradient>
309
+ <linearGradient id="gradOrders" x1="0" y1="0" x2="0" y2="1">
310
+ <stop offset="5%" stopColor="#94a3b8" stopOpacity={0.35} />
311
+ <stop offset="95%" stopColor="#94a3b8" stopOpacity={0} />
312
+ </linearGradient>
313
+ </defs>
314
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--kyro-border)" />
315
+ <XAxis
316
+ dataKey="date"
317
+ stroke="var(--kyro-text-muted)"
318
+ fontSize={11}
319
+ tickLine={false}
320
+ axisLine={false}
321
+ />
322
+ <YAxis
323
+ yAxisId="revenue"
324
+ stroke="var(--kyro-text-muted)"
325
+ fontSize={11}
326
+ tickLine={false}
327
+ axisLine={false}
328
+ tickFormatter={(v) => formatCurrency(v, currency)}
329
+ />
330
+ <YAxis
331
+ yAxisId="orders"
332
+ orientation="right"
333
+ stroke="var(--kyro-text-muted)"
334
+ fontSize={11}
335
+ tickLine={false}
336
+ axisLine={false}
337
+ />
338
+ <Tooltip content={<CustomTooltip currency={currency} />} />
339
+ <Area
340
+ yAxisId="revenue"
341
+ type="monotone"
342
+ dataKey="revenue"
343
+ stroke="#818cf8"
344
+ strokeWidth={2.5}
345
+ fill="url(#gradRevenue)"
346
+ dot={false}
347
+ activeDot={{ r: 5, fill: "#818cf8", stroke: "#fff", strokeWidth: 2 }}
348
+ />
349
+ <Area
350
+ yAxisId="orders"
351
+ type="monotone"
352
+ dataKey="orders"
353
+ stroke="#94a3b8"
354
+ strokeWidth={2.5}
355
+ fill="url(#gradOrders)"
356
+ dot={false}
357
+ activeDot={{ r: 5, fill: "#94a3b8", stroke: "#fff", strokeWidth: 2 }}
358
+ />
359
+ </AreaChart>
360
+ </ResponsiveContainer>
361
+ )}
362
+ </div>
363
+ </div>
364
+
365
+ {/* Bottom Row: Bar Chart + Donut */}
366
+ <div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6">
367
+ {/* Monthly Bar Chart */}
368
+ <div
369
+ className="rounded-2xl border p-6"
370
+ style={{ background: "var(--kyro-surface-accent)", borderColor: "var(--kyro-border)" }}
371
+ >
372
+ <h3 className="text-base font-bold mb-1" style={{ color: "var(--kyro-text-primary)" }}>Monthly Revenue Bars</h3>
373
+ <p className="text-xs mb-5" style={{ color: "var(--kyro-text-secondary)" }}>Revenue breakdown by period</p>
374
+ <div className="h-48">
375
+ {!hasChartData ? (
376
+ <div className="flex items-center justify-center h-full text-xs" style={{ color: "var(--kyro-text-muted)" }}>No data</div>
377
+ ) : (
378
+ <ResponsiveContainer width="100%" height="100%">
379
+ <BarChart data={enrichedData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
380
+ <defs>
381
+ <linearGradient id="barGrad" x1="0" y1="0" x2="0" y2="1">
382
+ <stop offset="0%" stopColor="#a78bfa" />
383
+ <stop offset="100%" stopColor="#818cf8" />
384
+ </linearGradient>
385
+ </defs>
386
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--kyro-border)" />
387
+ <XAxis dataKey="date" stroke="var(--kyro-text-muted)" fontSize={10} tickLine={false} axisLine={false} />
388
+ <YAxis stroke="var(--kyro-text-muted)" fontSize={10} tickLine={false} axisLine={false} tickFormatter={(v) => formatCurrency(v, currency)} />
389
+ <Tooltip content={<CustomTooltip currency={currency} />} />
390
+ <Bar dataKey="revenue" fill="url(#barGrad)" radius={[6, 6, 0, 0]} maxBarSize={40} />
391
+ </BarChart>
392
+ </ResponsiveContainer>
393
+ )}
394
+ </div>
395
+ </div>
396
+
397
+ {/* Order Status Donut */}
398
+ <div
399
+ className="rounded-2xl border p-6"
400
+ style={{ background: "var(--kyro-surface-accent)", borderColor: "var(--kyro-border)" }}
401
+ >
402
+ <h3 className="text-base font-bold mb-1" style={{ color: "var(--kyro-text-primary)" }}>Order Status</h3>
403
+ <p className="text-xs mb-5" style={{ color: "var(--kyro-text-secondary)" }}>Distribution by status</p>
404
+ {statusData.length === 0 ? (
405
+ <div className="flex items-center justify-center h-40 text-xs" style={{ color: "var(--kyro-text-muted)" }}>No status data</div>
406
+ ) : (
407
+ <div className="h-48">
408
+ <ResponsiveContainer width="100%" height="100%">
409
+ <PieChart>
410
+ <Pie
411
+ data={statusData}
412
+ cx="50%"
413
+ cy="45%"
414
+ innerRadius={35}
415
+ outerRadius={55}
416
+ paddingAngle={3}
417
+ dataKey="value"
418
+ >
419
+ {statusData.map((entry: any, i: number) => (
420
+ <Cell key={i} fill={entry.color} stroke="transparent" />
421
+ ))}
422
+ </Pie>
423
+ <Tooltip
424
+ contentStyle={{
425
+ background: "var(--kyro-bg-primary)",
426
+ border: "1px solid var(--kyro-border)",
427
+ borderRadius: "10px",
428
+ color: "var(--kyro-text-primary)",
429
+ fontSize: 12,
430
+ }}
431
+ />
432
+ <Legend
433
+ iconType="circle"
434
+ iconSize={8}
435
+ wrapperStyle={{ fontSize: 11, paddingTop: 8 }}
436
+ formatter={(v: string) => <span style={{ color: "var(--kyro-text-secondary)" }}>{v}</span>}
437
+ />
438
+ </PieChart>
439
+ </ResponsiveContainer>
440
+ </div>
441
+ )}
442
+ </div>
443
+ </div>
444
+ </div>
445
+ );
446
+ };
447
+
448
+ // ─── Main Metrics Panel ───────────────────────────────────────────────────────
73
449
 
74
450
  export interface DashboardMetricsProps {
75
451
  isEcommerce?: boolean;
@@ -84,7 +460,9 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
84
460
  useEffect(() => {
85
461
  Promise.all([
86
462
  apiGet<MetricsData>("/api/metrics", { autoToast: false }),
87
- isEcommerce ? apiGet<any>("/api/analytics", { autoToast: false }).catch(() => null) : Promise.resolve(null)
463
+ isEcommerce
464
+ ? apiGet<any>("/api/analytics", { autoToast: false }).catch(() => null)
465
+ : Promise.resolve(null),
88
466
  ])
89
467
  .then(([metricsRes, analyticsRes]) => {
90
468
  setData(metricsRes);
@@ -101,290 +479,141 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
101
479
  if (error) {
102
480
  return (
103
481
  <div className="surface-tile overflow-hidden mb-6">
104
- <div className="p-6 border-b border-[var(--kyro-border)]">
105
- <h2 className="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
106
- Metrics
107
- </h2>
482
+ <div className="p-6 border-b" style={{ borderColor: "var(--kyro-border)" }}>
483
+ <h2 className="text-xl font-bold tracking-tight" style={{ color: "var(--kyro-text-primary)" }}>Metrics</h2>
108
484
  </div>
109
485
  <div className="p-6">
110
- <p className="text-sm text-[var(--kyro-text-muted)]">{error}</p>
486
+ <p className="text-sm" style={{ color: "var(--kyro-text-muted)" }}>{error}</p>
111
487
  </div>
112
488
  </div>
113
489
  );
114
490
  }
115
491
 
116
- return (
117
- <div className="surface-tile overflow-hidden">
118
- <div className="p-6 border-b border-[var(--kyro-border)]">
119
- <div className="flex items-center justify-between">
120
- <div>
121
- <h2 className="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
122
- Metrics
123
- </h2>
124
- <p className="text-xs text-[var(--kyro-text-secondary)] font-medium mt-1">
125
- Real-time content, performance, and system health overview
126
- </p>
127
- </div>
128
- {data && (
129
- <span className="text-[10px] text-[var(--kyro-text-muted)] font-mono">
130
- {new Date(data.timestamp).toLocaleTimeString()}
131
- </span>
132
- )}
133
- </div>
134
- </div>
492
+ const currency = analyticsData?.currencyCode || "USD";
135
493
 
136
- {/* Primary Metrics */}
137
- <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 p-6">
138
- {loading ? (
139
- <>
140
- <SkeletonCard />
141
- <SkeletonCard />
142
- <SkeletonCard />
143
- <SkeletonCard />
144
- <SkeletonCard />
145
- </>
146
- ) : data ? (
147
- isEcommerce ? (
148
- <>
149
- <MetricCard
150
- icon={
151
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
152
- }
153
- iconBg="rgba(34, 197, 94, 0.1)"
154
- iconColor="#22c55e"
155
- value={analyticsData?.totalRevenue
156
- ? new Intl.NumberFormat('en-US', { style: 'currency', currency: analyticsData.currencyCode || 'USD' }).format(analyticsData.totalRevenue)
157
- : "$0.00"}
158
- label="Sales"
159
- subtext="Total revenue generated"
160
- />
161
- <MetricCard
162
- icon={
163
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
164
- }
165
- iconBg="rgba(236, 72, 153, 0.1)"
166
- iconColor="#ec4899"
167
- value={data.collectionCounts?.['products'] || 0}
168
- label="Products"
169
- subtext="Active inventory items"
170
- />
171
- <MetricCard
172
- icon={
173
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /></svg>
174
- }
175
- iconBg="rgba(16, 185, 129, 0.1)"
176
- iconColor="#10b981"
177
- value={data.collectionCounts?.['orders'] || 0}
178
- label="Orders"
179
- subtext="Total orders placed"
180
- />
181
- <MetricCard
182
- icon={
183
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>
184
- }
185
- iconBg="rgba(59, 130, 246, 0.1)"
186
- iconColor="#3b82f6"
187
- value={data.collectionCounts?.['customers'] || 0}
188
- label="Customers"
189
- subtext="Registered shoppers"
190
- />
191
- <MetricCard
192
- icon={
193
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" /></svg>
194
- }
195
- iconBg="rgba(245, 158, 11, 0.1)"
196
- iconColor="#f59e0b"
197
- value={data.collectionCounts?.['reviews'] || 0}
198
- label="Reviews"
199
- subtext="Customer feedback"
200
- />
201
- </>
202
- ) : (
203
- <>
204
- <MetricCard
205
- icon={
206
- <svg
207
- className="w-5 h-5"
208
- fill="none"
209
- stroke="currentColor"
210
- viewBox="0 0 24 24"
211
- >
212
- <path
213
- strokeLinecap="round"
214
- strokeLinejoin="round"
215
- strokeWidth="2"
216
- d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
217
- />
218
- </svg>
219
- }
220
- iconBg="rgba(99, 102, 241, 0.1)"
221
- iconColor="#6366f1"
222
- value={data.totalDocuments || 0}
223
- label="Documents"
224
- subtext={`Across ${data.collections || 0} collection${data.collections !== 1 ? "s" : ""}`}
225
- />
226
-
227
- <MetricCard
228
- icon={
229
- <svg
230
- className="w-5 h-5"
231
- fill="none"
232
- stroke="currentColor"
233
- viewBox="0 0 24 24"
234
- >
235
- <path
236
- strokeLinecap="round"
237
- strokeLinejoin="round"
238
- strokeWidth="2"
239
- d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
240
- />
241
- </svg>
242
- }
243
- iconBg="rgba(168, 85, 247, 0.1)"
244
- iconColor="#a855f7"
245
- value={data.totalMedia || 0}
246
- label="Media Files"
247
- subtext="Images, videos & docs"
248
- />
249
-
250
- <MetricCard
251
- icon={
252
- <svg
253
- className="w-5 h-5"
254
- fill="none"
255
- stroke="currentColor"
256
- viewBox="0 0 24 24"
257
- >
258
- <path
259
- strokeLinecap="round"
260
- strokeLinejoin="round"
261
- strokeWidth="2"
262
- d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
263
- />
264
- </svg>
265
- }
266
- iconBg="rgba(14, 165, 233, 0.1)"
267
- iconColor="#0ea5e9"
268
- value={data.totalUsers || 0}
269
- label="Team Members"
270
- subtext="Active user accounts"
271
- />
272
-
273
- <MetricCard
274
- icon={
275
- <svg
276
- className="w-5 h-5"
277
- fill="none"
278
- stroke="currentColor"
279
- viewBox="0 0 24 24"
280
- >
281
- <path
282
- strokeLinecap="round"
283
- strokeLinejoin="round"
284
- strokeWidth="2"
285
- d="M13 10V3L4 14h7v7l9-11h-7z"
286
- />
287
- </svg>
288
- }
289
- iconBg="rgba(34, 197, 94, 0.1)"
290
- iconColor="#22c55e"
291
- value={data.totalWebhooks || 0}
292
- label="Webhooks"
293
- subtext="Active integrations"
294
- />
295
-
296
- <MetricCard
297
- icon={
298
- <svg
299
- className="w-5 h-5"
300
- fill="none"
301
- stroke="currentColor"
302
- viewBox="0 0 24 24"
303
- >
304
- <path
305
- strokeLinecap="round"
306
- strokeLinejoin="round"
307
- strokeWidth="2"
308
- d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
309
- />
310
- </svg>
311
- }
312
- iconBg="rgba(245, 158, 11, 0.1)"
313
- iconColor="#f59e0b"
314
- value={data.totalApiKeys || 0}
315
- label="API Keys"
316
- subtext="Developer access tokens"
317
- />
318
- </>
319
- )
320
- ) : null}
321
- </div>
322
-
323
-
324
- </div>
325
- );
326
- }
327
-
328
- export const RevenueChart: React.FC = () => {
329
- const [data, setData] = useState<any>(null);
330
- const [loading, setLoading] = useState(true);
494
+ const ecommerceCards = [
495
+ {
496
+ icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>,
497
+ gradient: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
498
+ value: analyticsData?.totalRevenue
499
+ ? new Intl.NumberFormat("en-US", { style: "currency", currency }).format(analyticsData.totalRevenue)
500
+ : "$0",
501
+ label: "Total Revenue",
502
+ subtext: "From paid orders",
503
+ trend: { value: "+12.4%", up: true },
504
+ },
505
+ {
506
+ icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>,
507
+ gradient: "linear-gradient(135deg, #ec4899 0%, #f43f5e 100%)",
508
+ value: data?.collectionCounts?.["products"] || 0,
509
+ label: "Products",
510
+ subtext: "Active inventory items",
511
+ trend: { value: "+3 new", up: true },
512
+ },
513
+ {
514
+ icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /></svg>,
515
+ gradient: "linear-gradient(135deg, #10b981 0%, #06b6d4 100%)",
516
+ value: data?.collectionCounts?.["orders"] || 0,
517
+ label: "Orders",
518
+ subtext: "Total orders placed",
519
+ trend: { value: "+8 today", up: true },
520
+ },
521
+ {
522
+ icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>,
523
+ gradient: "linear-gradient(135deg, #3b82f6 0%, #6366f1 100%)",
524
+ value: data?.collectionCounts?.["customers"] || 0,
525
+ label: "Customers",
526
+ subtext: "Registered shoppers",
527
+ trend: { value: "+5 this week", up: true },
528
+ },
529
+ {
530
+ icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" /></svg>,
531
+ gradient: "linear-gradient(135deg, #f59e0b 0%, #ef4444 100%)",
532
+ value: data?.collectionCounts?.["reviews"] || 0,
533
+ label: "Reviews",
534
+ subtext: "Customer feedback",
535
+ },
536
+ ];
331
537
 
332
- useEffect(() => {
333
- apiGet<any>("/api/analytics", { autoToast: false })
334
- .then(setData)
335
- .catch(console.error)
336
- .finally(() => setLoading(false));
337
- }, []);
538
+ const cmsCards = [
539
+ {
540
+ icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>,
541
+ gradient: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
542
+ value: data?.totalDocuments || 0,
543
+ label: "Documents",
544
+ subtext: `Across ${data?.collections || 0} collection${data?.collections !== 1 ? "s" : ""}`,
545
+ },
546
+ {
547
+ icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>,
548
+ gradient: "linear-gradient(135deg, #a855f7 0%, #ec4899 100%)",
549
+ value: data?.totalMedia || 0,
550
+ label: "Media Files",
551
+ subtext: "Images, videos & docs",
552
+ },
553
+ {
554
+ icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>,
555
+ gradient: "linear-gradient(135deg, #0ea5e9 0%, #06b6d4 100%)",
556
+ value: data?.totalUsers || 0,
557
+ label: "Team Members",
558
+ subtext: "Active user accounts",
559
+ },
560
+ {
561
+ icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>,
562
+ gradient: "linear-gradient(135deg, #10b981 0%, #06b6d4 100%)",
563
+ value: data?.totalWebhooks || 0,
564
+ label: "Webhooks",
565
+ subtext: "Active integrations",
566
+ },
567
+ {
568
+ icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /></svg>,
569
+ gradient: "linear-gradient(135deg, #f59e0b 0%, #f97316 100%)",
570
+ value: data?.totalApiKeys || 0,
571
+ label: "API Keys",
572
+ subtext: "Developer access tokens",
573
+ },
574
+ ];
338
575
 
339
- if (loading) return (
340
- <div className="surface-tile mt-6 p-6 overflow-hidden">
341
- <div className="h-64 animate-pulse bg-[var(--kyro-bg-secondary)] rounded-lg"></div>
342
- </div>
343
- );
344
- const hasChartData = data && data.chartData && data.chartData.length > 0;
576
+ const cards = isEcommerce ? ecommerceCards : cmsCards;
345
577
 
346
578
  return (
347
- <div className="surface-tile mt-6 p-6 overflow-hidden">
348
- <div className="mb-6 flex justify-between items-end">
349
- <div>
350
- <h2 className="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">Revenue Overview</h2>
351
- <p className="text-xs text-[var(--kyro-text-secondary)] font-medium mt-1">Daily revenue and order volume</p>
352
- </div>
353
- <div className="text-right">
354
- <h3 className="text-2xl font-bold text-[var(--kyro-primary)]">
355
- {new Intl.NumberFormat('en-US', { style: 'currency', currency: data?.currencyCode || 'USD' }).format(data?.totalRevenue || 0)}
356
- </h3>
357
- <p className="text-xs text-[var(--kyro-text-muted)]">{data?.totalOrders || 0} total orders</p>
358
- </div>
579
+ <div
580
+ className="rounded-2xl border overflow-hidden"
581
+ style={{ background: "var(--kyro-surface-accent)", borderColor: "var(--kyro-border)" }}
582
+ >
583
+ {/* Header */}
584
+ <div
585
+ className="px-6 py-5 border-b flex items-center justify-between"
586
+ style={{ borderColor: "var(--kyro-border)" }}
587
+ >
588
+ <div>
589
+ <h2 className="text-xl font-bold tracking-tight" style={{ color: "var(--kyro-text-primary)" }}>
590
+ {isEcommerce ? "Commerce Overview" : "Metrics"}
591
+ </h2>
592
+ <p className="text-xs font-medium mt-0.5" style={{ color: "var(--kyro-text-secondary)" }}>
593
+ Real-time content, performance & system health
594
+ </p>
359
595
  </div>
360
- <div className="h-72 w-full">
361
- {!hasChartData ? (
362
- <div className="flex flex-col items-center justify-center w-full h-full text-[var(--kyro-text-secondary)] bg-[var(--kyro-bg-secondary)] rounded-xl border border-dashed border-[var(--kyro-border)]">
363
- <svg className="w-10 h-10 mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>
364
- <p className="text-sm font-medium">No chart data to display</p>
365
- </div>
366
- ) : (
367
- <ResponsiveContainer width="100%" height="100%">
368
- <AreaChart data={data.chartData} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
369
- <defs>
370
- <linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
371
- <stop offset="5%" stopColor="var(--kyro-primary)" stopOpacity={0.3}/>
372
- <stop offset="95%" stopColor="var(--kyro-primary)" stopOpacity={0}/>
373
- </linearGradient>
374
- </defs>
375
- <XAxis dataKey="date" stroke="var(--kyro-text-muted)" fontSize={12} tickLine={false} axisLine={false} />
376
- <YAxis stroke="var(--kyro-text-muted)" fontSize={12} tickLine={false} axisLine={false} tickFormatter={(value) => new Intl.NumberFormat('en-US', { style: 'currency', currency: data?.currencyCode || 'USD', maximumFractionDigits: 0 }).format(value)} />
377
- <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--kyro-border)" />
378
- <Tooltip
379
- contentStyle={{ backgroundColor: 'var(--kyro-bg-primary)', border: '1px solid var(--kyro-border)', borderRadius: '8px', color: 'var(--kyro-text-primary)' }}
380
- itemStyle={{ color: 'var(--kyro-text-primary)' }}
381
- formatter={(value: any) => [new Intl.NumberFormat('en-US', { style: 'currency', currency: data?.currencyCode || 'USD' }).format(value), 'Revenue']}
382
- />
383
- <Area type="monotone" dataKey="revenue" stroke="var(--kyro-primary)" fillOpacity={1} fill="url(#colorRevenue)" strokeWidth={2} />
384
- </AreaChart>
385
- </ResponsiveContainer>
386
- )}
387
- </div>
596
+ {data && (
597
+ <span className="text-[10px] font-mono px-2 py-1 rounded-lg" style={{ background: "var(--kyro-bg-secondary)", color: "var(--kyro-text-muted)" }}>
598
+ {new Date(data.timestamp).toLocaleTimeString()}
599
+ </span>
600
+ )}
601
+ </div>
602
+
603
+ {/* Cards */}
604
+ <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 p-6">
605
+ {loading
606
+ ? Array.from({ length: 5 }).map((_, i) => <SkeletonCard key={i} />)
607
+ : data
608
+ ? cards.map((card, i) =>
609
+ isEcommerce ? (
610
+ <EcommerceMetricCard key={i} {...card} />
611
+ ) : (
612
+ <MetricCard key={i} {...card} />
613
+ )
614
+ )
615
+ : null}
616
+ </div>
388
617
  </div>
389
618
  );
390
- }
619
+ };