@kyro-cms/admin 0.12.3 → 0.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +8 -2
- 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 +125 -50
- 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 +143 -15
- package/src/components/autoform/AutoFormApiView.tsx +3 -1
- package/src/components/autoform/AutoFormEditView.tsx +5 -3
- 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 -20
- 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/autoform-store.ts +4 -0
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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,
|
|
220
|
+
{Object.entries(node.value as object).map(([key, child]) => (
|
|
222
221
|
<TreeNodeView
|
|
223
222
|
key={key}
|
|
224
223
|
name={key}
|
|
225
|
-
value={
|
|
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>
|