@kyro-cms/admin 0.1.5 → 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.
Files changed (164) hide show
  1. package/README.md +149 -51
  2. package/package.json +52 -5
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +136 -27
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +1417 -661
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +3 -3
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +199 -57
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +786 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +191 -53
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +149 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/UserManagement.tsx +204 -0
  26. package/src/components/VersionHistoryPanel.tsx +3 -3
  27. package/src/components/WebhookManager.tsx +608 -0
  28. package/src/components/blocks/AccordionBlock.tsx +97 -0
  29. package/src/components/blocks/ArrayBlock.tsx +75 -0
  30. package/src/components/blocks/BlockEditModal.MARKER +12 -0
  31. package/src/components/blocks/BlockEditModal.tsx +774 -0
  32. package/src/components/blocks/ButtonBlock.tsx +165 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +66 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +151 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +64 -0
  38. package/src/components/blocks/HeadingBlock.tsx +81 -0
  39. package/src/components/blocks/HeroBlock.tsx +157 -0
  40. package/src/components/blocks/ImageBlock.tsx +83 -0
  41. package/src/components/blocks/LinkBlock.tsx +71 -0
  42. package/src/components/blocks/ListBlock.tsx +39 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +279 -0
  45. package/src/components/blocks/VStackBlock.tsx +75 -0
  46. package/src/components/blocks/VideoBlock.tsx +45 -0
  47. package/src/components/blocks/index.ts +10 -0
  48. package/src/components/fields/BlocksField.tsx +323 -0
  49. package/src/components/fields/CheckboxField.tsx +15 -9
  50. package/src/components/fields/CodeField.tsx +234 -0
  51. package/src/components/fields/DateField.tsx +38 -11
  52. package/src/components/fields/EditorClient.tsx +271 -0
  53. package/src/components/fields/FileField.tsx +390 -0
  54. package/src/components/fields/HybridContentField.tsx +109 -0
  55. package/src/components/fields/ImageField.tsx +429 -0
  56. package/src/components/fields/JSONField.tsx +361 -0
  57. package/src/components/fields/MarkdownField.tsx +282 -0
  58. package/src/components/fields/NumberField.tsx +42 -12
  59. package/src/components/fields/PortableTextField.tsx +143 -0
  60. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  61. package/src/components/fields/RelationshipField.tsx +231 -59
  62. package/src/components/fields/SelectField.tsx +25 -15
  63. package/src/components/fields/TextField.tsx +45 -14
  64. package/src/components/fields/extensions/blockComponents.tsx +237 -0
  65. package/src/components/fields/extensions/blocksStore.ts +273 -0
  66. package/src/components/fields/index.ts +13 -0
  67. package/src/components/index.ts +1 -2
  68. package/src/components/layout/Header.tsx +2 -2
  69. package/src/components/layout/Layout.tsx +2 -2
  70. package/src/components/ui/Badge.tsx +9 -4
  71. package/src/components/ui/BlockDrawer.tsx +79 -0
  72. package/src/components/ui/Button.tsx +1 -1
  73. package/src/components/ui/CommandPalette.tsx +362 -0
  74. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  75. package/src/components/ui/Dropdown.tsx +1 -1
  76. package/src/components/ui/Modal.tsx +37 -12
  77. package/src/components/ui/PromptModal.tsx +94 -0
  78. package/src/components/ui/SlidePanel.tsx +43 -16
  79. package/src/components/ui/Toast.tsx +80 -14
  80. package/src/env.d.ts +16 -0
  81. package/src/env.ts +20 -0
  82. package/src/index.ts +0 -1
  83. package/src/layouts/AdminLayout.astro +164 -170
  84. package/src/layouts/AuthLayout.astro +50 -0
  85. package/src/lib/MediaService.ts +541 -0
  86. package/src/lib/auth/sqlite-adapter.ts +319 -0
  87. package/src/lib/config.ts +22 -6
  88. package/src/lib/dataStore.ts +132 -74
  89. package/src/lib/db/adapter.ts +54 -0
  90. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  91. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  92. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  93. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  94. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  95. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  96. package/src/lib/db/index.ts +449 -0
  97. package/src/lib/db/mongodb-adapter.ts +207 -0
  98. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  99. package/src/lib/db/schema/mysql-auth.ts +113 -0
  100. package/src/lib/db/schema/mysql-content.ts +20 -0
  101. package/src/lib/db/schema/postgres-auth.ts +116 -0
  102. package/src/lib/db/schema/postgres-content.ts +35 -0
  103. package/src/lib/db/schema/postgres-media.ts +52 -0
  104. package/src/lib/db/schema/postgres-settings.ts +11 -0
  105. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  106. package/src/lib/db/schema/sqlite-content.ts +20 -0
  107. package/src/lib/graphql/index.ts +1 -0
  108. package/src/lib/graphql/schema.ts +443 -0
  109. package/src/lib/rate-limit.ts +267 -0
  110. package/src/lib/storage.ts +374 -0
  111. package/src/lib/store.ts +85 -0
  112. package/src/middleware.ts +116 -28
  113. package/src/pages/[collection]/[id].astro +178 -122
  114. package/src/pages/[collection]/index.astro +24 -156
  115. package/src/pages/admin/api-explorer.astro +98 -0
  116. package/src/pages/admin/graphql-explorer.astro +40 -0
  117. package/src/pages/admin/graphql.astro +97 -0
  118. package/src/pages/admin/index.astro +286 -0
  119. package/src/pages/admin/keys.astro +8 -0
  120. package/src/pages/admin/rest-playground.astro +44 -0
  121. package/src/pages/admin/webhooks.astro +8 -0
  122. package/src/pages/api/[collection]/[id]/publish.ts +44 -0
  123. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  124. package/src/pages/api/[collection]/[id]/versions.ts +36 -0
  125. package/src/pages/api/[collection]/[id].ts +102 -159
  126. package/src/pages/api/[collection]/index.ts +151 -230
  127. package/src/pages/api/auth/[id].ts +48 -69
  128. package/src/pages/api/auth/audit-logs.ts +20 -43
  129. package/src/pages/api/auth/login.ts +159 -45
  130. package/src/pages/api/auth/logout.ts +50 -20
  131. package/src/pages/api/auth/refresh.ts +119 -0
  132. package/src/pages/api/auth/register.ts +110 -40
  133. package/src/pages/api/auth/users.ts +22 -97
  134. package/src/pages/api/collections.ts +59 -0
  135. package/src/pages/api/globals/[slug]/test.ts +172 -0
  136. package/src/pages/api/globals/[slug].ts +42 -0
  137. package/src/pages/api/graphql.ts +90 -0
  138. package/src/pages/api/health.ts +417 -40
  139. package/src/pages/api/keys/[id].ts +26 -0
  140. package/src/pages/api/keys/index.ts +75 -0
  141. package/src/pages/api/media/[id].ts +309 -0
  142. package/src/pages/api/media/folders.ts +609 -0
  143. package/src/pages/api/media/index.ts +146 -0
  144. package/src/pages/api/media/resize.ts +267 -0
  145. package/src/pages/api/search.ts +82 -0
  146. package/src/pages/api/slug-availability.ts +70 -0
  147. package/src/pages/api/storage-config.ts +20 -0
  148. package/src/pages/api/storage-status.ts +206 -0
  149. package/src/pages/api/upload.ts +334 -0
  150. package/src/pages/api/webhooks/index.ts +71 -0
  151. package/src/pages/audit/index.astro +2 -104
  152. package/src/pages/login.astro +82 -0
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +102 -0
  156. package/src/pages/roles/index.astro +21 -21
  157. package/src/pages/settings/[slug].astro +162 -0
  158. package/src/pages/settings/index.astro +9 -0
  159. package/src/pages/users/[id].astro +29 -21
  160. package/src/pages/users/index.astro +22 -17
  161. package/src/pages/users/new.astro +18 -17
  162. package/src/styles/main.css +553 -128
  163. package/src/components/layout/Sidebar.tsx +0 -497
  164. package/src/pages/index.astro +0 -225
@@ -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';