@kyro-cms/admin 0.12.3 → 0.12.5

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 (110) 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 +8 -2
  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 +125 -50
  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 +143 -15
  29. package/src/components/autoform/AutoFormApiView.tsx +3 -1
  30. package/src/components/autoform/AutoFormEditView.tsx +5 -3
  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 -20
  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/autoform-store.ts +4 -0
  82. package/src/lib/deep-equal.ts +3 -3
  83. package/src/lib/i18n.ts +44 -0
  84. package/src/lib/vite-shim-plugin.ts +3 -0
  85. package/src/locales/de.json +163 -0
  86. package/src/locales/en.json +163 -0
  87. package/src/locales/es.json +163 -0
  88. package/src/locales/fr.json +163 -0
  89. package/src/locales/pt.json +163 -0
  90. package/src/pages/403.astro +2 -1
  91. package/src/pages/[collection]/[id].astro +23 -10
  92. package/src/pages/[collection]/index.astro +48 -34
  93. package/src/pages/audit/index.astro +2 -1
  94. package/src/pages/graphql-explorer.astro +2 -1
  95. package/src/pages/graphql.astro +2 -1
  96. package/src/pages/health.astro +9 -0
  97. package/src/pages/index.astro +6 -5
  98. package/src/pages/keys.astro +2 -1
  99. package/src/pages/marketplace.astro +2 -1
  100. package/src/pages/media.astro +2 -1
  101. package/src/pages/plugins.astro +2 -1
  102. package/src/pages/rest-playground.astro +2 -1
  103. package/src/pages/roles/index.astro +16 -52
  104. package/src/pages/sessions.astro +2 -1
  105. package/src/pages/settings/[slug].astro +45 -30
  106. package/src/pages/users/[id].astro +1 -0
  107. package/src/pages/users/index.astro +2 -1
  108. package/src/pages/users/new.astro +2 -1
  109. package/src/pages/webhooks.astro +2 -1
  110. package/src/pages/index_ALT.astro +0 -3
@@ -10,9 +10,12 @@ import { ActionBar, type DocumentStatus, type SaveStatus } from "./ActionBar";
10
10
  import { Spinner } from "./ui/Spinner";
11
11
  import { Shimmer } from "./ui/Shimmer";
12
12
  import { useUIStore, toast } from "../lib/stores";
13
+ import { useAutoFormStore } from "../lib/autoform-store";
13
14
  import { PageHeader } from "./ui/PageHeader";
14
15
  import { Badge } from "./ui/Badge";
15
16
  import { SplitButton } from "./ui/SplitButton";
17
+ import { useTranslation } from "react-i18next";
18
+ import "../lib/i18n";
16
19
  import { adminPath } from "../lib/paths";
17
20
  import { resolveFieldValue } from "../lib/resolve-field-value";
18
21
 
