@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,73 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { UploadField } from "./UploadField";
|
|
3
|
+
|
|
4
|
+
interface VideoFieldProps {
|
|
5
|
+
src?: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
onChange: (field: string, value: string) => void;
|
|
8
|
+
onUploadChange?: (value: any) => void;
|
|
9
|
+
compact?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const VideoField: React.FC<VideoFieldProps> = ({
|
|
13
|
+
src = "",
|
|
14
|
+
title = "",
|
|
15
|
+
onChange,
|
|
16
|
+
onUploadChange,
|
|
17
|
+
compact = false,
|
|
18
|
+
}) => {
|
|
19
|
+
const isExternalUrl =
|
|
20
|
+
src.includes("youtube.com") ||
|
|
21
|
+
src.includes("vimeo.com") ||
|
|
22
|
+
src.includes("youtu.be");
|
|
23
|
+
|
|
24
|
+
if (compact) {
|
|
25
|
+
return (
|
|
26
|
+
<div className="space-y-2">
|
|
27
|
+
<input
|
|
28
|
+
type="url"
|
|
29
|
+
value={src}
|
|
30
|
+
onChange={(e) => onChange("src", e.target.value)}
|
|
31
|
+
className="w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent font-mono text-xs"
|
|
32
|
+
placeholder="MP4 URL, YouTube, or Vimeo link..."
|
|
33
|
+
/>
|
|
34
|
+
<input
|
|
35
|
+
type="text"
|
|
36
|
+
value={title}
|
|
37
|
+
onChange={(e) => onChange("title", e.target.value)}
|
|
38
|
+
className="w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
39
|
+
placeholder="Video title (optional)..."
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="space-y-3">
|
|
47
|
+
<UploadField
|
|
48
|
+
field={{ label: "Video Asset", name: "src", maxCount: 1 }}
|
|
49
|
+
value={src}
|
|
50
|
+
onChange={onUploadChange || ((v) => onChange("src", v))}
|
|
51
|
+
/>
|
|
52
|
+
<span className="text-xs text-[var(--kyro-text-muted)]">
|
|
53
|
+
or paste a URL
|
|
54
|
+
</span>
|
|
55
|
+
<input
|
|
56
|
+
type="url"
|
|
57
|
+
value={src}
|
|
58
|
+
onChange={(e) => onChange("src", e.target.value)}
|
|
59
|
+
className="w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent font-mono text-xs"
|
|
60
|
+
placeholder="MP4 URL, YouTube, or Vimeo link..."
|
|
61
|
+
/>
|
|
62
|
+
<input
|
|
63
|
+
type="text"
|
|
64
|
+
value={title}
|
|
65
|
+
onChange={(e) => onChange("title", e.target.value)}
|
|
66
|
+
className="w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
67
|
+
placeholder="Video title (optional)..."
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default VideoField;
|
|
@@ -0,0 +1,247 @@
|
|
|
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
|
+
import { RichTextBlock } from "../../blocks/RichTextBlock";
|
|
16
|
+
|
|
17
|
+
import { HeroBlock } from "../../blocks/HeroBlock";
|
|
18
|
+
import { ArrayBlock } from "../../blocks/ArrayBlock";
|
|
19
|
+
import { RelationshipBlock } from "../../blocks/RelationshipBlock";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
Columns3,
|
|
23
|
+
Heading1,
|
|
24
|
+
AlignLeft,
|
|
25
|
+
Minus,
|
|
26
|
+
Image,
|
|
27
|
+
Video,
|
|
28
|
+
List,
|
|
29
|
+
Code,
|
|
30
|
+
Link,
|
|
31
|
+
File,
|
|
32
|
+
ArrowDown,
|
|
33
|
+
MousePointerClick,
|
|
34
|
+
ChevronDown,
|
|
35
|
+
Star,
|
|
36
|
+
ListOrdered,
|
|
37
|
+
Link2,
|
|
38
|
+
} from "lucide-react";
|
|
39
|
+
|
|
40
|
+
// Block component registry
|
|
41
|
+
export const BLOCK_COMPONENTS: Record<string, React.ComponentType<any>> = {
|
|
42
|
+
columns: ColumnsBlock,
|
|
43
|
+
heading: HeadingBlock,
|
|
44
|
+
paragraph: ParagraphBlock,
|
|
45
|
+
divider: DividerBlock,
|
|
46
|
+
image: ImageBlock,
|
|
47
|
+
video: VideoBlock,
|
|
48
|
+
list: ListBlock,
|
|
49
|
+
code: CodeBlock,
|
|
50
|
+
link: LinkBlock,
|
|
51
|
+
file: FileBlock,
|
|
52
|
+
vstack: VStackBlock,
|
|
53
|
+
button: ButtonBlock,
|
|
54
|
+
accordion: AccordionBlock,
|
|
55
|
+
richtext: RichTextBlock,
|
|
56
|
+
|
|
57
|
+
hero: HeroBlock,
|
|
58
|
+
array: ArrayBlock,
|
|
59
|
+
relationship: RelationshipBlock,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Icon mapping for drawer (actual Lucide components)
|
|
63
|
+
export const blockIcons: Record<string, React.ReactNode> = {
|
|
64
|
+
columns: <Columns3 className="w-4 h-4" />,
|
|
65
|
+
heading: <Heading1 className="w-4 h-4" />,
|
|
66
|
+
paragraph: <AlignLeft className="w-4 h-4" />,
|
|
67
|
+
divider: <Minus className="w-4 h-4" />,
|
|
68
|
+
image: <Image className="w-4 h-4" />,
|
|
69
|
+
video: <Video className="w-4 h-4" />,
|
|
70
|
+
list: <List className="w-4 h-4" />,
|
|
71
|
+
code: <Code className="w-4 h-4" />,
|
|
72
|
+
link: <Link className="w-4 h-4" />,
|
|
73
|
+
file: <File className="w-4 h-4" />,
|
|
74
|
+
vstack: <ArrowDown className="w-4 h-4" />,
|
|
75
|
+
button: <MousePointerClick className="w-4 h-4" />,
|
|
76
|
+
accordion: <ChevronDown className="w-4 h-4" />,
|
|
77
|
+
richtext: <AlignLeft className="w-4 h-4" />,
|
|
78
|
+
|
|
79
|
+
hero: <Star className="w-4 h-4" />,
|
|
80
|
+
array: <ListOrdered className="w-4 h-4" />,
|
|
81
|
+
relationship: <Link2 className="w-4 h-4" />,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Get block component by type
|
|
85
|
+
export function getBlockComponent(type: string) {
|
|
86
|
+
return BLOCK_COMPONENTS[type];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check if block type is supported
|
|
90
|
+
export function isSupportedBlockType(type: string): boolean {
|
|
91
|
+
return type in BLOCK_COMPONENTS;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Get human-readable label for block type
|
|
95
|
+
export function getBlockLabel(type: string): string {
|
|
96
|
+
const labelMap: Record<string, string> = {
|
|
97
|
+
paragraph: "Paragraph",
|
|
98
|
+
heading: "Heading",
|
|
99
|
+
image: "Image",
|
|
100
|
+
video: "Video",
|
|
101
|
+
link: "Link",
|
|
102
|
+
button: "Button",
|
|
103
|
+
list: "List",
|
|
104
|
+
code: "Code",
|
|
105
|
+
file: "File",
|
|
106
|
+
divider: "Divider",
|
|
107
|
+
accordion: "Accordion",
|
|
108
|
+
array: "Repeater",
|
|
109
|
+
hero: "Hero",
|
|
110
|
+
vstack: "VStack",
|
|
111
|
+
columns: "Columns",
|
|
112
|
+
relationship: "Relationship",
|
|
113
|
+
richtext: "Rich Text",
|
|
114
|
+
};
|
|
115
|
+
return labelMap[type] || type;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Block categories for the drawer
|
|
119
|
+
export const blockCategories = [
|
|
120
|
+
{
|
|
121
|
+
title: "Layout",
|
|
122
|
+
blocks: [
|
|
123
|
+
{
|
|
124
|
+
type: "columns",
|
|
125
|
+
label: "Columns",
|
|
126
|
+
icon: "columns",
|
|
127
|
+
description: "1-6 columns side-by-side",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: "vstack",
|
|
131
|
+
label: "VStack",
|
|
132
|
+
icon: "vstack",
|
|
133
|
+
description: "Stack blocks vertically",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: "hero",
|
|
137
|
+
label: "Hero",
|
|
138
|
+
icon: "hero",
|
|
139
|
+
description: "Hero with content + video",
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
title: "Text",
|
|
145
|
+
blocks: [
|
|
146
|
+
{
|
|
147
|
+
type: "heading",
|
|
148
|
+
label: "Heading",
|
|
149
|
+
icon: "heading",
|
|
150
|
+
description: "Heading text",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
type: "paragraph",
|
|
154
|
+
label: "Paragraph",
|
|
155
|
+
icon: "paragraph",
|
|
156
|
+
description: "Plain text content",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: "richtext",
|
|
160
|
+
label: "Rich Text",
|
|
161
|
+
icon: "richtext",
|
|
162
|
+
description: "Formatted text with links & styles",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: "list",
|
|
166
|
+
label: "List",
|
|
167
|
+
icon: "list",
|
|
168
|
+
description: "Ordered/unordered list",
|
|
169
|
+
},
|
|
170
|
+
{ type: "link", label: "Link", icon: "link", description: "Hyperlink" },
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
title: "Media",
|
|
175
|
+
blocks: [
|
|
176
|
+
{
|
|
177
|
+
type: "image",
|
|
178
|
+
label: "Image",
|
|
179
|
+
icon: "image",
|
|
180
|
+
description: "Single image",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
type: "video",
|
|
184
|
+
label: "Video",
|
|
185
|
+
icon: "video",
|
|
186
|
+
description: "Embed video",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
type: "file",
|
|
190
|
+
label: "File",
|
|
191
|
+
icon: "file",
|
|
192
|
+
description: "File download link",
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
title: "Interactive",
|
|
198
|
+
blocks: [
|
|
199
|
+
{
|
|
200
|
+
type: "button",
|
|
201
|
+
label: "Button",
|
|
202
|
+
icon: "button",
|
|
203
|
+
description: "CTA button",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
type: "accordion",
|
|
207
|
+
label: "Accordion",
|
|
208
|
+
icon: "accordion",
|
|
209
|
+
description: "Collapsible sections",
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
title: "Data",
|
|
215
|
+
blocks: [
|
|
216
|
+
{
|
|
217
|
+
type: "array",
|
|
218
|
+
label: "Repeater",
|
|
219
|
+
icon: "array",
|
|
220
|
+
description: "Add multiple child blocks",
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
type: "code",
|
|
224
|
+
label: "Code",
|
|
225
|
+
icon: "code",
|
|
226
|
+
description: "Code snippet",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
type: "relationship",
|
|
230
|
+
label: "Relationship",
|
|
231
|
+
icon: "relationship",
|
|
232
|
+
description: "Link to other collection",
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
title: "Basic",
|
|
238
|
+
blocks: [
|
|
239
|
+
{
|
|
240
|
+
type: "divider",
|
|
241
|
+
label: "Divider",
|
|
242
|
+
icon: "divider",
|
|
243
|
+
description: "Horizontal separator",
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
];
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import type { BlockData } from "@kyro-cms/core/client";
|
|
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,24 @@
|
|
|
1
|
+
export { default as PortableTextField } from "./PortableTextField";
|
|
2
|
+
export { PortableTextRenderer } from "./PortableTextRenderer";
|
|
3
|
+
export { CodeField } from "./CodeField";
|
|
4
|
+
export { JSONField } from "./JSONField";
|
|
5
|
+
export { MarkdownField } from "./MarkdownField";
|
|
6
|
+
export { default as TextField } from "./TextField";
|
|
7
|
+
export { default as NumberField } from "./NumberField";
|
|
8
|
+
export { default as CheckboxField } from "./CheckboxField";
|
|
9
|
+
export { default as DateField } from "./DateField";
|
|
10
|
+
export { default as SelectField } from "./SelectField";
|
|
11
|
+
export { default as RelationshipField } from "./RelationshipField";
|
|
12
|
+
export { BlocksField } from "./BlocksField";
|
|
13
|
+
export { AccordionField } from "./AccordionField";
|
|
14
|
+
export { ButtonField } from "./ButtonField";
|
|
15
|
+
export { UploadField } from "./UploadField";
|
|
16
|
+
export { LinkField } from "./LinkField";
|
|
17
|
+
export { HeadingField } from "./HeadingField";
|
|
18
|
+
export { VideoField } from "./VideoField";
|
|
19
|
+
export { ListField } from "./ListField";
|
|
20
|
+
export { HeroField } from "./HeroField";
|
|
21
|
+
export { ArrayField } from "./ArrayField";
|
|
22
|
+
export { ChildrenField } from "./ChildrenField";
|
|
23
|
+
export { ColumnsField } from "./ColumnsField";
|
|
24
|
+
export { RelationshipBlockField } from "./RelationshipBlockField";
|
package/src/components/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { Admin } from "./Admin";
|
|
|
2
2
|
export { ListView } from "./ListView";
|
|
3
3
|
export { DetailView } from "./DetailView";
|
|
4
4
|
export { CreateView } from "./CreateView";
|
|
5
|
+
export { Dashboard } from "./Dashboard";
|
|
5
6
|
export { AutoForm } from "./AutoForm";
|
|
6
7
|
export {
|
|
7
8
|
ActionBar,
|
|
@@ -19,8 +20,6 @@ export {
|
|
|
19
20
|
useTheme,
|
|
20
21
|
type ThemeMode,
|
|
21
22
|
} from "./ThemeProvider";
|
|
22
|
-
export * from "./layout/Header";
|
|
23
|
-
export * from "./layout/Sidebar";
|
|
24
23
|
export * from "./ui/Button";
|
|
25
24
|
export * from "./ui/Badge";
|
|
26
25
|
export * from "./ui/Spinner";
|
|
@@ -10,7 +10,7 @@ export function Header({ title, onMenuClick, actions }: HeaderProps) {
|
|
|
10
10
|
return (
|
|
11
11
|
<header className="kyro-header">
|
|
12
12
|
<div className="kyro-header-left">
|
|
13
|
-
<button
|
|
13
|
+
<button type="button"
|
|
14
14
|
className="kyro-header-menu"
|
|
15
15
|
onClick={onMenuClick}
|
|
16
16
|
aria-label="Toggle menu"
|
|
@@ -24,7 +24,7 @@ export function Header({ title, onMenuClick, actions }: HeaderProps) {
|
|
|
24
24
|
<div className="kyro-header-right">
|
|
25
25
|
{actions}
|
|
26
26
|
<div className="kyro-header-user">
|
|
27
|
-
<button className="kyro-header-user-btn">
|
|
27
|
+
<button type="button" className="kyro-header-user-btn">
|
|
28
28
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
29
29
|
<circle cx="12" cy="8" r="4" />
|
|
30
30
|
<path d="M4 20c0-4 4-6 8-6s8 2 8 6" />
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CollectionConfig } from '@kyro-cms/core';
|
|
1
|
+
import { type CollectionConfig } from '@kyro-cms/core/client';
|
|
2
2
|
|
|
3
3
|
interface LayoutProps {
|
|
4
4
|
children: any;
|
|
@@ -14,10 +14,10 @@ export default function Layout({ children, collections = [], currentSlug }: Layo
|
|
|
14
14
|
<div id="sidebar-root" className="hidden lg:block">
|
|
15
15
|
{/* Sidebar rendered here */}
|
|
16
16
|
</div>
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
{/* Main content */}
|
|
19
19
|
<main className="flex-1 min-w-0">
|
|
20
|
-
{children}
|
|
20
|
+
{/* {children} */}
|
|
21
21
|
</main>
|
|
22
22
|
</div>
|
|
23
23
|
</div>
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import type { ReactNode } from
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
interface BadgeProps {
|
|
4
|
-
variant?:
|
|
4
|
+
variant?: "default" | "success" | "warning" | "danger" | "info";
|
|
5
|
+
className?: string;
|
|
5
6
|
children: ReactNode;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
export function Badge({
|
|
9
|
+
export function Badge({
|
|
10
|
+
variant = "default",
|
|
11
|
+
className = "",
|
|
12
|
+
children,
|
|
13
|
+
}: BadgeProps) {
|
|
9
14
|
return (
|
|
10
|
-
<span className={`kyro-badge kyro-badge-${variant}`}>
|
|
15
|
+
<span className={`kyro-badge kyro-badge-${variant} ${className}`}>
|
|
11
16
|
{children}
|
|
12
17
|
</span>
|
|
13
18
|
);
|