@kyro-cms/admin 0.1.7 → 0.1.9
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/package.json +7 -2
- package/src/components/Admin.tsx +1 -1
- package/src/components/AutoForm.tsx +966 -337
- package/src/components/CreateView.tsx +1 -1
- package/src/components/DetailView.tsx +1 -1
- package/src/components/EnhancedListView.tsx +156 -52
- package/src/components/ListView.tsx +1 -1
- package/src/components/Modal.tsx +65 -8
- package/src/components/Sidebar.astro +2 -2
- package/src/components/ThemeProvider.tsx +8 -2
- package/src/components/blocks/AccordionBlock.tsx +20 -52
- package/src/components/blocks/ArrayBlock.tsx +40 -31
- package/src/components/blocks/BlockEditModal.tsx +170 -581
- package/src/components/blocks/ButtonBlock.tsx +27 -128
- package/src/components/blocks/CodeBlock.tsx +88 -40
- package/src/components/blocks/ColumnsBlock.tsx +27 -85
- package/src/components/blocks/FileBlock.tsx +38 -39
- package/src/components/blocks/HeadingBlock.tsx +9 -31
- package/src/components/blocks/HeroBlock.tsx +42 -100
- package/src/components/blocks/ImageBlock.tsx +6 -7
- package/src/components/blocks/LinkBlock.tsx +27 -33
- package/src/components/blocks/ListBlock.tsx +47 -26
- package/src/components/blocks/RelationshipBlock.tsx +26 -233
- package/src/components/blocks/RichTextBlock.tsx +66 -0
- package/src/components/blocks/VStackBlock.tsx +23 -37
- package/src/components/blocks/VideoBlock.tsx +52 -32
- package/src/components/fields/AccordionField.tsx +213 -0
- package/src/components/fields/ArrayField.tsx +241 -0
- package/src/components/fields/BlocksField.tsx +5 -5
- package/src/components/fields/ButtonField.tsx +53 -0
- package/src/components/fields/CheckboxField.tsx +7 -3
- package/src/components/fields/ChildrenField.tsx +48 -0
- package/src/components/fields/CodeField.tsx +154 -94
- package/src/components/fields/ColumnsField.tsx +137 -0
- package/src/components/fields/DateField.tsx +9 -24
- package/src/components/fields/EditorClient.tsx +426 -160
- package/src/components/fields/HeadingField.tsx +31 -0
- package/src/components/fields/HeroField.tsx +101 -0
- package/src/components/fields/JSONField.tsx +7 -27
- package/src/components/fields/LinkField.tsx +81 -0
- package/src/components/fields/ListField.tsx +74 -0
- package/src/components/fields/MarkdownField.tsx +4 -26
- package/src/components/fields/NumberField.tsx +9 -27
- package/src/components/fields/PortableTextField.tsx +61 -49
- package/src/components/fields/RelationshipBlockField.tsx +233 -0
- package/src/components/fields/RelationshipField.tsx +59 -13
- package/src/components/fields/SelectField.tsx +6 -4
- package/src/components/fields/TextField.tsx +9 -24
- package/src/components/fields/UploadField.tsx +613 -0
- package/src/components/fields/VideoField.tsx +73 -0
- package/src/components/fields/extensions/blockComponents.tsx +11 -1
- package/src/components/fields/extensions/blocksStore.ts +1 -1
- package/src/components/fields/index.ts +12 -1
- package/src/components/layout/Layout.tsx +1 -1
- package/src/lib/api.ts +163 -0
- package/src/lib/config.ts +1 -1
- package/src/lib/dataStore.ts +87 -30
- package/src/lib/date-utils.ts +69 -0
- package/src/lib/db/version-adapter.ts +248 -0
- package/src/lib/i18n.tsx +353 -0
- package/src/lib/slugify.ts +15 -0
- package/src/lib/validation.ts +250 -0
- package/src/pages/api/[collection]/[id]/publish.ts +12 -4
- package/src/pages/api/[collection]/[id]/versions.ts +39 -9
- package/src/pages/api/[collection]/[id].ts +13 -1
- package/src/pages/api/[collection]/index.ts +5 -6
- package/src/styles/main.css +12 -2
- package/src/components/blocks/BlockEditModal.MARKER +0 -12
- package/src/components/fields/FileField.tsx +0 -390
- package/src/components/fields/HybridContentField.tsx +0 -109
- package/src/components/fields/ImageField.tsx +0 -429
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
export function isEmail(value: string): boolean {
|
|
2
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
3
|
+
return emailRegex.test(value);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function isUrl(value: string): boolean {
|
|
7
|
+
try {
|
|
8
|
+
new URL(value);
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isSlug(value: string): boolean {
|
|
16
|
+
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
17
|
+
return slugRegex.test(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isRequired(value: any): boolean {
|
|
21
|
+
if (value === null || value === undefined) return false;
|
|
22
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
23
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function minLength(value: string, min: number): boolean {
|
|
28
|
+
return typeof value === "string" && value.length >= min;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function maxLength(value: string, max: number): boolean {
|
|
32
|
+
return typeof value === "string" && value.length <= max;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isNumber(value: any): boolean {
|
|
36
|
+
return !isNaN(parseFloat(value)) && isFinite(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isInteger(value: any): boolean {
|
|
40
|
+
return Number.isInteger(Number(value));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function inRange(value: number, min: number, max: number): boolean {
|
|
44
|
+
const num = Number(value);
|
|
45
|
+
return num >= min && num <= max;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isJson(value: string): boolean {
|
|
49
|
+
try {
|
|
50
|
+
JSON.parse(value);
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isHexColor(value: string): boolean {
|
|
58
|
+
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
|
|
59
|
+
return hexRegex.test(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isAlpha(value: string): boolean {
|
|
63
|
+
const alphaRegex = /^[a-zA-Z]+$/;
|
|
64
|
+
return alphaRegex.test(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isAlphaNumeric(value: string): boolean {
|
|
68
|
+
const alphaNumRegex = /^[a-zA-Z0-9]+$/;
|
|
69
|
+
return alphaNumRegex.test(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isPhone(value: string): boolean {
|
|
73
|
+
const phoneRegex = /^[\d\s\-+()]+$/;
|
|
74
|
+
return phoneRegex.test(value) && value.replace(/\D/g, "").length >= 10;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function isPostalCode(value: string, country: string = "US"): boolean {
|
|
78
|
+
const codes: Record<string, RegExp> = {
|
|
79
|
+
US: /^\d{5}(-\d{4})?$/,
|
|
80
|
+
UK: /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i,
|
|
81
|
+
CA: /^[A-Z]\d[A-Z] ?\d[A-Z]\d$/i,
|
|
82
|
+
};
|
|
83
|
+
return codes[country]?.test(value) || false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function matches(value: string, other: string): boolean {
|
|
87
|
+
return value === other;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function notMatches(value: string, other: string): boolean {
|
|
91
|
+
return value !== other;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ValidationRule {
|
|
95
|
+
validate: (value: any, ...args: any[]) => boolean;
|
|
96
|
+
message: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function required(message = "This field is required"): ValidationRule {
|
|
100
|
+
return {
|
|
101
|
+
validate: isRequired,
|
|
102
|
+
message,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function email(message = "Invalid email address"): ValidationRule {
|
|
107
|
+
return {
|
|
108
|
+
validate: isEmail,
|
|
109
|
+
message,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function url(message = "Invalid URL"): ValidationRule {
|
|
114
|
+
return {
|
|
115
|
+
validate: isUrl,
|
|
116
|
+
message,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function minLengthRule(min: number, message?: string): ValidationRule {
|
|
121
|
+
return {
|
|
122
|
+
validate: (value: string) => minLength(value, min),
|
|
123
|
+
message: message || `Minimum ${min} characters required`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function maxLengthRule(max: number, message?: string): ValidationRule {
|
|
128
|
+
return {
|
|
129
|
+
validate: (value: string) => maxLength(value, max),
|
|
130
|
+
message: message || `Maximum ${max} characters allowed`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function pattern(
|
|
135
|
+
regex: RegExp,
|
|
136
|
+
message = "Invalid format",
|
|
137
|
+
): ValidationRule {
|
|
138
|
+
return {
|
|
139
|
+
validate: (value: string) => regex.test(value || ""),
|
|
140
|
+
message,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function matchesRule(
|
|
145
|
+
otherField: string,
|
|
146
|
+
message = "Values do not match",
|
|
147
|
+
): ValidationRule {
|
|
148
|
+
return {
|
|
149
|
+
validate: (value: string, data: Record<string, any>) =>
|
|
150
|
+
matches(value, data[otherField]),
|
|
151
|
+
message,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function notMatchesRule(
|
|
156
|
+
otherField: string,
|
|
157
|
+
message = "Value must be different",
|
|
158
|
+
): ValidationRule {
|
|
159
|
+
return {
|
|
160
|
+
validate: (value: string, data: Record<string, any>) =>
|
|
161
|
+
notMatches(value, data[otherField]),
|
|
162
|
+
message,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function phone(message = "Invalid phone number"): ValidationRule {
|
|
167
|
+
return {
|
|
168
|
+
validate: isPhone,
|
|
169
|
+
message,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function postalCodeRule(
|
|
174
|
+
country: string = "US",
|
|
175
|
+
message?: string,
|
|
176
|
+
): ValidationRule {
|
|
177
|
+
return {
|
|
178
|
+
validate: (value: string) => isPostalCode(value, country),
|
|
179
|
+
message: message || `Invalid postal code for ${country}`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function numberRule(message = "Must be a valid number"): ValidationRule {
|
|
184
|
+
return {
|
|
185
|
+
validate: isNumber,
|
|
186
|
+
message,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function integerRule(
|
|
191
|
+
message = "Must be a whole number",
|
|
192
|
+
): ValidationRule {
|
|
193
|
+
return {
|
|
194
|
+
validate: isInteger,
|
|
195
|
+
message,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function rangeRule(
|
|
200
|
+
min: number,
|
|
201
|
+
max: number,
|
|
202
|
+
message?: string,
|
|
203
|
+
): ValidationRule {
|
|
204
|
+
return {
|
|
205
|
+
validate: (value: number) => inRange(Number(value), min, max),
|
|
206
|
+
message: message || `Must be between ${min} and ${max}`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function jsonRule(message = "Must be valid JSON"): ValidationRule {
|
|
211
|
+
return {
|
|
212
|
+
validate: isJson,
|
|
213
|
+
message,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function hexColorRule(
|
|
218
|
+
message = "Must be a valid hex color",
|
|
219
|
+
): ValidationRule {
|
|
220
|
+
return {
|
|
221
|
+
validate: isHexColor,
|
|
222
|
+
message,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface ValidationResult {
|
|
227
|
+
valid: boolean;
|
|
228
|
+
errors: Record<string, string>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function validate(
|
|
232
|
+
data: Record<string, any>,
|
|
233
|
+
rules: Record<string, ValidationRule[]>,
|
|
234
|
+
): ValidationResult {
|
|
235
|
+
const errors: Record<string, string> = {};
|
|
236
|
+
|
|
237
|
+
for (const [field, fieldRules] of Object.entries(rules)) {
|
|
238
|
+
for (const rule of fieldRules) {
|
|
239
|
+
if (!rule.validate(data[field], data)) {
|
|
240
|
+
errors[field] = rule.message;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
valid: Object.keys(errors).length === 0,
|
|
248
|
+
errors,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -25,10 +25,18 @@ export const POST: APIRoute = async ({ params, request }) => {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const now = new Date().toISOString();
|
|
28
|
-
const updated = await dataStore.update(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const updated = await dataStore.update(
|
|
29
|
+
collection,
|
|
30
|
+
id,
|
|
31
|
+
{
|
|
32
|
+
status: "published",
|
|
33
|
+
publishedAt: now,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
versionStatus: "published",
|
|
37
|
+
changeDescription: "Published changes",
|
|
38
|
+
},
|
|
39
|
+
);
|
|
32
40
|
|
|
33
41
|
return new Response(JSON.stringify({ success: true, data: updated }), {
|
|
34
42
|
status: 200,
|
|
@@ -1,18 +1,36 @@
|
|
|
1
1
|
import type { APIRoute } from "astro";
|
|
2
2
|
import { dataStore } from "@/lib/dataStore";
|
|
3
3
|
|
|
4
|
-
export const GET: APIRoute = async ({ params }) => {
|
|
4
|
+
export const GET: APIRoute = async ({ params, url }) => {
|
|
5
5
|
const { collection, id } = params;
|
|
6
6
|
if (!collection || !id) return new Response(null, { status: 400 });
|
|
7
7
|
|
|
8
8
|
try {
|
|
9
|
+
const compareA = url.searchParams.get("compareA");
|
|
10
|
+
const compareB = url.searchParams.get("compareB");
|
|
11
|
+
|
|
12
|
+
if (compareA && compareB) {
|
|
13
|
+
const diffs = await dataStore.compareVersions(
|
|
14
|
+
collection,
|
|
15
|
+
id,
|
|
16
|
+
isNaN(Number(compareA)) ? compareA : Number(compareA),
|
|
17
|
+
isNaN(Number(compareB)) ? compareB : Number(compareB),
|
|
18
|
+
);
|
|
19
|
+
return new Response(JSON.stringify({ diffs }), {
|
|
20
|
+
status: 200,
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
const versions = await dataStore.findVersions(collection, id);
|
|
10
26
|
return new Response(JSON.stringify({ docs: versions }), {
|
|
11
27
|
status: 200,
|
|
12
|
-
headers: { "Content-Type": "application/json" }
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
13
29
|
});
|
|
14
30
|
} catch (error) {
|
|
15
|
-
return new Response(JSON.stringify({ error: "Failed to fetch versions" }), {
|
|
31
|
+
return new Response(JSON.stringify({ error: "Failed to fetch versions" }), {
|
|
32
|
+
status: 500,
|
|
33
|
+
});
|
|
16
34
|
}
|
|
17
35
|
};
|
|
18
36
|
|
|
@@ -22,15 +40,27 @@ export const POST: APIRoute = async ({ params, request }) => {
|
|
|
22
40
|
|
|
23
41
|
try {
|
|
24
42
|
const { versionId, action } = await request.json();
|
|
25
|
-
|
|
26
|
-
if (action ===
|
|
27
|
-
const restored = await dataStore.restoreVersion(
|
|
28
|
-
|
|
43
|
+
|
|
44
|
+
if (action === "restore" && versionId) {
|
|
45
|
+
const restored = await dataStore.restoreVersion(
|
|
46
|
+
collection,
|
|
47
|
+
id,
|
|
48
|
+
versionId,
|
|
49
|
+
);
|
|
50
|
+
if (!restored)
|
|
51
|
+
return new Response(JSON.stringify({ error: "Restore failed" }), {
|
|
52
|
+
status: 400,
|
|
53
|
+
});
|
|
29
54
|
return new Response(JSON.stringify({ data: restored }), { status: 200 });
|
|
30
55
|
}
|
|
31
56
|
|
|
32
|
-
return new Response(JSON.stringify({ error: "Invalid action" }), {
|
|
57
|
+
return new Response(JSON.stringify({ error: "Invalid action" }), {
|
|
58
|
+
status: 400,
|
|
59
|
+
});
|
|
33
60
|
} catch (error) {
|
|
34
|
-
return new Response(
|
|
61
|
+
return new Response(
|
|
62
|
+
JSON.stringify({ error: "Failed to perform version action" }),
|
|
63
|
+
{ status: 500 },
|
|
64
|
+
);
|
|
35
65
|
}
|
|
36
66
|
};
|
|
@@ -130,7 +130,19 @@ export const PATCH: APIRoute = async ({ params, request }) => {
|
|
|
130
130
|
|
|
131
131
|
try {
|
|
132
132
|
const body = await request.json();
|
|
133
|
-
const
|
|
133
|
+
const versionStatus =
|
|
134
|
+
body.status === "draft"
|
|
135
|
+
? "draft"
|
|
136
|
+
: body.status === "published"
|
|
137
|
+
? "published"
|
|
138
|
+
: undefined;
|
|
139
|
+
const changeDescription = body._changeDescription;
|
|
140
|
+
const { _changeDescription, ...cleanBody } = body;
|
|
141
|
+
|
|
142
|
+
const doc = await dataStore.update(collection, id, cleanBody, {
|
|
143
|
+
versionStatus,
|
|
144
|
+
changeDescription,
|
|
145
|
+
});
|
|
134
146
|
if (!doc) {
|
|
135
147
|
return new Response(JSON.stringify({ error: "Document not found" }), {
|
|
136
148
|
status: 404,
|
|
@@ -82,8 +82,8 @@ const initSeedData = async () => {
|
|
|
82
82
|
const siteGlobal = await store.findGlobal("site-settings");
|
|
83
83
|
if (!siteGlobal.siteName) {
|
|
84
84
|
await store.seedGlobal("site-settings", {
|
|
85
|
-
siteName: "
|
|
86
|
-
siteDescription: "
|
|
85
|
+
siteName: "",
|
|
86
|
+
siteDescription: "",
|
|
87
87
|
allowRegistration: true,
|
|
88
88
|
});
|
|
89
89
|
}
|
|
@@ -92,10 +92,9 @@ const initSeedData = async () => {
|
|
|
92
92
|
const seoGlobal = await store.findGlobal("seo-settings");
|
|
93
93
|
if (!seoGlobal.defaultTitle) {
|
|
94
94
|
await store.seedGlobal("seo-settings", {
|
|
95
|
-
defaultTitle: "
|
|
96
|
-
titleTemplate: "%s
|
|
97
|
-
defaultDescription:
|
|
98
|
-
"High-performance content management for the modern web.",
|
|
95
|
+
defaultTitle: "",
|
|
96
|
+
titleTemplate: "%s",
|
|
97
|
+
defaultDescription: "",
|
|
99
98
|
});
|
|
100
99
|
}
|
|
101
100
|
};
|
package/src/styles/main.css
CHANGED
|
@@ -95,9 +95,14 @@
|
|
|
95
95
|
--kyro-success: #22c55e;
|
|
96
96
|
--kyro-success-bg: rgba(34, 197, 94, 0.1);
|
|
97
97
|
|
|
98
|
-
/* Danger Color */
|
|
98
|
+
/* Danger/Error Color */
|
|
99
99
|
--kyro-danger: #ef4444;
|
|
100
100
|
--kyro-danger-bg: rgba(239, 68, 68, 0.1);
|
|
101
|
+
--kyro-error: #ef4444;
|
|
102
|
+
|
|
103
|
+
/* Warning Color */
|
|
104
|
+
--kyro-warning: #ffae00;
|
|
105
|
+
--kyro-warning-bg: rgba(255, 174, 0, 0.1);
|
|
101
106
|
|
|
102
107
|
/* Border Active */
|
|
103
108
|
--kyro-border-active: #0b1222;
|
|
@@ -141,9 +146,14 @@
|
|
|
141
146
|
--kyro-success: #22c55e;
|
|
142
147
|
--kyro-success-bg: rgba(34, 197, 94, 0.15);
|
|
143
148
|
|
|
144
|
-
/* Danger Color (dark) */
|
|
149
|
+
/* Danger/Error Color (dark) */
|
|
145
150
|
--kyro-danger: #f87171;
|
|
146
151
|
--kyro-danger-bg: rgba(248, 113, 113, 0.15);
|
|
152
|
+
--kyro-error: #f87171;
|
|
153
|
+
|
|
154
|
+
/* Warning Color (dark) */
|
|
155
|
+
--kyro-warning: #ffae00;
|
|
156
|
+
--kyro-warning-bg: rgba(255, 174, 0, 0.15);
|
|
147
157
|
|
|
148
158
|
/* Border Active (dark) */
|
|
149
159
|
--kyro-border-active: #ffffff;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# BlockEditModal Children Marker
|
|
2
|
-
# Created: 2026-04-29
|
|
3
|
-
#
|
|
4
|
-
# This marks the point where BlockEditModal was updated to include:
|
|
5
|
-
# - Children (via ChildBlocksTree) for array, vstack, accordion, columns, hero
|
|
6
|
-
# - Matching styling to original block components
|
|
7
|
-
#
|
|
8
|
-
# To revert:
|
|
9
|
-
# 1. Check git history for previous BlockEditModal.tsx
|
|
10
|
-
# 2. Or use: git show HEAD~1:admin/src/components/blocks/BlockEditModal.tsx > BlockEditModal.tsx
|
|
11
|
-
#
|
|
12
|
-
# Marker Line in code: // @MARKER: BlockEditModal with children - 2026-04-29
|