@@ -40,6 +43,7 @@ export function DetailView({
40
43
  onError,
41
44
  mode = "collection",
42
45
  }: DetailViewProps) {
46
+ const { t } = useTranslation();
43
47
  const { confirm, alert } = useUIStore();
44
48
  const [data, setData] = useState<Record<string, unknown>>({});
45
49
  const [originalData, setOriginalData] = useState<Record<string, unknown>>({});
@@ -53,6 +57,10 @@ export function DetailView({
53
57
  const [updatedAt, setUpdatedAt] = useState<string | null>(null);
54
58
  const [publishedAt, setPublishedAt] = useState<string | null>(null);
55
59
  const [justSaved, setJustSaved] = useState(false);
60
+ const showPreview = useAutoFormStore((state) => state.showPreview);
61
+ const setShowPreview = useAutoFormStore((state) => state.setShowPreview);
62
+ const previewUrl = useAutoFormStore((state) => state.previewUrl);
63
+ const setPreviewUrl = useAutoFormStore((state) => state.setPreviewUrl);
56
64
 
57
65
  const fields = global?.fields || collection?.fields || [];
58
66
  const label = global?.label || collection?.label || "Document";
@@ -155,18 +163,23 @@ export function DetailView({
155
163
 
156
164
  if (!isAutosave) {
157
165
  const isDraft = status === "draft" || (savedData as any)?.status === "draft";
158
- if (isDraft) toast.warning("Draft saved");
159
- else toast.success("Updated");
166
+ if (isDraft) toast.warning(t("toast.draftSaved", { defaultValue: "Draft saved" }));
167
+ else toast.success(t("toast.updated", { defaultValue: "Updated" }));
160
168
  }
161
169
 
162
170
  setTimeout(() => {
163
171
  setSaveStatus("idle");
164
172
  }, 2000);
165
- } catch {
173
+
174
+ if (showPreview) {
175
+ refreshPreviewUrl(savedData);
176
+ }
177
+
178
+ } catch (e: any) {
166
179
  setSaveStatus("error");
167
180
  if (!isAutosave) {
168
- onError("Failed to save changes");
169
- 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" }));
170
183
  }
171
184
  } finally {
172
185
  setSaving(false);
@@ -184,11 +197,11 @@ export function DetailView({
184
197
  } as any);
185
198
  setStatus("published");
186
199
  setPublishedAt(new Date().toISOString());
187
- toast.success("Published successfully");
200
+ toast.success(t("toast.published", { defaultValue: "Published successfully" }));
188
201
  onSave();
189
202
  } catch {
190
- onError("Failed to publish");
191
- toast.error("Failed to publish");
203
+ onError(t("toast.publishError", { defaultValue: "Failed to publish" }));
204
+ toast.error(t("toast.publishError", { defaultValue: "Failed to publish" }));
192
205
  } finally {
193
206
  setSaving(false);
194
207
  }
@@ -202,11 +215,11 @@ export function DetailView({
202
215
  headers: { "X-Draft": "false" },
203
216
  } as any);
204
217
  setStatus("draft");
205
- toast.warning("Document unpublished");
218
+ toast.warning(t("toast.unpublished", { defaultValue: "Document unpublished" }));
206
219
  onSave();
207
220
  } catch {
208
- onError("Failed to unpublish");
209
- toast.error("Failed to unpublish");
221
+ onError(t("toast.unpublishError", { defaultValue: "Failed to unpublish" }));
222
+ toast.error(t("toast.unpublishError", { defaultValue: "Failed to unpublish" }));
210
223
  } finally {
211
224
  setSaving(false);
212
225
  }
@@ -216,14 +229,14 @@ export function DetailView({
216
229
  try {
217
230
  setIsDuplicating(true);
218
231
  const response = await apiPost(`/api/${slug}/${documentId}/duplicate`, undefined, { autoToast: false }) as { data?: { id?: string } };
219
- toast.success("Document duplicated");
232
+ toast.success(t("toast.duplicated", { defaultValue: "Document duplicated" }));
220
233
  if (response?.data?.id) {
221
234
  window.location.href = `${adminPath}/${slug}/${response.data.id}`;
222
235
  } else {
223
236
  window.location.href = `${adminPath}/${slug}`;
224
237
  }
225
238
  } catch (e) {
226
- 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" }));
227
240
  } finally {
228
241
  setIsDuplicating(false);
229
242
  }
@@ -239,9 +252,9 @@ export function DetailView({
239
252
  delete (copyPayload as any).status;
240
253
 
241
254
  await navigator.clipboard.writeText(JSON.stringify(copyPayload));
242
- toast.success("Document data copied to clipboard");
255
+ toast.success(t("toast.dataCopied", { defaultValue: "Document data copied to clipboard" }));
243
256
  } catch (e) {
244
- toast.error("Failed to copy document data");
257
+ toast.error(t("toast.copyError", { defaultValue: "Failed to copy document data" }));
245
258
  }
246
259
  };
247
260
 
@@ -262,16 +275,16 @@ export function DetailView({
262
275
  updatedAt: prev.updatedAt,
263
276
  status: prev.status,
264
277
  }));
265
- toast.success("Document data pasted");
278
+ toast.success(t("toast.dataPasted", { defaultValue: "Document data pasted" }));
266
279
  } catch (e) {
267
- toast.error("Clipboard does not contain valid document JSON");
280
+ toast.error(t("toast.pasteError", { defaultValue: "Clipboard does not contain valid document JSON" }));
268
281
  }
269
282
  };
270
283
 
271
284
  const handleDeleteTrigger = () => {
272
285
  confirm({
273
- title: `Delete ${label}?`,
274
- 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." }),
275
288
  variant: "danger",
276
289
  onConfirm: async () => {
277
290
  try {
@@ -284,7 +297,37 @@ export function DetailView({
284
297
  }
285
298
  });
286
299
  };
300
+ const refreshPreviewUrl = async (docData: any = data) => {
301
+ try {
302
+ const endpoint = mode === "global" ? `/api/globals/${slug}/preview-url` : `/api/${slug}/preview-url`;
303
+ console.log("[Kyro Preview] Calling endpoint:", endpoint, "with data keys:", Object.keys(docData || {}), "documentId:", documentId);
304
+ const res = await apiPost(endpoint, { ...docData, id: documentId }, { autoToast: false });
305
+ console.log("[Kyro Preview] Response:", JSON.stringify(res));
306
+ if (res && (res as any).url) {
307
+ console.log("[Kyro Preview] Setting previewUrl:", (res as any).url);
308
+ setPreviewUrl((res as any).url);
309
+ } else {
310
+ console.warn("[Kyro Preview] No url in response:", res);
311
+ }
312
+ } catch (e: any) {
313
+ console.error("[Kyro Preview] Error:", e.message, e);
314
+ toast.error(e.message || "Failed to generate preview URL");
315
+ }
316
+ };
317
+
318
+ useEffect(() => {
319
+ if (showPreview) {
320
+ refreshPreviewUrl(data);
321
+ }
322
+ }, [showPreview]);
287
323
 
324
+ const togglePreview = () => {
325
+ const nextState = !showPreview;
326
+ setShowPreview(nextState);
327
+ if (nextState) {
328
+ refreshPreviewUrl(data);
329
+ }
330
+ };
288
331
  if (loading) {
289
332
  return (
290
333
  <div className="kyro-detail">
@@ -309,15 +352,15 @@ export function DetailView({
309
352
  <PageHeader
310
353
  back={{ onClick: onBack }}
311
354
  breadcrumbs={[
312
- { label: mode === "global" ? "Globals" : "Collections" },
355
+ { label: mode === "global" ? t("detailView.globals", { defaultValue: "Globals" }) : t("detailView.collections", { defaultValue: "Collections" }) },
313
356
  {
314
357
  label: label,
315
358
  href: mode === "collection" ? `${adminPath}/${slug}` : undefined
316
359
  },
317
- { 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" }) }
318
361
  ]}
319
362
  title={
320
- (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 })))
321
364
  }
322
365
  metadata={[
323
366
  <Badge
@@ -326,7 +369,7 @@ export function DetailView({
326
369
  dot
327
370
  className="text-[10px] font-bold "
328
371
  >
329
- {status}
372
+ {t(`status.${status}`, { defaultValue: status })}
330
373
  </Badge>
331
374
  ]}
332
375
  />
@@ -344,9 +387,7 @@ export function DetailView({
344
387
  onViewHistory={() => {
345
388
  window.dispatchEvent(new CustomEvent('kyro:show-version-history'));
346
389
  }}
347
- onPreview={() =>
348
- window.open(`/preview/${slug}/${documentId}`, "_blank")
349
- }
390
+ onPreview={((collection as any)?.admin?.disablePreview || (global as any)?.admin?.disablePreview) ? undefined : togglePreview}
350
391
  onDelete={handleDeleteTrigger}
351
392
  onBack={onBack}
352
393
  onToggleSidebar={() => window.dispatchEvent(new CustomEvent("toggle-sidebar"))}
@@ -356,16 +397,18 @@ export function DetailView({
356
397
 
357
398
  <div
358
399
  className={
359
- isSingleLayout
360
- ? "w-full pt-4 md:pt-8"
361
- : "w-full mx-auto grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-4 md:gap-8 pt-4 md:pt-0"
400
+ showPreview
401
+ ? "w-full mx-auto grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-8 pt-4 md:pt-0 h-[calc(100vh-140px)]"
402
+ : isSingleLayout
403
+ ? "w-full pt-4 md:pt-8"
404
+ : "w-full mx-auto grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-4 md:gap-8 pt-4 md:pt-0"
362
405
  }
363
406
  >
364
- <div className="space-y-4 md:space-y-8 min-w-0">
407
+ <div className={`space-y-4 md:space-y-8 min-w-0 ${showPreview ? "overflow-y-auto pr-2 pb-20" : ""}`}>
365
408
  <div className="surface-tile p-4 md:p-8">
366
409
  <div className="flex items-center justify-between mb-8 px-1">
367
410
  <h2 className="text-[10px] font-bold tracking-[0.2em] opacity-40">
368
- Core Configuration
411
+ {t("detailView.coreConfiguration", { defaultValue: "Core Configuration" })}
369
412
  </h2>
370
413
  <div className="h-px flex-1 bg-[var(--kyro-border)] ml-6 opacity-30" />
371
414
  </div>
@@ -394,7 +437,7 @@ export function DetailView({
394
437
  disabled={isDeleting || saving}
395
438
  className="kyro-btn kyro-btn-sm text-[var(--kyro-danger)] hover:bg-[var(--kyro-danger)]/10 w-full justify-start mt-2"
396
439
  >
397
- {isDeleting ? "Deleting..." : "Delete Document"}
440
+ {isDeleting ? t("actions.deleting", { defaultValue: "Deleting..." }) : t("actions.deleteDocument", { defaultValue: "Delete Document" })}
398
441
  </button>
399
442
  )}
400
443
  <SplitButton
@@ -409,16 +452,46 @@ export function DetailView({
409
452
  </div>
410
453
  </div>
411
454
 
412
- {!isSingleLayout && (
455
+ {showPreview && (
456
+ <div className="surface-tile flex flex-col overflow-hidden h-full border border-[var(--kyro-border)] rounded-xl animate-in fade-in slide-in-from-right-4 duration-500 shadow-xl bg-white dark:bg-black">
457
+ <div className="bg-[var(--kyro-surface-accent)] border-b border-[var(--kyro-border)] p-2 flex items-center justify-between shrink-0">
458
+ <div className="flex items-center gap-2 pl-2">
459
+ <div className="w-3 h-3 rounded-full bg-red-400/80"></div>
460
+ <div className="w-3 h-3 rounded-full bg-amber-400/80"></div>
461
+ <div className="w-3 h-3 rounded-full bg-green-400/80"></div>
462
+ </div>
463
+ <div className="text-[10px] font-mono opacity-50 truncate max-w-[60%] bg-[var(--kyro-bg)] px-3 py-1 rounded">
464
+ {previewUrl || t("detailView.generatingPreview", { defaultValue: "Generating preview URL..." })}
465
+ </div>
466
+ <div className="w-16"></div>
467
+ </div>
468
+ <div className="flex-1 bg-white relative">
469
+ {!previewUrl ? (
470
+ <div className="absolute inset-0 flex items-center justify-center">
471
+ <Spinner className="w-6 h-6 text-[var(--kyro-primary)]" />
472
+ </div>
473
+ ) : (
474
+ <iframe
475
+ src={previewUrl}
476
+ className="w-full h-full border-0 bg-white"
477
+ title={t("tooltips.preview", { defaultValue: "Preview" })}
478
+ sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
479
+ />
480
+ )}
481
+ </div>
482
+ </div>
483
+ )}
484
+
485
+ {!isSingleLayout && !showPreview && (
413
486
  <div className="space-y-4 md:space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
414
487
  <div className="surface-tile p-4 md:p-8">
415
488
  <h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-4 md:mb-6">
416
- Metadata
489
+ {t("detailView.metadata", { defaultValue: "Metadata" })}
417
490
  </h3>
418
491
  <div className="space-y-4 md:space-y-6">
419
492
  <div className="flex flex-col gap-2">
420
493
  <span className="text-[10px] font-bold tracking-widest opacity-40">
421
- Dynamic Status
494
+ {t("detailView.dynamicStatus", { defaultValue: "Dynamic Status" })}
422
495
  </span>
423
496
  <div>
424
497
  <span
@@ -430,7 +503,7 @@ export function DetailView({
430
503
  </div>
431
504
  <div className="flex flex-col gap-2">
432
505
  <span className="text-[10px] font-bold tracking-widest opacity-40">
433
- Date Created
506
+ {t("detailView.dateCreated", { defaultValue: "Date Created" })}
434
507
  </span>
435
508
  <span className="text-sm font-bold text-[var(--kyro-text-secondary)]">
436
509
  {createdAt
@@ -438,12 +511,12 @@ export function DetailView({
438
511
  dateStyle: "medium",
439
512
  timeStyle: "short",
440
513
  })
441
- : "N/A"}
514
+ : t("common.na", { defaultValue: "N/A" })}
442
515
  </span>
443
516
  </div>
444
517
  <div className="flex flex-col gap-2">
445
518
  <span className="text-[10px] font-bold tracking-widest opacity-40">
446
- Last Modified
519
+ {t("detailView.lastModified", { defaultValue: "Last Modified" })}
447
520
  </span>
448
521
  <span className="text-sm font-bold text-[var(--kyro-text-secondary)]">
449
522
  {updatedAt
@@ -451,13 +524,13 @@ export function DetailView({
451
524
  dateStyle: "medium",
452
525
  timeStyle: "short",
453
526
  })
454
- : "Just now"}
527
+ : t("detailView.justNow", { defaultValue: "Just now" })}
455
528
  </span>
456
529
  </div>
457
530
  {publishedAt && (
458
531
  <div className="flex flex-col gap-2">
459
532
  <span className="text-[10px] font-bold tracking-widest opacity-40">
460
- Public At
533
+ {t("detailView.publicAt", { defaultValue: "Public At" })}
461
534
  </span>
462
535
  <span className="text-sm font-bold text-[var(--kyro-text-secondary)]">
463
536
  {new Date(publishedAt).toLocaleString("en-US", {
@@ -472,7 +545,7 @@ export function DetailView({
472
545
 
473
546
  <div className="surface-tile p-4 md:p-8 bg-[var(--kyro-bg-secondary)]">
474
547
  <h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-3 md:mb-4">
475
- Quick Links
548
+ {t("detailView.quickLinks", { defaultValue: "Quick Links" })}
476
549
  </h3>
477
550
  <div className="space-y-2 md:space-y-3">
478
551
  <button
@@ -481,21 +554,23 @@ export function DetailView({
481
554
  disabled={isDuplicating || saving}
482
555
  className="kyro-btn kyro-btn-sm kyro-btn-ghost w-full justify-start"
483
556
  >
484
- {isDuplicating ? "Duplicating..." : "Duplicate Document"}
485
- </button>
486
- <button
487
- type="button"
488
- onClick={() => window.open(`/preview/${slug}/${documentId}`, "_blank")}
489
- className="kyro-btn kyro-btn-sm kyro-btn-ghost w-full justify-start"
490
- >
491
- View Public Page
557
+ {isDuplicating ? t("actions.duplicating", { defaultValue: "Duplicating..." }) : t("actions.duplicateDocument", { defaultValue: "Duplicate Document" })}
492
558
  </button>
559
+ {previewUrl && (
560
+ <button
561
+ type="button"
562
+ onClick={() => window.open(previewUrl, "_blank")}
563
+ className="kyro-btn kyro-btn-sm kyro-btn-ghost w-full justify-start"
564
+ >
565
+ {t("detailView.openPreview", { defaultValue: "Open Preview in New Tab" })}
566
+ </button>
567
+ )}
493
568
  <button
494
569
  type="button"
495
570
  onClick={handleDeleteTrigger}
496
571
  className="kyro-btn kyro-btn-sm kyro-btn-ghost w-full justify-start text-[var(--kyro-error)] hover:bg-[var(--kyro-danger-bg)]"
497
572
  >
498
- Delete Entry
573
+ {t("actions.deleteEntry", { defaultValue: "Delete Entry" })}
499
574
  </button>
500
575
  </div>
501
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>
@@ -197,6 +197,7 @@ import { javascript } from "@codemirror/lang-javascript";
197
197
  import { json } from "@codemirror/lang-json";
198
198
  import { CompletionContext, autocompletion } from "@codemirror/autocomplete";
199
199
  import { aura } from "@uiw/codemirror-theme-aura";
200
+ import { useTranslation } from "react-i18next";
200
201
 
201
202
  function prettifyQuery(query: string): string {
202
203
  let indent = 0;
@@ -421,6 +422,7 @@ export function GraphQLPlayground({
421
422
  initialVariables,
422
423
  initialShowDocs = false,
423
424
  }: GraphQLPlaygroundProps) {
425
+ const { t } = useTranslation();
424
426
  const [token, setToken] = useState<string>("");
425
427
  const [showToken, setShowToken] = useState(false);
426
428
  const [isConnected, setIsConnected] = useState(false);
@@ -837,7 +839,7 @@ export function GraphQLPlayground({
837
839
  type={showToken ? "text" : "password"}
838
840
  value={token}
839
841
  onChange={(e) => setToken(e.target.value)}
840
- placeholder="API key"
842
+ placeholder={t("fields.apiKey", { defaultValue: "API key" })}
841
843
  className="w-24 px-2 py-1 text-[10px] font-mono bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-md text-[var(--kyro-text-primary)] placeholder:text-[var(--kyro-text-muted)] focus:outline-none focus:border-[var(--kyro-primary)]"
842
844
  />
843
845
  <button onClick={() => setShowToken(!showToken)} className="p-1 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]">
@@ -846,22 +848,22 @@ export function GraphQLPlayground({
846
848
  </div>
847
849
  )}
848
850
  <div className="ml-auto flex items-center gap-1 flex-wrap justify-end">
849
- <button onClick={() => { setShowDocs(!showDocs); setRightTab("docs"); }} className={`p-1.5 rounded-lg transition-all ${rightTab === "docs" && showDocs ? "bg-[var(--kyro-primary)] text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`} title="Schema docs">
851
+ <button onClick={() => { setShowDocs(!showDocs); setRightTab("docs"); }} className={`p-1.5 rounded-lg transition-all ${rightTab === "docs" && showDocs ? "bg-[var(--kyro-primary)] text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`} title={t("tooltips.schemaDocs", { defaultValue: "Schema docs" })}>
850
852
  <Book className="w-3.5 h-3.5" />
851
853
  </button>
852
- <button onClick={() => setRightTab("history")} className={`p-1.5 rounded-lg transition-all ${rightTab === "history" ? "bg-[var(--kyro-primary)] text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`} title="History">
854
+ <button onClick={() => setRightTab("history")} className={`p-1.5 rounded-lg transition-all ${rightTab === "history" ? "bg-[var(--kyro-primary)] text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"}`} title={t("tooltips.history", { defaultValue: "History" })}>
853
855
  <Clock className="w-3.5 h-3.5" />
854
856
  </button>
855
- <button onClick={handlePrettify} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Prettify (Cmd+Shift+P)">
857
+ <button onClick={handlePrettify} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title={t("tooltips.prettifyCmdshiftp", { defaultValue: "Prettify (Cmd+Shift+P)" })}>
856
858
  <Code2 className="w-3.5 h-3.5" />
857
859
  </button>
858
- <button onClick={handleCopyResponse} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Copy response">
860
+ <button onClick={handleCopyResponse} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title={t("tooltips.copyResponse", { defaultValue: "Copy response" })}>
859
861
  {copied ? <Check className="w-3.5 h-3.5 text-[var(--kyro-success)]" /> : <Copy className="w-3.5 h-3.5" />}
860
862
  </button>
861
- <button onClick={handleDownloadResponse} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Download response">
863
+ <button onClick={handleDownloadResponse} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title={t("tooltips.downloadResponse", { defaultValue: "Download response" })}>
862
864
  <Download className="w-3.5 h-3.5" />
863
865
  </button>
864
- <button onClick={handleClearEditor} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-danger)] hover:bg-[var(--kyro-danger-bg)]" title="Clear editor">
866
+ <button onClick={handleClearEditor} className="p-1.5 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-danger)] hover:bg-[var(--kyro-danger-bg)]" title={t("tooltips.clearEditor", { defaultValue: "Clear editor" })}>
865
867
  <Trash2 className="w-3.5 h-3.5" />
866
868
  </button>
867
869
  <div className="h-4 w-px bg-[var(--kyro-border)] mx-1" />
@@ -977,7 +979,7 @@ export function GraphQLPlayground({
977
979
  type="text"
978
980
  value={searchQuery}
979
981
  onChange={(e) => setSearchQuery(e.target.value)}
980
- placeholder="Search types and fields..."
982
+ placeholder={t("fields.searchTypesAndFields", { defaultValue: "Search types and fields..." })}
981
983
  className="w-full pl-7 pr-6 py-1 text-[10px] bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-md text-[var(--kyro-text-primary)] placeholder:text-[var(--kyro-text-muted)] focus:outline-none focus:border-[var(--kyro-primary)]"
982
984
  />
983
985
  {searchQuery && (