@kyro-cms/admin 0.12.4 → 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 (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 +9 -3
  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
@@ -48,6 +48,7 @@ interface BlocksFieldProps {
48
48
  }
49
49
 
50
50
  import { GripVertical } from "../ui/icons";
51
+ import { useTranslation } from "react-i18next";
51
52
 
52
53
  function getBlockPreviewSnippet(
53
54
  data: Record<string, any>,
@@ -82,6 +83,7 @@ const SortableBlockComponent = ({
82
83
  onDuplicate: (id: string) => void;
83
84
  compact?: boolean;
84
85
  }) => {
86
+ const { t } = useTranslation();
85
87
  const {
86
88
  attributes,
87
89
  listeners,
@@ -89,7 +91,7 @@ const SortableBlockComponent = ({
89
91
  transform,
90
92
  transition,
91
93
  isDragging,
92
- } = useSortable({ id: block.id });
94
+ } = useSortable({ id: block.id as string });
93
95
 
94
96
  const { removeBlock, updateBlock } = useBlockActions();
95
97
  const isEditing = editingBlockId === block.id;
@@ -99,19 +101,6 @@ const SortableBlockComponent = ({
99
101
  const nameInputRef = useRef<HTMLInputElement>(null);
100
102
  const [copied, setCopied] = useState(false);
101
103
 
102
- const copyToClipboard = useCallback(async (e: React.MouseEvent) => {
103
- e.stopPropagation();
104
- try {
105
- const { id, ...blockProps } = block;
106
- const payload = JSON.stringify({ __kyro_block: true, type: block.type, blockProps });
107
- await navigator.clipboard.writeText(payload);
108
- setCopied(true);
109
- setTimeout(() => setCopied(false), 2000);
110
- } catch (err) {
111
- console.error("Failed to copy block", err);
112
- }
113
- }, [block]);
114
-
115
104
  useEffect(() => {
116
105
  if (editingName && nameInputRef.current) {
117
106
  nameInputRef.current.focus();
@@ -211,7 +200,7 @@ const SortableBlockComponent = ({
211
200
  setEditingName(true);
212
201
  }}
213
202
  className="p-0.5 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
214
- title="Rename"
203
+ title={t("tooltips.rename", { defaultValue: "Rename" })}
215
204
  >
216
205
  <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
217
206
  </button>
@@ -222,18 +211,11 @@ const SortableBlockComponent = ({
222
211
  onDuplicate(block.id as string);
223
212
  }}
224
213
  className="p-0.5 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
225
- title="Duplicate in place"
214
+ title={t("tooltips.duplicateInPlace", { defaultValue: "Duplicate in place" })}
226
215
  >
227
216
  <Copy className="w-3 h-3" />
228
217
  </button>
229
- <button
230
- type="button"
231
- onClick={copyToClipboard}
232
- className="p-0.5 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
233
- title="Copy Block to Clipboard"
234
- >
235
- {copied ? <Check className="w-3 h-3 text-green-500" /> : <ClipboardCopy className="w-3 h-3" />}
236
- </button>
218
+
237
219
  <button
238
220
  type="button"
239
221
  onClick={(e) => {
@@ -241,7 +223,7 @@ const SortableBlockComponent = ({
241
223
  setShowDeleteConfirm(true);
242
224
  }}
243
225
  className="p-0.5 hover:bg-[var(--kyro-danger-bg)] hover:text-[var(--kyro-danger)] rounded text-[var(--kyro-text-muted)] transition-colors"
244
- title="Remove"
226
+ title={t("tooltips.remove", { defaultValue: "Remove" })}
245
227
  >
246
228
  <X className="w-3 h-3" />
247
229
  </button>
@@ -308,16 +290,16 @@ const SortableBlockComponent = ({
308
290
  )}
309
291
  </div>
310
292
  )}
311
- {blockSchema?.admin?.description && (
293
+ {!!blockSchema?.admin?.description && (
312
294
  <div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5 truncate opacity-80">
313
- {blockSchema.admin.description}
295
+ {blockSchema.admin.description as string}
314
296
  </div>
315
297
  )}
316
298
  </div>
317
299
 
318
- {block.children && Array.isArray(block.children) && block.children.length > 0 && (
300
+ {!!block.children && Array.isArray(block.children) && (block.children as any[]).length > 0 && (
319
301
  <span className="text-[10px] bg-[var(--kyro-surface-accent)] px-2 py-0.5 rounded text-[var(--kyro-text-muted)] font-medium">
320
- {block.children.length} nested
302
+ {(block.children as any[]).length} nested
321
303
  </span>
322
304
  )}
323
305
 
@@ -354,7 +336,7 @@ const SortableBlockComponent = ({
354
336
  setEditingName(true);
355
337
  }}
356
338
  className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
357
- title="Rename Block"
339
+ title={t("tooltips.renameBlock", { defaultValue: "Rename Block" })}
358
340
  >
359
341
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
360
342
  </button>
@@ -365,18 +347,11 @@ const SortableBlockComponent = ({
365
347
  onDuplicate(block.id as string);
366
348
  }}
367
349
  className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
368
- title="Duplicate Block"
350
+ title={t("tooltips.duplicateBlock", { defaultValue: "Duplicate Block" })}
369
351
  >
370
352
  <Copy className="w-3.5 h-3.5" />
371
353
  </button>
372
- <button
373
- type="button"
374
- onClick={copyToClipboard}
375
- className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
376
- title="Copy Block to Clipboard"
377
- >
378
- {copied ? <Check className="w-3.5 h-3.5 text-green-500" /> : <ClipboardCopy className="w-3.5 h-3.5" />}
379
- </button>
354
+
380
355
  <button
381
356
  type="button"
382
357
  onClick={(e) => {
@@ -384,7 +359,7 @@ const SortableBlockComponent = ({
384
359
  setShowDeleteConfirm(true);
385
360
  }}
386
361
  className="p-1 hover:bg-[var(--kyro-danger-bg)] hover:text-[var(--kyro-danger)] rounded text-[var(--kyro-text-muted)] transition-colors"
387
- title="Remove Block"
362
+ title={t("tooltips.removeBlock", { defaultValue: "Remove Block" })}
388
363
  >
389
364
  <X className="w-3.5 h-3.5" />
390
365
  </button>
@@ -504,8 +479,8 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
504
479
  if (!deepEqual(valueArray, lastValueArray)) {
505
480
  const valueArrayCopy = [...valueArray];
506
481
  prevBlocksLengthRef.current = valueArrayCopy.length;
507
- prevBlockIdsRef.current = new Set(valueArrayCopy.map((b: Record<string, unknown>) => b.id));
508
- store.getState().setBlocks(valueArrayCopy);
482
+ prevBlockIdsRef.current = new Set(valueArrayCopy.map((b: any) => b.id as string));
483
+ store.getState().setBlocks(valueArrayCopy as any);
509
484
  lastValueRef.current = valueArrayCopy;
510
485
  isInitializedRef.current = true;
511
486
  } else if (valueArray.length === 0 && !isInitializedRef.current) {
@@ -571,32 +546,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
571
546
  setIsDrawerOpen(false);
572
547
  }, [field.blocks, blocks, store]);
573
548
 
574
- const handleContainerPaste = useCallback((e: React.ClipboardEvent<HTMLDivElement>) => {
575
- const activeEl = document.activeElement;
576
- if (
577
- activeEl &&
578
- (activeEl.tagName === "INPUT" ||
579
- activeEl.tagName === "TEXTAREA" ||
580
- activeEl.getAttribute("contenteditable") === "true")
581
- ) {
582
- return;
583
- }
584
-
585
- const text = e.clipboardData?.getData("text");
586
- if (!text) return;
587
549
 
588
- try {
589
- const parsed = JSON.parse(text);
590
- if (parsed && parsed.__kyro_block) {
591
- e.preventDefault();
592
- e.stopPropagation();
593
- handlePasteBlock(parsed);
594
- toast.success(`Block pasted: ${parsed.type}`);
595
- }
596
- } catch {
597
- // Ignore
598
- }
599
- }, [handlePasteBlock]);
600
550
 
601
551
  const duplicateBlock = useCallback(
602
552
  (blockId: string) => {
@@ -693,14 +643,14 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
693
643
  const activeBlock = activeDrag
694
644
  ? dynamicCategories
695
645
  .flatMap((cat) => cat.blocks)
696
- .find((b) => `drawer-${b.type}` === activeDrag.id) ||
646
+ .find((b: any) => `drawer-${b.type}` === activeDrag.id) ||
697
647
  blocks.find((b) => b.id === activeDrag.id)
698
648
  : null;
699
649
 
700
650
  const activeBlockLabel = activeBlock
701
- ? "label" in activeBlock
651
+ ? "label" in (activeBlock as object)
702
652
  ? (activeBlock as Record<string, unknown>).label
703
- : activeBlock.type
653
+ : (activeBlock as any).type
704
654
  : "Block";
705
655
 
706
656
  const borderClass = getBorderClass();
@@ -720,7 +670,6 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
720
670
  <BlocksContext.Provider value={storeRef.current}>
721
671
  <div
722
672
  className="kyro-blocks-field"
723
- onPaste={handleContainerPaste}
724
673
  tabIndex={0}
725
674
  style={{ outline: "none" }}
726
675
  >
@@ -731,7 +680,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
731
680
  onDragEnd={handleDragEnd}
732
681
  >
733
682
  <div className={pickerMode === "dropdown" ? "mb-4" : "flex items-center justify-between mb-2"}>
734
- <label className={`kyro-form-label ${pickerMode === "dropdown" ? "block mb-2" : ""}`}>{field.label || field.name}</label>
683
+ <label className={`kyro-form-label ${pickerMode === "dropdown" ? "block mb-2" : ""}`}>{(field.label || field.name) as string}</label>
735
684
  {pickerMode === "dropdown" ? (
736
685
  <div ref={dropdownRef} className="relative w-full">
737
686
  <button
@@ -756,7 +705,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
756
705
  {category.title}
757
706
  </div>
758
707
  )}
759
- {category.blocks.map((block) => (
708
+ {(category as any).blocks.map((block: any) => (
760
709
  <button
761
710
  key={block.slug}
762
711
  type="button"
@@ -809,7 +758,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
809
758
  {category.title}
810
759
  </h3>
811
760
  <div className="grid grid-cols-3 gap-2">
812
- {category.blocks.map((block) => (
761
+ {(category as any).blocks.map((block: any) => (
813
762
  <DraggableBlockType
814
763
  key={block.slug}
815
764
  block={{
@@ -869,7 +818,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
869
818
  <DragOverlay>
870
819
  {activeDrag && activeBlock && (
871
820
  <div className="bg-[var(--kyro-surface)] border border-[var(--kyro-primary)] rounded-md p-3 shadow-lg">
872
- {(activeBlock as Record<string, unknown>).label || activeBlock.type || "Block"}
821
+ {(activeBlock as Record<string, unknown>).label as string || (activeBlock as any).type || "Block"}
873
822
  </div>
874
823
  )}
875
824
  </DragOverlay>
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { ExternalLink } from "../ui/icons";
3
+ import { useTranslation } from "react-i18next";
3
4
 
4
5
  interface ButtonFieldProps {
5
6
  text?: string;
@@ -14,6 +15,7 @@ export const ButtonField: React.FC<ButtonFieldProps> = ({
14
15
  onChange,
15
16
  compact = false,
16
17
  }) => {
18
+ const { t } = useTranslation();
17
19
  const inputClass = compact
18
20
  ? "flex-1 px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
19
21
  : "flex-1 px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
@@ -25,7 +27,7 @@ export const ButtonField: React.FC<ButtonFieldProps> = ({
25
27
  value={text}
26
28
  onChange={(e) => onChange("text", e.target.value)}
27
29
  className={inputClass}
28
- placeholder="Button text..."
30
+ placeholder={t("fields.buttonText", { defaultValue: "Button text..." })}
29
31
  />
30
32
  <span className="text-[var(--kyro-text-muted)] text-xs">→</span>
31
33
  <input
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { useTranslation } from "react-i18next";
2
3
 
3
4
  interface CardFieldProps {
4
5
  title?: string;
@@ -19,6 +20,7 @@ export const CardField: React.FC<CardFieldProps> = ({
19
20
  onChange,
20
21
  compact = false,
21
22
  }) => {
23
+ const { t } = useTranslation();
22
24
  const inputClass = compact
23
25
  ? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
24
26
  : "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
@@ -34,20 +36,20 @@ export const CardField: React.FC<CardFieldProps> = ({
34
36
  value={title}
35
37
  onChange={(e) => onChange("title", e.target.value)}
36
38
  className={`${inputClass} font-bold text-base`}
37
- placeholder="Card title..."
39
+ placeholder={t("fields.cardTitle", { defaultValue: "Card title..." })}
38
40
  />
39
41
  <textarea
40
42
  value={description}
41
43
  onChange={(e) => onChange("description", e.target.value)}
42
44
  className={textareaClass}
43
- placeholder="Card description..."
45
+ placeholder={t("fields.cardDescription", { defaultValue: "Card description..." })}
44
46
  />
45
47
  <input
46
48
  type="text"
47
49
  value={icon}
48
50
  onChange={(e) => onChange("icon", e.target.value)}
49
51
  className={inputClass}
50
- placeholder="Icon (emoji or name)..."
52
+ placeholder={t("fields.iconEmojiOrName", { defaultValue: "Icon (emoji or name)..." })}
51
53
  />
52
54
  <div className="flex items-center gap-2">
53
55
  <input
@@ -55,7 +57,7 @@ export const CardField: React.FC<CardFieldProps> = ({
55
57
  value={linkText}
56
58
  onChange={(e) => onChange("linkText", e.target.value)}
57
59
  className="flex-1 px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
58
- placeholder="Link text..."
60
+ placeholder={t("fields.linkText", { defaultValue: "Link text..." })}
59
61
  />
60
62
  <span className="text-[var(--kyro-text-muted)] text-xs">→</span>
61
63
  <input
@@ -1,6 +1,4 @@
1
- import React, { useState, useCallback } from "react";
2
- import type { Field } from "@kyro-cms/core/client";
3
- import { Copy, ClipboardPaste, Check } from "../ui/icons";
1
+ import React from "react";
4
2
  import type { Field } from "@kyro-cms/core/client";
5
3
 
6
4
  interface GroupLayoutProps {
@@ -21,34 +19,6 @@ export function GroupLayout({
21
19
  renderField,
22
20
  }: GroupLayoutProps) {
23
21
  const groupData = value || {};
24
- const [copied, setCopied] = useState(false);
25
-
26
- const handleCopy = useCallback(async (e: React.MouseEvent) => {
27
- e.preventDefault();
28
- e.stopPropagation();
29
- try {
30
- const payload = JSON.stringify({ __kyro_container: true, value: groupData });
31
- await navigator.clipboard.writeText(payload);
32
- setCopied(true);
33
- setTimeout(() => setCopied(false), 2000);
34
- } catch (err) {
35
- console.error("Failed to copy", err);
36
- }
37
- }, [groupData]);
38
-
39
- const handlePaste = useCallback(async (e: React.MouseEvent) => {
40
- e.preventDefault();
41
- e.stopPropagation();
42
- try {
43
- const text = await navigator.clipboard.readText();
44
- const parsed = JSON.parse(text);
45
- if (parsed && (parsed.__kyro_group || parsed.__kyro_container) && parsed.value) {
46
- onChange({ ...groupData, ...parsed.value });
47
- }
48
- } catch (err) {
49
- console.error("Failed to paste", err);
50
- }
51
- }, [groupData, onChange]);
52
22
 
53
23
  return (
54
24
  <div className="kyro-form-group border border-[var(--kyro-border)] rounded-[var(--kyro-radius-lg)] p-6 bg-[var(--kyro-surface-accent)]/30">
@@ -56,24 +26,7 @@ export function GroupLayout({
56
26
  <h3 className="text-sm font-bold tracking-widest text-[var(--kyro-text-primary)]">
57
27
  {field.label || field.name}
58
28
  </h3>
59
- <div className="flex items-center gap-1">
60
- <button
61
- type="button"
62
- onClick={handleCopy}
63
- className="p-1.5 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-colors rounded hover:bg-[var(--kyro-surface)]"
64
- title="Copy Group Data"
65
- >
66
- {copied ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
67
- </button>
68
- <button
69
- type="button"
70
- onClick={handlePaste}
71
- className="p-1.5 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-colors rounded hover:bg-[var(--kyro-surface)]"
72
- title="Paste Group Data"
73
- >
74
- <ClipboardPaste className="w-4 h-4" />
75
- </button>
76
- </div>
29
+
77
30
  </div>
78
31
  <div className={field.admin?.inline ? "flex items-start gap-4" : "space-y-6"}>
79
32
  {(field as Field & { fields?: Field[] }).fields.map((f: Field) =>
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { useTranslation } from "react-i18next";
2
3
 
3
4
  interface HeadingFieldProps {
4
5
  text?: string;
@@ -11,6 +12,7 @@ export const HeadingField: React.FC<HeadingFieldProps> = ({
11
12
  onChange,
12
13
  compact = false,
13
14
  }) => {
15
+ const { t } = useTranslation();
14
16
  const inputClass = compact
15
17
  ? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
16
18
  : "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
@@ -22,7 +24,7 @@ export const HeadingField: React.FC<HeadingFieldProps> = ({
22
24
  value={text}
23
25
  onChange={(e) => onChange("text", e.target.value)}
24
26
  className={inputClass}
25
- placeholder="Enter heading text..."
27
+ placeholder={t("fields.enterHeadingText", { defaultValue: "Enter heading text..." })}
26
28
  />
27
29
  </div>
28
30
  );
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { useTranslation } from "react-i18next";
2
3
 
3
4
  interface HeadingSubheadingFieldProps {
4
5
  heading?: string;
@@ -13,6 +14,7 @@ export const HeadingSubheadingField: React.FC<HeadingSubheadingFieldProps> = ({
13
14
  onChange,
14
15
  compact = false,
15
16
  }) => {
17
+ const { t } = useTranslation();
16
18
  const inputClass = compact
17
19
  ? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
18
20
  : "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
@@ -28,13 +30,13 @@ export const HeadingSubheadingField: React.FC<HeadingSubheadingFieldProps> = ({
28
30
  value={heading}
29
31
  onChange={(e) => onChange("title", e.target.value)}
30
32
  className={`${inputClass} font-bold text-base`}
31
- placeholder="Heading..."
33
+ placeholder={t("fields.heading", { defaultValue: "Heading..." })}
32
34
  />
33
35
  <textarea
34
36
  value={subheading}
35
37
  onChange={(e) => onChange("subtitle", e.target.value)}
36
38
  className={textareaClass}
37
- placeholder="Subheading..."
39
+ placeholder={t("fields.subheading", { defaultValue: "Subheading..." })}
38
40
  />
39
41
  </div>
40
42
  );
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { useTranslation } from "react-i18next";
2
3
 
3
4
  interface HeroFieldProps {
4
5
  heading?: string;
@@ -17,6 +18,7 @@ export const HeroField: React.FC<HeroFieldProps> = ({
17
18
  onChange,
18
19
  compact = false,
19
20
  }) => {
21
+ const { t } = useTranslation();
20
22
  const inputClass = compact
21
23
  ? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
22
24
  : "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
@@ -33,13 +35,13 @@ export const HeroField: React.FC<HeroFieldProps> = ({
33
35
  value={heading}
34
36
  onChange={(e) => onChange("title", e.target.value)}
35
37
  className={`${inputClass} font-bold text-base`}
36
- placeholder="Hero heading..."
38
+ placeholder={t("fields.heroHeading", { defaultValue: "Hero heading..." })}
37
39
  />
38
40
  <textarea
39
41
  value={subheading}
40
42
  onChange={(e) => onChange("subtitle", e.target.value)}
41
43
  className={textareaClass}
42
- placeholder="Hero subheading..."
44
+ placeholder={t("fields.heroSubheading", { defaultValue: "Hero subheading..." })}
43
45
  />
44
46
  <div className="flex items-center gap-2">
45
47
  <input
@@ -47,7 +49,7 @@ export const HeroField: React.FC<HeroFieldProps> = ({
47
49
  value={ctaText}
48
50
  onChange={(e) => onChange("ctaText", e.target.value)}
49
51
  className="flex-1 px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
50
- placeholder="CTA text..."
52
+ placeholder={t("fields.ctaText", { defaultValue: "CTA text..." })}
51
53
  />
52
54
  <span className="text-[var(--kyro-text-muted)] text-xs">→</span>
53
55
  <input
@@ -69,13 +71,13 @@ export const HeroField: React.FC<HeroFieldProps> = ({
69
71
  value={heading}
70
72
  onChange={(e) => onChange("title", e.target.value)}
71
73
  className={`${inputClass} font-bold text-base`}
72
- placeholder="Hero heading..."
74
+ placeholder={t("fields.heroHeading", { defaultValue: "Hero heading..." })}
73
75
  />
74
76
  <textarea
75
77
  value={subheading}
76
78
  onChange={(e) => onChange("subtitle", e.target.value)}
77
79
  className={textareaClass}
78
- placeholder="Hero subheading..."
80
+ placeholder={t("fields.heroSubheading", { defaultValue: "Hero subheading..." })}
79
81
  />
80
82
  <div className="flex items-center gap-2">
81
83
  <input
@@ -83,7 +85,7 @@ export const HeroField: React.FC<HeroFieldProps> = ({
83
85
  value={ctaText}
84
86
  onChange={(e) => onChange("ctaText", e.target.value)}
85
87
  className="flex-1 px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
86
- placeholder="CTA text..."
88
+ placeholder={t("fields.ctaText", { defaultValue: "CTA text..." })}
87
89
  />
88
90
  <span className="text-[var(--kyro-text-muted)] text-xs">→</span>
89
91
  <input
@@ -89,7 +89,7 @@ export const JSONField: React.FC<JSONFieldProps> = ({
89
89
  return (
90
90
  <div className="kyro-form-field">
91
91
  <label className="kyro-form-label">
92
- {field.label || field.name}
92
+ {(field.label || field.name) as string}
93
93
  {field.required && (
94
94
  <span className="kyro-form-label-required">*</span>
95
95
  )}
@@ -103,7 +103,7 @@ export const JSONField: React.FC<JSONFieldProps> = ({
103
103
  <div className="kyro-form-field">
104
104
  <div className="flex items-center justify-between mb-2">
105
105
  <label className="kyro-form-label">
106
- {field.label || field.name}
106
+ {(field.label || field.name) as string}
107
107
  {field.required && (
108
108
  <span className="kyro-form-label-required">*</span>
109
109
  )}
@@ -199,8 +199,8 @@ export const JSONField: React.FC<JSONFieldProps> = ({
199
199
  )}
200
200
  </div>
201
201
 
202
- {field.admin?.description && !error && !parseError && (
203
- <p className="kyro-form-help">{field.admin.description}</p>
202
+ {!!field.admin?.description && !error && !parseError && (
203
+ <p className="kyro-form-help">{(field as any).admin.description}</p>
204
204
  )}
205
205
  {(error || parseError) && (
206
206
  <p className="kyro-form-error">{error || parseError}</p>
@@ -215,14 +215,13 @@ const TreeView: React.FC<{ data: TreeNode }> = ({ data }) => {
215
215
 
216
216
  const renderValue = (node: TreeNode) => {
217
217
  if (node.type === "object") {
218
- const entries = Object.entries(node.value);
219
218
  return (
220
219
  <div className="pl-4 border-l border-[var(--kyro-border)]">
221
- {entries.map(([key, val]) => (
220
+ {Object.entries(node.value as object).map(([key, child]) => (
222
221
  <TreeNodeView
223
222
  key={key}
224
223
  name={key}
225
- value={val}
224
+ value={child}
226
225
  path={[...node.path, key]}
227
226
  />
228
227
  ))}
@@ -232,7 +231,7 @@ const TreeView: React.FC<{ data: TreeNode }> = ({ data }) => {
232
231
  if (node.type === "array") {
233
232
  return (
234
233
  <div className="pl-4 border-l border-[var(--kyro-border)]">
235
- {node.value.map((item: unknown, index: number) => (
234
+ {(node.value as any[]).map((item: unknown, index: number) => (
236
235
  <TreeNodeView
237
236
  key={index}
238
237
  name={String(index)}
@@ -269,11 +268,11 @@ const TreeView: React.FC<{ data: TreeNode }> = ({ data }) => {
269
268
 
270
269
  const getPreview = (item: TreeNode) => {
271
270
  if (item.type === "object") {
272
- const keys = Object.keys(item.value);
271
+ const keys = Object.keys(item.value as object);
273
272
  return `{${keys.length} ${keys.length === 1 ? "key" : "keys"}}`;
274
273
  }
275
274
  if (item.type === "array") {
276
- return `[${item.value.length} ${item.value.length === 1 ? "item" : "items"}]`;
275
+ return `[${(item.value as any[]).length} ${(item.value as any[]).length === 1 ? "item" : "items"}]`;
277
276
  }
278
277
  return null;
279
278
  };
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { ExternalLink } from "../ui/icons";
3
+ import { useTranslation } from "react-i18next";
3
4
 
4
5
  interface LinkFieldProps {
5
6
  text?: string;
@@ -14,6 +15,7 @@ export const LinkField: React.FC<LinkFieldProps> = ({
14
15
  onChange,
15
16
  compact = false,
16
17
  }) => {
18
+ const { t } = useTranslation();
17
19
  return (
18
20
  <div className={compact ? "flex items-center gap-2" : "space-y-2"}>
19
21
  {compact ? (
@@ -23,7 +25,7 @@ export const LinkField: React.FC<LinkFieldProps> = ({
23
25
  value={text}
24
26
  onChange={(e) => onChange("text", e.target.value)}
25
27
  className="flex-1 min-w-0 px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
26
- placeholder="Link text..."
28
+ placeholder={t("fields.linkText", { defaultValue: "Link text..." })}
27
29
  />
28
30
  <span className="text-[var(--kyro-text-muted)] text-xs">→</span>
29
31
  <input
@@ -52,7 +54,7 @@ export const LinkField: React.FC<LinkFieldProps> = ({
52
54
  value={text}
53
55
  onChange={(e) => onChange("text", e.target.value)}
54
56
  className="w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
55
- placeholder="Link text..."
57
+ placeholder={t("fields.linkText", { defaultValue: "Link text..." })}
56
58
  />
57
59
  <input
58
60
  type="url"
@@ -14,6 +14,7 @@ import {
14
14
  verticalListSortingStrategy,
15
15
  } from "@dnd-kit/sortable";
16
16
  import { CSS } from "@dnd-kit/utilities";
17
+ import { useTranslation } from "react-i18next";
17
18
 
18
19
  interface ListFieldProps {
19
20
  items?: string[];
@@ -79,6 +80,7 @@ export const ListField: React.FC<ListFieldProps> = ({
79
80
  onChange,
80
81
  compact = false,
81
82
  }) => {
83
+ const { t } = useTranslation();
82
84
  const [inputValue, setInputValue] = React.useState("");
83
85
  const [mappedItems, setMappedItems] = React.useState<{ id: string; text: string }[]>([]);
84
86
 
@@ -174,7 +176,7 @@ export const ListField: React.FC<ListFieldProps> = ({
174
176
  onChange={(e) => setInputValue(e.target.value)}
175
177
  onKeyDown={handleKeyDown}
176
178
  className={inputClass}
177
- placeholder="Type and press Enter to add..."
179
+ placeholder={t("fields.typeAndPressEnter", { defaultValue: "Type and press Enter to add..." })}
178
180
  />
179
181
  </div>
180
182
  </DndContext>