@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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useBlocksStore } from "../fields/extensions/blocksStore";
|
|
3
|
+
import { ChevronRight, X } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
export const ListBlock: React.FC<{ block: any; index: number }> = ({ block, index }) => {
|
|
6
|
+
const { updateBlock, removeBlock, moveBlock } = useBlocksStore();
|
|
7
|
+
|
|
8
|
+
const handleChange = (field: string, value: any) => {
|
|
9
|
+
updateBlock(block.id, { data: { ...block.data, [field]: value } });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="block-list border border-[var(--kyro-border)] rounded-md p-4 mb-2 relative group">
|
|
14
|
+
<div className="flex items-center justify-between mb-1">
|
|
15
|
+
<span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">List</span>
|
|
16
|
+
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
17
|
+
<button type="button" onClick={() => moveBlock(block.id, "up")} className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded" title="Move up"><ChevronRight className="w-3 h-3 rotate-90" /></button>
|
|
18
|
+
<button type="button" onClick={() => removeBlock(block.id)} className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded" title="Remove"><X className="w-3 h-3" /></button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<div className="space-y-2">
|
|
22
|
+
<select
|
|
23
|
+
value={block.data.type || "unordered"}
|
|
24
|
+
onChange={(e) => handleChange("type", e.target.value)}
|
|
25
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm"
|
|
26
|
+
>
|
|
27
|
+
<option value="unordered">Unordered</option>
|
|
28
|
+
<option value="ordered">Ordered</option>
|
|
29
|
+
</select>
|
|
30
|
+
<textarea
|
|
31
|
+
value={block.data.items || ""}
|
|
32
|
+
onChange={(e) => handleChange("items", e.target.value)}
|
|
33
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm min-h-[80px] resize-none"
|
|
34
|
+
placeholder="Enter list items (one per line)..."
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -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
|
+
|
|
8
|
+
export const ParagraphBlock: React.FC<{ block: any; index: number }> = ({
|
|
9
|
+
block,
|
|
10
|
+
index,
|
|
11
|
+
}) => {
|
|
12
|
+
const blockData = useBlockById(block.id);
|
|
13
|
+
const { updateBlock, removeBlock, moveBlock } = useBlockActions();
|
|
14
|
+
|
|
15
|
+
const data = blockData?.data || block.data || {};
|
|
16
|
+
|
|
17
|
+
const handleChange = (field: string, value: any) => {
|
|
18
|
+
updateBlock(block.id, { data: { ...data, [field]: value } });
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="block-paragraph border-l-4 border-[var(--kyro-border)] pl-4 py-2 mb-2 relative group">
|
|
23
|
+
<div className="flex items-center justify-between mb-1">
|
|
24
|
+
<span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
|
|
25
|
+
Paragraph
|
|
26
|
+
</span>
|
|
27
|
+
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
28
|
+
<button type="button"
|
|
29
|
+
onClick={() => moveBlock(block.id, "up")}
|
|
30
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
31
|
+
title="Move up"
|
|
32
|
+
>
|
|
33
|
+
<ChevronRight className="w-3 h-3 rotate-90" />
|
|
34
|
+
</button>
|
|
35
|
+
<button 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 type="button"
|
|
43
|
+
onClick={() => removeBlock(block.id)}
|
|
44
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
45
|
+
title="Remove"
|
|
46
|
+
>
|
|
47
|
+
<X className="w-3 h-3" />
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div>
|
|
52
|
+
<textarea
|
|
53
|
+
value={data.text || ""}
|
|
54
|
+
onChange={(e) => handleChange("text", e.target.value)}
|
|
55
|
+
className="w-full px-3 py-3 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm min-h-[100px] resize-none"
|
|
56
|
+
placeholder="Enter paragraph text..."
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useBlockById,
|
|
4
|
+
useBlockActions,
|
|
5
|
+
} from "../fields/extensions/blocksStore";
|
|
6
|
+
import { ChevronRight, X, Search, Loader2, ExternalLink } from "lucide-react";
|
|
7
|
+
|
|
8
|
+
interface RelationshipBlockProps {
|
|
9
|
+
block: any;
|
|
10
|
+
index: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const RelationshipBlock: React.FC<RelationshipBlockProps> = ({
|
|
14
|
+
block,
|
|
15
|
+
index,
|
|
16
|
+
}) => {
|
|
17
|
+
const blockData = useBlockById(block.id);
|
|
18
|
+
const { updateBlock, removeBlock, moveBlock } = useBlockActions();
|
|
19
|
+
const data = blockData?.data ?? block.data ?? {};
|
|
20
|
+
|
|
21
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
22
|
+
const [search, setSearch] = useState("");
|
|
23
|
+
const [options, setOptions] = useState<any[]>([]);
|
|
24
|
+
const [loading, setLoading] = useState(false);
|
|
25
|
+
const [collections, setCollections] = useState<string[]>([]);
|
|
26
|
+
const [loadingCollections, setLoadingCollections] = useState(true);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
fetch("/api/collections", { credentials: "include" })
|
|
30
|
+
.then((res) => res.json())
|
|
31
|
+
.then((data) => {
|
|
32
|
+
setCollections(
|
|
33
|
+
(data.collections || []).map((c: any) => c.slug || c.name || c),
|
|
34
|
+
);
|
|
35
|
+
setLoadingCollections(false);
|
|
36
|
+
})
|
|
37
|
+
.catch(() => setLoadingCollections(false));
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const relationTo = data.relationTo || "pages";
|
|
41
|
+
const isMultiple = data.hasMany;
|
|
42
|
+
|
|
43
|
+
const fetchOptions = (query: string = "") => {
|
|
44
|
+
setLoading(true);
|
|
45
|
+
const url = query
|
|
46
|
+
? `/api/${relationTo}?where[title][contains]=${encodeURIComponent(query)}&limit=20`
|
|
47
|
+
: `/api/${relationTo}?limit=20`;
|
|
48
|
+
|
|
49
|
+
fetch(url, { credentials: "include" })
|
|
50
|
+
.then((res) => res.json())
|
|
51
|
+
.then((data) => {
|
|
52
|
+
setOptions(data.docs || []);
|
|
53
|
+
setLoading(false);
|
|
54
|
+
})
|
|
55
|
+
.catch(() => setLoading(false));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (isOpen) fetchOptions(search);
|
|
60
|
+
}, [isOpen, search]);
|
|
61
|
+
|
|
62
|
+
const getLabel = (opt: any) => {
|
|
63
|
+
return (
|
|
64
|
+
opt?.title ||
|
|
65
|
+
opt?.name ||
|
|
66
|
+
opt?.label ||
|
|
67
|
+
opt?.filename ||
|
|
68
|
+
opt?.slug ||
|
|
69
|
+
opt?.id ||
|
|
70
|
+
"Untitled"
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const selectedIds = Array.isArray(data.selectedIds)
|
|
75
|
+
? data.selectedIds
|
|
76
|
+
: data.selectedId
|
|
77
|
+
? [data.selectedId]
|
|
78
|
+
: [];
|
|
79
|
+
|
|
80
|
+
const handleSelect = (opt: any) => {
|
|
81
|
+
if (isMultiple) {
|
|
82
|
+
const current = selectedIds;
|
|
83
|
+
if (current.includes(opt.id)) {
|
|
84
|
+
updateBlock(block.id, {
|
|
85
|
+
data: {
|
|
86
|
+
...data,
|
|
87
|
+
selectedIds: current.filter((id: string) => id !== opt.id),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
updateBlock(block.id, {
|
|
92
|
+
data: { ...data, selectedIds: [...current, opt.id] },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
updateBlock(block.id, {
|
|
97
|
+
data: { ...data, selectedId: opt.id, selectedIds: [opt.id] },
|
|
98
|
+
});
|
|
99
|
+
setIsOpen(false);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const isSelected = (optId: string) => selectedIds.includes(optId);
|
|
104
|
+
|
|
105
|
+
const handleChange = (field: string, value: any) => {
|
|
106
|
+
updateBlock(block.id, { data: { ...data, [field]: value } });
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const selectedOptions = options.filter((o) => selectedIds.includes(o.id));
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="block-relationship border border-[var(--kyro-border)] rounded-lg p-4 mb-4 relative group bg-[var(--kyro-surface)]">
|
|
113
|
+
<div className="flex items-center justify-between mb-4">
|
|
114
|
+
<div className="flex items-center gap-2">
|
|
115
|
+
<span className="text-sm font-medium text-[var(--kyro-text-primary)]">
|
|
116
|
+
Relationship
|
|
117
|
+
</span>
|
|
118
|
+
<span className="text-xs text-[var(--kyro-text-muted)]">
|
|
119
|
+
→ {relationTo}
|
|
120
|
+
</span>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={() => moveBlock(block.id, "up")}
|
|
126
|
+
className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
127
|
+
title="Move up"
|
|
128
|
+
>
|
|
129
|
+
<ChevronRight className="w-3.5 h-3.5 rotate-[-90deg] text-[var(--kyro-text-muted)]" />
|
|
130
|
+
</button>
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
onClick={() => moveBlock(block.id, "down")}
|
|
134
|
+
className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
135
|
+
title="Move down"
|
|
136
|
+
>
|
|
137
|
+
<ChevronRight className="w-3.5 h-3.5 rotate-90 text-[var(--kyro-text-muted)]" />
|
|
138
|
+
</button>
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
onClick={() => removeBlock(block.id)}
|
|
142
|
+
className="p-1.5 hover:bg-red-50 rounded"
|
|
143
|
+
title="Remove"
|
|
144
|
+
>
|
|
145
|
+
<X className="w-3.5 h-3.5 text-red-500" />
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="space-y-3">
|
|
151
|
+
<div className="grid grid-cols-2 gap-3">
|
|
152
|
+
<div>
|
|
153
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
|
|
154
|
+
Target Collection
|
|
155
|
+
</label>
|
|
156
|
+
{loadingCollections ? (
|
|
157
|
+
<div className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-muted)]">
|
|
158
|
+
Loading...
|
|
159
|
+
</div>
|
|
160
|
+
) : (
|
|
161
|
+
<select
|
|
162
|
+
value={relationTo}
|
|
163
|
+
onChange={(e) => handleChange("relationTo", e.target.value)}
|
|
164
|
+
className="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"
|
|
165
|
+
>
|
|
166
|
+
<option value="">Select collection...</option>
|
|
167
|
+
{collections.map((col) => (
|
|
168
|
+
<option key={col} value={col}>
|
|
169
|
+
{col}
|
|
170
|
+
</option>
|
|
171
|
+
))}
|
|
172
|
+
</select>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
<div>
|
|
176
|
+
<label className="flex items-center gap-2 cursor-pointer mt-5">
|
|
177
|
+
<input
|
|
178
|
+
type="checkbox"
|
|
179
|
+
checked={isMultiple || false}
|
|
180
|
+
onChange={(e) => handleChange("hasMany", e.target.checked)}
|
|
181
|
+
className="w-4 h-4 rounded border-[var(--kyro-border)] focus:ring-[var(--kyro-sidebar-active)] focus:ring-offset-0"
|
|
182
|
+
/>
|
|
183
|
+
<span className="text-sm text-[var(--kyro-text-primary)]">
|
|
184
|
+
Allow multiple
|
|
185
|
+
</span>
|
|
186
|
+
</label>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div className="relative">
|
|
191
|
+
<div className="relative">
|
|
192
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
193
|
+
<input
|
|
194
|
+
type="text"
|
|
195
|
+
value={search}
|
|
196
|
+
onChange={(e) => {
|
|
197
|
+
setSearch(e.target.value);
|
|
198
|
+
setIsOpen(true);
|
|
199
|
+
}}
|
|
200
|
+
onFocus={() => setIsOpen(true)}
|
|
201
|
+
placeholder={`Search ${relationTo}...`}
|
|
202
|
+
className="w-full pl-9 pr-10 py-2 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"
|
|
203
|
+
/>
|
|
204
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
205
|
+
{loading ? (
|
|
206
|
+
<Loader2 className="w-4 h-4 text-[var(--kyro-text-muted)] animate-spin" />
|
|
207
|
+
) : null}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{isOpen && (
|
|
212
|
+
<div className="absolute z-20 w-full mt-1 border border-[var(--kyro-border)] rounded-lg shadow-lg bg-[var(--kyro-surface)] max-h-48 overflow-auto">
|
|
213
|
+
{loading ? (
|
|
214
|
+
<div className="p-3 text-center text-sm text-[var(--kyro-text-muted)]">
|
|
215
|
+
Loading...
|
|
216
|
+
</div>
|
|
217
|
+
) : options.length === 0 ? (
|
|
218
|
+
<div className="p-3 text-center text-sm text-[var(--kyro-text-muted)]">
|
|
219
|
+
No results found
|
|
220
|
+
</div>
|
|
221
|
+
) : (
|
|
222
|
+
<div className="py-1">
|
|
223
|
+
{options.map((opt) => (
|
|
224
|
+
<button
|
|
225
|
+
key={opt.id}
|
|
226
|
+
type="button"
|
|
227
|
+
onClick={() => handleSelect(opt)}
|
|
228
|
+
className={`w-full px-3 py-2 text-left text-sm hover:bg-[var(--kyro-surface-accent)] transition-colors flex items-center justify-between ${
|
|
229
|
+
isSelected(opt.id)
|
|
230
|
+
? "bg-[var(--kyro-sidebar-active)]/10 text-[var(--kyro-sidebar-active)]"
|
|
231
|
+
: "text-[var(--kyro-text-primary)]"
|
|
232
|
+
}`}
|
|
233
|
+
>
|
|
234
|
+
<span>{getLabel(opt)}</span>
|
|
235
|
+
{isSelected(opt) && <span>✓</span>}
|
|
236
|
+
</button>
|
|
237
|
+
))}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{selectedIds.length > 0 && (
|
|
245
|
+
<div className="flex flex-wrap gap-2">
|
|
246
|
+
{selectedIds.map((id: string) => {
|
|
247
|
+
const opt = options.find((o) => o.id === id) || { id, title: id };
|
|
248
|
+
return (
|
|
249
|
+
<span
|
|
250
|
+
key={id}
|
|
251
|
+
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-md bg-[var(--kyro-sidebar-active)]/10 text-[var(--kyro-sidebar-active)]"
|
|
252
|
+
>
|
|
253
|
+
{getLabel(opt)}
|
|
254
|
+
<button
|
|
255
|
+
type="button"
|
|
256
|
+
onClick={() => {
|
|
257
|
+
if (isMultiple) {
|
|
258
|
+
handleChange(
|
|
259
|
+
"selectedIds",
|
|
260
|
+
selectedIds.filter((sid: string) => sid !== id),
|
|
261
|
+
);
|
|
262
|
+
} else {
|
|
263
|
+
handleChange("selectedId", null);
|
|
264
|
+
handleChange("selectedIds", []);
|
|
265
|
+
}
|
|
266
|
+
}}
|
|
267
|
+
className="hover:opacity-70"
|
|
268
|
+
>
|
|
269
|
+
<X className="w-3 h-3" />
|
|
270
|
+
</button>
|
|
271
|
+
</span>
|
|
272
|
+
);
|
|
273
|
+
})}
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
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 { ChildBlocksTree } from "./ChildBlocksTree";
|
|
8
|
+
|
|
9
|
+
interface VStackBlockProps {
|
|
10
|
+
block: any;
|
|
11
|
+
index: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const VStackBlock: React.FC<VStackBlockProps> = ({ block, index }) => {
|
|
15
|
+
const blockData = useBlockById(block.id);
|
|
16
|
+
const { updateBlock, removeBlock, moveBlock } = useBlockActions();
|
|
17
|
+
const data = blockData?.data ?? block.data ?? {};
|
|
18
|
+
const children = blockData?.children ?? block.children ?? [];
|
|
19
|
+
|
|
20
|
+
const handleUpdateChildren = (newChildren: any[]) => {
|
|
21
|
+
updateBlock(block.id, { children: newChildren });
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="block-vstack border border-[var(--kyro-border)] rounded-lg p-4 mb-4 relative group bg-[var(--kyro-surface)]">
|
|
26
|
+
<div className="flex items-center justify-between mb-4">
|
|
27
|
+
<div className="flex items-center gap-2">
|
|
28
|
+
<span className="text-sm font-medium text-[var(--kyro-text-primary)]">
|
|
29
|
+
Vertical Stack
|
|
30
|
+
</span>
|
|
31
|
+
<span className="text-xs text-[var(--kyro-text-muted)]">
|
|
32
|
+
({children.length} children)
|
|
33
|
+
</span>
|
|
34
|
+
</div>
|
|
35
|
+
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
onClick={() => moveBlock(block.id, "up")}
|
|
39
|
+
className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
40
|
+
title="Move up"
|
|
41
|
+
>
|
|
42
|
+
<ChevronRight className="w-3.5 h-3.5 rotate-[-90deg] text-[var(--kyro-text-muted)]" />
|
|
43
|
+
</button>
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={() => moveBlock(block.id, "down")}
|
|
47
|
+
className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded"
|
|
48
|
+
title="Move down"
|
|
49
|
+
>
|
|
50
|
+
<ChevronRight className="w-3.5 h-3.5 rotate-90 text-[var(--kyro-text-muted)]" />
|
|
51
|
+
</button>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={() => removeBlock(block.id)}
|
|
55
|
+
className="p-1.5 hover:bg-red-50 rounded"
|
|
56
|
+
title="Remove"
|
|
57
|
+
>
|
|
58
|
+
<X className="w-3.5 h-3.5 text-red-500" />
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div className="pt-4 border-t border-[var(--kyro-border)]">
|
|
64
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-2 block">
|
|
65
|
+
Children
|
|
66
|
+
</label>
|
|
67
|
+
<ChildBlocksTree
|
|
68
|
+
blockId={block.id}
|
|
69
|
+
children={children}
|
|
70
|
+
onUpdateChildren={handleUpdateChildren}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useBlocksStore } from "../fields/extensions/blocksStore";
|
|
3
|
+
import { ChevronRight, X } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
export const VideoBlock: React.FC<{ block: any; index: number }> = ({ block, index }) => {
|
|
6
|
+
const { updateBlock, removeBlock, moveBlock } = useBlocksStore();
|
|
7
|
+
|
|
8
|
+
const handleChange = (field: string, value: any) => {
|
|
9
|
+
updateBlock(block.id, { data: { ...block.data, [field]: value } });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="block-video border border-[var(--kyro-border)] rounded-md p-4 mb-4 relative group">
|
|
14
|
+
<div className="flex items-center justify-between mb-2">
|
|
15
|
+
<span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">Video</span>
|
|
16
|
+
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
17
|
+
<button type="button" onClick={() => moveBlock(block.id, "up")} className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded" title="Move up"><ChevronRight className="w-3 h-3 rotate-90" /></button>
|
|
18
|
+
<button type="button" onClick={() => removeBlock(block.id)} className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded" title="Remove"><X className="w-3 h-3" /></button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<div className="space-y-3">
|
|
22
|
+
<div>
|
|
23
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">Video URL</label>
|
|
24
|
+
<input
|
|
25
|
+
type="url"
|
|
26
|
+
value={block.data.src || ""}
|
|
27
|
+
onChange={(e) => handleChange("src", e.target.value)}
|
|
28
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm"
|
|
29
|
+
placeholder="https://..."
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
<div>
|
|
33
|
+
<label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">Title</label>
|
|
34
|
+
<input
|
|
35
|
+
type="text"
|
|
36
|
+
value={block.data.title || ""}
|
|
37
|
+
onChange={(e) => handleChange("title", e.target.value)}
|
|
38
|
+
className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm"
|
|
39
|
+
placeholder="Video title..."
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -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';
|