@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
|
@@ -3,7 +3,7 @@ import type {
|
|
|
3
3
|
KyroConfig,
|
|
4
4
|
CollectionConfig,
|
|
5
5
|
GlobalConfig,
|
|
6
|
-
} from "@kyro-cms/core";
|
|
6
|
+
} from "@kyro-cms/core/client";
|
|
7
7
|
import { AutoForm } from "./AutoForm";
|
|
8
8
|
import { ActionBar, type DocumentStatus, type SaveStatus } from "./ActionBar";
|
|
9
9
|
import { ConfirmModal } from "./ui/Modal";
|
|
@@ -8,6 +8,8 @@ export interface FieldConfig {
|
|
|
8
8
|
label?: string;
|
|
9
9
|
required?: boolean;
|
|
10
10
|
options?: { value: string; label: string }[];
|
|
11
|
+
fields?: FieldConfig[];
|
|
12
|
+
tabs?: Array<{ label?: string; name?: string; fields?: FieldConfig[] }>;
|
|
11
13
|
admin?: {
|
|
12
14
|
hidden?: boolean;
|
|
13
15
|
readonly?: boolean;
|
|
@@ -23,20 +25,21 @@ export interface CollectionConfig {
|
|
|
23
25
|
admin?: {
|
|
24
26
|
description?: string;
|
|
25
27
|
defaultColumns?: string[];
|
|
28
|
+
useAsTitle?: string;
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
interface FilterConfig {
|
|
30
33
|
field: string;
|
|
31
34
|
operator:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
| "equals"
|
|
36
|
+
| "contains"
|
|
37
|
+
| "gt"
|
|
38
|
+
| "lt"
|
|
39
|
+
| "gte"
|
|
40
|
+
| "lte"
|
|
41
|
+
| "between"
|
|
42
|
+
| "in";
|
|
40
43
|
value: string;
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -91,19 +94,75 @@ export function EnhancedListView({
|
|
|
91
94
|
const [showFilters, setShowFilters] = useState(false);
|
|
92
95
|
const [showColumns, setShowColumns] = useState(false);
|
|
93
96
|
|
|
97
|
+
function flattenFields(fields: FieldConfig[]): FieldConfig[] {
|
|
98
|
+
const result: FieldConfig[] = [];
|
|
99
|
+
for (const field of fields) {
|
|
100
|
+
if (!field.name || field.admin?.hidden || field.name === "id") continue;
|
|
101
|
+
if (field.type === "tabs" && field.tabs) {
|
|
102
|
+
for (const tab of field.tabs) {
|
|
103
|
+
if (tab.fields) {
|
|
104
|
+
result.push(...flattenFields(tab.fields));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} else if (
|
|
108
|
+
(field.type === "row" || field.type === "collapsible") &&
|
|
109
|
+
field.fields
|
|
110
|
+
) {
|
|
111
|
+
result.push(...flattenFields(field.fields));
|
|
112
|
+
} else {
|
|
113
|
+
result.push(field);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
94
119
|
const allFields = useMemo(
|
|
95
|
-
() =>
|
|
96
|
-
collection.fields.filter(
|
|
97
|
-
(f) => f.name && !f.admin?.hidden && f.name !== "id",
|
|
98
|
-
),
|
|
120
|
+
() => flattenFields(collection.fields),
|
|
99
121
|
[collection.fields],
|
|
100
122
|
);
|
|
101
123
|
|
|
102
124
|
const [visibleColumns, setVisibleColumns] = useState<Set<string>>(() => {
|
|
125
|
+
if (collection.admin?.defaultColumns) {
|
|
126
|
+
return new Set(collection.admin.defaultColumns);
|
|
127
|
+
}
|
|
103
128
|
const defaultCols = allFields.slice(0, 4).map((f) => f.name);
|
|
104
129
|
return new Set(defaultCols);
|
|
105
130
|
});
|
|
106
131
|
|
|
132
|
+
const toggleColumn = useCallback((fieldName: string) => {
|
|
133
|
+
setVisibleColumns((prev) => {
|
|
134
|
+
const next = new Set(prev);
|
|
135
|
+
if (next.has(fieldName)) {
|
|
136
|
+
next.delete(fieldName);
|
|
137
|
+
} else {
|
|
138
|
+
next.add(fieldName);
|
|
139
|
+
}
|
|
140
|
+
return next;
|
|
141
|
+
});
|
|
142
|
+
}, []);
|
|
143
|
+
|
|
144
|
+
function resolveSortField(fieldName: string): string {
|
|
145
|
+
const field = allFields.find((f) => f.name === fieldName);
|
|
146
|
+
if (!field) return fieldName;
|
|
147
|
+
if (field.type === "group" && field.fields?.[0]?.name) {
|
|
148
|
+
return `${fieldName}.${field.fields[0].name}`;
|
|
149
|
+
}
|
|
150
|
+
return fieldName;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const handleSort = useCallback((fieldName: string) => {
|
|
154
|
+
const resolvedField = resolveSortField(fieldName);
|
|
155
|
+
setSort((prev) => {
|
|
156
|
+
if (prev && prev.field === resolvedField) {
|
|
157
|
+
return {
|
|
158
|
+
field: resolvedField,
|
|
159
|
+
direction: prev.direction === "asc" ? "desc" : "asc",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return { field: resolvedField, direction: "asc" };
|
|
163
|
+
});
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
107
166
|
const [deleteConfirm, setDeleteConfirm] = useState<{
|
|
108
167
|
open: boolean;
|
|
109
168
|
count: number;
|
|
@@ -115,6 +174,34 @@ export function EnhancedListView({
|
|
|
115
174
|
[allFields, visibleColumns],
|
|
116
175
|
);
|
|
117
176
|
|
|
177
|
+
const titleField =
|
|
178
|
+
collection.admin?.useAsTitle ||
|
|
179
|
+
allFields.find((f) => f.type !== "group")?.name;
|
|
180
|
+
|
|
181
|
+
function fieldContainsTitle(field: FieldConfig): boolean {
|
|
182
|
+
if (field.name === titleField) return true;
|
|
183
|
+
if (field.type === "group" && field.fields?.[0]?.name === titleField)
|
|
184
|
+
return true;
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function extractFieldValue(doc: any, field: FieldConfig): any {
|
|
189
|
+
if (doc[field.name] !== undefined && doc[field.name] !== null) {
|
|
190
|
+
return doc[field.name];
|
|
191
|
+
}
|
|
192
|
+
if (field.type === "group" && typeof doc[field.name] === "object") {
|
|
193
|
+
if (
|
|
194
|
+
field.fields?.[0]?.name &&
|
|
195
|
+
doc[field.name][field.fields[0].name] !== undefined
|
|
196
|
+
) {
|
|
197
|
+
return doc[field.name][field.fields[0].name];
|
|
198
|
+
}
|
|
199
|
+
const firstKey = Object.keys(doc[field.name])[0];
|
|
200
|
+
if (firstKey) return doc[field.name][firstKey];
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
118
205
|
const fetchDocs = useCallback(async () => {
|
|
119
206
|
setLoading(true);
|
|
120
207
|
try {
|
|
@@ -188,7 +275,6 @@ export function EnhancedListView({
|
|
|
188
275
|
const totalPages = Math.ceil(totalDocs / limit);
|
|
189
276
|
const hasActiveFilters = search || filters.length > 0 || sort;
|
|
190
277
|
|
|
191
|
-
|
|
192
278
|
return (
|
|
193
279
|
<div className="space-y-6">
|
|
194
280
|
<ConfirmModal
|
|
@@ -214,7 +300,8 @@ export function EnhancedListView({
|
|
|
214
300
|
)}
|
|
215
301
|
</p>
|
|
216
302
|
</div>
|
|
217
|
-
<button
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
218
305
|
onClick={onCreate}
|
|
219
306
|
className="flex items-center gap-2 px-5 py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold transition-all hover:opacity-90 active:scale-95 shadow-lg"
|
|
220
307
|
>
|
|
@@ -264,12 +351,14 @@ export function EnhancedListView({
|
|
|
264
351
|
|
|
265
352
|
<div className="flex items-center gap-2 flex-wrap">
|
|
266
353
|
{/* Filter Toggle */}
|
|
267
|
-
<button
|
|
354
|
+
<button
|
|
355
|
+
type="button"
|
|
268
356
|
onClick={() => setShowFilters(!showFilters)}
|
|
269
|
-
className={`flex items-center gap-2 px-4 py-2 rounded-xl font-bold text-sm transition-all ${
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
357
|
+
className={`flex items-center gap-2 px-4 py-2 rounded-xl font-bold text-sm transition-all ${
|
|
358
|
+
showFilters || filters.length > 0
|
|
359
|
+
? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)]"
|
|
360
|
+
: "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
|
|
361
|
+
}`}
|
|
273
362
|
>
|
|
274
363
|
<svg
|
|
275
364
|
className="w-4 h-4"
|
|
@@ -294,7 +383,8 @@ export function EnhancedListView({
|
|
|
294
383
|
|
|
295
384
|
{/* Column Toggle */}
|
|
296
385
|
<div className="relative">
|
|
297
|
-
<button
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
298
388
|
onClick={() => setShowColumns(!showColumns)}
|
|
299
389
|
className="flex items-center gap-2 px-4 py-2 rounded-xl font-bold text-sm bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-all"
|
|
300
390
|
>
|
|
@@ -314,7 +404,7 @@ export function EnhancedListView({
|
|
|
314
404
|
Columns
|
|
315
405
|
</button>
|
|
316
406
|
{showColumns && (
|
|
317
|
-
<div className="absolute right-0 top-full mt-2 w-56 surface-tile border border-[var(--kyro-border)] rounded-
|
|
407
|
+
<div className="absolute right-0 top-full mt-2 w-56 surface-tile border border-[var(--kyro-border)] rounded-lg shadow-xl z-50 overflow-hidden">
|
|
318
408
|
<div className="p-3 border-b border-[var(--kyro-border)]">
|
|
319
409
|
<span className="text-xs font-bold uppercase tracking-wider text-[var(--kyro-text-secondary)]">
|
|
320
410
|
Toggle Columns
|
|
@@ -344,7 +434,8 @@ export function EnhancedListView({
|
|
|
344
434
|
|
|
345
435
|
{/* Clear All */}
|
|
346
436
|
{hasActiveFilters && (
|
|
347
|
-
<button
|
|
437
|
+
<button
|
|
438
|
+
type="button"
|
|
348
439
|
onClick={clearAll}
|
|
349
440
|
className="px-4 py-2 rounded-xl font-bold text-sm text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 transition-all"
|
|
350
441
|
>
|
|
@@ -361,7 +452,8 @@ export function EnhancedListView({
|
|
|
361
452
|
<h3 className="font-bold text-[var(--kyro-text-primary)]">
|
|
362
453
|
Advanced Filters
|
|
363
454
|
</h3>
|
|
364
|
-
<button
|
|
455
|
+
<button
|
|
456
|
+
type="button"
|
|
365
457
|
onClick={addFilter}
|
|
366
458
|
className="flex items-center gap-2 px-3 py-1.5 text-sm font-bold text-[var(--kyro-sidebar-active)] hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all"
|
|
367
459
|
>
|
|
@@ -422,7 +514,8 @@ export function EnhancedListView({
|
|
|
422
514
|
placeholder="Value..."
|
|
423
515
|
className="flex-1 min-w-[150px] px-3 py-2 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg text-sm font-medium text-[var(--kyro-text-primary)]"
|
|
424
516
|
/>
|
|
425
|
-
<button
|
|
517
|
+
<button
|
|
518
|
+
type="button"
|
|
426
519
|
onClick={() => removeFilter(index)}
|
|
427
520
|
className="p-2 text-[var(--kyro-text-muted)] hover:text-red-500 transition-colors"
|
|
428
521
|
>
|
|
@@ -458,7 +551,8 @@ export function EnhancedListView({
|
|
|
458
551
|
{selectedIds.size} selected
|
|
459
552
|
</span>
|
|
460
553
|
<div className="flex gap-2">
|
|
461
|
-
<button
|
|
554
|
+
<button
|
|
555
|
+
type="button"
|
|
462
556
|
onClick={handleBulkDelete}
|
|
463
557
|
className="flex items-center gap-2 px-4 py-2 bg-red-500 text-white rounded-lg font-bold text-sm hover:bg-red-600 transition-all"
|
|
464
558
|
>
|
|
@@ -477,7 +571,8 @@ export function EnhancedListView({
|
|
|
477
571
|
</svg>
|
|
478
572
|
Delete Selected
|
|
479
573
|
</button>
|
|
480
|
-
<button
|
|
574
|
+
<button
|
|
575
|
+
type="button"
|
|
481
576
|
onClick={() => setSelectedIds(new Set())}
|
|
482
577
|
className="px-4 py-2 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] font-bold text-sm transition-all"
|
|
483
578
|
>
|
|
@@ -519,7 +614,8 @@ export function EnhancedListView({
|
|
|
519
614
|
: `Get started by creating your first ${(collection.singularLabel || collection.label || collectionSlug).toLowerCase()}.`}
|
|
520
615
|
</p>
|
|
521
616
|
{!hasActiveFilters && (
|
|
522
|
-
<button
|
|
617
|
+
<button
|
|
618
|
+
type="button"
|
|
523
619
|
onClick={onCreate}
|
|
524
620
|
className="mt-4 inline-flex items-center gap-2 px-5 py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-lg font-bold text-sm shadow-md"
|
|
525
621
|
>
|
|
@@ -563,7 +659,8 @@ export function EnhancedListView({
|
|
|
563
659
|
onClick={() => handleSort(field.name)}
|
|
564
660
|
>
|
|
565
661
|
<div className="flex items-center gap-2">
|
|
566
|
-
{checkTabbedValue(displayFields, field.type) ??
|
|
662
|
+
{checkTabbedValue(displayFields, field.type) ??
|
|
663
|
+
(field.label || field.name)}
|
|
567
664
|
{sort?.field === field.name && (
|
|
568
665
|
<svg
|
|
569
666
|
className={`w-3 h-3 ${sort.direction === "desc" ? "rotate-180" : ""}`}
|
|
@@ -606,29 +703,33 @@ export function EnhancedListView({
|
|
|
606
703
|
className="w-4 h-4 rounded border-[var(--kyro-border-strong)] text-[var(--kyro-sidebar-active)] focus:ring-[var(--kyro-sidebar-active)]"
|
|
607
704
|
/>
|
|
608
705
|
</td>
|
|
609
|
-
{displayFields.map((field
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
706
|
+
{displayFields.map((field) => {
|
|
707
|
+
const rawValue = extractFieldValue(doc, field);
|
|
708
|
+
const cellValue =
|
|
709
|
+
field.type === "select" && rawValue
|
|
710
|
+
? field.options?.find((o) => o.value === rawValue)
|
|
711
|
+
?.label || rawValue
|
|
712
|
+
: formatCellValue(rawValue, field.type);
|
|
713
|
+
return (
|
|
714
|
+
<td
|
|
715
|
+
key={field.name}
|
|
716
|
+
className={`px-4 py-3 ${fieldContainsTitle(field) ? "font-bold text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)]"}`}
|
|
717
|
+
>
|
|
718
|
+
{cellValue}
|
|
719
|
+
</td>
|
|
720
|
+
);
|
|
721
|
+
})}
|
|
621
722
|
{collection.timestamps && (
|
|
622
723
|
<td className="px-4 py-3 text-sm text-[var(--kyro-text-secondary)]">
|
|
623
724
|
{doc.createdAt
|
|
624
725
|
? new Date(doc.createdAt).toLocaleDateString(
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
726
|
+
"en-US",
|
|
727
|
+
{
|
|
728
|
+
month: "short",
|
|
729
|
+
day: "numeric",
|
|
730
|
+
year: "numeric",
|
|
731
|
+
},
|
|
732
|
+
)
|
|
632
733
|
: "—"}
|
|
633
734
|
</td>
|
|
634
735
|
)}
|
|
@@ -637,7 +738,8 @@ export function EnhancedListView({
|
|
|
637
738
|
onClick={(e) => e.stopPropagation()}
|
|
638
739
|
>
|
|
639
740
|
<div className="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
640
|
-
<button
|
|
741
|
+
<button
|
|
742
|
+
type="button"
|
|
641
743
|
onClick={() => onEdit(doc.slug || doc.id)}
|
|
642
744
|
className="flex items-center gap-2 px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg text-sm font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-all"
|
|
643
745
|
title="Edit"
|
|
@@ -656,7 +758,8 @@ export function EnhancedListView({
|
|
|
656
758
|
/>
|
|
657
759
|
</svg>
|
|
658
760
|
</button>
|
|
659
|
-
<button
|
|
761
|
+
<button
|
|
762
|
+
type="button"
|
|
660
763
|
onClick={() => handleDeleteSingle(doc.id)}
|
|
661
764
|
className="inline-flex items-center justify-center w-8 h-8 rounded-md text-[var(--kyro-text-muted)] hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-500/10 transition-colors"
|
|
662
765
|
title="Delete"
|
|
@@ -719,7 +822,8 @@ export function EnhancedListView({
|
|
|
719
822
|
</div>
|
|
720
823
|
<div className="flex gap-2">
|
|
721
824
|
{page > 1 && (
|
|
722
|
-
<button
|
|
825
|
+
<button
|
|
826
|
+
type="button"
|
|
723
827
|
onClick={() => setPage(page - 1)}
|
|
724
828
|
className="px-4 py-2 border border-[var(--kyro-border)] rounded-lg text-sm font-bold text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
|
|
725
829
|
>
|
|
@@ -727,7 +831,8 @@ export function EnhancedListView({
|
|
|
727
831
|
</button>
|
|
728
832
|
)}
|
|
729
833
|
{page < totalPages && (
|
|
730
|
-
<button
|
|
834
|
+
<button
|
|
835
|
+
type="button"
|
|
731
836
|
onClick={() => setPage(page + 1)}
|
|
732
837
|
className="px-4 py-2 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-lg text-sm font-bold hover:opacity-90 transition-all"
|
|
733
838
|
>
|
|
@@ -778,7 +883,6 @@ function formatCellValue(value: any, type?: string): string {
|
|
|
778
883
|
return String(value).slice(0, 60);
|
|
779
884
|
}
|
|
780
885
|
|
|
781
|
-
|
|
782
886
|
function checkTabbedValue(data: any[], type: string): string | undefined {
|
|
783
887
|
if (type !== "tabs") return;
|
|
784
888
|
const label = data[0]?.tabs[0]?.fields[0]?.label;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
|
-
import type { CollectionConfig, KyroConfig } from "@kyro-cms/core";
|
|
2
|
+
import type { CollectionConfig, KyroConfig } from "@kyro-cms/core/client";
|
|
3
3
|
import { Spinner } from "./ui/Spinner";
|
|
4
4
|
import { ConfirmModal } from "./ui/Modal";
|
|
5
5
|
import { Search, Plus, Settings } from "lucide-react";
|
package/src/components/Modal.tsx
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useEffect,
|
|
3
|
-
useState,
|
|
4
|
-
useCallback,
|
|
1
|
+
import React, {
|
|
5
2
|
createContext,
|
|
6
3
|
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
useCallback,
|
|
7
6
|
type ReactNode,
|
|
8
7
|
} from "react";
|
|
9
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
Modal as UIModal,
|
|
10
|
+
ModalContent,
|
|
11
|
+
ModalActions,
|
|
12
|
+
ConfirmModal,
|
|
13
|
+
} from "./ui/Modal";
|
|
14
|
+
import { PromptModal } from "./ui/PromptModal";
|
|
10
15
|
|
|
11
|
-
export { Modal, ModalContent, ModalActions, ConfirmModal }
|
|
16
|
+
export { UIModal as Modal, UIModal, ModalContent, ModalActions, ConfirmModal };
|
|
12
17
|
|
|
13
18
|
// ============================================================================
|
|
14
19
|
// Global Modal Context for programmatic access
|
|
@@ -25,6 +30,7 @@ interface ModalState {
|
|
|
25
30
|
defaultValue?: string;
|
|
26
31
|
onConfirm?: (value?: string) => void;
|
|
27
32
|
onCancel?: () => void;
|
|
33
|
+
danger?: boolean;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
const initialState: ModalState = {
|
|
@@ -34,11 +40,17 @@ const initialState: ModalState = {
|
|
|
34
40
|
message: "",
|
|
35
41
|
onConfirm: () => {},
|
|
36
42
|
onCancel: () => {},
|
|
43
|
+
danger: false,
|
|
37
44
|
};
|
|
38
45
|
|
|
39
46
|
interface ModalContextType {
|
|
40
47
|
showAlert: (title: string, message?: string) => void;
|
|
41
|
-
showConfirm: (
|
|
48
|
+
showConfirm: (
|
|
49
|
+
title: string,
|
|
50
|
+
message: string,
|
|
51
|
+
onConfirm: () => void,
|
|
52
|
+
options?: { danger?: boolean },
|
|
53
|
+
) => void;
|
|
42
54
|
showPrompt: (
|
|
43
55
|
title: string,
|
|
44
56
|
message: string,
|
|
@@ -78,12 +90,18 @@ export function ModalProvider({ children }: ModalProviderProps) {
|
|
|
78
90
|
}, []);
|
|
79
91
|
|
|
80
92
|
const showConfirm = useCallback(
|
|
81
|
-
(
|
|
93
|
+
(
|
|
94
|
+
title: string,
|
|
95
|
+
message: string,
|
|
96
|
+
onConfirm: () => void,
|
|
97
|
+
options?: { danger?: boolean },
|
|
98
|
+
) => {
|
|
82
99
|
setState({
|
|
83
100
|
variant: "confirm",
|
|
84
101
|
open: true,
|
|
85
102
|
title,
|
|
86
103
|
message,
|
|
104
|
+
danger: options?.danger,
|
|
87
105
|
onConfirm: () => {
|
|
88
106
|
onConfirm();
|
|
89
107
|
setState((s) => ({ ...s, open: false }));
|
|
@@ -144,6 +162,45 @@ export function ModalProvider({ children }: ModalProviderProps) {
|
|
|
144
162
|
}}
|
|
145
163
|
>
|
|
146
164
|
{children}
|
|
165
|
+
{state.variant === "confirm" && (
|
|
166
|
+
<ConfirmModal
|
|
167
|
+
open={state.open}
|
|
168
|
+
onClose={state.onCancel || closeModal}
|
|
169
|
+
onConfirm={() => state.onConfirm?.()}
|
|
170
|
+
title={state.title}
|
|
171
|
+
message={state.message || ""}
|
|
172
|
+
variant={state.danger ? "danger" : "default"}
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
{state.variant === "alert" && (
|
|
176
|
+
<UIModal
|
|
177
|
+
open={state.open}
|
|
178
|
+
onClose={state.onCancel || closeModal}
|
|
179
|
+
title={state.title}
|
|
180
|
+
size="sm"
|
|
181
|
+
footer={
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
onClick={() => state.onConfirm?.()}
|
|
185
|
+
className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
|
|
186
|
+
>
|
|
187
|
+
OK
|
|
188
|
+
</button>
|
|
189
|
+
}
|
|
190
|
+
>
|
|
191
|
+
<p className="text-[var(--kyro-text-secondary)]">{state.message}</p>
|
|
192
|
+
</UIModal>
|
|
193
|
+
)}
|
|
194
|
+
{state.variant === "prompt" && (
|
|
195
|
+
<PromptModal
|
|
196
|
+
open={state.open}
|
|
197
|
+
onClose={state.onCancel || closeModal}
|
|
198
|
+
onSubmit={(value) => state.onConfirm?.(value)}
|
|
199
|
+
title={state.title}
|
|
200
|
+
placeholder={state.placeholder}
|
|
201
|
+
defaultValue={state.defaultValue}
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
147
204
|
</ModalContext.Provider>
|
|
148
205
|
);
|
|
149
206
|
}
|
|
@@ -116,7 +116,7 @@ function isActive(item: NavItem): boolean {
|
|
|
116
116
|
{section.items.map((item) => (
|
|
117
117
|
<a
|
|
118
118
|
href={item.href}
|
|
119
|
-
class={`flex items-center gap-4 px-6 py-2 rounded-2xl transition-all font-
|
|
119
|
+
class={`flex items-center gap-4 px-6 py-2 rounded-2xl transition-all font-semibold ${
|
|
120
120
|
item.icon === "collection"
|
|
121
121
|
? currentPath === item.href ||
|
|
122
122
|
currentPath.startsWith(item.href + "/")
|
|
@@ -215,7 +215,7 @@ function isActive(item: NavItem): boolean {
|
|
|
215
215
|
</a>
|
|
216
216
|
<button
|
|
217
217
|
id="logout-btn"
|
|
218
|
-
class="flex justify-center p-2.5 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-xl transition-all shadow-sm active:scale-95 font-
|
|
218
|
+
class="flex justify-center p-2.5 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-xl transition-all shadow-sm active:scale-95 font-semibold"
|
|
219
219
|
title="Logout"
|
|
220
220
|
>
|
|
221
221
|
<svg
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
defaultLightTheme,
|
|
10
10
|
defaultDarkTheme,
|
|
11
11
|
type ThemeConfig,
|
|
12
|
-
} from "@kyro-cms/core";
|
|
12
|
+
} from "@kyro-cms/core/client";
|
|
13
13
|
|
|
14
14
|
export type ThemeMode = "light" | "dark" | "system";
|
|
15
15
|
|
|
@@ -25,7 +25,13 @@ const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
|
25
25
|
export function useTheme() {
|
|
26
26
|
const context = useContext(ThemeContext);
|
|
27
27
|
if (!context) {
|
|
28
|
-
|
|
28
|
+
// Return default light theme if used outside of a provider to prevent crashes
|
|
29
|
+
return {
|
|
30
|
+
mode: "light" as ThemeMode,
|
|
31
|
+
theme: defaultLightTheme,
|
|
32
|
+
setMode: () => {},
|
|
33
|
+
setCustomTheme: () => {},
|
|
34
|
+
};
|
|
29
35
|
}
|
|
30
36
|
return context;
|
|
31
37
|
}
|