@kyro-cms/admin 0.1.6 → 0.1.8
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/README.md +149 -51
- package/package.json +54 -5
- package/src/collections/auth/index.ts +2 -2
- package/src/collections/portfolio/index.ts +343 -0
- package/src/components/ActionBar.tsx +153 -16
- package/src/components/Admin.tsx +137 -28
- package/src/components/ApiExplorer.tsx +325 -0
- package/src/components/ApiKeysManager.tsx +563 -0
- package/src/components/AuditLogsPage.tsx +664 -0
- package/src/components/AutoForm.tsx +2155 -770
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +4 -4
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +200 -58
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +890 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +192 -54
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +206 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- package/src/components/ThemeProvider.tsx +8 -2
- package/src/components/UserManagement.tsx +204 -0
- package/src/components/VersionHistoryPanel.tsx +3 -3
- package/src/components/WebhookManager.tsx +608 -0
- package/src/components/blocks/AccordionBlock.tsx +65 -0
- package/src/components/blocks/ArrayBlock.tsx +84 -0
- package/src/components/blocks/BlockEditModal.tsx +363 -0
- package/src/components/blocks/ButtonBlock.tsx +64 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +114 -0
- package/src/components/blocks/ColumnsBlock.tsx +93 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +63 -0
- package/src/components/blocks/HeadingBlock.tsx +59 -0
- package/src/components/blocks/HeroBlock.tsx +99 -0
- package/src/components/blocks/ImageBlock.tsx +82 -0
- package/src/components/blocks/LinkBlock.tsx +65 -0
- package/src/components/blocks/ListBlock.tsx +60 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +72 -0
- package/src/components/blocks/RichTextBlock.tsx +66 -0
- package/src/components/blocks/VStackBlock.tsx +61 -0
- package/src/components/blocks/VideoBlock.tsx +65 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/AccordionField.tsx +213 -0
- package/src/components/fields/ArrayField.tsx +241 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/ButtonField.tsx +53 -0
- package/src/components/fields/CheckboxField.tsx +18 -8
- package/src/components/fields/ChildrenField.tsx +48 -0
- package/src/components/fields/CodeField.tsx +294 -0
- package/src/components/fields/ColumnsField.tsx +137 -0
- package/src/components/fields/DateField.tsx +24 -12
- package/src/components/fields/EditorClient.tsx +537 -0
- package/src/components/fields/HeadingField.tsx +31 -0
- package/src/components/fields/HeroField.tsx +101 -0
- package/src/components/fields/JSONField.tsx +341 -0
- package/src/components/fields/LinkField.tsx +81 -0
- package/src/components/fields/ListField.tsx +74 -0
- package/src/components/fields/MarkdownField.tsx +260 -0
- package/src/components/fields/NumberField.tsx +25 -13
- package/src/components/fields/PortableTextField.tsx +155 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipBlockField.tsx +233 -0
- package/src/components/fields/RelationshipField.tsx +278 -60
- package/src/components/fields/SelectField.tsx +28 -16
- package/src/components/fields/TextField.tsx +31 -15
- package/src/components/fields/UploadField.tsx +613 -0
- package/src/components/fields/VideoField.tsx +73 -0
- package/src/components/fields/extensions/blockComponents.tsx +247 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +24 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +3 -3
- package/src/components/ui/Badge.tsx +9 -4
- package/src/components/ui/BlockDrawer.tsx +79 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +362 -0
- package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
- package/src/components/ui/Dropdown.tsx +1 -1
- package/src/components/ui/Modal.tsx +37 -12
- package/src/components/ui/PromptModal.tsx +94 -0
- package/src/components/ui/SlidePanel.tsx +43 -16
- package/src/components/ui/Toast.tsx +80 -14
- package/src/env.d.ts +16 -0
- package/src/env.ts +20 -0
- package/src/index.ts +0 -1
- package/src/layouts/AdminLayout.astro +164 -170
- package/src/layouts/AuthLayout.astro +23 -6
- package/src/lib/MediaService.ts +541 -0
- package/src/lib/api.ts +163 -0
- package/src/lib/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +23 -7
- package/src/lib/dataStore.ts +188 -73
- package/src/lib/date-utils.ts +69 -0
- package/src/lib/db/adapter.ts +54 -0
- package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
- package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
- package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
- package/src/lib/db/index.ts +449 -0
- package/src/lib/db/mongodb-adapter.ts +207 -0
- package/src/lib/db/mongodb-auth-adapter.ts +305 -0
- package/src/lib/db/schema/mysql-auth.ts +113 -0
- package/src/lib/db/schema/mysql-content.ts +20 -0
- package/src/lib/db/schema/postgres-auth.ts +116 -0
- package/src/lib/db/schema/postgres-content.ts +35 -0
- package/src/lib/db/schema/postgres-media.ts +52 -0
- package/src/lib/db/schema/postgres-settings.ts +11 -0
- package/src/lib/db/schema/sqlite-auth.ts +112 -0
- package/src/lib/db/schema/sqlite-content.ts +20 -0
- package/src/lib/db/version-adapter.ts +248 -0
- package/src/lib/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/i18n.tsx +353 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/slugify.ts +15 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -0
- package/src/lib/validation.ts +250 -0
- package/src/middleware.ts +70 -11
- package/src/pages/[collection]/[id].astro +178 -122
- package/src/pages/[collection]/index.astro +24 -156
- package/src/pages/admin/api-explorer.astro +98 -0
- package/src/pages/admin/graphql-explorer.astro +40 -0
- package/src/pages/admin/graphql.astro +97 -0
- package/src/pages/admin/index.astro +200 -139
- package/src/pages/admin/keys.astro +8 -0
- package/src/pages/admin/rest-playground.astro +44 -0
- package/src/pages/admin/webhooks.astro +8 -0
- package/src/pages/api/[collection]/[id]/publish.ts +52 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +66 -0
- package/src/pages/api/[collection]/[id].ts +114 -159
- package/src/pages/api/[collection]/index.ts +150 -230
- package/src/pages/api/auth/[id].ts +48 -69
- package/src/pages/api/auth/audit-logs.ts +20 -43
- package/src/pages/api/auth/login.ts +159 -45
- package/src/pages/api/auth/logout.ts +42 -24
- package/src/pages/api/auth/refresh.ts +119 -0
- package/src/pages/api/auth/register.ts +110 -40
- package/src/pages/api/auth/users.ts +22 -97
- package/src/pages/api/collections.ts +59 -0
- package/src/pages/api/globals/[slug]/test.ts +172 -0
- package/src/pages/api/globals/[slug].ts +42 -0
- package/src/pages/api/graphql.ts +90 -0
- package/src/pages/api/health.ts +417 -40
- package/src/pages/api/keys/[id].ts +26 -0
- package/src/pages/api/keys/index.ts +75 -0
- package/src/pages/api/media/[id].ts +309 -0
- package/src/pages/api/media/folders.ts +609 -0
- package/src/pages/api/media/index.ts +146 -0
- package/src/pages/api/media/resize.ts +267 -0
- package/src/pages/api/search.ts +82 -0
- package/src/pages/api/slug-availability.ts +70 -0
- package/src/pages/api/storage-config.ts +20 -0
- package/src/pages/api/storage-status.ts +206 -0
- package/src/pages/api/upload.ts +334 -0
- package/src/pages/api/webhooks/index.ts +71 -0
- package/src/pages/audit/index.astro +2 -104
- package/src/pages/login.astro +11 -11
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +13 -13
- package/src/pages/roles/index.astro +21 -21
- package/src/pages/settings/[slug].astro +162 -0
- package/src/pages/settings/index.astro +9 -0
- package/src/pages/users/[id].astro +29 -21
- package/src/pages/users/index.astro +22 -17
- package/src/pages/users/new.astro +18 -17
- package/src/styles/main.css +563 -128
- package/src/components/layout/Sidebar.tsx +0 -497
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
useBlockById,
|
|
4
|
+
useBlockActions,
|
|
5
|
+
} from "../fields/extensions/blocksStore";
|
|
6
|
+
import { ChevronRight, X } from "lucide-react";
|
|
7
|
+
import { RelationshipBlockField } from "../fields/RelationshipBlockField";
|
|
8
|
+
|
|
9
|
+
export const RelationshipBlock: React.FC<{ block: any; index: number }> = ({
|
|
10
|
+
block,
|
|
11
|
+
index,
|
|
12
|
+
}) => {
|
|
13
|
+
const blockData = useBlockById(block.id);
|
|
14
|
+
const { updateBlock, removeBlock, moveBlock } = useBlockActions();
|
|
15
|
+
|
|
16
|
+
const data = blockData?.data ?? block.data ?? {};
|
|
17
|
+
|
|
18
|
+
const handleChange = (field: string, value: any) => {
|
|
19
|
+
updateBlock(block.id, { data: { ...data, [field]: value } });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="block-relationship border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
|
|
24
|
+
<div className="flex items-center justify-between mb-1">
|
|
25
|
+
<div className="flex items-center gap-2">
|
|
26
|
+
<span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
|
|
27
|
+
Relationship
|
|
28
|
+
</span>
|
|
29
|
+
<span className="text-[10px] text-[var(--kyro-text-muted)]">
|
|
30
|
+
→ {data.relationTo || "pages"}
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={() => moveBlock(block.id, "up")}
|
|
37
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
38
|
+
title="Move up"
|
|
39
|
+
>
|
|
40
|
+
<ChevronRight className="w-3 h-3 rotate-90" />
|
|
41
|
+
</button>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={() => moveBlock(block.id, "down")}
|
|
45
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
46
|
+
title="Move down"
|
|
47
|
+
>
|
|
48
|
+
<ChevronRight className="w-3 h-3" />
|
|
49
|
+
</button>
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
onClick={() => removeBlock(block.id)}
|
|
53
|
+
className="p-1 hover:bg-red-50 rounded text-red-500"
|
|
54
|
+
title="Remove"
|
|
55
|
+
>
|
|
56
|
+
<X className="w-3 h-3" />
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<RelationshipBlockField
|
|
62
|
+
relationTo={data.relationTo || "pages"}
|
|
63
|
+
hasMany={data.hasMany || false}
|
|
64
|
+
selectedIds={Array.isArray(data.selectedIds) ? data.selectedIds : []}
|
|
65
|
+
selectedId={data.selectedId}
|
|
66
|
+
labelField={data.labelField || "title"}
|
|
67
|
+
onChange={handleChange}
|
|
68
|
+
compact
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
useBlockById,
|
|
4
|
+
useBlockActions,
|
|
5
|
+
} from "../fields/extensions/blocksStore";
|
|
6
|
+
import { ChevronRight, X, AlignLeft } from "lucide-react";
|
|
7
|
+
import PortableTextField from "../fields/PortableTextField";
|
|
8
|
+
|
|
9
|
+
export const RichTextBlock: React.FC<{ block: any; index: number }> = ({
|
|
10
|
+
block,
|
|
11
|
+
index,
|
|
12
|
+
}) => {
|
|
13
|
+
const blockData = useBlockById(block.id);
|
|
14
|
+
const { updateBlock, removeBlock, moveBlock } = useBlockActions();
|
|
15
|
+
|
|
16
|
+
const data = blockData?.data || block.data || {};
|
|
17
|
+
|
|
18
|
+
const handleChange = (newValue: any) => {
|
|
19
|
+
updateBlock(block.id, { data: { ...data, content: newValue } });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="block-richtext border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
|
|
24
|
+
<div className="flex items-center justify-between mb-2">
|
|
25
|
+
<div className="flex items-center gap-2">
|
|
26
|
+
<AlignLeft className="w-3.5 h-3.5 text-[var(--kyro-primary)]" />
|
|
27
|
+
<span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
|
|
28
|
+
Rich Text
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
onClick={() => moveBlock(block.id, "up")}
|
|
35
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
36
|
+
title="Move up"
|
|
37
|
+
>
|
|
38
|
+
<ChevronRight className="w-3 h-3 rotate-90" />
|
|
39
|
+
</button>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={() => moveBlock(block.id, "down")}
|
|
43
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
44
|
+
title="Move down"
|
|
45
|
+
>
|
|
46
|
+
<ChevronRight className="w-3 h-3" />
|
|
47
|
+
</button>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={() => removeBlock(block.id)}
|
|
51
|
+
className="p-1 hover:bg-red-50 rounded text-red-500"
|
|
52
|
+
title="Remove"
|
|
53
|
+
>
|
|
54
|
+
<X className="w-3 h-3" />
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<PortableTextField
|
|
60
|
+
field={{ name: "content", label: "Content" }}
|
|
61
|
+
value={data.content}
|
|
62
|
+
onChange={handleChange}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
useBlockById,
|
|
4
|
+
useBlockActions,
|
|
5
|
+
} from "../fields/extensions/blocksStore";
|
|
6
|
+
import { ChevronRight, X } from "lucide-react";
|
|
7
|
+
import { ChildrenField } from "../fields/ChildrenField";
|
|
8
|
+
|
|
9
|
+
export const VStackBlock: React.FC<{ block: any; index: number }> = ({
|
|
10
|
+
block,
|
|
11
|
+
index,
|
|
12
|
+
}) => {
|
|
13
|
+
const blockData = useBlockById(block.id);
|
|
14
|
+
const { updateBlock, removeBlock, moveBlock } = useBlockActions();
|
|
15
|
+
|
|
16
|
+
const data = blockData?.data ?? block.data ?? {};
|
|
17
|
+
const children = blockData?.children ?? block.children ?? [];
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="block-vstack border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
|
|
21
|
+
<div className="flex items-center justify-between mb-1">
|
|
22
|
+
<span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
|
|
23
|
+
Stack
|
|
24
|
+
</span>
|
|
25
|
+
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={() => moveBlock(block.id, "up")}
|
|
29
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
30
|
+
title="Move up"
|
|
31
|
+
>
|
|
32
|
+
<ChevronRight className="w-3 h-3 rotate-90" />
|
|
33
|
+
</button>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={() => moveBlock(block.id, "down")}
|
|
37
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
38
|
+
title="Move down"
|
|
39
|
+
>
|
|
40
|
+
<ChevronRight className="w-3 h-3" />
|
|
41
|
+
</button>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={() => removeBlock(block.id)}
|
|
45
|
+
className="p-1 hover:bg-red-50 rounded text-red-500"
|
|
46
|
+
title="Remove"
|
|
47
|
+
>
|
|
48
|
+
<X className="w-3 h-3" />
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<ChildrenField
|
|
54
|
+
blockId={block.id}
|
|
55
|
+
children={children}
|
|
56
|
+
onUpdateChildren={(c) => updateBlock(block.id, { children: c })}
|
|
57
|
+
compact
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
useBlockById,
|
|
4
|
+
useBlockActions,
|
|
5
|
+
} from "../fields/extensions/blocksStore";
|
|
6
|
+
import { ChevronRight, X } from "lucide-react";
|
|
7
|
+
import { VideoField } from "../fields/VideoField";
|
|
8
|
+
|
|
9
|
+
export const VideoBlock: React.FC<{ block: any; index: number }> = ({
|
|
10
|
+
block,
|
|
11
|
+
index,
|
|
12
|
+
}) => {
|
|
13
|
+
const blockData = useBlockById(block.id);
|
|
14
|
+
const { updateBlock, removeBlock, moveBlock } = useBlockActions();
|
|
15
|
+
|
|
16
|
+
const data = blockData?.data ?? block.data ?? {};
|
|
17
|
+
|
|
18
|
+
const handleChange = (field: string, value: any) => {
|
|
19
|
+
updateBlock(block.id, { data: { ...data, [field]: value } });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="block-video border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
|
|
24
|
+
<div className="flex items-center justify-between mb-1">
|
|
25
|
+
<span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
|
|
26
|
+
Video
|
|
27
|
+
</span>
|
|
28
|
+
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
onClick={() => moveBlock(block.id, "up")}
|
|
32
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
33
|
+
title="Move up"
|
|
34
|
+
>
|
|
35
|
+
<ChevronRight className="w-3 h-3 rotate-90" />
|
|
36
|
+
</button>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={() => moveBlock(block.id, "down")}
|
|
40
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
41
|
+
title="Move down"
|
|
42
|
+
>
|
|
43
|
+
<ChevronRight className="w-3 h-3" />
|
|
44
|
+
</button>
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
onClick={() => removeBlock(block.id)}
|
|
48
|
+
className="p-1 hover:bg-red-50 rounded text-red-500"
|
|
49
|
+
title="Remove"
|
|
50
|
+
>
|
|
51
|
+
<X className="w-3 h-3" />
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<VideoField
|
|
57
|
+
src={data.src || ""}
|
|
58
|
+
title={data.title || ""}
|
|
59
|
+
onChange={handleChange}
|
|
60
|
+
onUploadChange={(v) => handleChange("src", v)}
|
|
61
|
+
compact
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { ColumnsBlock } from './ColumnsBlock';
|
|
2
|
+
export { HeadingBlock } from './HeadingBlock';
|
|
3
|
+
export { ParagraphBlock } from './ParagraphBlock';
|
|
4
|
+
export { DividerBlock } from './DividerBlock';
|
|
5
|
+
export { ImageBlock } from './ImageBlock';
|
|
6
|
+
export { VideoBlock } from './VideoBlock';
|
|
7
|
+
export { ListBlock } from './ListBlock';
|
|
8
|
+
export { CodeBlock } from './CodeBlock';
|
|
9
|
+
export { LinkBlock } from './LinkBlock';
|
|
10
|
+
export { FileBlock } from './FileBlock';
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ChevronDown, ChevronUp, Plus, X } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface AccordionItem {
|
|
5
|
+
title: string;
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface AccordionFieldProps {
|
|
10
|
+
items?: AccordionItem[];
|
|
11
|
+
onChange: (items: AccordionItem[]) => void;
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const AccordionField: React.FC<AccordionFieldProps> = ({
|
|
16
|
+
items = [],
|
|
17
|
+
onChange,
|
|
18
|
+
compact = false,
|
|
19
|
+
}) => {
|
|
20
|
+
const [openIndex, setOpenIndex] = React.useState<number | null>(0);
|
|
21
|
+
|
|
22
|
+
const handleTitleChange = (index: number, value: string) => {
|
|
23
|
+
const newItems = [...items];
|
|
24
|
+
newItems[index] = { ...newItems[index], title: value };
|
|
25
|
+
onChange(newItems);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleContentChange = (index: number, value: string) => {
|
|
29
|
+
const newItems = [...items];
|
|
30
|
+
newItems[index] = { ...newItems[index], content: value };
|
|
31
|
+
onChange(newItems);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const handleRemove = (index: number) => {
|
|
35
|
+
const newItems = items.filter((_, i) => i !== index);
|
|
36
|
+
onChange(newItems);
|
|
37
|
+
if (openIndex === index) setOpenIndex(null);
|
|
38
|
+
else if (openIndex !== null && openIndex > index)
|
|
39
|
+
setOpenIndex(openIndex - 1);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleAdd = () => {
|
|
43
|
+
onChange([...items, { title: `Item ${items.length + 1}`, content: "" }]);
|
|
44
|
+
setOpenIndex(items.length);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const baseInputClass =
|
|
48
|
+
"w-full px-3 py-2 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";
|
|
49
|
+
const smallInputClass =
|
|
50
|
+
"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";
|
|
51
|
+
|
|
52
|
+
if (compact) {
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-2">
|
|
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-lg">
|
|
57
|
+
No items. Click "Add Item" to create one.
|
|
58
|
+
</div>
|
|
59
|
+
) : (
|
|
60
|
+
<div className="space-y-1.5">
|
|
61
|
+
{items.map((item: AccordionItem, index: number) => {
|
|
62
|
+
const isOpen = openIndex === index;
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
key={index}
|
|
66
|
+
className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
|
|
67
|
+
>
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
71
|
+
className="w-full flex items-center justify-between p-2.5 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors"
|
|
72
|
+
>
|
|
73
|
+
<span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
|
|
74
|
+
{item.title || `Item ${index + 1}`}
|
|
75
|
+
</span>
|
|
76
|
+
<div className="flex items-center gap-1">
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={(e) => {
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
handleRemove(index);
|
|
82
|
+
}}
|
|
83
|
+
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
|
|
84
|
+
title="Remove"
|
|
85
|
+
>
|
|
86
|
+
<X className="w-3.5 h-3.5" />
|
|
87
|
+
</button>
|
|
88
|
+
{isOpen ? (
|
|
89
|
+
<ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
90
|
+
) : (
|
|
91
|
+
<ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
</button>
|
|
95
|
+
{isOpen && (
|
|
96
|
+
<div className="p-2.5 bg-[var(--kyro-surface)] space-y-2">
|
|
97
|
+
<input
|
|
98
|
+
type="text"
|
|
99
|
+
value={item.title || ""}
|
|
100
|
+
onChange={(e) =>
|
|
101
|
+
handleTitleChange(index, e.target.value)
|
|
102
|
+
}
|
|
103
|
+
onClick={(e) => e.stopPropagation()}
|
|
104
|
+
className={smallInputClass}
|
|
105
|
+
placeholder="Item title..."
|
|
106
|
+
/>
|
|
107
|
+
<textarea
|
|
108
|
+
value={item.content || ""}
|
|
109
|
+
onChange={(e) =>
|
|
110
|
+
handleContentChange(index, e.target.value)
|
|
111
|
+
}
|
|
112
|
+
onClick={(e) => e.stopPropagation()}
|
|
113
|
+
className={`${smallInputClass} min-h-[60px] resize-none`}
|
|
114
|
+
placeholder="Item content..."
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
})}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={handleAdd}
|
|
126
|
+
className="flex items-center justify-center gap-1.5 w-full px-3 py-2 text-xs font-medium rounded-lg border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-sidebar-active)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
127
|
+
>
|
|
128
|
+
<Plus className="w-3.5 h-3.5" />
|
|
129
|
+
Add Item
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className="space-y-2">
|
|
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-lg">
|
|
139
|
+
No items. Click "Add Item" to create one.
|
|
140
|
+
</div>
|
|
141
|
+
) : (
|
|
142
|
+
<div className="space-y-2">
|
|
143
|
+
{items.map((item: AccordionItem, index: number) => {
|
|
144
|
+
const isOpen = openIndex === index;
|
|
145
|
+
return (
|
|
146
|
+
<div
|
|
147
|
+
key={index}
|
|
148
|
+
className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
|
|
149
|
+
>
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
onClick={() => setOpenIndex(isOpen ? null : index)}
|
|
153
|
+
className="w-full flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors"
|
|
154
|
+
>
|
|
155
|
+
<span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
|
|
156
|
+
{item.title || `Item ${index + 1}`}
|
|
157
|
+
</span>
|
|
158
|
+
<div className="flex items-center gap-1">
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
onClick={(e) => {
|
|
162
|
+
e.stopPropagation();
|
|
163
|
+
handleRemove(index);
|
|
164
|
+
}}
|
|
165
|
+
className="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
|
|
166
|
+
title="Remove"
|
|
167
|
+
>
|
|
168
|
+
<X className="w-4 h-4" />
|
|
169
|
+
</button>
|
|
170
|
+
{isOpen ? (
|
|
171
|
+
<ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
172
|
+
) : (
|
|
173
|
+
<ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
</button>
|
|
177
|
+
{isOpen && (
|
|
178
|
+
<div className="p-3 bg-[var(--kyro-surface)] space-y-2">
|
|
179
|
+
<input
|
|
180
|
+
type="text"
|
|
181
|
+
value={item.title || ""}
|
|
182
|
+
onChange={(e) => handleTitleChange(index, e.target.value)}
|
|
183
|
+
className={baseInputClass}
|
|
184
|
+
placeholder="Item title..."
|
|
185
|
+
/>
|
|
186
|
+
<textarea
|
|
187
|
+
value={item.content || ""}
|
|
188
|
+
onChange={(e) =>
|
|
189
|
+
handleContentChange(index, e.target.value)
|
|
190
|
+
}
|
|
191
|
+
className={`${baseInputClass} min-h-[60px] resize-none`}
|
|
192
|
+
placeholder="Item content..."
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
})}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={handleAdd}
|
|
204
|
+
className="flex items-center justify-center gap-1.5 w-full px-3 py-2 text-xs font-medium rounded-lg border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-sidebar-active)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
205
|
+
>
|
|
206
|
+
<Plus className="w-3.5 h-3.5" />
|
|
207
|
+
Add Item
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export default AccordionField;
|