@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
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useCallback, useMemo, useEffect } from "react";
2
2
  import type { MarkdownField as MarkdownFieldType } from "@kyro-cms/core/client";
3
+ import { useTranslation } from "react-i18next";
3
4
 
4
5
  interface MarkdownFieldProps {
5
6
  field: MarkdownFieldType;
@@ -109,6 +110,7 @@ export const MarkdownField: React.FC<MarkdownFieldProps> = ({
109
110
  error,
110
111
  disabled,
111
112
  }) => {
113
+ const { t } = useTranslation();
112
114
  const [showPreview, setShowPreview] = useState(false);
113
115
  const [isMounted, setIsMounted] = useState(false);
114
116
 
@@ -204,23 +206,7 @@ export const MarkdownField: React.FC<MarkdownFieldProps> = ({
204
206
  rows={12}
205
207
  style={{ pointerEvents: "auto", cursor: "text", zIndex: 100 }}
206
208
  className={`w-full p-4 font-mono text-sm resize-y focus:outline-none bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]`}
207
- placeholder="Enter markdown content...
208
-
209
- # Heading 1
210
- ## Heading 2
211
-
212
- **Bold text** and *italic text*
213
-
214
- - List item 1
215
- - List item 2
216
-
217
- [Link text](https://example.com)
218
-
219
- `inline code`
220
-
221
- ```
222
- code block
223
- ```"
209
+ placeholder={t("fields.enterMarkdownContentHeading", { defaultValue: "Enter markdown content...\n\n# Heading 1\n## Heading 2\n\n**Bold text** and *italic text*\n\n- List item 1\n- List item 2\n\n[Link text](https://example.com)\n\n`inline code`\n\n```\ncode block\n```" })}
224
210
  />
225
211
  ) : (
226
212
  <div
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
2
2
  import { Search, Loader2, X } from "../ui/icons";
3
3
  import { apiGet, buildSearchQuery } from "../../lib/api";
4
4
  import { EmptyState } from "../ui/EmptyState";
5
+ import { useTranslation } from "react-i18next";
5
6
 
6
7
  interface RelationshipBlockFieldProps {
7
8
  relationTo?: string;
@@ -22,9 +23,10 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
22
23
  onChange,
23
24
  compact = false,
24
25
  }) => {
26
+ const { t } = useTranslation();
25
27
  const [isOpen, setIsOpen] = useState(false);
26
28
  const [search, setSearch] = useState("");
27
- const [options, setOptions] = useState<Record<string, unknown>[]>([]);
29
+ const [options, setOptions] = useState<any[]>([]);
28
30
  const [loading, setLoading] = useState(false);
29
31
  const [collections, setCollections] = useState<string[]>([]);
30
32
  const [loadingCollections, setLoadingCollections] = useState(true);
@@ -33,7 +35,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
33
35
  apiGet("/api/collections")
34
36
  .then((data) => {
35
37
  setCollections(
36
- (data.collections || []).map((c: Record<string, unknown>) => c.slug || c.name || c),
38
+ (data.collections || []).map((c: any) => c.slug || c.name || c),
37
39
  );
38
40
  setLoadingCollections(false);
39
41
  })
@@ -56,7 +58,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
56
58
  if (isOpen) fetchOptions(search);
57
59
  }, [isOpen, search, relationTo, labelField]);
58
60
 
59
- const getLabel = (opt: Record<string, unknown>) => {
61
+ const getLabel = (opt: any) => {
60
62
  return (
61
63
  opt?.[labelField] ||
62
64
  opt?.title ||
@@ -71,7 +73,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
71
73
 
72
74
  const activeIds = hasMany ? selectedIds : selectedId ? [selectedId] : [];
73
75
 
74
- const handleSelect = (opt: Record<string, unknown>) => {
76
+ const handleSelect = (opt: any) => {
75
77
  if (hasMany) {
76
78
  if (activeIds.includes(opt.id)) {
77
79
  onChange(
@@ -172,7 +174,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
172
174
  Loading...
173
175
  </div>
174
176
  ) : options.length === 0 ? (
175
- <EmptyState title="No results found" />
177
+ <EmptyState title={t("tooltips.noResultsFound", { defaultValue: "No results found" })} />
176
178
  ) : (
177
179
  <div className="py-1">
178
180
  {options.map((opt) => (
@@ -16,6 +16,7 @@ import {
16
16
  horizontalListSortingStrategy,
17
17
  } from "@dnd-kit/sortable";
18
18
  import { CSS } from "@dnd-kit/utilities";
19
+ import { useTranslation } from "react-i18next";
19
20
 
20
21
  interface RelationshipFieldProps {
21
22
  field: {
@@ -121,6 +122,7 @@ export function RelationshipField({
121
122
  error,
122
123
  disabled,
123
124
  }: RelationshipFieldProps) {
125
+ const { t } = useTranslation();
124
126
  const [isOpen, setIsOpen] = useState(false);
125
127
  const [search, setSearch] = useState("");
126
128
  const [options, setOptions] = useState<ResolvedDoc[]>([]);
@@ -452,7 +454,7 @@ export function RelationshipField({
452
454
  Loading...
453
455
  </div>
454
456
  ) : options.length === 0 ? (
455
- <EmptyState title="No results found" />
457
+ <EmptyState title={t("tooltips.noResultsFound", { defaultValue: "No results found" })} />
456
458
  ) : (
457
459
  <div className="py-1">
458
460
  {options.map((opt) => (
@@ -45,6 +45,7 @@ import {
45
45
  Minimize2,
46
46
  ChevronDown,
47
47
  } from "lucide-react";
48
+ import { useTranslation } from "react-i18next";
48
49
 
49
50
  interface RichTextFieldProps {
50
51
  field: Field;
@@ -78,6 +79,7 @@ const MenuBar = ({
78
79
  setIsExpanded: (expanded: boolean) => void;
79
80
  onOpenMediaPicker: () => void;
80
81
  }) => {
82
+ const { t } = useTranslation();
81
83
  const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
82
84
  const menuBarRef = useRef<HTMLDivElement>(null);
83
85
 
@@ -196,14 +198,14 @@ const MenuBar = ({
196
198
  <ToolbarButton
197
199
  onClick={() => editor.chain().focus().undo().run()}
198
200
  disabled={!editor.can().chain().focus().undo().run()}
199
- title="Undo (Ctrl+Z)"
201
+ title={t("tooltips.undoCtrlz", { defaultValue: "Undo (Ctrl+Z)" })}
200
202
  >
201
203
  <Undo size={12} />
202
204
  </ToolbarButton>
203
205
  <ToolbarButton
204
206
  onClick={() => editor.chain().focus().redo().run()}
205
207
  disabled={!editor.can().chain().focus().redo().run()}
206
- title="Redo (Ctrl+Y)"
208
+ title={t("tooltips.redoCtrly", { defaultValue: "Redo (Ctrl+Y)" })}
207
209
  >
208
210
  <Redo size={12} />
209
211
  </ToolbarButton>
@@ -215,7 +217,7 @@ const MenuBar = ({
215
217
  onClick={() => editor.chain().focus().toggleBold().run()}
216
218
  disabled={!editor.can().chain().focus().toggleBold().run()}
217
219
  isActive={editor.isActive("bold")}
218
- title="Bold (Ctrl+B)"
220
+ title={t("tooltips.boldCtrlb", { defaultValue: "Bold (Ctrl+B)" })}
219
221
  >
220
222
  <Bold size={12} />
221
223
  </ToolbarButton>
@@ -223,7 +225,7 @@ const MenuBar = ({
223
225
  onClick={() => editor.chain().focus().toggleItalic().run()}
224
226
  disabled={!editor.can().chain().focus().toggleItalic().run()}
225
227
  isActive={editor.isActive("italic")}
226
- title="Italic (Ctrl+I)"
228
+ title={t("tooltips.italicCtrli", { defaultValue: "Italic (Ctrl+I)" })}
227
229
  >
228
230
  <Italic size={12} />
229
231
  </ToolbarButton>
@@ -231,7 +233,7 @@ const MenuBar = ({
231
233
  onClick={() => editor.chain().focus().toggleUnderline().run()}
232
234
  disabled={!editor.can().chain().focus().toggleUnderline().run()}
233
235
  isActive={editor.isActive("underline")}
234
- title="Underline (Ctrl+U)"
236
+ title={t("tooltips.underlineCtrlu", { defaultValue: "Underline (Ctrl+U)" })}
235
237
  >
236
238
  <UnderlineIcon size={12} />
237
239
  </ToolbarButton>
@@ -239,7 +241,7 @@ const MenuBar = ({
239
241
  onClick={() => editor.chain().focus().toggleStrike().run()}
240
242
  disabled={!editor.can().chain().focus().toggleStrike().run()}
241
243
  isActive={editor.isActive("strike")}
242
- title="Strikethrough"
244
+ title={t("tooltips.strikethrough", { defaultValue: "Strikethrough" })}
243
245
  >
244
246
  <Strikethrough size={12} />
245
247
  </ToolbarButton>
@@ -247,14 +249,14 @@ const MenuBar = ({
247
249
  onClick={() => editor.chain().focus().toggleCode().run()}
248
250
  disabled={!editor.can().chain().focus().toggleCode().run()}
249
251
  isActive={editor.isActive("code")}
250
- title="Inline Code"
252
+ title={t("tooltips.inlineCode", { defaultValue: "Inline Code" })}
251
253
  >
252
254
  <Code size={12} />
253
255
  </ToolbarButton>
254
256
  <ToolbarButton
255
257
  onClick={() => editor.chain().focus().toggleHighlight().run()}
256
258
  isActive={editor.isActive("highlight")}
257
- title="Highlight Text"
259
+ title={t("tooltips.highlightText", { defaultValue: "Highlight Text" })}
258
260
  >
259
261
  <Highlighter size={12} />
260
262
  </ToolbarButton>
@@ -263,7 +265,7 @@ const MenuBar = ({
263
265
  <div className="relative flex items-center justify-center">
264
266
  <ToolbarButton
265
267
  onClick={() => toggleDropdown("color")}
266
- title="Text Color"
268
+ title={t("tooltips.textColor", { defaultValue: "Text Color" })}
267
269
  isActive={activeDropdown === "color" || editor.isActive("textStyle")}
268
270
  >
269
271
  <Palette size={12} />
@@ -298,7 +300,7 @@ const MenuBar = ({
298
300
  <DropdownTrigger
299
301
  onClick={() => toggleDropdown("heading")}
300
302
  isActive={activeDropdown === "heading"}
301
- title="Heading hierarchy"
303
+ title={t("tooltips.headingHierarchy", { defaultValue: "Heading hierarchy" })}
302
304
  >
303
305
  <span className="font-medium text-[11px] leading-none min-w-[70px] text-left">
304
306
  {getHeadingLabel()}
@@ -340,7 +342,7 @@ const MenuBar = ({
340
342
  <DropdownTrigger
341
343
  onClick={() => toggleDropdown("lists")}
342
344
  isActive={activeDropdown === "lists"}
343
- title="List Types"
345
+ title={t("tooltips.listTypes", { defaultValue: "List Types" })}
344
346
  >
345
347
  {getListIcon()}
346
348
  </DropdownTrigger>
@@ -391,7 +393,7 @@ const MenuBar = ({
391
393
  <DropdownTrigger
392
394
  onClick={() => toggleDropdown("blocks")}
393
395
  isActive={activeDropdown === "blocks"}
394
- title="Structural Blocks"
396
+ title={t("tooltips.structuralBlocks", { defaultValue: "Structural Blocks" })}
395
397
  >
396
398
  {getBlockIcon()}
397
399
  </DropdownTrigger>
@@ -441,7 +443,7 @@ const MenuBar = ({
441
443
  <DropdownTrigger
442
444
  onClick={() => toggleDropdown("align")}
443
445
  isActive={activeDropdown === "align"}
444
- title="Alignment"
446
+ title={t("tooltips.alignment", { defaultValue: "Alignment" })}
445
447
  >
446
448
  {getAlignIcon()}
447
449
  </DropdownTrigger>
@@ -492,11 +494,11 @@ const MenuBar = ({
492
494
  <ToolbarButton
493
495
  onClick={setLink}
494
496
  isActive={editor.isActive("link")}
495
- title="Link"
497
+ title={t("tooltips.link", { defaultValue: "Link" })}
496
498
  >
497
499
  <LinkIcon size={12} />
498
500
  </ToolbarButton>
499
- <ToolbarButton onClick={addImage} title="Add Image">
501
+ <ToolbarButton onClick={addImage} title={t("tooltips.addImage", { defaultValue: "Add Image" })}>
500
502
  <ImageIcon size={12} />
501
503
  </ToolbarButton>
502
504
  </div>
@@ -560,6 +562,7 @@ function RichTextEditor({
560
562
  error,
561
563
  disabled,
562
564
  }: RichTextFieldProps) {
565
+ const { t } = useTranslation();
563
566
  const [isExpanded, setIsExpanded] = useState(false);
564
567
  const [panelWidth, setPanelWidth] = useState(0);
565
568
  const [isMediaPickerOpen, setIsMediaPickerOpen] = useState(false);
@@ -772,7 +775,7 @@ function RichTextEditor({
772
775
  <SlidePanel
773
776
  open={isMediaPickerOpen}
774
777
  onClose={() => setIsMediaPickerOpen(false)}
775
- title="Select Image"
778
+ title={t("tooltips.selectImage", { defaultValue: "Select Image" })}
776
779
  width="xl"
777
780
  >
778
781
  <MediaGallery
@@ -1,6 +1,4 @@
1
- import { useState } from "react";
2
1
  import type { SecretField as SecretFieldType } from "@kyro-cms/core/client";
3
- import { IconCopy, IconCheck, IconRefreshCw } from "../ui/icons";
4
2
  import FieldLayout from "./FieldLayout";
5
3
 
6
4
  interface SecretFieldComponentProps {
@@ -18,84 +16,26 @@ export default function SecretField({
18
16
  error,
19
17
  disabled,
20
18
  }: SecretFieldComponentProps) {
21
- const [copied, setCopied] = useState(false);
22
- const [regenerating, setRegenerating] = useState(false);
23
19
 
24
20
  const fullValue = value ?? "";
25
21
  const displayValue = fullValue.length > 8
26
22
  ? fullValue.slice(0, -8) + "*".repeat(8)
27
23
  : fullValue;
28
24
 
29
- const handleCopy = async () => {
30
- if (!fullValue) return;
31
- try {
32
- await navigator.clipboard.writeText(fullValue);
33
- setCopied(true);
34
- setTimeout(() => setCopied(false), 1800);
35
- } catch {
36
- const ta = document.createElement("textarea");
37
- ta.value = fullValue;
38
- ta.style.position = "fixed";
39
- ta.style.opacity = "0";
40
- document.body.appendChild(ta);
41
- ta.select();
42
- document.execCommand("copy");
43
- document.body.removeChild(ta);
44
- setCopied(true);
45
- setTimeout(() => setCopied(false), 1800);
46
- }
47
- };
48
25
 
49
- const handleRegenerate = () => {
50
- if (regenerating || disabled) return;
51
- setRegenerating(true);
52
- const bytes = new Uint8Array(32);
53
- crypto.getRandomValues(bytes);
54
- const hex = Array.from(bytes)
55
- .map((b) => b.toString(16).padStart(2, "0"))
56
- .join("");
57
- onChange?.(hex);
58
- setTimeout(() => setRegenerating(false), 400);
59
- };
60
26
 
61
27
  return (
62
28
  <FieldLayout field={field} error={error}>
63
- <div className="flex items-center gap-1.5">
64
- <div className="relative flex-1">
65
- <input
66
- id={field.name}
67
- type="text"
68
- value={displayValue}
69
- readOnly
70
- disabled={disabled}
71
- className="kyro-form-input font-mono text-xs tracking-wider pr-10 opacity-70 bg-[var(--kyro-bg-secondary)] cursor-not-allowed select-none"
72
- spellCheck={false}
73
- />
74
- </div>
75
- <button
76
- type="button"
77
- onClick={handleCopy}
78
- disabled={!fullValue || disabled}
79
- className="p-2 rounded-lg border border-[var(--kyro-border)] bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:border-[var(--kyro-primary)] hover:bg-[var(--kyro-primary-alpha)] transition-all disabled:opacity-40 disabled:cursor-not-allowed active:scale-95"
80
- title={copied ? "Copied!" : "Copy full secret"}
81
- >
82
- {copied ? (
83
- <IconCheck className="w-3.5 h-3.5 text-[var(--kyro-success)]" />
84
- ) : (
85
- <IconCopy className="w-3.5 h-3.5" />
86
- )}
87
- </button>
88
- <button
89
- type="button"
90
- onClick={handleRegenerate}
29
+ <div className="relative">
30
+ <input
31
+ id={field.name}
32
+ type="text"
33
+ value={displayValue}
34
+ readOnly
91
35
  disabled={disabled}
92
- className="p-2 rounded-lg border border-[var(--kyro-border)] bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:border-[var(--kyro-warning)] hover:bg-[var(--kyro-warning)]/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed active:scale-95"
93
- title="Regenerate secret"
94
- >
95
- <IconRefreshCw
96
- className={`w-3.5 h-3.5 ${regenerating ? "animate-spin" : ""}`}
97
- />
98
- </button>
36
+ className="kyro-form-input font-mono text-xs tracking-wider opacity-70 bg-[var(--kyro-bg-secondary)] cursor-not-allowed select-none"
37
+ spellCheck={false}
38
+ />
99
39
  </div>
100
40
  </FieldLayout>
101
41
  );
@@ -40,7 +40,7 @@ export default function SelectField({
40
40
  const siblingDataStr = JSON.stringify(siblingData || {});
41
41
 
42
42
  useEffect(() => {
43
- if (field.options !== "__KYRO_DYNAMIC_OPTIONS__") return;
43
+ if ((field.options as any) !== "__KYRO_DYNAMIC_OPTIONS__") return;
44
44
 
45
45
  const fetchOptions = async () => {
46
46
  try {
@@ -80,11 +80,11 @@ export default function SelectField({
80
80
  // Resolve dynamic options at runtime if configured
81
81
  let options: Array<{ label: string; value: string }> = dynamicOptions || [];
82
82
 
83
- if (field.options !== "__KYRO_DYNAMIC_OPTIONS__") {
83
+ if ((field.options as any) !== "__KYRO_DYNAMIC_OPTIONS__") {
84
84
  if (typeof field.options === "function") {
85
- options = field.options({ data: formData || {}, siblingData: siblingData || {} });
85
+ options = (field.options as any)({ data: formData || {}, siblingData: siblingData || {} });
86
86
  } else if (Array.isArray(field.options)) {
87
- options = field.options;
87
+ options = field.options as any;
88
88
  }
89
89
  }
90
90
 
@@ -120,13 +120,18 @@ export default function SelectField({
120
120
  multiple={field.hasMany}
121
121
  disabled={disabled || isReadOnly}
122
122
  required={field.required}
123
+ style={{
124
+ color: field.hasMany
125
+ ? undefined
126
+ : (options as any[]).find(opt => opt.value === value)?.color || undefined,
127
+ }}
123
128
  className={`kyro-form-input ${
124
129
  disabled || isReadOnly ? "opacity-50 cursor-not-allowed" : ""
125
130
  }`}
126
131
  >
127
132
  {!field.required && !field.hasMany && <option value="">Select...</option>}
128
- {options.map((option) => (
129
- <option key={option.value} value={option.value}>
133
+ {options.map((option: any) => (
134
+ <option key={option.value} value={option.value} style={{ color: option.color || undefined, fontWeight: option.color ? "bold" : "normal" }}>
130
135
  {option.label}
131
136
  </option>
132
137
  ))}
@@ -1,7 +1,6 @@
1
- import React, { useState, useCallback } from "react";
1
+ import React, { useState } from "react";
2
2
  import type { Field } from "@kyro-cms/core/client";
3
3
  import { SeoPreview } from "../ui/SeoPreview";
4
- import { Copy, ClipboardPaste, Check } from "../ui/icons";
5
4
 
6
5
  interface TabsLayoutProps {
7
6
  field: Field;
@@ -30,34 +29,7 @@ export function TabsLayout({
30
29
  ? (formData[field.name] as Record<string, unknown>) || {}
31
30
  : formData;
32
31
 
33
- const [copied, setCopied] = useState(false);
34
-
35
- const handleCopy = useCallback(async (e: React.MouseEvent) => {
36
- e.preventDefault();
37
- e.stopPropagation();
38
- try {
39
- const payload = JSON.stringify({ __kyro_container: true, value: tabData });
40
- await navigator.clipboard.writeText(payload);
41
- setCopied(true);
42
- setTimeout(() => setCopied(false), 2000);
43
- } catch (err) {
44
- console.error("Failed to copy", err);
45
- }
46
- }, [tabData]);
47
-
48
- const handlePaste = useCallback(async (e: React.MouseEvent) => {
49
- e.preventDefault();
50
- e.stopPropagation();
51
- try {
52
- const text = await navigator.clipboard.readText();
53
- const parsed = JSON.parse(text);
54
- if (parsed && (parsed.__kyro_group || parsed.__kyro_container) && parsed.value) {
55
- onTabDataChange({ ...tabData, ...parsed.value });
56
- }
57
- } catch (err) {
58
- console.error("Failed to paste", err);
59
- }
60
- }, [tabData, onTabDataChange]);
32
+ // Removed copy/paste logic
61
33
 
62
34
  return (
63
35
  <div className="space-y-8">
@@ -78,24 +50,6 @@ export function TabsLayout({
78
50
  </button>
79
51
  ))}
80
52
  </div>
81
- <div className="flex items-center gap-1 pr-2">
82
- <button
83
- type="button"
84
- onClick={handleCopy}
85
- className="p-1.5 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-colors rounded hover:bg-[var(--kyro-surface)]"
86
- title="Copy Tab Data"
87
- >
88
- {copied ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
89
- </button>
90
- <button
91
- type="button"
92
- onClick={handlePaste}
93
- className="p-1.5 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-colors rounded hover:bg-[var(--kyro-surface)]"
94
- title="Paste Tab Data"
95
- >
96
- <ClipboardPaste className="w-4 h-4" />
97
- </button>
98
- </div>
99
53
  </div>
100
54
  <div className="space-y-6">
101
55
  {currentTab?.fields.map((f: Field) =>
@@ -2,6 +2,7 @@ import type { TextField as TextFieldType } from "@kyro-cms/core/client";
2
2
  import FieldLayout from "./FieldLayout";
3
3
  import { useAutoFormStore } from "../../lib/autoform-store";
4
4
  import { slugifyText } from "../../lib/slugify";
5
+ import { useTranslation } from "react-i18next";
5
6
 
6
7
  interface TextFieldComponentProps {
7
8
  field: TextFieldType;
@@ -18,6 +19,7 @@ export default function TextField({
18
19
  error,
19
20
  disabled,
20
21
  }: TextFieldComponentProps) {
22
+ const { t } = useTranslation();
21
23
  const isReadOnly =
22
24
  typeof field.admin?.readOnly === "function"
23
25
  ? false
@@ -42,10 +44,10 @@ export default function TextField({
42
44
  id: field.name,
43
45
  value: normalizedValue,
44
46
  onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => onChange?.(e.target.value),
45
- placeholder: field.admin?.placeholder,
47
+ placeholder: field.admin?.placeholder as string | undefined,
46
48
  disabled: disabled || isReadOnly || (isSlug && isSlugLocked),
47
- minLength: field.minLength,
48
- maxLength: field.maxLength,
49
+ minLength: field.minLength as number | undefined,
50
+ maxLength: field.maxLength as number | undefined,
49
51
  required: field.required,
50
52
  className: `kyro-form-input ${isSlug ? "pr-24" : ""
51
53
  } ${disabled || isReadOnly || (isSlug && isSlugLocked) ? "opacity-70 bg-[var(--kyro-bg-secondary)] cursor-not-allowed" : ""}`,
@@ -55,9 +57,9 @@ export default function TextField({
55
57
  <FieldLayout field={field} error={error}>
56
58
  <div className="relative">
57
59
  {isTextarea ? (
58
- <textarea {...commonProps} rows={(field as TextFieldType).rows || 4} />
60
+ <textarea {...commonProps} rows={(field as any).admin?.rows || 4} />
59
61
  ) : (
60
- <input type={inputType} {...commonProps} pattern={field.pattern} />
62
+ <input type={inputType} {...commonProps} pattern={(field as any).pattern} />
61
63
  )}
62
64
 
63
65
  {isSlug && (
@@ -68,12 +70,12 @@ export default function TextField({
68
70
  onClick={() =>
69
71
  onChange?.(
70
72
  slugifyText(
71
- formData[field.admin?.autoGenerate || "title"] || "",
73
+ (formData[(field as any).admin?.autoGenerate || "title"] || "") as string,
72
74
  ),
73
75
  )
74
76
  }
75
77
  className="p-1 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)]"
76
- title="Regenerate slug"
78
+ title={t("tooltips.regenerateSlug", { defaultValue: "Regenerate slug" })}
77
79
  >
78
80
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
79
81
  <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
@@ -3,6 +3,7 @@ import { createPortal } from "react-dom";
3
3
  import { Image as ImageIcon, Film, FileText, Music, File, X, Loader2, Check } from "../ui/icons";
4
4
  import { apiGet, withCacheBust, apiPost, apiUpload, resolveApi, resolveMedia } from "../../lib/api";
5
5
  import { toast } from "../../lib/stores";
6
+ import { useTranslation } from "react-i18next";
6
7
 
7
8
  interface UploadFieldProps {
8
9
  field: any;
@@ -502,6 +503,7 @@ function MediaPickerContent({
502
503
  setIsPickerFullscreen: (v: boolean) => void;
503
504
  setShowPicker: (v: boolean) => void;
504
505
  }) {
506
+ const { t } = useTranslation();
505
507
  const isItemSelected = (id: string) => selectedItems.some(i => i.id === id);
506
508
 
507
509
  return (
@@ -514,7 +516,7 @@ function MediaPickerContent({
514
516
  <div className="p-2 border-b border-[var(--kyro-border)] flex flex-col gap-2">
515
517
  <input
516
518
  type="text"
517
- placeholder="Search media..."
519
+ placeholder={t("fields.searchMedia", { defaultValue: "Search media..." })}
518
520
  value={pickerSearch}
519
521
  onChange={(e) => setPickerSearch(e.target.value)}
520
522
  className="w-full px-2 py-1.5 text-xs rounded border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)]"
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { UploadField } from "./UploadField";
3
+ import { useTranslation } from "react-i18next";
3
4
 
4
5
  interface VideoFieldProps {
5
6
  src?: string;
@@ -16,6 +17,7 @@ export const VideoField: React.FC<VideoFieldProps> = ({
16
17
  onUploadChange,
17
18
  compact = false,
18
19
  }) => {
20
+ const { t } = useTranslation();
19
21
  const isExternalUrl =
20
22
  src.includes("youtube.com") ||
21
23
  src.includes("vimeo.com") ||
@@ -29,14 +31,14 @@ export const VideoField: React.FC<VideoFieldProps> = ({
29
31
  value={src}
30
32
  onChange={(e) => onChange("src", e.target.value)}
31
33
  className="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 font-mono text-xs"
32
- placeholder="MP4 URL, YouTube, or Vimeo link..."
34
+ placeholder={t("fields.mp4UrlYoutubeOr", { defaultValue: "MP4 URL, YouTube, or Vimeo link..." })}
33
35
  />
34
36
  <input
35
37
  type="text"
36
38
  value={title}
37
39
  onChange={(e) => onChange("title", e.target.value)}
38
40
  className="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"
39
- placeholder="Video title (optional)..."
41
+ placeholder={t("fields.videoTitleOptional", { defaultValue: "Video title (optional)..." })}
40
42
  />
41
43
  </div>
42
44
  );
@@ -57,14 +59,14 @@ export const VideoField: React.FC<VideoFieldProps> = ({
57
59
  value={src}
58
60
  onChange={(e) => onChange("src", e.target.value)}
59
61
  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 font-mono text-xs"
60
- placeholder="MP4 URL, YouTube, or Vimeo link..."
62
+ placeholder={t("fields.mp4UrlYoutubeOr", { defaultValue: "MP4 URL, YouTube, or Vimeo link..." })}
61
63
  />
62
64
  <input
63
65
  type="text"
64
66
  value={title}
65
67
  onChange={(e) => onChange("title", e.target.value)}
66
68
  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"
67
- placeholder="Video title (optional)..."
69
+ placeholder={t("fields.videoTitleOptional", { defaultValue: "Video title (optional)..." })}
68
70
  />
69
71
  </div>
70
72
  );
@@ -2,6 +2,7 @@ import React, { type ReactNode } from "react";
2
2
  import { useDraggable } from "@dnd-kit/core";
3
3
  import { SlidePanel } from "./SlidePanel";
4
4
  import { ClipboardPaste } from "lucide-react";
5
+ import { useTranslation } from "react-i18next";
5
6
 
6
7
  interface BlockDrawerProps {
7
8
  open: boolean;
@@ -18,6 +19,7 @@ export function BlockDrawer({
18
19
  onPasteBlock,
19
20
  children,
20
21
  }: BlockDrawerProps) {
22
+ const { t } = useTranslation();
21
23
  const [pasteError, setPasteError] = React.useState<string | null>(null);
22
24
 
23
25
  const handlePaste = async () => {
@@ -41,7 +43,7 @@ export function BlockDrawer({
41
43
  if (!open) return null;
42
44
 
43
45
  return (
44
- <SlidePanel open={open} onClose={onClose} title="Insert Block" width="md">
46
+ <SlidePanel open={open} onClose={onClose} title={t("tooltips.insertBlock", { defaultValue: "Insert Block" })} width="md">
45
47
  <p className="text-sm text-[var(--kyro-text-muted)] mb-4">
46
48
  Drag blocks into the editor or click to insert
47
49
  </p>