@kyro-cms/admin 0.12.4 → 0.12.6

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 (109) hide show
  1. package/dist/index.cjs +64 -26
  2. package/dist/index.d.cts +2 -1
  3. package/dist/index.d.ts +2 -1
  4. package/dist/index.js +64 -26
  5. package/package.json +10 -4
  6. package/src/components/ActionBar.tsx +16 -16
  7. package/src/components/ApiHealth.tsx +522 -0
  8. package/src/components/ApiKeysManager.tsx +6 -4
  9. package/src/components/AuditLogsPage.tsx +4 -2
  10. package/src/components/AutoForm.tsx +10 -7
  11. package/src/components/BrandingHub.tsx +3 -1
  12. package/src/components/Dashboard.tsx +44 -37
  13. package/src/components/DashboardMetrics.tsx +168 -58
  14. package/src/components/DetailView.tsx +41 -38
  15. package/src/components/DeveloperCenter.tsx +5 -3
  16. package/src/components/GraphQLPlayground.tsx +10 -8
  17. package/src/components/ListView.tsx +66 -45
  18. package/src/components/LoginPage.tsx +21 -19
  19. package/src/components/MarketplaceManager.tsx +3 -1
  20. package/src/components/MediaGallery.tsx +12 -9
  21. package/src/components/PluginsManager.tsx +6 -4
  22. package/src/components/RestPlayground.tsx +8 -6
  23. package/src/components/SessionsManager.tsx +3 -1
  24. package/src/components/Sidebar.astro +180 -92
  25. package/src/components/UserManagement.tsx +8 -6
  26. package/src/components/UserMenu.tsx +19 -17
  27. package/src/components/VersionHistoryPanel.tsx +6 -4
  28. package/src/components/WebhookManager.tsx +11 -10
  29. package/src/components/autoform/AutoFormApiView.tsx +3 -1
  30. package/src/components/autoform/AutoFormEditView.tsx +3 -1
  31. package/src/components/autoform/AutoFormHeader.tsx +6 -6
  32. package/src/components/blocks/AccordionBlock.tsx +5 -3
  33. package/src/components/blocks/ArrayBlock.tsx +5 -3
  34. package/src/components/blocks/BlockWrapper.tsx +5 -3
  35. package/src/components/blocks/CodeBlock.tsx +4 -2
  36. package/src/components/blocks/FileBlock.tsx +5 -3
  37. package/src/components/blocks/HeroBlock.tsx +5 -3
  38. package/src/components/blocks/ImageBlock.tsx +6 -4
  39. package/src/components/blocks/ListBlock.tsx +5 -3
  40. package/src/components/blocks/ParagraphBlock.tsx +3 -1
  41. package/src/components/blocks/RelationshipBlock.tsx +5 -3
  42. package/src/components/blocks/RichTextBlock.tsx +5 -3
  43. package/src/components/blocks/VideoBlock.tsx +5 -3
  44. package/src/components/fields/AccordionField.tsx +10 -51
  45. package/src/components/fields/ArrayField.tsx +9 -7
  46. package/src/components/fields/ArrayLayout.tsx +3 -1
  47. package/src/components/fields/BlocksField.tsx +24 -75
  48. package/src/components/fields/ButtonField.tsx +3 -1
  49. package/src/components/fields/CardField.tsx +6 -4
  50. package/src/components/fields/GroupLayout.tsx +2 -49
  51. package/src/components/fields/HeadingField.tsx +3 -1
  52. package/src/components/fields/HeadingSubheadingField.tsx +4 -2
  53. package/src/components/fields/HeroField.tsx +8 -6
  54. package/src/components/fields/JSONField.tsx +9 -10
  55. package/src/components/fields/LinkField.tsx +4 -2
  56. package/src/components/fields/ListField.tsx +3 -1
  57. package/src/components/fields/MarkdownField.tsx +3 -17
  58. package/src/components/fields/RelationshipBlockField.tsx +7 -5
  59. package/src/components/fields/RelationshipField.tsx +3 -1
  60. package/src/components/fields/RichTextField.tsx +19 -16
  61. package/src/components/fields/SecretField.tsx +9 -69
  62. package/src/components/fields/SelectField.tsx +11 -6
  63. package/src/components/fields/TabsLayout.tsx +2 -48
  64. package/src/components/fields/TextField.tsx +9 -7
  65. package/src/components/fields/UploadField.tsx +3 -1
  66. package/src/components/fields/VideoField.tsx +6 -4
  67. package/src/components/ui/BlockDrawer.tsx +3 -1
  68. package/src/components/ui/CommandPalette.tsx +9 -7
  69. package/src/components/ui/CommandPaletteWrapper.tsx +3 -1
  70. package/src/components/ui/IconPickerModal.tsx +4 -2
  71. package/src/components/ui/PageHeader.tsx +48 -19
  72. package/src/components/users/UserDetail.tsx +3 -1
  73. package/src/components/users/UserForm.tsx +5 -3
  74. package/src/fields/examples/sample-field-2.tsx +3 -1
  75. package/src/fields/examples/sample-field.tsx +3 -1
  76. package/src/hooks/useAutoFormState.ts +19 -11
  77. package/src/integration.ts +21 -0
  78. package/src/layouts/AdminLayout.astro +252 -189
  79. package/src/layouts/AuthLayout.astro +52 -31
  80. package/src/lib/api.ts +7 -7
  81. package/src/lib/deep-equal.ts +3 -3
  82. package/src/lib/i18n.ts +44 -0
  83. package/src/lib/vite-shim-plugin.ts +3 -0
  84. package/src/locales/de.json +163 -0
  85. package/src/locales/en.json +163 -0
  86. package/src/locales/es.json +163 -0
  87. package/src/locales/fr.json +163 -0
  88. package/src/locales/pt.json +163 -0
  89. package/src/pages/403.astro +2 -1
  90. package/src/pages/[collection]/[id].astro +23 -10
  91. package/src/pages/[collection]/index.astro +48 -34
  92. package/src/pages/audit/index.astro +2 -1
  93. package/src/pages/graphql-explorer.astro +2 -1
  94. package/src/pages/graphql.astro +2 -1
  95. package/src/pages/health.astro +9 -0
  96. package/src/pages/index.astro +6 -5
  97. package/src/pages/keys.astro +2 -1
  98. package/src/pages/marketplace.astro +2 -1
  99. package/src/pages/media.astro +2 -1
  100. package/src/pages/plugins.astro +2 -1
  101. package/src/pages/rest-playground.astro +2 -1
  102. package/src/pages/roles/index.astro +16 -52
  103. package/src/pages/sessions.astro +2 -1
  104. package/src/pages/settings/[slug].astro +45 -30
  105. package/src/pages/users/[id].astro +1 -0
  106. package/src/pages/users/index.astro +2 -1
  107. package/src/pages/users/new.astro +2 -1
  108. package/src/pages/webhooks.astro +2 -1
  109. package/src/pages/index_ALT.astro +0 -3
