@kyro-cms/admin 0.8.0 → 0.9.1
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 +11960 -11006
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +67 -65
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +563 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +12183 -11238
- package/dist/index.js.map +1 -1
- package/package.json +15 -11
- package/src/components/ActionBar.tsx +27 -14
- package/src/components/Admin.tsx +1 -1
- package/src/components/ApiKeysManager.tsx +5 -5
- package/src/components/AutoForm.tsx +585 -369
- package/src/components/BrandingHub.tsx +7 -4
- package/src/components/CreateView.tsx +2 -0
- package/src/components/DetailView.tsx +71 -56
- package/src/components/DeveloperCenter.tsx +8 -6
- package/src/components/FieldRenderer.tsx +94 -19
- package/src/components/ListView.tsx +33 -20
- package/src/components/MediaGallery.tsx +219 -194
- package/src/components/PluginsManager.tsx +197 -70
- package/src/components/RestPlayground.tsx +7 -7
- package/src/components/SessionsManager.tsx +1 -1
- package/src/components/SettingsPage.tsx +22 -0
- package/src/components/Sidebar.astro +13 -41
- package/src/components/UserManagement.tsx +153 -15
- package/src/components/UserMenu.tsx +30 -4
- package/src/components/VersionHistoryPanel.tsx +112 -119
- package/src/components/WebhookManager.tsx +6 -4
- package/src/components/blocks/ArrayBlock.tsx +6 -23
- package/src/components/blocks/BlockEditModal.tsx +82 -309
- package/src/components/blocks/CardBlock.tsx +35 -0
- package/src/components/blocks/ChildBlocksTree.tsx +57 -31
- package/src/components/blocks/GenericBlock.tsx +44 -0
- package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
- package/src/components/blocks/HeroBlock.tsx +5 -14
- package/src/components/blocks/RichTextBlock.tsx +5 -5
- package/src/components/blocks/index.ts +5 -3
- package/src/components/fields/AccordionField.tsx +2 -2
- package/src/components/fields/ArrayField.tsx +1 -1
- package/src/components/fields/ArrayLayout.tsx +120 -29
- package/src/components/fields/BlocksField.tsx +430 -50
- package/src/components/fields/CardField.tsx +73 -0
- package/src/components/fields/CheckboxField.tsx +7 -3
- package/src/components/fields/DateField.tsx +4 -1
- package/src/components/fields/GroupLayout.tsx +2 -2
- package/src/components/fields/HeadingSubheadingField.tsx +43 -0
- package/src/components/fields/ListField.tsx +2 -2
- package/src/components/fields/NumberField.tsx +4 -1
- package/src/components/fields/RelationshipField.tsx +153 -87
- package/src/components/fields/RichTextField.tsx +781 -0
- package/src/components/fields/SecretField.tsx +102 -0
- package/src/components/fields/SelectField.tsx +19 -6
- package/src/components/fields/TabsLayout.tsx +19 -9
- package/src/components/fields/TextField.tsx +4 -1
- package/src/components/fields/UploadField.tsx +122 -56
- package/src/components/fields/extensions/blockComponents.tsx +103 -174
- package/src/components/fields/extensions/blocksStore.ts +8 -1
- package/src/components/fields/index.ts +4 -2
- package/src/components/ui/PageHeader.tsx +5 -5
- package/src/components/ui/SlidePanel.tsx +8 -3
- package/src/components/ui/icons.tsx +109 -109
- package/src/components/users/UserDetail.tsx +79 -16
- package/src/hooks/useAutoFormState.ts +125 -62
- package/src/integration.ts +148 -46
- package/src/kyro-cms.d.ts +7 -2
- package/src/layouts/AuthLayout.astro +14 -2
- package/src/lib/autoform-store.ts +85 -52
- package/src/lib/change-source.ts +9 -0
- package/src/lib/config.ts +104 -8
- package/src/lib/globals.ts +44 -9
- package/src/lib/normalize-upload-fields.ts +41 -0
- package/src/lib/paths.ts +2 -2
- package/src/lib/resolve-field-value.ts +110 -0
- package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
- package/src/lib/shim/use-sync-external-store.js +1 -0
- package/src/lib/stores/index.ts +1 -0
- package/src/lib/useResourceManager.ts +4 -4
- package/src/lib/vite-shim-plugin.ts +100 -0
- package/src/pages/[collection]/[id].astro +1 -1
- package/src/pages/preview/[collection]/[id].astro +4 -4
- package/src/pages/settings/[slug].astro +2 -2
- package/src/styles/main.css +60 -54
- package/README.md +0 -46
- package/dist/EditorClient-Q23UXR37.cjs +0 -468
- package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
- package/dist/EditorClient-T5PASFNR.js +0 -466
- package/dist/EditorClient-T5PASFNR.js.map +0 -1
- package/dist/chunk-3BGDYKTD.cjs +0 -348
- package/dist/chunk-3BGDYKTD.cjs.map +0 -1
- package/dist/chunk-EEFXLQVT.js +0 -3
- package/dist/chunk-EEFXLQVT.js.map +0 -1
- package/src/components/blocks/ButtonBlock.tsx +0 -64
- package/src/components/blocks/ColumnsBlock.tsx +0 -55
- package/src/components/blocks/DividerBlock.tsx +0 -43
- package/src/components/blocks/LinkBlock.tsx +0 -65
- package/src/components/blocks/VStackBlock.tsx +0 -29
- package/src/components/fields/EditorClient.tsx +0 -535
- package/src/components/fields/PortableTextField.tsx +0 -155
- package/src/components/fields/PortableTextRenderer.tsx +0 -68
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
2
|
import { Plus, X, ChevronRight, ChevronDown } from "../ui/icons";
|
|
3
3
|
import {
|
|
4
|
-
blockCategories,
|
|
5
4
|
blockIcons,
|
|
6
5
|
getBlockComponent,
|
|
7
6
|
getBlockLabel,
|
|
@@ -9,6 +8,9 @@ import {
|
|
|
9
8
|
import { createNewBlock } from "../fields/extensions/blocksStore";
|
|
10
9
|
import { BlockDrawer } from "../ui/BlockDrawer";
|
|
11
10
|
import { BlockEditModal } from "./BlockEditModal";
|
|
11
|
+
import { useStore } from "zustand";
|
|
12
|
+
import { BlocksContext } from "../fields/extensions/blocksStore";
|
|
13
|
+
import { useContext } from "react";
|
|
12
14
|
|
|
13
15
|
interface ChildBlocksTreeProps {
|
|
14
16
|
blockId: string;
|
|
@@ -32,7 +34,11 @@ export const ChildBlocksTree: React.FC<ChildBlocksTreeProps> = ({
|
|
|
32
34
|
const [editingBlockId, setEditingBlockId] = useState<string | null>(null);
|
|
33
35
|
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
|
34
36
|
|
|
35
|
-
const
|
|
37
|
+
const store = useContext(BlocksContext);
|
|
38
|
+
if (!store) throw new Error("ChildBlocksTree must be used within a BlocksContext");
|
|
39
|
+
const dynamicCategories = useStore(store, (s) => s.dynamicCategories);
|
|
40
|
+
const allowedBlocks = useStore(store, (s) => s.allowedBlocks);
|
|
41
|
+
|
|
36
42
|
const canAddChildren = depth < maxDepth;
|
|
37
43
|
const indentWidth = 16;
|
|
38
44
|
|
|
@@ -88,6 +94,7 @@ export const ChildBlocksTree: React.FC<ChildBlocksTreeProps> = ({
|
|
|
88
94
|
const BlockComponent = getBlockComponent(child.type);
|
|
89
95
|
const childHasOwnChildren = hasChildren;
|
|
90
96
|
const isEditing = editingBlockId === child.id;
|
|
97
|
+
const blockSchema = allowedBlocks.find((b: any) => b.slug === child.type);
|
|
91
98
|
|
|
92
99
|
return (
|
|
93
100
|
<div key={child.id} className="relative group">
|
|
@@ -123,20 +130,27 @@ export const ChildBlocksTree: React.FC<ChildBlocksTreeProps> = ({
|
|
|
123
130
|
)}
|
|
124
131
|
|
|
125
132
|
{blockIcons[child.type] && (
|
|
126
|
-
<
|
|
133
|
+
<div className="w-8 h-8 rounded bg-[var(--kyro-surface-accent)] flex items-center justify-center text-[var(--kyro-text-secondary)]">
|
|
127
134
|
{blockIcons[child.type]}
|
|
128
|
-
</
|
|
135
|
+
</div>
|
|
129
136
|
)}
|
|
130
137
|
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
<div className="flex-1 min-w-0">
|
|
139
|
+
<div className="text-xs font-medium text-[var(--kyro-text-secondary)] truncate">
|
|
140
|
+
{getBlockLabel(child.type)}
|
|
141
|
+
{child.data?.text ? ` - ${child.data.text.slice(0, 30)}` : ""}
|
|
142
|
+
{child.data?.heading ? ` - ${child.data.heading.slice(0, 30)}` : ""}
|
|
143
|
+
</div>
|
|
144
|
+
{blockSchema?.admin?.description && (
|
|
145
|
+
<div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5 truncate opacity-80">
|
|
146
|
+
{blockSchema.admin.description}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
136
150
|
|
|
137
151
|
{hasChildren && (
|
|
138
|
-
<span className="text-[10px] text-[var(--kyro-text-muted)]">
|
|
139
|
-
|
|
152
|
+
<span className="text-[10px] bg-[var(--kyro-surface-accent)] px-2 py-0.5 rounded text-[var(--kyro-text-muted)] font-medium">
|
|
153
|
+
{child.children.length} nested
|
|
140
154
|
</span>
|
|
141
155
|
)}
|
|
142
156
|
|
|
@@ -223,7 +237,7 @@ export const ChildBlocksTree: React.FC<ChildBlocksTreeProps> = ({
|
|
|
223
237
|
onClose={() => setShowAddModal(false)}
|
|
224
238
|
onSelect={handleAddChild}
|
|
225
239
|
>
|
|
226
|
-
{
|
|
240
|
+
{dynamicCategories.map((category) => (
|
|
227
241
|
<div key={category.title} className="mb-4">
|
|
228
242
|
<h3 className="text-xs font-semibold text-[var(--kyro-text-muted)] tracking-wide mb-2">
|
|
229
243
|
{category.title}
|
|
@@ -231,23 +245,23 @@ export const ChildBlocksTree: React.FC<ChildBlocksTreeProps> = ({
|
|
|
231
245
|
<div className="grid grid-cols-3 gap-2">
|
|
232
246
|
{category.blocks.map((block) => (
|
|
233
247
|
<button
|
|
234
|
-
key={block.
|
|
248
|
+
key={block.slug}
|
|
235
249
|
type="button"
|
|
236
250
|
onClick={() => {
|
|
237
|
-
handleAddChild(block.
|
|
251
|
+
handleAddChild(block.slug);
|
|
238
252
|
setShowAddModal(false);
|
|
239
253
|
}}
|
|
240
254
|
className="flex flex-col items-center text-center gap-1 p-2 rounded-md border border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/60 hover:bg-[var(--kyro-surface-accent)]/30 transition-all cursor-pointer group"
|
|
241
255
|
>
|
|
242
256
|
<div className="w-6 h-6 flex items-center justify-center rounded group-hover:bg-[var(--kyro-primary)]/10 group-hover:text-[var(--kyro-primary)] transition-all">
|
|
243
|
-
{blockIcons[block.
|
|
257
|
+
{blockIcons[block.slug as keyof typeof blockIcons]}
|
|
244
258
|
</div>
|
|
245
259
|
<div className="flex-1 min-w-0">
|
|
246
260
|
<div className="text-xs font-medium tracking-tight text-[var(--kyro-text-primary)]">
|
|
247
261
|
{block.label}
|
|
248
262
|
</div>
|
|
249
263
|
<div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5">
|
|
250
|
-
{block.description}
|
|
264
|
+
{block.admin?.description || ""}
|
|
251
265
|
</div>
|
|
252
266
|
</div>
|
|
253
267
|
</button>
|
|
@@ -300,7 +314,11 @@ const NestedChildBlocks: React.FC<NestedChildBlocksProps> = ({
|
|
|
300
314
|
const [editingBlockId, setEditingBlockId] = useState<string | null>(null);
|
|
301
315
|
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
|
302
316
|
|
|
303
|
-
const
|
|
317
|
+
const store = useContext(BlocksContext);
|
|
318
|
+
if (!store) throw new Error("NestedChildBlocks must be used within a BlocksContext");
|
|
319
|
+
const dynamicCategories = useStore(store, (s) => s.dynamicCategories);
|
|
320
|
+
const allowedBlocks = useStore(store, (s) => s.allowedBlocks);
|
|
321
|
+
|
|
304
322
|
const canAddChildren = depth < maxDepth;
|
|
305
323
|
const indentWidth = 16;
|
|
306
324
|
|
|
@@ -356,6 +374,7 @@ const NestedChildBlocks: React.FC<NestedChildBlocksProps> = ({
|
|
|
356
374
|
const BlockComponent = getBlockComponent(child.type);
|
|
357
375
|
const childHasOwnChildren = hasChildren;
|
|
358
376
|
const isEditing = editingBlockId === child.id;
|
|
377
|
+
const blockSchema = allowedBlocks.find((b: any) => b.slug === child.type);
|
|
359
378
|
|
|
360
379
|
return (
|
|
361
380
|
<div key={child.id} className="relative group">
|
|
@@ -391,20 +410,27 @@ const NestedChildBlocks: React.FC<NestedChildBlocksProps> = ({
|
|
|
391
410
|
)}
|
|
392
411
|
|
|
393
412
|
{blockIcons[child.type] && (
|
|
394
|
-
<
|
|
413
|
+
<div className="w-8 h-8 rounded bg-[var(--kyro-surface-accent)] flex items-center justify-center text-[var(--kyro-text-secondary)]">
|
|
395
414
|
{blockIcons[child.type]}
|
|
396
|
-
</
|
|
415
|
+
</div>
|
|
397
416
|
)}
|
|
398
417
|
|
|
399
|
-
<
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
418
|
+
<div className="flex-1 min-w-0">
|
|
419
|
+
<div className="text-xs font-medium text-[var(--kyro-text-secondary)] truncate">
|
|
420
|
+
{getBlockLabel(child.type)}
|
|
421
|
+
{child.data?.text ? ` - ${child.data.text.slice(0, 30)}` : ""}
|
|
422
|
+
{child.data?.heading ? ` - ${child.data.heading.slice(0, 30)}` : ""}
|
|
423
|
+
</div>
|
|
424
|
+
{blockSchema?.admin?.description && (
|
|
425
|
+
<div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5 truncate opacity-80">
|
|
426
|
+
{blockSchema.admin.description}
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
429
|
+
</div>
|
|
404
430
|
|
|
405
431
|
{hasChildren && (
|
|
406
|
-
<span className="text-[10px] text-[var(--kyro-text-muted)]">
|
|
407
|
-
|
|
432
|
+
<span className="text-[10px] bg-[var(--kyro-surface-accent)] px-2 py-0.5 rounded text-[var(--kyro-text-muted)] font-medium">
|
|
433
|
+
{child.children.length} nested
|
|
408
434
|
</span>
|
|
409
435
|
)}
|
|
410
436
|
|
|
@@ -491,7 +517,7 @@ const NestedChildBlocks: React.FC<NestedChildBlocksProps> = ({
|
|
|
491
517
|
onClose={() => setShowAddModal(false)}
|
|
492
518
|
onSelect={handleAddChild}
|
|
493
519
|
>
|
|
494
|
-
{
|
|
520
|
+
{dynamicCategories.map((category) => (
|
|
495
521
|
<div key={category.title} className="mb-4">
|
|
496
522
|
<h3 className="text-xs font-semibold text-[var(--kyro-text-muted)] tracking-wide mb-2">
|
|
497
523
|
{category.title}
|
|
@@ -499,23 +525,23 @@ const NestedChildBlocks: React.FC<NestedChildBlocksProps> = ({
|
|
|
499
525
|
<div className="grid grid-cols-3 gap-2">
|
|
500
526
|
{category.blocks.map((block) => (
|
|
501
527
|
<button
|
|
502
|
-
key={block.
|
|
528
|
+
key={block.slug}
|
|
503
529
|
type="button"
|
|
504
530
|
onClick={() => {
|
|
505
|
-
handleAddChild(block.
|
|
531
|
+
handleAddChild(block.slug);
|
|
506
532
|
setShowAddModal(false);
|
|
507
533
|
}}
|
|
508
534
|
className="flex flex-col items-center text-center gap-1 p-2 rounded-md border border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/60 hover:bg-[var(--kyro-surface-accent)]/30 transition-all cursor-pointer group"
|
|
509
535
|
>
|
|
510
536
|
<div className="w-6 h-6 flex items-center justify-center rounded group-hover:bg-[var(--kyro-primary)]/10 group-hover:text-[var(--kyro-primary)] transition-all">
|
|
511
|
-
{blockIcons[block.
|
|
537
|
+
{blockIcons[block.slug as keyof typeof blockIcons]}
|
|
512
538
|
</div>
|
|
513
539
|
<div className="flex-1 min-w-0">
|
|
514
540
|
<div className="text-xs font-medium tracking-tight text-[var(--kyro-text-primary)]">
|
|
515
541
|
{block.label}
|
|
516
542
|
</div>
|
|
517
543
|
<div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5">
|
|
518
|
-
{block.description}
|
|
544
|
+
{block.admin?.description || ""}
|
|
519
545
|
</div>
|
|
520
546
|
</div>
|
|
521
547
|
</button>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { BlockWrapper } from "./BlockWrapper";
|
|
3
|
+
import { FieldRenderer } from "../FieldRenderer";
|
|
4
|
+
import { useBlockById, useBlockActions } from "../fields/extensions/blocksStore";
|
|
5
|
+
|
|
6
|
+
interface GenericBlockProps {
|
|
7
|
+
block: Record<string, unknown>;
|
|
8
|
+
index: number;
|
|
9
|
+
blockSchema: Record<string, any>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const GenericBlock: React.FC<GenericBlockProps> = ({
|
|
13
|
+
block,
|
|
14
|
+
index,
|
|
15
|
+
blockSchema,
|
|
16
|
+
}) => {
|
|
17
|
+
const blockData = useBlockById(block.id);
|
|
18
|
+
const { updateBlock } = useBlockActions();
|
|
19
|
+
|
|
20
|
+
const data = (blockData?.data || block.data || {}) as Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
const handleChange = (fieldName: string, value: unknown) => {
|
|
23
|
+
updateBlock(block.id, { data: { ...data, [fieldName]: value } });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<BlockWrapper id={block.id} type={block.type as string} label={blockSchema.label as string}>
|
|
28
|
+
<div className="space-y-4 pt-2">
|
|
29
|
+
{blockSchema.fields?.map((field: any) => {
|
|
30
|
+
const value = data[field.name];
|
|
31
|
+
return (
|
|
32
|
+
<div key={field.name} className="kyro-block-field-row border-b border-[var(--kyro-border)]/30 pb-3 last:border-b-0 last:pb-0">
|
|
33
|
+
<FieldRenderer
|
|
34
|
+
field={field}
|
|
35
|
+
value={value}
|
|
36
|
+
onChange={(val) => handleChange(field.name, val)}
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
})}
|
|
41
|
+
</div>
|
|
42
|
+
</BlockWrapper>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
useBlockById,
|
|
4
|
+
useBlockActions,
|
|
5
|
+
} from "../fields/extensions/blocksStore";
|
|
6
|
+
import { HeadingSubheadingField } from "../fields/HeadingSubheadingField";
|
|
7
|
+
import { BlockWrapper } from "./BlockWrapper";
|
|
8
|
+
|
|
9
|
+
export const HeadingSubheadingBlock: React.FC<{ block: Record<string, unknown>; index: number }> = ({
|
|
10
|
+
block,
|
|
11
|
+
index,
|
|
12
|
+
}) => {
|
|
13
|
+
const blockData = useBlockById(block.id);
|
|
14
|
+
const { updateBlock } = useBlockActions();
|
|
15
|
+
|
|
16
|
+
const data = blockData?.data || block.data || {};
|
|
17
|
+
|
|
18
|
+
const handleChange = (field: string, value: unknown) => {
|
|
19
|
+
updateBlock(block.id, { data: { ...data, [field]: value } });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<BlockWrapper id={block.id} type="heading-subheading" label="Heading + Subheading">
|
|
24
|
+
<HeadingSubheadingField
|
|
25
|
+
heading={data.heading || ""}
|
|
26
|
+
subheading={data.subheading || ""}
|
|
27
|
+
onChange={handleChange}
|
|
28
|
+
compact
|
|
29
|
+
/>
|
|
30
|
+
</BlockWrapper>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -68,20 +68,11 @@ export const HeroBlock: React.FC<{ block: Record<string, unknown>; index: number
|
|
|
68
68
|
compact
|
|
69
69
|
/>
|
|
70
70
|
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/>
|
|
77
|
-
<input
|
|
78
|
-
type="url"
|
|
79
|
-
value={data.videoUrl || ""}
|
|
80
|
-
onChange={(e) => handleChange("videoUrl", e.target.value)}
|
|
81
|
-
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"
|
|
82
|
-
placeholder="Video URL..."
|
|
83
|
-
/>
|
|
84
|
-
</div>
|
|
71
|
+
<UploadField
|
|
72
|
+
field={{ label: "Background", name: "bgImage", maxCount: 1 }}
|
|
73
|
+
value={data.bgImage}
|
|
74
|
+
onChange={(v) => handleChange("bgImage", v)}
|
|
75
|
+
/>
|
|
85
76
|
|
|
86
77
|
<div className="pt-3 border-t border-[var(--kyro-border)]">
|
|
87
78
|
<label className="text-[10px] font-medium text-[var(--kyro-text-muted)] mb-1.5 block">
|
|
@@ -4,16 +4,16 @@ import {
|
|
|
4
4
|
useBlockActions,
|
|
5
5
|
} from "../fields/extensions/blocksStore";
|
|
6
6
|
import { ChevronRight, X, AlignLeft } from "../ui/icons";
|
|
7
|
-
import
|
|
7
|
+
import { RichTextField } from "../fields";
|
|
8
8
|
|
|
9
|
-
export const RichTextBlock: React.FC<{ block:
|
|
9
|
+
export const RichTextBlock: React.FC<{ block: any; index: number }> = ({
|
|
10
10
|
block,
|
|
11
11
|
index,
|
|
12
12
|
}) => {
|
|
13
13
|
const blockData = useBlockById(block.id);
|
|
14
14
|
const { updateBlock, removeBlock, moveBlock } = useBlockActions();
|
|
15
15
|
|
|
16
|
-
const data = blockData?.data || block.data || {}
|
|
16
|
+
const data = (blockData?.data || block.data || {}) as Record<string, any>;
|
|
17
17
|
|
|
18
18
|
const handleChange = (newValue: unknown) => {
|
|
19
19
|
updateBlock(block.id, { data: { ...data, content: newValue } });
|
|
@@ -56,8 +56,8 @@ export const RichTextBlock: React.FC<{ block: Record<string, unknown>; index: nu
|
|
|
56
56
|
</div>
|
|
57
57
|
</div>
|
|
58
58
|
|
|
59
|
-
<
|
|
60
|
-
field={{ name: "content", label: "Content" }}
|
|
59
|
+
<RichTextField
|
|
60
|
+
field={{ name: "content", label: "Content", type: "richtext" } as any}
|
|
61
61
|
value={data.content}
|
|
62
62
|
onChange={handleChange}
|
|
63
63
|
/>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
export { ColumnsBlock } from './ColumnsBlock';
|
|
2
1
|
export { HeadingBlock } from './HeadingBlock';
|
|
2
|
+
export { HeadingSubheadingBlock } from './HeadingSubheadingBlock';
|
|
3
3
|
export { ParagraphBlock } from './ParagraphBlock';
|
|
4
|
-
export { DividerBlock } from './DividerBlock';
|
|
5
4
|
export { ImageBlock } from './ImageBlock';
|
|
6
5
|
export { VideoBlock } from './VideoBlock';
|
|
7
6
|
export { ListBlock } from './ListBlock';
|
|
8
7
|
export { CodeBlock } from './CodeBlock';
|
|
9
|
-
export { LinkBlock } from './LinkBlock';
|
|
10
8
|
export { FileBlock } from './FileBlock';
|
|
9
|
+
export { CardBlock } from './CardBlock';
|
|
10
|
+
export { GenericBlock } from './GenericBlock';
|
|
11
|
+
|
|
12
|
+
|
|
@@ -53,7 +53,7 @@ export const AccordionField: React.FC<AccordionFieldProps> = ({
|
|
|
53
53
|
return (
|
|
54
54
|
<div className="space-y-2">
|
|
55
55
|
{items.length === 0 ? (
|
|
56
|
-
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-
|
|
56
|
+
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-md">
|
|
57
57
|
No items. Click "Add Item" to create one.
|
|
58
58
|
</div>
|
|
59
59
|
) : (
|
|
@@ -135,7 +135,7 @@ export const AccordionField: React.FC<AccordionFieldProps> = ({
|
|
|
135
135
|
return (
|
|
136
136
|
<div className="space-y-2">
|
|
137
137
|
{items.length === 0 ? (
|
|
138
|
-
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-
|
|
138
|
+
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-md">
|
|
139
139
|
No items. Click "Add Item" to create one.
|
|
140
140
|
</div>
|
|
141
141
|
) : (
|
|
@@ -55,7 +55,7 @@ export const ArrayField: React.FC<ArrayFieldProps> = ({
|
|
|
55
55
|
return (
|
|
56
56
|
<div className="space-y-1.5">
|
|
57
57
|
{items.length === 0 ? (
|
|
58
|
-
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-
|
|
58
|
+
<div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-md">
|
|
59
59
|
No items. Click "Add Item" to create one.
|
|
60
60
|
</div>
|
|
61
61
|
) : (
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Field } from "@kyro-cms/core/client";
|
|
3
3
|
import RelationshipField from "./RelationshipField";
|
|
4
|
+
import { ChevronDown, ChevronUp } from "../ui/icons";
|
|
5
|
+
|
|
6
|
+
const SIMPLE_TYPES = new Set(["text", "textarea", "number", "checkbox", "select", "radio", "color", "email", "password", "code", "markdown", "upload"]);
|
|
4
7
|
|
|
5
8
|
interface ArrayLayoutProps {
|
|
6
9
|
field: Field;
|
|
@@ -14,6 +17,12 @@ interface ArrayLayoutProps {
|
|
|
14
17
|
disabled?: boolean;
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
function isCompactArray(field: Field): boolean {
|
|
21
|
+
const subFields = (field as Field & { fields?: Field[] }).fields || [];
|
|
22
|
+
if (subFields.length === 0 || subFields.length > 4) return false;
|
|
23
|
+
return subFields.every((f: Field) => SIMPLE_TYPES.has(f.type));
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
export function ArrayLayout({
|
|
18
27
|
field,
|
|
19
28
|
value,
|
|
@@ -22,8 +31,26 @@ export function ArrayLayout({
|
|
|
22
31
|
disabled,
|
|
23
32
|
}: ArrayLayoutProps) {
|
|
24
33
|
const items = Array.isArray(value) ? value : [];
|
|
25
|
-
const
|
|
26
|
-
const
|
|
34
|
+
const fields = (field as Field & { fields?: { name?: string; type?: string; relationTo?: string; label?: string }[] }).fields || [];
|
|
35
|
+
const firstField = fields[0];
|
|
36
|
+
const labelField = firstField?.name || "user";
|
|
37
|
+
const isRelationship = firstField?.type === "relationship";
|
|
38
|
+
const [openIndex, setOpenIndex] = React.useState<number | null>(0);
|
|
39
|
+
|
|
40
|
+
function getItemLabel(item: Record<string, unknown>): string {
|
|
41
|
+
for (const key of ["label", "title", "name"]) {
|
|
42
|
+
const val = item[key];
|
|
43
|
+
if (val && typeof val === "string") return val;
|
|
44
|
+
}
|
|
45
|
+
for (const f of fields) {
|
|
46
|
+
if (f.type === "text" || f.type === "textarea") {
|
|
47
|
+
const val = item[f.name!];
|
|
48
|
+
if (val && typeof val === "string") return val;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
const compact = isCompactArray(field);
|
|
27
54
|
|
|
28
55
|
return (
|
|
29
56
|
<div className="kyro-form-field">
|
|
@@ -45,42 +72,106 @@ export function ArrayLayout({
|
|
|
45
72
|
}}
|
|
46
73
|
disabled={disabled}
|
|
47
74
|
/>
|
|
48
|
-
) : (
|
|
49
|
-
<div className="kyro-form-array border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30 rounded-
|
|
75
|
+
) : compact ? (
|
|
76
|
+
<div className="kyro-form-array kyro-form-array--compact border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30 rounded-md overflow-hidden">
|
|
50
77
|
{(items as Record<string, unknown>[]).map((item, index) => (
|
|
51
78
|
<div
|
|
52
79
|
key={index}
|
|
53
|
-
className="
|
|
80
|
+
className="flex items-start gap-2 px-3 py-1.5 border-b border-[var(--kyro-border)] last:border-b-0 hover:bg-[var(--kyro-sidebar-active)]/5 transition-colors"
|
|
54
81
|
>
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<div className="space-y-4">
|
|
71
|
-
{(field as Field & { fields?: Field[] }).fields.map((f: Field) =>
|
|
72
|
-
renderField(f, item, (newItem) => {
|
|
73
|
-
const newItems = [...items];
|
|
74
|
-
newItems[index] = newItem;
|
|
75
|
-
onChange(newItems);
|
|
76
|
-
}),
|
|
77
|
-
)}
|
|
82
|
+
<span className="text-[10px] font-bold text-[var(--kyro-text-muted)] pt-2 min-w-[18px] text-center">
|
|
83
|
+
{index + 1}
|
|
84
|
+
</span>
|
|
85
|
+
<div className="flex-1 flex items-start gap-1.5 min-w-0">
|
|
86
|
+
{(field as Field & { fields?: Field[] }).fields.map((f: Field) => {
|
|
87
|
+
return (
|
|
88
|
+
<div key={f.name} className="flex-1 min-w-0">
|
|
89
|
+
{renderField(f, item, (newItem) => {
|
|
90
|
+
const newItems = [...items];
|
|
91
|
+
newItems[index] = newItem;
|
|
92
|
+
onChange(newItems);
|
|
93
|
+
})}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
})}
|
|
78
97
|
</div>
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
disabled={disabled}
|
|
101
|
+
onClick={() => onChange(items.filter((_: unknown, i: number) => i !== index))}
|
|
102
|
+
className="text-[var(--kyro-text-muted)] hover:text-[var(--kyro-error)] transition-colors disabled:opacity-30 p-0.5 mt-1.5 flex-shrink-0"
|
|
103
|
+
title="Remove"
|
|
104
|
+
>
|
|
105
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
106
|
+
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
107
|
+
</svg>
|
|
108
|
+
</button>
|
|
79
109
|
</div>
|
|
80
110
|
))}
|
|
81
111
|
<button
|
|
82
112
|
type="button"
|
|
83
|
-
className="w-full py-
|
|
113
|
+
className="w-full py-2 border-2 border-dashed border-[var(--kyro-border)] rounded-none text-xs font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:border-[var(--kyro-primary)] transition-all disabled:opacity-50"
|
|
114
|
+
disabled={disabled}
|
|
115
|
+
onClick={() => onChange([...items, {}])}
|
|
116
|
+
>
|
|
117
|
+
+ Add Item
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
) : (
|
|
121
|
+
<div className="kyro-form-array border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/30 rounded-md p-3 space-y-4">
|
|
122
|
+
{(items as Record<string, unknown>[]).map((item, index) => {
|
|
123
|
+
const isOpen = openIndex === index;
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
key={index}
|
|
127
|
+
className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
|
|
128
|
+
>
|
|
129
|
+
<div
|
|
130
|
+
role="button"
|
|
131
|
+
tabIndex={0}
|
|
132
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
133
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpenIndex(isOpen ? null : index); } }}
|
|
134
|
+
className="w-full flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors cursor-pointer"
|
|
135
|
+
>
|
|
136
|
+
<span className="text-xs font-bold tracking-widest text-[var(--kyro-text-muted)] truncate">
|
|
137
|
+
{getItemLabel(item) || `Item ${index + 1}`}
|
|
138
|
+
</span>
|
|
139
|
+
<div className="flex items-center gap-1">
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
onClick={(e) => {
|
|
144
|
+
e.stopPropagation();
|
|
145
|
+
onChange(items.filter((_: unknown, i: number) => i !== index));
|
|
146
|
+
}}
|
|
147
|
+
className="text-[11px] font-bold text-[var(--kyro-error)] opacity-0 group-hover:opacity-100 transition-opacity disabled:opacity-30 hover:bg-[var(--kyro-danger-bg)] rounded px-1.5 py-0.5"
|
|
148
|
+
>
|
|
149
|
+
Remove
|
|
150
|
+
</button>
|
|
151
|
+
{isOpen ? (
|
|
152
|
+
<ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
153
|
+
) : (
|
|
154
|
+
<ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
{isOpen && (
|
|
159
|
+
<div className="p-4 bg-[var(--kyro-surface)] space-y-4">
|
|
160
|
+
{(field as Field & { fields?: Field[] }).fields.map((f: Field) =>
|
|
161
|
+
renderField(f, item, (newItem) => {
|
|
162
|
+
const newItems = [...items];
|
|
163
|
+
newItems[index] = newItem;
|
|
164
|
+
onChange(newItems);
|
|
165
|
+
}),
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
className="w-full py-3 border-2 border-dashed border-[var(--kyro-border)] rounded-lg text-xs font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:border-[var(--kyro-primary)] transition-all disabled:opacity-50"
|
|
84
175
|
disabled={disabled}
|
|
85
176
|
onClick={() => onChange([...items, {}])}
|
|
86
177
|
>
|