@kyro-cms/admin 0.1.6 → 0.1.7
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 +53 -6
- 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 +136 -27
- 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 +1417 -661
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +3 -3
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +199 -57
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +786 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +191 -53
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +149 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- 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 +97 -0
- package/src/components/blocks/ArrayBlock.tsx +75 -0
- package/src/components/blocks/BlockEditModal.MARKER +12 -0
- package/src/components/blocks/BlockEditModal.tsx +774 -0
- package/src/components/blocks/ButtonBlock.tsx +165 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +66 -0
- package/src/components/blocks/ColumnsBlock.tsx +151 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +64 -0
- package/src/components/blocks/HeadingBlock.tsx +81 -0
- package/src/components/blocks/HeroBlock.tsx +157 -0
- package/src/components/blocks/ImageBlock.tsx +83 -0
- package/src/components/blocks/LinkBlock.tsx +71 -0
- package/src/components/blocks/ListBlock.tsx +39 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +279 -0
- package/src/components/blocks/VStackBlock.tsx +75 -0
- package/src/components/blocks/VideoBlock.tsx +45 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/CheckboxField.tsx +15 -9
- package/src/components/fields/CodeField.tsx +234 -0
- package/src/components/fields/DateField.tsx +38 -11
- package/src/components/fields/EditorClient.tsx +271 -0
- package/src/components/fields/FileField.tsx +390 -0
- package/src/components/fields/HybridContentField.tsx +109 -0
- package/src/components/fields/ImageField.tsx +429 -0
- package/src/components/fields/JSONField.tsx +361 -0
- package/src/components/fields/MarkdownField.tsx +282 -0
- package/src/components/fields/NumberField.tsx +42 -12
- package/src/components/fields/PortableTextField.tsx +143 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipField.tsx +231 -59
- package/src/components/fields/SelectField.tsx +25 -15
- package/src/components/fields/TextField.tsx +45 -14
- package/src/components/fields/extensions/blockComponents.tsx +237 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +13 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +2 -2
- 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/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +22 -6
- package/src/lib/dataStore.ts +132 -74
- 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/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -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 +44 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +36 -0
- package/src/pages/api/[collection]/[id].ts +102 -159
- package/src/pages/api/[collection]/index.ts +151 -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 +553 -128
- package/src/components/layout/Sidebar.tsx +0 -497
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SelectField as SelectFieldType } from
|
|
1
|
+
import type { SelectField as SelectFieldType } from "@kyro-cms/core";
|
|
2
2
|
|
|
3
3
|
interface SelectFieldComponentProps {
|
|
4
4
|
field: SelectFieldType;
|
|
@@ -8,20 +8,32 @@ interface SelectFieldComponentProps {
|
|
|
8
8
|
disabled?: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export default function SelectField({
|
|
11
|
+
export default function SelectField({
|
|
12
|
+
field,
|
|
13
|
+
value,
|
|
14
|
+
onChange,
|
|
15
|
+
error,
|
|
16
|
+
disabled,
|
|
17
|
+
}: SelectFieldComponentProps) {
|
|
12
18
|
return (
|
|
13
19
|
<div className="space-y-1">
|
|
14
20
|
{field.label && (
|
|
15
|
-
<label className="block text-sm font-medium text-
|
|
21
|
+
<label className="block text-sm font-medium text-[var(--kyro-text-primary)]">
|
|
16
22
|
{field.label}
|
|
17
23
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
18
24
|
</label>
|
|
19
25
|
)}
|
|
20
26
|
<select
|
|
21
|
-
value={
|
|
27
|
+
value={
|
|
28
|
+
field.hasMany
|
|
29
|
+
? Array.isArray(value)
|
|
30
|
+
? value.join(",")
|
|
31
|
+
: ""
|
|
32
|
+
: value || ""
|
|
33
|
+
}
|
|
22
34
|
onChange={(e) => {
|
|
23
35
|
if (field.hasMany) {
|
|
24
|
-
const selected = e.target.value ? e.target.value.split(
|
|
36
|
+
const selected = e.target.value ? e.target.value.split(",") : [];
|
|
25
37
|
onChange?.(selected);
|
|
26
38
|
} else {
|
|
27
39
|
onChange?.(e.target.value);
|
|
@@ -32,13 +44,11 @@ export default function SelectField({ field, value, onChange, error, disabled }:
|
|
|
32
44
|
required={field.required}
|
|
33
45
|
className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
|
|
34
46
|
error
|
|
35
|
-
?
|
|
36
|
-
:
|
|
37
|
-
} ${disabled || field.admin?.readOnly ?
|
|
47
|
+
? "border-red-500 focus:border-red-500 focus:ring-red-500"
|
|
48
|
+
: "border-[var(--kyro-border)] focus:border-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
|
|
49
|
+
} ${disabled || field.admin?.readOnly ? "bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-muted)]" : "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"}`}
|
|
38
50
|
>
|
|
39
|
-
{!field.required &&
|
|
40
|
-
<option value="">Select...</option>
|
|
41
|
-
)}
|
|
51
|
+
{!field.required && <option value="">Select...</option>}
|
|
42
52
|
{field.options.map((option) => (
|
|
43
53
|
<option key={option.value} value={option.value}>
|
|
44
54
|
{option.label}
|
|
@@ -46,11 +56,11 @@ export default function SelectField({ field, value, onChange, error, disabled }:
|
|
|
46
56
|
))}
|
|
47
57
|
</select>
|
|
48
58
|
{field.admin?.description && !error && (
|
|
49
|
-
<p className="text-xs text-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<p className="text-xs text-red-600">{error}</p>
|
|
59
|
+
<p className="text-xs text-[var(--kyro-text-secondary)]">
|
|
60
|
+
{field.admin.description}
|
|
61
|
+
</p>
|
|
53
62
|
)}
|
|
63
|
+
{error && <p className="text-xs text-red-500">{error}</p>}
|
|
54
64
|
</div>
|
|
55
65
|
);
|
|
56
66
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import type { TextField as TextFieldType } from "@kyro-cms/core";
|
|
2
3
|
|
|
3
4
|
interface TextFieldComponentProps {
|
|
4
5
|
field: TextFieldType;
|
|
@@ -8,16 +9,40 @@ interface TextFieldComponentProps {
|
|
|
8
9
|
disabled?: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export default function TextField({
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
export default function TextField({
|
|
13
|
+
field,
|
|
14
|
+
value = "",
|
|
15
|
+
onChange,
|
|
16
|
+
error,
|
|
17
|
+
disabled,
|
|
18
|
+
}: TextFieldComponentProps) {
|
|
19
|
+
const [isDark, setIsDark] = useState(false);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setIsDark(document.documentElement.classList.contains("dark"));
|
|
23
|
+
const observer = new MutationObserver(() => {
|
|
24
|
+
setIsDark(document.documentElement.classList.contains("dark"));
|
|
25
|
+
});
|
|
26
|
+
observer.observe(document.documentElement, {
|
|
27
|
+
attributes: true,
|
|
28
|
+
attributeFilter: ["class"],
|
|
29
|
+
});
|
|
30
|
+
return () => observer.disconnect();
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const inputType =
|
|
34
|
+
field.variant === "email"
|
|
35
|
+
? "email"
|
|
36
|
+
: field.variant === "password"
|
|
37
|
+
? "password"
|
|
38
|
+
: field.variant === "url"
|
|
39
|
+
? "url"
|
|
40
|
+
: "text";
|
|
16
41
|
|
|
17
42
|
return (
|
|
18
43
|
<div className="space-y-1">
|
|
19
44
|
{field.label && (
|
|
20
|
-
<label className="block text-sm font-medium
|
|
45
|
+
<label className="block text-sm font-medium">
|
|
21
46
|
{field.label}
|
|
22
47
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
23
48
|
</label>
|
|
@@ -34,16 +59,22 @@ export default function TextField({ field, value = '', onChange, error, disabled
|
|
|
34
59
|
required={field.required}
|
|
35
60
|
className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
|
|
36
61
|
error
|
|
37
|
-
?
|
|
38
|
-
:
|
|
39
|
-
} ${
|
|
62
|
+
? "border-red-300 focus:border-red-500 focus:ring-red-500"
|
|
63
|
+
: "border-[var(--kyro-border)] focus:border-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
|
|
64
|
+
} ${
|
|
65
|
+
disabled || field.admin?.readOnly
|
|
66
|
+
? "bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] opacity-50"
|
|
67
|
+
: isDark
|
|
68
|
+
? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
|
|
69
|
+
: "bg-white text-gray-900"
|
|
70
|
+
}`}
|
|
40
71
|
/>
|
|
41
72
|
{field.admin?.description && !error && (
|
|
42
|
-
<p className="text-xs text-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<p className="text-xs text-red-600">{error}</p>
|
|
73
|
+
<p className="text-xs text-[var(--kyro-text-secondary)]">
|
|
74
|
+
{field.admin.description}
|
|
75
|
+
</p>
|
|
46
76
|
)}
|
|
77
|
+
{error && <p className="text-xs text-red-600">{error}</p>}
|
|
47
78
|
</div>
|
|
48
79
|
);
|
|
49
80
|
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ColumnsBlock } from "../../blocks/ColumnsBlock";
|
|
3
|
+
import { HeadingBlock } from "../../blocks/HeadingBlock";
|
|
4
|
+
import { ParagraphBlock } from "../../blocks/ParagraphBlock";
|
|
5
|
+
import { DividerBlock } from "../../blocks/DividerBlock";
|
|
6
|
+
import { ImageBlock } from "../../blocks/ImageBlock";
|
|
7
|
+
import { VideoBlock } from "../../blocks/VideoBlock";
|
|
8
|
+
import { ListBlock } from "../../blocks/ListBlock";
|
|
9
|
+
import { CodeBlock } from "../../blocks/CodeBlock";
|
|
10
|
+
import { LinkBlock } from "../../blocks/LinkBlock";
|
|
11
|
+
import { FileBlock } from "../../blocks/FileBlock";
|
|
12
|
+
import { VStackBlock } from "../../blocks/VStackBlock";
|
|
13
|
+
import { ButtonBlock } from "../../blocks/ButtonBlock";
|
|
14
|
+
import { AccordionBlock } from "../../blocks/AccordionBlock";
|
|
15
|
+
|
|
16
|
+
import { HeroBlock } from "../../blocks/HeroBlock";
|
|
17
|
+
import { ArrayBlock } from "../../blocks/ArrayBlock";
|
|
18
|
+
import { RelationshipBlock } from "../../blocks/RelationshipBlock";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
Columns3,
|
|
22
|
+
Heading1,
|
|
23
|
+
AlignLeft,
|
|
24
|
+
Minus,
|
|
25
|
+
Image,
|
|
26
|
+
Video,
|
|
27
|
+
List,
|
|
28
|
+
Code,
|
|
29
|
+
Link,
|
|
30
|
+
File,
|
|
31
|
+
ArrowDown,
|
|
32
|
+
MousePointerClick,
|
|
33
|
+
ChevronDown,
|
|
34
|
+
Star,
|
|
35
|
+
ListOrdered,
|
|
36
|
+
Link2,
|
|
37
|
+
} from "lucide-react";
|
|
38
|
+
|
|
39
|
+
// Block component registry
|
|
40
|
+
export const BLOCK_COMPONENTS: Record<string, React.ComponentType<any>> = {
|
|
41
|
+
columns: ColumnsBlock,
|
|
42
|
+
heading: HeadingBlock,
|
|
43
|
+
paragraph: ParagraphBlock,
|
|
44
|
+
divider: DividerBlock,
|
|
45
|
+
image: ImageBlock,
|
|
46
|
+
video: VideoBlock,
|
|
47
|
+
list: ListBlock,
|
|
48
|
+
code: CodeBlock,
|
|
49
|
+
link: LinkBlock,
|
|
50
|
+
file: FileBlock,
|
|
51
|
+
vstack: VStackBlock,
|
|
52
|
+
button: ButtonBlock,
|
|
53
|
+
accordion: AccordionBlock,
|
|
54
|
+
|
|
55
|
+
hero: HeroBlock,
|
|
56
|
+
array: ArrayBlock,
|
|
57
|
+
relationship: RelationshipBlock,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Icon mapping for drawer (actual Lucide components)
|
|
61
|
+
export const blockIcons: Record<string, React.ReactNode> = {
|
|
62
|
+
columns: <Columns3 className="w-4 h-4" />,
|
|
63
|
+
heading: <Heading1 className="w-4 h-4" />,
|
|
64
|
+
paragraph: <AlignLeft className="w-4 h-4" />,
|
|
65
|
+
divider: <Minus className="w-4 h-4" />,
|
|
66
|
+
image: <Image className="w-4 h-4" />,
|
|
67
|
+
video: <Video className="w-4 h-4" />,
|
|
68
|
+
list: <List className="w-4 h-4" />,
|
|
69
|
+
code: <Code className="w-4 h-4" />,
|
|
70
|
+
link: <Link className="w-4 h-4" />,
|
|
71
|
+
file: <File className="w-4 h-4" />,
|
|
72
|
+
vstack: <ArrowDown className="w-4 h-4" />,
|
|
73
|
+
button: <MousePointerClick className="w-4 h-4" />,
|
|
74
|
+
accordion: <ChevronDown className="w-4 h-4" />,
|
|
75
|
+
|
|
76
|
+
hero: <Star className="w-4 h-4" />,
|
|
77
|
+
array: <ListOrdered className="w-4 h-4" />,
|
|
78
|
+
relationship: <Link2 className="w-4 h-4" />,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Get block component by type
|
|
82
|
+
export function getBlockComponent(type: string) {
|
|
83
|
+
return BLOCK_COMPONENTS[type];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check if block type is supported
|
|
87
|
+
export function isSupportedBlockType(type: string): boolean {
|
|
88
|
+
return type in BLOCK_COMPONENTS;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Get human-readable label for block type
|
|
92
|
+
export function getBlockLabel(type: string): string {
|
|
93
|
+
const labelMap: Record<string, string> = {
|
|
94
|
+
paragraph: "Paragraph",
|
|
95
|
+
heading: "Heading",
|
|
96
|
+
image: "Image",
|
|
97
|
+
video: "Video",
|
|
98
|
+
link: "Link",
|
|
99
|
+
button: "Button",
|
|
100
|
+
list: "List",
|
|
101
|
+
code: "Code",
|
|
102
|
+
file: "File",
|
|
103
|
+
divider: "Divider",
|
|
104
|
+
accordion: "Accordion",
|
|
105
|
+
array: "Repeater",
|
|
106
|
+
hero: "Hero",
|
|
107
|
+
vstack: "VStack",
|
|
108
|
+
columns: "Columns",
|
|
109
|
+
relationship: "Relationship",
|
|
110
|
+
};
|
|
111
|
+
return labelMap[type] || type;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Block categories for the drawer
|
|
115
|
+
export const blockCategories = [
|
|
116
|
+
{
|
|
117
|
+
title: "Layout",
|
|
118
|
+
blocks: [
|
|
119
|
+
{
|
|
120
|
+
type: "columns",
|
|
121
|
+
label: "Columns",
|
|
122
|
+
icon: "columns",
|
|
123
|
+
description: "1-6 columns side-by-side",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: "vstack",
|
|
127
|
+
label: "VStack",
|
|
128
|
+
icon: "vstack",
|
|
129
|
+
description: "Stack blocks vertically",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: "hero",
|
|
133
|
+
label: "Hero",
|
|
134
|
+
icon: "hero",
|
|
135
|
+
description: "Hero with content + video",
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
title: "Text",
|
|
141
|
+
blocks: [
|
|
142
|
+
{
|
|
143
|
+
type: "heading",
|
|
144
|
+
label: "Heading",
|
|
145
|
+
icon: "heading",
|
|
146
|
+
description: "H1-H3 heading",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: "paragraph",
|
|
150
|
+
label: "Paragraph",
|
|
151
|
+
icon: "paragraph",
|
|
152
|
+
description: "Plain text content",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
type: "list",
|
|
156
|
+
label: "List",
|
|
157
|
+
icon: "list",
|
|
158
|
+
description: "Ordered/unordered list",
|
|
159
|
+
},
|
|
160
|
+
{ type: "link", label: "Link", icon: "link", description: "Hyperlink" },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
title: "Media",
|
|
165
|
+
blocks: [
|
|
166
|
+
{
|
|
167
|
+
type: "image",
|
|
168
|
+
label: "Image",
|
|
169
|
+
icon: "image",
|
|
170
|
+
description: "Single image",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: "video",
|
|
174
|
+
label: "Video",
|
|
175
|
+
icon: "video",
|
|
176
|
+
description: "Embed video",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: "file",
|
|
180
|
+
label: "File",
|
|
181
|
+
icon: "file",
|
|
182
|
+
description: "File download link",
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
title: "Interactive",
|
|
188
|
+
blocks: [
|
|
189
|
+
{
|
|
190
|
+
type: "button",
|
|
191
|
+
label: "Button",
|
|
192
|
+
icon: "button",
|
|
193
|
+
description: "CTA button",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
type: "accordion",
|
|
197
|
+
label: "Accordion",
|
|
198
|
+
icon: "accordion",
|
|
199
|
+
description: "Collapsible sections",
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
title: "Data",
|
|
205
|
+
blocks: [
|
|
206
|
+
{
|
|
207
|
+
type: "array",
|
|
208
|
+
label: "Repeater",
|
|
209
|
+
icon: "array",
|
|
210
|
+
description: "Add multiple child blocks",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
type: "code",
|
|
214
|
+
label: "Code",
|
|
215
|
+
icon: "code",
|
|
216
|
+
description: "Code snippet",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: "relationship",
|
|
220
|
+
label: "Relationship",
|
|
221
|
+
icon: "relationship",
|
|
222
|
+
description: "Link to other collection",
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
title: "Basic",
|
|
228
|
+
blocks: [
|
|
229
|
+
{
|
|
230
|
+
type: "divider",
|
|
231
|
+
label: "Divider",
|
|
232
|
+
icon: "divider",
|
|
233
|
+
description: "Horizontal separator",
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
];
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import type { BlockData } from "@kyro-cms/core";
|
|
3
|
+
|
|
4
|
+
export interface BlocksStore {
|
|
5
|
+
blocks: BlockData[];
|
|
6
|
+
setBlocks: (blocks: BlockData[]) => void;
|
|
7
|
+
addBlock: (type: string, index?: number) => void;
|
|
8
|
+
updateBlock: (id: string, data: Partial<BlockData>) => void;
|
|
9
|
+
removeBlock: (id: string) => void;
|
|
10
|
+
moveBlock: (id: string, direction: "up" | "down") => void;
|
|
11
|
+
onBlocksChange: (() => void) | null;
|
|
12
|
+
setOnBlocksChange: (cb: () => void) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Helper to create new block (since we can't import createBlock from core in zustand)
|
|
16
|
+
export function createNewBlock(type: string): BlockData {
|
|
17
|
+
const defaultData = getDefaultData(type);
|
|
18
|
+
// Extract options and children from defaultData if present
|
|
19
|
+
const { options, children, ...data } = defaultData;
|
|
20
|
+
return {
|
|
21
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
22
|
+
type,
|
|
23
|
+
data,
|
|
24
|
+
options,
|
|
25
|
+
children,
|
|
26
|
+
order: Date.now(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getDefaultData(type: string): Record<string, any> {
|
|
31
|
+
const defaults: Record<string, any> = {
|
|
32
|
+
heading: { level: 1, text: "" },
|
|
33
|
+
paragraph: { text: "" },
|
|
34
|
+
divider: {},
|
|
35
|
+
callout: { text: "", variant: "info" },
|
|
36
|
+
image: { src: "", alt: "", caption: "" },
|
|
37
|
+
video: { src: "", title: "" },
|
|
38
|
+
list: { type: "unordered", items: "" },
|
|
39
|
+
code: { language: "plaintext", code: "" },
|
|
40
|
+
link: { url: "", text: "" },
|
|
41
|
+
table: { rows: 3, columns: 3, content: "" },
|
|
42
|
+
quote: { text: "", author: "" },
|
|
43
|
+
file: { filename: "", url: "" },
|
|
44
|
+
kyroColumns: { columns: 2, direction: "horizontal" },
|
|
45
|
+
vstack: { direction: "vertical", gap: "md" },
|
|
46
|
+
container: {
|
|
47
|
+
options: {
|
|
48
|
+
backgroundColor: "transparent",
|
|
49
|
+
padding: "md",
|
|
50
|
+
width: "full",
|
|
51
|
+
margin: "none",
|
|
52
|
+
minHeight: "none",
|
|
53
|
+
borderRadius: "none",
|
|
54
|
+
},
|
|
55
|
+
children: [],
|
|
56
|
+
},
|
|
57
|
+
button: { text: "Button", url: "", variant: "primary", size: "md" },
|
|
58
|
+
accordion: { title: "Accordion Item", content: "" },
|
|
59
|
+
gallery: { images: [] },
|
|
60
|
+
tabs: { tabs: [{ label: "Tab 1", content: "" }] },
|
|
61
|
+
};
|
|
62
|
+
return defaults[type] || {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const useBlocksStore = create<BlocksStore>((set, get) => ({
|
|
66
|
+
blocks: [],
|
|
67
|
+
setBlocks: (blocks) => set({ blocks }),
|
|
68
|
+
onBlocksChange: null,
|
|
69
|
+
setOnBlocksChange: (cb) => set({ onBlocksChange: cb }),
|
|
70
|
+
addBlock: (type, index) => {
|
|
71
|
+
const newBlock = createNewBlock(type);
|
|
72
|
+
const { blocks } = get();
|
|
73
|
+
const newBlocks = [...blocks];
|
|
74
|
+
if (index !== undefined) {
|
|
75
|
+
newBlocks.splice(index, 0, newBlock);
|
|
76
|
+
} else {
|
|
77
|
+
newBlocks.push(newBlock);
|
|
78
|
+
}
|
|
79
|
+
set({ blocks: newBlocks });
|
|
80
|
+
const { onBlocksChange } = get();
|
|
81
|
+
if (onBlocksChange) onBlocksChange();
|
|
82
|
+
},
|
|
83
|
+
updateBlock: (id, data) => {
|
|
84
|
+
const { blocks } = get();
|
|
85
|
+
|
|
86
|
+
const updateRecursive = (blocksList: BlockData[]): BlockData[] => {
|
|
87
|
+
let changed = false;
|
|
88
|
+
const newList = blocksList.map(b => {
|
|
89
|
+
if (b.id === id) {
|
|
90
|
+
changed = true;
|
|
91
|
+
return { ...b, ...data };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let newB = b;
|
|
95
|
+
if (b.children && b.children.length > 0) {
|
|
96
|
+
const newChildren = updateRecursive(b.children);
|
|
97
|
+
if (newChildren !== b.children) {
|
|
98
|
+
newB = { ...newB, children: newChildren };
|
|
99
|
+
changed = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (b.data?.columnData) {
|
|
104
|
+
const newColumnData = b.data.columnData.map((col: any) => {
|
|
105
|
+
if (col.children && col.children.length > 0) {
|
|
106
|
+
const newChildren = updateRecursive(col.children);
|
|
107
|
+
if (newChildren !== col.children) {
|
|
108
|
+
return { ...col, children: newChildren };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return col;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (newColumnData.some((col: any, i: number) => col !== b.data.columnData[i])) {
|
|
115
|
+
newB = { ...newB, data: { ...newB.data, columnData: newColumnData } };
|
|
116
|
+
changed = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return newB;
|
|
121
|
+
});
|
|
122
|
+
return changed ? newList : blocksList;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const newBlocks = updateRecursive(blocks);
|
|
126
|
+
if (newBlocks !== blocks) {
|
|
127
|
+
set({ blocks: newBlocks });
|
|
128
|
+
const { onBlocksChange } = get();
|
|
129
|
+
if (onBlocksChange) onBlocksChange();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
removeBlock: (id) => {
|
|
133
|
+
const { blocks } = get();
|
|
134
|
+
|
|
135
|
+
const removeRecursive = (blocksList: BlockData[]): BlockData[] => {
|
|
136
|
+
const filtered = blocksList.filter(b => b.id !== id);
|
|
137
|
+
if (filtered.length !== blocksList.length) {
|
|
138
|
+
return filtered; // found and removed at this level
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let changed = false;
|
|
142
|
+
const newList = blocksList.map(b => {
|
|
143
|
+
let newB = b;
|
|
144
|
+
if (b.children && b.children.length > 0) {
|
|
145
|
+
const newChildren = removeRecursive(b.children);
|
|
146
|
+
if (newChildren !== b.children) {
|
|
147
|
+
newB = { ...newB, children: newChildren };
|
|
148
|
+
changed = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (b.data?.columnData) {
|
|
153
|
+
const newColumnData = b.data.columnData.map((col: any) => {
|
|
154
|
+
if (col.children && col.children.length > 0) {
|
|
155
|
+
const newChildren = removeRecursive(col.children);
|
|
156
|
+
if (newChildren !== col.children) {
|
|
157
|
+
return { ...col, children: newChildren };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return col;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (newColumnData.some((col: any, i: number) => col !== b.data.columnData[i])) {
|
|
164
|
+
newB = { ...newB, data: { ...newB.data, columnData: newColumnData } };
|
|
165
|
+
changed = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return newB;
|
|
170
|
+
});
|
|
171
|
+
return changed ? newList : blocksList;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const newBlocks = removeRecursive(blocks);
|
|
175
|
+
if (newBlocks !== blocks) {
|
|
176
|
+
set({ blocks: newBlocks });
|
|
177
|
+
const { onBlocksChange } = get();
|
|
178
|
+
if (onBlocksChange) onBlocksChange();
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
moveBlock: (id, direction) => {
|
|
182
|
+
const { blocks } = get();
|
|
183
|
+
|
|
184
|
+
const moveRecursive = (blocksList: BlockData[]): BlockData[] => {
|
|
185
|
+
const index = blocksList.findIndex(b => b.id === id);
|
|
186
|
+
if (index !== -1) {
|
|
187
|
+
const targetIndex = direction === "up" ? index - 1 : index + 1;
|
|
188
|
+
if (targetIndex >= 0 && targetIndex < blocksList.length) {
|
|
189
|
+
const newBlocksList = [...blocksList];
|
|
190
|
+
[newBlocksList[index], newBlocksList[targetIndex]] = [
|
|
191
|
+
newBlocksList[targetIndex],
|
|
192
|
+
newBlocksList[index],
|
|
193
|
+
];
|
|
194
|
+
return newBlocksList;
|
|
195
|
+
}
|
|
196
|
+
return blocksList;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let changed = false;
|
|
200
|
+
const newList = blocksList.map(b => {
|
|
201
|
+
let newB = b;
|
|
202
|
+
if (b.children && b.children.length > 0) {
|
|
203
|
+
const newChildren = moveRecursive(b.children);
|
|
204
|
+
if (newChildren !== b.children) {
|
|
205
|
+
newB = { ...newB, children: newChildren };
|
|
206
|
+
changed = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (b.data?.columnData) {
|
|
211
|
+
const newColumnData = b.data.columnData.map((col: any) => {
|
|
212
|
+
if (col.children && col.children.length > 0) {
|
|
213
|
+
const newChildren = moveRecursive(col.children);
|
|
214
|
+
if (newChildren !== col.children) {
|
|
215
|
+
return { ...col, children: newChildren };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return col;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (newColumnData.some((col: any, i: number) => col !== b.data.columnData[i])) {
|
|
222
|
+
newB = { ...newB, data: { ...newB.data, columnData: newColumnData } };
|
|
223
|
+
changed = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return newB;
|
|
228
|
+
});
|
|
229
|
+
return changed ? newList : blocksList;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const newBlocks = moveRecursive(blocks);
|
|
233
|
+
if (newBlocks !== blocks) {
|
|
234
|
+
set({ blocks: newBlocks });
|
|
235
|
+
const { onBlocksChange } = get();
|
|
236
|
+
if (onBlocksChange) onBlocksChange();
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
// Selector for individual block - only re-renders when that specific block changes
|
|
242
|
+
export const useBlockById = (id: string) =>
|
|
243
|
+
useBlocksStore((state) => {
|
|
244
|
+
const findRecursive = (blocksList: BlockData[]): BlockData | undefined => {
|
|
245
|
+
for (const b of blocksList) {
|
|
246
|
+
if (b.id === id) return b;
|
|
247
|
+
if (b.children && b.children.length > 0) {
|
|
248
|
+
const found = findRecursive(b.children);
|
|
249
|
+
if (found) return found;
|
|
250
|
+
}
|
|
251
|
+
if (b.data?.columnData) {
|
|
252
|
+
for (const col of b.data.columnData) {
|
|
253
|
+
if (col.children && col.children.length > 0) {
|
|
254
|
+
const found = findRecursive(col.children);
|
|
255
|
+
if (found) return found;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
};
|
|
262
|
+
return findRecursive(state.blocks);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Selector for block count - for quick checks
|
|
266
|
+
export const useBlockCount = () =>
|
|
267
|
+
useBlocksStore((state) => state.blocks.length);
|
|
268
|
+
|
|
269
|
+
// Get action functions without subscription - uses getState() for direct access
|
|
270
|
+
// This prevents re-renders when other blocks change
|
|
271
|
+
export const useBlockActions = () => {
|
|
272
|
+
return useBlocksStore.getState();
|
|
273
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { default as PortableTextField } from "./PortableTextField";
|
|
2
|
+
export { PortableTextRenderer } from "./PortableTextRenderer";
|
|
3
|
+
export { HybridContentField } from "./HybridContentField";
|
|
4
|
+
export { CodeField } from "./CodeField";
|
|
5
|
+
export { JSONField } from "./JSONField";
|
|
6
|
+
export { MarkdownField } from "./MarkdownField";
|
|
7
|
+
export { default as TextField } from "./TextField";
|
|
8
|
+
export { default as NumberField } from "./NumberField";
|
|
9
|
+
export { default as CheckboxField } from "./CheckboxField";
|
|
10
|
+
export { default as DateField } from "./DateField";
|
|
11
|
+
export { default as SelectField } from "./SelectField";
|
|
12
|
+
export { default as RelationshipField } from "./RelationshipField";
|
|
13
|
+
export { BlocksField } from "./BlocksField";
|