@@ -1,5 +1,6 @@
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
4
 
4
5
  interface MetricsData {
5
6
  totalDocuments: number;
@@ -70,15 +71,24 @@ function MetricCard({
70
71
 
71
72
 
72
73
 
73
- export function DashboardMetrics() {
74
+ export interface DashboardMetricsProps {
75
+ isEcommerce?: boolean;
76
+ }
77
+
78
+ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce }) => {
74
79
  const [data, setData] = useState<MetricsData | null>(null);
80
+ const [analyticsData, setAnalyticsData] = useState<any>(null);
75
81
  const [loading, setLoading] = useState(true);
76
82
  const [error, setError] = useState<string | null>(null);
77
83
 
78
84
  useEffect(() => {
79
- apiGet<MetricsData>("/api/metrics", { autoToast: false })
80
- .then((res) => {
81
- setData(res);
85
+ Promise.all([
86
+ apiGet<MetricsData>("/api/metrics", { autoToast: false }),
87
+ isEcommerce ? apiGet<any>("/api/analytics", { autoToast: false }).catch(() => null) : Promise.resolve(null)
88
+ ])
89
+ .then(([metricsRes, analyticsRes]) => {
90
+ setData(metricsRes);
91
+ if (analyticsRes) setAnalyticsData(analyticsRes);
82
92
  setError(null);
83
93
  })
84
94
  .catch((err) => {
@@ -86,14 +96,14 @@ export function DashboardMetrics() {
86
96
  setError("Unable to load metrics");
87
97
  })
88
98
  .finally(() => setLoading(false));
89
- }, []);
99
+ }, [isEcommerce]);
90
100
 
91
101
  if (error) {
92
102
  return (
93
- <div className="surface-tile overflow-hidden">
103
+ <div className="surface-tile overflow-hidden mb-6">
94
104
  <div className="p-6 border-b border-[var(--kyro-border)]">
95
105
  <h2 className="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
96
- CMS Metrics
106
+ Metrics
97
107
  </h2>
98
108
  </div>
99
109
  <div className="p-6">
@@ -108,21 +118,8 @@ export function DashboardMetrics() {
108
118
  <div className="p-6 border-b border-[var(--kyro-border)]">
109
119
  <div className="flex items-center justify-between">
110
120
  <div>
111
- <h2 className="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)] flex items-center gap-2">
112
- <svg
113
- className="w-5 h-5 text-[var(--kyro-primary)]"
114
- fill="none"
115
- stroke="currentColor"
116
- viewBox="0 0 24 24"
117
- >
118
- <path
119
- strokeLinecap="round"
120
- strokeLinejoin="round"
121
- strokeWidth="2"
122
- 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"
123
- />
124
- </svg>
125
- CMS Metrics
121
+ <h2 className="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
122
+ Metrics
126
123
  </h2>
127
124
  <p className="text-xs text-[var(--kyro-text-secondary)] font-medium mt-1">
128
125
  Real-time content, performance, and system health overview
@@ -147,31 +144,86 @@ export function DashboardMetrics() {
147
144
  <SkeletonCard />
148
145
  </>
149
146
  ) : data ? (
150
- <>
151
- <MetricCard
152
- icon={
153
- <svg
154
- className="w-5 h-5"
155
- fill="none"
156
- stroke="currentColor"
157
- viewBox="0 0 24 24"
158
- >
159
- <path
160
- strokeLinecap="round"
161
- strokeLinejoin="round"
162
- strokeWidth="2"
163
- 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"
164
- />
165
- </svg>
166
- }
167
- iconBg="rgba(99, 102, 241, 0.1)"
168
- iconColor="#6366f1"
169
- value={data.totalDocuments}
170
- label="Documents"
171
- subtext={`Across ${data.collections} collection${data.collections !== 1 ? "s" : ""}`}
172
- />
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
+ />
173
226
 
174
- {data.totalMedia !== undefined && (
175
227
  <MetricCard
176
228
  icon={
177
229
  <svg
@@ -190,13 +242,11 @@ export function DashboardMetrics() {
190
242
  }
191
243
  iconBg="rgba(168, 85, 247, 0.1)"
192
244
  iconColor="#a855f7"
193
- value={data.totalMedia}
245
+ value={data.totalMedia || 0}
194
246
  label="Media Files"
195
247
  subtext="Images, videos & docs"
196
248
  />
197
- )}
198
249
 
199
- {data.totalUsers !== undefined && (
200
250
  <MetricCard
201
251
  icon={
202
252
  <svg
@@ -215,13 +265,11 @@ export function DashboardMetrics() {
215
265
  }
216
266
  iconBg="rgba(14, 165, 233, 0.1)"
217
267
  iconColor="#0ea5e9"
218
- value={data.totalUsers}
268
+ value={data.totalUsers || 0}
219
269
  label="Team Members"
220
270
  subtext="Active user accounts"
221
271
  />
222
- )}
223
272
 
224
- {data.totalWebhooks !== undefined && (
225
273
  <MetricCard
226
274
  icon={
227
275
  <svg
@@ -240,13 +288,11 @@ export function DashboardMetrics() {
240
288
  }
241
289
  iconBg="rgba(34, 197, 94, 0.1)"
242
290
  iconColor="#22c55e"
243
- value={data.totalWebhooks}
291
+ value={data.totalWebhooks || 0}
244
292
  label="Webhooks"
245
293
  subtext="Active integrations"
246
294
  />
247
- )}
248
295
 
249
- {data.totalApiKeys !== undefined && (
250
296
  <MetricCard
251
297
  icon={
252
298
  <svg
@@ -265,12 +311,12 @@ export function DashboardMetrics() {
265
311
  }
266
312
  iconBg="rgba(245, 158, 11, 0.1)"
267
313
  iconColor="#f59e0b"
268
- value={data.totalApiKeys}
314
+ value={data.totalApiKeys || 0}
269
315
  label="API Keys"
270
316
  subtext="Developer access tokens"
271
317
  />
272
- )}
273
- </>
318
+ </>
319
+ )
274
320
  ) : null}
275
321
  </div>
276
322
 
@@ -278,3 +324,67 @@ export function DashboardMetrics() {
278
324
  </div>
279
325
  );
280
326
  }
327
+
328
+ export const RevenueChart: React.FC = () => {
329
+ const [data, setData] = useState<any>(null);
330
+ const [loading, setLoading] = useState(true);
331
+
332
+ useEffect(() => {
333
+ apiGet<any>("/api/analytics", { autoToast: false })
334
+ .then(setData)
335
+ .catch(console.error)
336
+ .finally(() => setLoading(false));
337
+ }, []);
338
+
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;
345
+
346
+ 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>
359
+ </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>
388
+ </div>
389
+ );
390
+ }
@@ -14,6 +14,8 @@ import { useAutoFormStore } from "../lib/autoform-store";
14
14
  import { PageHeader } from "./ui/PageHeader";
15
15
  import { Badge } from "./ui/Badge";
16
16
  import { SplitButton } from "./ui/SplitButton";
17
+ import { useTranslation } from "react-i18next";
18
+ import "../lib/i18n";
17
19
  import { adminPath } from "../lib/paths";
18
20
  import { resolveFieldValue } from "../lib/resolve-field-value";
19
21
 
@@ -41,6 +43,7 @@ export function DetailView({
41
43
  onError,
42
44
  mode = "collection",
43
45
  }: DetailViewProps) {
46
+ const { t } = useTranslation();
44
47
  const { confirm, alert } = useUIStore();
45
48
  const [data, setData] = useState<Record<string, unknown>>({});
46
49
  const [originalData, setOriginalData] = useState<Record<string, unknown>>({});
@@ -160,8 +163,8 @@ export function DetailView({
160
163
 
161
164
  if (!isAutosave) {
162
165
  const isDraft = status === "draft" || (savedData as any)?.status === "draft";
163
- if (isDraft) toast.warning("Draft saved");
164
- else toast.success("Updated");
166
+ if (isDraft) toast.warning(t("toast.draftSaved", { defaultValue: "Draft saved" }));
167
+ else toast.success(t("toast.updated", { defaultValue: "Updated" }));
165
168
  }
166
169
 
167
170
  setTimeout(() => {
@@ -175,8 +178,8 @@ export function DetailView({
175
178
  } catch (e: any) {
176
179
  setSaveStatus("error");
177
180
  if (!isAutosave) {
178
- onError("Failed to save changes");
179
- toast.error("Failed to save changes");
181
+ onError(t("toast.saveError", { defaultValue: "Failed to save changes" }));
182
+ toast.error(t("toast.saveError", { defaultValue: "Failed to save changes" }));
180
183
  }
181
184
  } finally {
182
185
  setSaving(false);
@@ -194,11 +197,11 @@ export function DetailView({
194
197
  } as any);
195
198
  setStatus("published");
196
199
  setPublishedAt(new Date().toISOString());
197
- toast.success("Published successfully");
200
+ toast.success(t("toast.published", { defaultValue: "Published successfully" }));
198
201
  onSave();
199
202
  } catch {
200
- onError("Failed to publish");
201
- toast.error("Failed to publish");
203
+ onError(t("toast.publishError", { defaultValue: "Failed to publish" }));
204
+ toast.error(t("toast.publishError", { defaultValue: "Failed to publish" }));
202
205
  } finally {
203
206
  setSaving(false);
204
207
  }
@@ -212,11 +215,11 @@ export function DetailView({
212
215
  headers: { "X-Draft": "false" },
213
216
  } as any);
214
217
  setStatus("draft");
215
- toast.warning("Document unpublished");
218
+ toast.warning(t("toast.unpublished", { defaultValue: "Document unpublished" }));
216
219
  onSave();
217
220
  } catch {
218
- onError("Failed to unpublish");
219
- toast.error("Failed to unpublish");
221
+ onError(t("toast.unpublishError", { defaultValue: "Failed to unpublish" }));
222
+ toast.error(t("toast.unpublishError", { defaultValue: "Failed to unpublish" }));
220
223
  } finally {
221
224
  setSaving(false);
222
225
  }
@@ -226,14 +229,14 @@ export function DetailView({
226
229
  try {
227
230
  setIsDuplicating(true);
228
231
  const response = await apiPost(`/api/${slug}/${documentId}/duplicate`, undefined, { autoToast: false }) as { data?: { id?: string } };
229
- toast.success("Document duplicated");
232
+ toast.success(t("toast.duplicated", { defaultValue: "Document duplicated" }));
230
233
  if (response?.data?.id) {
231
234
  window.location.href = `${adminPath}/${slug}/${response.data.id}`;
232
235
  } else {
233
236
  window.location.href = `${adminPath}/${slug}`;
234
237
  }
235
238
  } catch (e) {
236
- toast.error((e as Error).message || "Failed to duplicate document");
239
+ toast.error((e as Error).message || t("toast.duplicateError", { defaultValue: "Failed to duplicate document" }));
237
240
  } finally {
238
241
  setIsDuplicating(false);
239
242
  }
@@ -249,9 +252,9 @@ export function DetailView({
249
252
  delete (copyPayload as any).status;
250
253
 
251
254
  await navigator.clipboard.writeText(JSON.stringify(copyPayload));
252
- toast.success("Document data copied to clipboard");
255
+ toast.success(t("toast.dataCopied", { defaultValue: "Document data copied to clipboard" }));
253
256
  } catch (e) {
254
- toast.error("Failed to copy document data");
257
+ toast.error(t("toast.copyError", { defaultValue: "Failed to copy document data" }));
255
258
  }
256
259
  };
257
260
 
@@ -272,16 +275,16 @@ export function DetailView({
272
275
  updatedAt: prev.updatedAt,
273
276
  status: prev.status,
274
277
  }));
275
- toast.success("Document data pasted");
278
+ toast.success(t("toast.dataPasted", { defaultValue: "Document data pasted" }));
276
279
  } catch (e) {
277
- toast.error("Clipboard does not contain valid document JSON");
280
+ toast.error(t("toast.pasteError", { defaultValue: "Clipboard does not contain valid document JSON" }));
278
281
  }
279
282
  };
280
283
 
281
284
  const handleDeleteTrigger = () => {
282
285
  confirm({
283
- title: `Delete ${label}?`,
284
- message: "This action cannot be undone.",
286
+ title: t("detailView.deleteTitle", { defaultValue: "Delete {{label}}?", label }),
287
+ message: t("detailView.deleteMessage", { defaultValue: "This action cannot be undone." }),
285
288
  variant: "danger",
286
289
  onConfirm: async () => {
287
290
  try {
@@ -349,15 +352,15 @@ export function DetailView({
349
352
  <PageHeader
350
353
  back={{ onClick: onBack }}
351
354
  breadcrumbs={[
352
- { label: mode === "global" ? "Globals" : "Collections" },
355
+ { label: mode === "global" ? t("detailView.globals", { defaultValue: "Globals" }) : t("detailView.collections", { defaultValue: "Collections" }) },
353
356
  {
354
357
  label: label,
355
358
  href: mode === "collection" ? `${adminPath}/${slug}` : undefined
356
359
  },
357
- { label: mode === "global" ? "Edit" : documentId ? "Edit" : "New" }
360
+ { label: mode === "global" ? t("actions.edit", { defaultValue: "Edit" }) : documentId ? t("actions.edit", { defaultValue: "Edit" }) : t("actions.new", { defaultValue: "New" }) }
358
361
  ]}
359
362
  title={
360
- (mode === "global" ? label : ((resolveFieldValue(collection?.fields as any, data, collection?.admin?.useAsTitle || "title") as string) || data.name as string || documentId || `New ${collection?.singularLabel || label}`))
363
+ (mode === "global" ? label : ((resolveFieldValue(collection?.fields as any, data, collection?.admin?.useAsTitle || "title") as string) || data.name as string || documentId || t("detailView.newDocument", { defaultValue: "New {{label}}", label: collection?.singularLabel || label })))
361
364
  }
362
365
  metadata={[
363
366
  <Badge
@@ -366,7 +369,7 @@ export function DetailView({
366
369
  dot
367
370
  className="text-[10px] font-bold "
368
371
  >
369
- {status}
372
+ {t(`status.${status}`, { defaultValue: status })}
370
373
  </Badge>
371
374
  ]}
372
375
  />
@@ -384,7 +387,7 @@ export function DetailView({
384
387
  onViewHistory={() => {
385
388
  window.dispatchEvent(new CustomEvent('kyro:show-version-history'));
386
389
  }}
387
- onPreview={togglePreview}
390
+ onPreview={((collection as any)?.admin?.disablePreview || (global as any)?.admin?.disablePreview) ? undefined : togglePreview}
388
391
  onDelete={handleDeleteTrigger}
389
392
  onBack={onBack}
390
393
  onToggleSidebar={() => window.dispatchEvent(new CustomEvent("toggle-sidebar"))}
@@ -405,7 +408,7 @@ export function DetailView({
405
408
  <div className="surface-tile p-4 md:p-8">
406
409
  <div className="flex items-center justify-between mb-8 px-1">
407
410
  <h2 className="text-[10px] font-bold tracking-[0.2em] opacity-40">
408
- Core Configuration
411
+ {t("detailView.coreConfiguration", { defaultValue: "Core Configuration" })}
409
412
  </h2>
410
413
  <div className="h-px flex-1 bg-[var(--kyro-border)] ml-6 opacity-30" />
411
414
  </div>
@@ -434,7 +437,7 @@ export function DetailView({
434
437
  disabled={isDeleting || saving}
435
438
  className="kyro-btn kyro-btn-sm text-[var(--kyro-danger)] hover:bg-[var(--kyro-danger)]/10 w-full justify-start mt-2"
436
439
  >
437
- {isDeleting ? "Deleting..." : "Delete Document"}
440
+ {isDeleting ? t("actions.deleting", { defaultValue: "Deleting..." }) : t("actions.deleteDocument", { defaultValue: "Delete Document" })}
438
441
  </button>
439
442
  )}
440
443
  <SplitButton
@@ -458,7 +461,7 @@ export function DetailView({
458
461
  <div className="w-3 h-3 rounded-full bg-green-400/80"></div>
459
462
  </div>
460
463
  <div className="text-[10px] font-mono opacity-50 truncate max-w-[60%] bg-[var(--kyro-bg)] px-3 py-1 rounded">
461
- {previewUrl || "Generating preview URL..."}
464
+ {previewUrl || t("detailView.generatingPreview", { defaultValue: "Generating preview URL..." })}
462
465
  </div>
463
466
  <div className="w-16"></div>
464
467
  </div>
@@ -471,7 +474,7 @@ export function DetailView({
471
474
  <iframe
472
475
  src={previewUrl}
473
476
  className="w-full h-full border-0 bg-white"
474
- title="Preview"
477
+ title={t("tooltips.preview", { defaultValue: "Preview" })}
475
478
  sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
476
479
  />
477
480
  )}
@@ -483,12 +486,12 @@ export function DetailView({
483
486
  <div className="space-y-4 md:space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
484
487
  <div className="surface-tile p-4 md:p-8">
485
488
  <h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-4 md:mb-6">
486
- Metadata
489
+ {t("detailView.metadata", { defaultValue: "Metadata" })}
487
490
  </h3>
488
491
  <div className="space-y-4 md:space-y-6">
489
492
  <div className="flex flex-col gap-2">
490
493
  <span className="text-[10px] font-bold tracking-widest opacity-40">
491
- Dynamic Status
494
+ {t("detailView.dynamicStatus", { defaultValue: "Dynamic Status" })}
492
495
  </span>
493
496
  <div>
494
497
  <span
@@ -500,7 +503,7 @@ export function DetailView({
500
503
  </div>
501
504
  <div className="flex flex-col gap-2">
502
505
  <span className="text-[10px] font-bold tracking-widest opacity-40">
503
- Date Created
506
+ {t("detailView.dateCreated", { defaultValue: "Date Created" })}
504
507
  </span>
505
508
  <span className="text-sm font-bold text-[var(--kyro-text-secondary)]">
506
509
  {createdAt
@@ -508,12 +511,12 @@ export function DetailView({
508
511
  dateStyle: "medium",
509
512
  timeStyle: "short",
510
513
  })
511
- : "N/A"}
514
+ : t("common.na", { defaultValue: "N/A" })}
512
515
  </span>
513
516
  </div>
514
517
  <div className="flex flex-col gap-2">
515
518
  <span className="text-[10px] font-bold tracking-widest opacity-40">
516
- Last Modified
519
+ {t("detailView.lastModified", { defaultValue: "Last Modified" })}
517
520
  </span>
518
521
  <span className="text-sm font-bold text-[var(--kyro-text-secondary)]">
519
522
  {updatedAt
@@ -521,13 +524,13 @@ export function DetailView({
521
524
  dateStyle: "medium",
522
525
  timeStyle: "short",
523
526
  })
524
- : "Just now"}
527
+ : t("detailView.justNow", { defaultValue: "Just now" })}
525
528
  </span>
526
529
  </div>
527
530
  {publishedAt && (
528
531
  <div className="flex flex-col gap-2">
529
532
  <span className="text-[10px] font-bold tracking-widest opacity-40">
530
- Public At
533
+ {t("detailView.publicAt", { defaultValue: "Public At" })}
531
534
  </span>
532
535
  <span className="text-sm font-bold text-[var(--kyro-text-secondary)]">
533
536
  {new Date(publishedAt).toLocaleString("en-US", {
@@ -542,7 +545,7 @@ export function DetailView({
542
545
 
543
546
  <div className="surface-tile p-4 md:p-8 bg-[var(--kyro-bg-secondary)]">
544
547
  <h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-3 md:mb-4">
545
- Quick Links
548
+ {t("detailView.quickLinks", { defaultValue: "Quick Links" })}
546
549
  </h3>
547
550
  <div className="space-y-2 md:space-y-3">
548
551
  <button
@@ -551,7 +554,7 @@ export function DetailView({
551
554
  disabled={isDuplicating || saving}
552
555
  className="kyro-btn kyro-btn-sm kyro-btn-ghost w-full justify-start"
553
556
  >
554
- {isDuplicating ? "Duplicating..." : "Duplicate Document"}
557
+ {isDuplicating ? t("actions.duplicating", { defaultValue: "Duplicating..." }) : t("actions.duplicateDocument", { defaultValue: "Duplicate Document" })}
555
558
  </button>
556
559
  {previewUrl && (
557
560
  <button
@@ -559,7 +562,7 @@ export function DetailView({
559
562
  onClick={() => window.open(previewUrl, "_blank")}
560
563
  className="kyro-btn kyro-btn-sm kyro-btn-ghost w-full justify-start"
561
564
  >
562
- Open Preview in New Tab
565
+ {t("detailView.openPreview", { defaultValue: "Open Preview in New Tab" })}
563
566
  </button>
564
567
  )}
565
568
  <button
@@ -567,7 +570,7 @@ export function DetailView({
567
570
  onClick={handleDeleteTrigger}
568
571
  className="kyro-btn kyro-btn-sm kyro-btn-ghost w-full justify-start text-[var(--kyro-error)] hover:bg-[var(--kyro-danger-bg)]"
569
572
  >
570
- Delete Entry
573
+ {t("actions.deleteEntry", { defaultValue: "Delete Entry" })}
571
574
  </button>
572
575
  </div>
573
576
  </div>
@@ -21,6 +21,7 @@ import { useUIStore, toast } from "../lib/stores";
21
21
  import { Modal, ModalContent, ModalActions } from "./ui/Modal";
22
22
  import { PageHeader } from "./ui/PageHeader";
23
23
  import { Badge } from "./ui/Badge";
24
+ import { useTranslation } from "react-i18next";
24
25
 
25
26
  // @ts-ignore
26
27
  const API_BASE = typeof __KYRO_API_PATH__ !== 'undefined' ? __KYRO_API_PATH__ : '/api';
@@ -34,6 +35,7 @@ interface ApiKey {
34
35
  }
35
36
 
36
37
  export function DeveloperCenter({ collections }: { collections: Record<string, unknown> }) {
38
+ const { t } = useTranslation();
37
39
  const [keys, setKeys] = useState<ApiKey[]>([]);
38
40
  const [loading, setLoading] = useState(false);
39
41
  const [showKey, setShowKey] = useState<string | null>(null);
@@ -114,7 +116,7 @@ export function DeveloperCenter({ collections }: { collections: Record<string, u
114
116
  return (
115
117
  <div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
116
118
  <PageHeader
117
- title="Developer Center"
119
+ title={t("tooltips.developerCenter", { defaultValue: "Developer Center" })}
118
120
  description="Provision access keys and explore the headless API ecosystem."
119
121
  icon={Code2}
120
122
  actions={[
@@ -206,7 +208,7 @@ export function DeveloperCenter({ collections }: { collections: Record<string, u
206
208
  type="button"
207
209
  onClick={() => handleRevokeKey(key.id)}
208
210
  className="p-3.5 bg-red-500/5 text-red-500 rounded-2xl hover:bg-red-500/10 transition-all border border-transparent hover:border-red-500/30 self-start md:self-center"
209
- title="Revoke Key"
211
+ title={t("tooltips.revokeKey", { defaultValue: "Revoke Key" })}
210
212
  >
211
213
  <Trash2 className="w-5 h-5" />
212
214
  </button>
@@ -367,7 +369,7 @@ export function DeveloperCenter({ collections }: { collections: Record<string, u
367
369
  <Modal
368
370
  open={showCreateModal}
369
371
  onClose={() => setShowCreateModal(false)}
370
- title="Generate Access Token"
372
+ title={t("tooltips.generateAccessToken", { defaultValue: "Generate Access Token" })}
371
373
  size="lg"
372
374
  >
373
375
  <ModalContent>