@kyro-cms/admin 0.12.4 → 0.12.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +64 -26
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +64 -26
- package/package.json +10 -4
- package/src/components/ActionBar.tsx +16 -16
- package/src/components/ApiHealth.tsx +522 -0
- package/src/components/ApiKeysManager.tsx +6 -4
- package/src/components/AuditLogsPage.tsx +4 -2
- package/src/components/AutoForm.tsx +10 -7
- package/src/components/BrandingHub.tsx +3 -1
- package/src/components/Dashboard.tsx +44 -37
- package/src/components/DashboardMetrics.tsx +168 -58
- package/src/components/DetailView.tsx +41 -38
- package/src/components/DeveloperCenter.tsx +5 -3
- package/src/components/GraphQLPlayground.tsx +10 -8
- package/src/components/ListView.tsx +66 -45
- package/src/components/LoginPage.tsx +21 -19
- package/src/components/MarketplaceManager.tsx +3 -1
- package/src/components/MediaGallery.tsx +12 -9
- package/src/components/PluginsManager.tsx +6 -4
- package/src/components/RestPlayground.tsx +8 -6
- package/src/components/SessionsManager.tsx +3 -1
- package/src/components/Sidebar.astro +180 -92
- package/src/components/UserManagement.tsx +8 -6
- package/src/components/UserMenu.tsx +19 -17
- package/src/components/VersionHistoryPanel.tsx +6 -4
- package/src/components/WebhookManager.tsx +11 -10
- package/src/components/autoform/AutoFormApiView.tsx +3 -1
- package/src/components/autoform/AutoFormEditView.tsx +3 -1
- package/src/components/autoform/AutoFormHeader.tsx +6 -6
- package/src/components/blocks/AccordionBlock.tsx +5 -3
- package/src/components/blocks/ArrayBlock.tsx +5 -3
- package/src/components/blocks/BlockWrapper.tsx +5 -3
- package/src/components/blocks/CodeBlock.tsx +4 -2
- package/src/components/blocks/FileBlock.tsx +5 -3
- package/src/components/blocks/HeroBlock.tsx +5 -3
- package/src/components/blocks/ImageBlock.tsx +6 -4
- package/src/components/blocks/ListBlock.tsx +5 -3
- package/src/components/blocks/ParagraphBlock.tsx +3 -1
- package/src/components/blocks/RelationshipBlock.tsx +5 -3
- package/src/components/blocks/RichTextBlock.tsx +5 -3
- package/src/components/blocks/VideoBlock.tsx +5 -3
- package/src/components/fields/AccordionField.tsx +10 -51
- package/src/components/fields/ArrayField.tsx +9 -7
- package/src/components/fields/ArrayLayout.tsx +3 -1
- package/src/components/fields/BlocksField.tsx +24 -75
- package/src/components/fields/ButtonField.tsx +3 -1
- package/src/components/fields/CardField.tsx +6 -4
- package/src/components/fields/GroupLayout.tsx +2 -49
- package/src/components/fields/HeadingField.tsx +3 -1
- package/src/components/fields/HeadingSubheadingField.tsx +4 -2
- package/src/components/fields/HeroField.tsx +8 -6
- package/src/components/fields/JSONField.tsx +9 -10
- package/src/components/fields/LinkField.tsx +4 -2
- package/src/components/fields/ListField.tsx +3 -1
- package/src/components/fields/MarkdownField.tsx +3 -17
- package/src/components/fields/RelationshipBlockField.tsx +7 -5
- package/src/components/fields/RelationshipField.tsx +3 -1
- package/src/components/fields/RichTextField.tsx +19 -16
- package/src/components/fields/SecretField.tsx +9 -69
- package/src/components/fields/SelectField.tsx +11 -6
- package/src/components/fields/TabsLayout.tsx +2 -48
- package/src/components/fields/TextField.tsx +9 -7
- package/src/components/fields/UploadField.tsx +3 -1
- package/src/components/fields/VideoField.tsx +6 -4
- package/src/components/ui/BlockDrawer.tsx +3 -1
- package/src/components/ui/CommandPalette.tsx +9 -7
- package/src/components/ui/CommandPaletteWrapper.tsx +3 -1
- package/src/components/ui/IconPickerModal.tsx +4 -2
- package/src/components/ui/PageHeader.tsx +48 -19
- package/src/components/users/UserDetail.tsx +3 -1
- package/src/components/users/UserForm.tsx +5 -3
- package/src/fields/examples/sample-field-2.tsx +3 -1
- package/src/fields/examples/sample-field.tsx +3 -1
- package/src/hooks/useAutoFormState.ts +19 -11
- package/src/integration.ts +21 -0
- package/src/layouts/AdminLayout.astro +252 -189
- package/src/layouts/AuthLayout.astro +52 -31
- package/src/lib/api.ts +7 -7
- package/src/lib/deep-equal.ts +3 -3
- package/src/lib/i18n.ts +44 -0
- package/src/lib/vite-shim-plugin.ts +3 -0
- package/src/locales/de.json +163 -0
- package/src/locales/en.json +163 -0
- package/src/locales/es.json +163 -0
- package/src/locales/fr.json +163 -0
- package/src/locales/pt.json +163 -0
- package/src/pages/403.astro +2 -1
- package/src/pages/[collection]/[id].astro +23 -10
- package/src/pages/[collection]/index.astro +48 -34
- package/src/pages/audit/index.astro +2 -1
- package/src/pages/graphql-explorer.astro +2 -1
- package/src/pages/graphql.astro +2 -1
- package/src/pages/health.astro +9 -0
- package/src/pages/index.astro +6 -5
- package/src/pages/keys.astro +2 -1
- package/src/pages/marketplace.astro +2 -1
- package/src/pages/media.astro +2 -1
- package/src/pages/plugins.astro +2 -1
- package/src/pages/rest-playground.astro +2 -1
- package/src/pages/roles/index.astro +16 -52
- package/src/pages/sessions.astro +2 -1
- package/src/pages/settings/[slug].astro +45 -30
- package/src/pages/users/[id].astro +1 -0
- package/src/pages/users/index.astro +2 -1
- package/src/pages/users/new.astro +2 -1
- package/src/pages/webhooks.astro +2 -1
- 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<
|
|
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:
|
|
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:
|
|
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:
|
|
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="
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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="
|
|
93
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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>
|