@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.
Files changed (163) hide show
  1. package/README.md +149 -51
  2. package/package.json +53 -6
  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 +23 -6
  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 +70 -11
  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 +200 -139
  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 +42 -24
  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 +11 -11
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +13 -13
  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
@@ -0,0 +1,151 @@
1
+ import React, { useState } from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { ChevronRight, X, Plus, Minus, Columns3 } from "lucide-react";
7
+ import { ChildBlocksTree } from "./ChildBlocksTree";
8
+
9
+ interface ColumnsBlockProps {
10
+ block: any;
11
+ index: number;
12
+ }
13
+
14
+ export const ColumnsBlock: React.FC<ColumnsBlockProps> = ({ block, index }) => {
15
+ const blockData = useBlockById(block.id);
16
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
17
+
18
+ const data = blockData?.data ?? block.data ?? {};
19
+ const columns = data.columns || 2;
20
+ const columnData = data.columnData || [];
21
+
22
+ const ensureColumnData = () => {
23
+ if (columnData.length !== columns) {
24
+ return Array.from({ length: columns }, (_, i) => ({
25
+ id: i,
26
+ children: columnData[i]?.children || [],
27
+ }));
28
+ }
29
+ return columnData;
30
+ };
31
+
32
+ const currentColumnData = ensureColumnData();
33
+
34
+ const handleColumnsChange = (newColumns: number) => {
35
+ if (newColumns < 1 || newColumns > 6) return;
36
+ const newColumnData = Array.from({ length: newColumns }, (_, i) => ({
37
+ id: i,
38
+ children: currentColumnData[i]?.children || [],
39
+ }));
40
+ updateBlock(block.id, {
41
+ data: { ...data, columns: newColumns, columnData: newColumnData },
42
+ });
43
+ };
44
+
45
+ const handleUpdateColumnChildren = (
46
+ columnIndex: number,
47
+ newChildren: any[],
48
+ ) => {
49
+ const newColumnData = [...currentColumnData];
50
+ newColumnData[columnIndex] = {
51
+ ...newColumnData[columnIndex],
52
+ children: newChildren,
53
+ };
54
+ updateBlock(block.id, {
55
+ data: { ...data, columnData: newColumnData },
56
+ });
57
+ };
58
+
59
+ return (
60
+ <div className="block-columns border border-[var(--kyro-border)] rounded-lg mb-4 overflow-hidden">
61
+ <div className="flex items-center justify-between px-4 py-2 bg-[var(--kyro-surface)] border-b border-[var(--kyro-border)]">
62
+ <div className="flex items-center gap-2">
63
+ <Columns3 className="w-4 h-4 text-[var(--kyro-primary)]" />
64
+ <span className="text-sm font-medium text-[var(--kyro-text-primary)]">
65
+ Columns
66
+ </span>
67
+ <span className="text-xs text-[var(--kyro-text-muted)]">
68
+ ({columns})
69
+ </span>
70
+ </div>
71
+ <div className="flex items-center gap-1 bg-[var(--kyro-surface-accent)] rounded">
72
+ <button
73
+ type="button"
74
+ onClick={() => handleColumnsChange(columns - 1)}
75
+ disabled={columns <= 1}
76
+ className="p-1.5 hover:bg-[var(--kyro-surface)] rounded disabled:opacity-30"
77
+ title="Remove column"
78
+ >
79
+ <Minus className="w-3 h-3" />
80
+ </button>
81
+ <span className="px-2 text-xs font-medium">{columns}</span>
82
+ <button
83
+ type="button"
84
+ onClick={() => handleColumnsChange(columns + 1)}
85
+ disabled={columns >= 6}
86
+ className="p-1.5 hover:bg-[var(--kyro-surface)] rounded disabled:opacity-30"
87
+ title="Add column"
88
+ >
89
+ <Plus className="w-3 h-3" />
90
+ </button>
91
+ </div>
92
+ <div className="flex items-center gap-1">
93
+ <button
94
+ type="button"
95
+ onClick={() => moveBlock(block.id, "up")}
96
+ className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded"
97
+ title="Move up"
98
+ >
99
+ <ChevronRight className="w-3.5 h-3.5 rotate-[-90deg]" />
100
+ </button>
101
+ <button
102
+ type="button"
103
+ onClick={() => moveBlock(block.id, "down")}
104
+ className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded"
105
+ title="Move down"
106
+ >
107
+ <ChevronRight className="w-3.5 h-3.5 rotate-90" />
108
+ </button>
109
+ <button
110
+ type="button"
111
+ onClick={() => removeBlock(block.id)}
112
+ className="p-1.5 hover:bg-red-50 rounded"
113
+ title="Remove"
114
+ >
115
+ <X className="w-3.5 h-3.5 text-red-500" />
116
+ </button>
117
+ </div>
118
+ </div>
119
+
120
+ <div
121
+ className="grid gap-4 p-4"
122
+ style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}
123
+ >
124
+ {currentColumnData.map((col: any, colIndex: number) => (
125
+ <div
126
+ key={colIndex}
127
+ className="border-2 border-dashed border-[var(--kyro-border)] rounded-lg overflow-hidden"
128
+ >
129
+ <div className="px-3 py-2 bg-[var(--kyro-surface-accent)]/30 border-b border-[var(--kyro-border)] flex items-center justify-between">
130
+ <span className="text-xs font-medium text-[var(--kyro-text-muted)]">
131
+ Column {colIndex + 1}
132
+ </span>
133
+ <span className="text-xs text-[var(--kyro-text-muted)]">
134
+ ({col.children?.length || 0})
135
+ </span>
136
+ </div>
137
+ <div className="p-3 min-h-[80px]">
138
+ <ChildBlocksTree
139
+ blockId={`${block.id}-col-${colIndex}`}
140
+ children={col.children || []}
141
+ onUpdateChildren={(newChildren) =>
142
+ handleUpdateColumnChildren(colIndex, newChildren)
143
+ }
144
+ />
145
+ </div>
146
+ </div>
147
+ ))}
148
+ </div>
149
+ </div>
150
+ );
151
+ };
@@ -0,0 +1,43 @@
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 DividerBlock: React.FC<{ block: any; index: number }> = ({
9
+ block,
10
+ index,
11
+ }) => {
12
+ const blockData = useBlockById(block.id);
13
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
14
+ const data = blockData?.data ?? block.data ?? {};
15
+ const handleChange = (field: string, value: any) => {
16
+ updateBlock(block.id, { data: { ...data, [field]: value } });
17
+ };
18
+
19
+ return (
20
+ <div className="my-4">
21
+ <div className="border-t border-[var(--kyro-border)]" />
22
+ <div className="flex justify-between text-[var(--kyro-text-muted)] text-xs mt-1">
23
+ <span>Divider</span>
24
+ <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
25
+ <button type="button"
26
+ onClick={() => moveBlock(block.id, "up")}
27
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
28
+ title="Move up"
29
+ >
30
+ <ChevronRight className="w-3 h-3 rotate-90" />
31
+ </button>
32
+ <button type="button"
33
+ onClick={() => removeBlock(block.id)}
34
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
35
+ title="Remove"
36
+ >
37
+ <X className="w-3 h-3" />
38
+ </button>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ );
43
+ };
@@ -0,0 +1,64 @@
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 FileBlock: React.FC<{ block: any; index: number }> = ({
9
+ block,
10
+ index,
11
+ }) => {
12
+ const blockData = useBlockById(block.id);
13
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
14
+ const data = blockData?.data ?? block.data ?? {};
15
+
16
+ const handleChange = (field: string, value: any) => {
17
+ updateBlock(block.id, { data: { ...data, [field]: value } });
18
+ };
19
+
20
+ return (
21
+ <div className="block-file border border-[var(--kyro-border)] rounded-md p-4 mb-2 relative group flex items-center gap-3">
22
+ <div className="flex-1">
23
+ <div className="flex items-center justify-between mb-1">
24
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
25
+ File
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={() => removeBlock(block.id)}
37
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
38
+ title="Remove"
39
+ >
40
+ <X className="w-3 h-3" />
41
+ </button>
42
+ </div>
43
+ </div>
44
+ <input
45
+ type="text"
46
+ value={block.data.filename || ""}
47
+ onChange={(e) => handleChange("filename", e.target.value)}
48
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm"
49
+ placeholder="File name..."
50
+ />
51
+ <input
52
+ type="url"
53
+ value={block.data.url || ""}
54
+ onChange={(e) => handleChange("url", e.target.value)}
55
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm mt-2"
56
+ placeholder="Download URL..."
57
+ />
58
+ </div>
59
+ <div className="w-6 h-6 flex items-center justify-center text-[var(--kyro-text-muted)] flex-shrink-0">
60
+ 📄
61
+ </div>
62
+ </div>
63
+ );
64
+ };
@@ -0,0 +1,81 @@
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 HeadingBlock: 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-heading border border-[var(--kyro-border)] rounded-md p-3 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
+ Heading
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 className="space-y-2">
52
+ <div>
53
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
54
+ Level
55
+ </label>
56
+ <select
57
+ value={data.level || 1}
58
+ onChange={(e) => handleChange("level", parseInt(e.target.value))}
59
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm"
60
+ >
61
+ <option value={1}>H1</option>
62
+ <option value={2}>H2</option>
63
+ <option value={3}>H3</option>
64
+ </select>
65
+ </div>
66
+ <div>
67
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
68
+ Text
69
+ </label>
70
+ <input
71
+ type="text"
72
+ value={data.text || ""}
73
+ onChange={(e) => handleChange("text", e.target.value)}
74
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm"
75
+ placeholder="Enter heading text..."
76
+ />
77
+ </div>
78
+ </div>
79
+ </div>
80
+ );
81
+ };
@@ -0,0 +1,157 @@
1
+ import React from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { X, ChevronRight } from "lucide-react";
7
+ import { ChildBlocksTree } from "./ChildBlocksTree";
8
+
9
+ interface HeroBlockProps {
10
+ block: any;
11
+ index: number;
12
+ }
13
+
14
+ export const HeroBlock: React.FC<HeroBlockProps> = ({ 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 handleChange = (field: string, value: any) => {
21
+ updateBlock(block.id, { data: { ...data, [field]: value } });
22
+ };
23
+
24
+ const handleUpdateChildren = (newChildren: any[]) => {
25
+ updateBlock(block.id, { children: newChildren });
26
+ };
27
+
28
+ return (
29
+ <div className="border border-[var(--kyro-border)] rounded-lg p-4 mb-4 relative group bg-[var(--kyro-surface)]">
30
+ <div className="flex items-center justify-between mb-4">
31
+ <div className="flex items-center gap-2">
32
+ <span className="text-sm font-medium text-[var(--kyro-text-primary)]">
33
+ Hero Section
34
+ </span>
35
+ </div>
36
+ <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
37
+ <button
38
+ type="button"
39
+ onClick={() => moveBlock(block.id, "up")}
40
+ className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded"
41
+ title="Move up"
42
+ >
43
+ <ChevronRight className="w-3.5 h-3.5 rotate-[-90deg] text-[var(--kyro-text-muted)]" />
44
+ </button>
45
+ <button
46
+ type="button"
47
+ onClick={() => moveBlock(block.id, "down")}
48
+ className="p-1.5 hover:bg-[var(--kyro-surface-accent)] rounded"
49
+ title="Move down"
50
+ >
51
+ <ChevronRight className="w-3.5 h-3.5 rotate-90 text-[var(--kyro-text-muted)]" />
52
+ </button>
53
+ <button
54
+ type="button"
55
+ onClick={() => removeBlock(block.id)}
56
+ className="p-1.5 hover:bg-red-50 rounded"
57
+ title="Remove"
58
+ >
59
+ <X className="w-3.5 h-3.5 text-red-500" />
60
+ </button>
61
+ </div>
62
+ </div>
63
+
64
+ <div className="space-y-4">
65
+ <div>
66
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
67
+ Heading
68
+ </label>
69
+ <input
70
+ type="text"
71
+ value={data.heading || ""}
72
+ onChange={(e) => handleChange("heading", e.target.value)}
73
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-primary)] text-lg font-bold"
74
+ placeholder="Hero heading..."
75
+ />
76
+ </div>
77
+
78
+ <div>
79
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
80
+ Subheading
81
+ </label>
82
+ <textarea
83
+ value={data.subheading || ""}
84
+ onChange={(e) => handleChange("subheading", e.target.value)}
85
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-primary)] text-sm min-h-[60px] resize-none"
86
+ placeholder="Hero subheading..."
87
+ />
88
+ </div>
89
+
90
+ <div className="grid grid-cols-2 gap-3">
91
+ <div>
92
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
93
+ Background Image URL
94
+ </label>
95
+ <input
96
+ type="url"
97
+ value={data.bgImage || ""}
98
+ onChange={(e) => handleChange("bgImage", e.target.value)}
99
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-primary)] text-sm"
100
+ placeholder="https://..."
101
+ />
102
+ </div>
103
+ <div>
104
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
105
+ Video URL (YouTube/Vimeo)
106
+ </label>
107
+ <input
108
+ type="url"
109
+ value={data.videoUrl || ""}
110
+ onChange={(e) => handleChange("videoUrl", e.target.value)}
111
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-primary)] text-sm"
112
+ placeholder="https://youtube.com/..."
113
+ />
114
+ </div>
115
+ </div>
116
+
117
+ <div className="grid grid-cols-2 gap-3">
118
+ <div>
119
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
120
+ CTA Text
121
+ </label>
122
+ <input
123
+ type="text"
124
+ value={data.ctaText || ""}
125
+ onChange={(e) => handleChange("ctaText", e.target.value)}
126
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-primary)] text-sm"
127
+ placeholder="Button text..."
128
+ />
129
+ </div>
130
+ <div>
131
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
132
+ CTA URL
133
+ </label>
134
+ <input
135
+ type="url"
136
+ value={data.ctaUrl || ""}
137
+ onChange={(e) => handleChange("ctaUrl", e.target.value)}
138
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-primary)] text-sm"
139
+ placeholder="https://..."
140
+ />
141
+ </div>
142
+ </div>
143
+
144
+ <div className="pt-4 border-t border-[var(--kyro-border)]">
145
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-2 block">
146
+ Children ({children.length})
147
+ </label>
148
+ <ChildBlocksTree
149
+ blockId={block.id}
150
+ children={children}
151
+ onUpdateChildren={handleUpdateChildren}
152
+ />
153
+ </div>
154
+ </div>
155
+ </div>
156
+ );
157
+ };
@@ -0,0 +1,83 @@
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 ImageBlock: React.FC<{ block: any; index: number }> = ({
9
+ block,
10
+ index,
11
+ }) => {
12
+ const blockData = useBlockById(block.id);
13
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
14
+ const data = blockData?.data || block.data || {};
15
+
16
+ const handleChange = (field: string, value: any) => {
17
+ updateBlock(block.id, { data: { ...data, [field]: value } });
18
+ };
19
+
20
+ return (
21
+ <div className="block-image border border-[var(--kyro-border)] rounded-lg p-4 mb-4 relative group">
22
+ <div className="flex items-center justify-between mb-2">
23
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
24
+ Image
25
+ </span>
26
+ <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
27
+ <button type="button"
28
+ onClick={() => moveBlock(block.id, "up")}
29
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
30
+ title="Move up"
31
+ >
32
+ <ChevronRight className="w-3 h-3 rotate-90" />
33
+ </button>
34
+ <button type="button"
35
+ onClick={() => removeBlock(block.id)}
36
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
37
+ title="Remove"
38
+ >
39
+ <X className="w-3 h-3" />
40
+ </button>
41
+ </div>
42
+ </div>
43
+ <div className="space-y-3">
44
+ <div>
45
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
46
+ Image URL
47
+ </label>
48
+ <input
49
+ type="url"
50
+ value={data.src || ""}
51
+ onChange={(e) => handleChange("src", e.target.value)}
52
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm"
53
+ placeholder="https://..."
54
+ />
55
+ </div>
56
+ <div>
57
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
58
+ Alt Text
59
+ </label>
60
+ <input
61
+ type="text"
62
+ value={data.alt || ""}
63
+ onChange={(e) => handleChange("alt", e.target.value)}
64
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm"
65
+ placeholder="Alternative text..."
66
+ />
67
+ </div>
68
+ <div>
69
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
70
+ Caption
71
+ </label>
72
+ <input
73
+ type="text"
74
+ value={data.caption || ""}
75
+ onChange={(e) => handleChange("caption", e.target.value)}
76
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm"
77
+ placeholder="Image caption..."
78
+ />
79
+ </div>
80
+ </div>
81
+ </div>
82
+ );
83
+ };
@@ -0,0 +1,71 @@
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 LinkBlock: React.FC<{ block: any; index: number }> = ({
9
+ block,
10
+ index,
11
+ }) => {
12
+ const blockData = useBlockById(block.id);
13
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
14
+ const data = blockData?.data ?? block.data ?? {};
15
+
16
+ const handleChange = (field: string, value: any) => {
17
+ updateBlock(block.id, { data: { ...data, [field]: value } });
18
+ };
19
+
20
+ return (
21
+ <div className="block-link border border-dashed border-[var(--kyro-border)] rounded-md p-4 mb-2 relative group">
22
+ <div className="flex items-center justify-between mb-1">
23
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
24
+ Link
25
+ </span>
26
+ <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
27
+ <button type="button"
28
+ onClick={() => moveBlock(block.id, "up")}
29
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
30
+ title="Move up"
31
+ >
32
+ <ChevronRight className="w-3 h-3 rotate-90" />
33
+ </button>
34
+ <button type="button"
35
+ onClick={() => removeBlock(block.id)}
36
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
37
+ title="Remove"
38
+ >
39
+ <X className="w-3 h-3" />
40
+ </button>
41
+ </div>
42
+ </div>
43
+ <div className="space-y-3">
44
+ <div>
45
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
46
+ URL
47
+ </label>
48
+ <input
49
+ type="url"
50
+ value={block.data.url || ""}
51
+ onChange={(e) => handleChange("url", e.target.value)}
52
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm"
53
+ placeholder="https://..."
54
+ />
55
+ </div>
56
+ <div>
57
+ <label className="text-xs font-medium text-[var(--kyro-text-muted)] mb-1 block">
58
+ Text
59
+ </label>
60
+ <input
61
+ type="text"
62
+ value={block.data.text || ""}
63
+ onChange={(e) => handleChange("text", e.target.value)}
64
+ className="w-full px-3 py-2 border border-[var(--kyro-border)] rounded bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] text-sm"
65
+ placeholder="Link text..."
66
+ />
67
+ </div>
68
+ </div>
69
+ </div>
70
+ );
71
+ };