@kyro-cms/admin 0.11.4 → 0.11.5
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/dist/index.cjs +19 -19
- package/dist/index.css +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +19 -19
- package/package.json +1 -1
- package/src/components/AutoForm.tsx +47 -27
- package/src/components/CreateView.tsx +1 -0
- package/src/components/DashboardMetrics.tsx +280 -0
- package/src/components/DetailView.tsx +22 -20
- package/src/components/MediaGallery.tsx +80 -123
- package/src/components/SessionsManager.tsx +5 -0
- package/src/components/Sidebar.astro +36 -17
- package/src/components/autoform/AutoFormHeader.tsx +5 -2
- package/src/components/fields/TabsLayout.tsx +1 -1
- package/src/components/ui/CommandPaletteWrapper.tsx +2 -3
- package/src/components/ui/ImageFocalEditor.tsx +299 -0
- package/src/components/ui/Modal.tsx +1 -1
- package/src/components/ui/PageHeader.tsx +5 -2
- package/src/integration.ts +32 -0
- package/src/layouts/AdminLayout.astro +1 -1
- package/src/lib/globals.ts +53 -9
- package/src/pages/index.astro +34 -113
- package/src/pages/media.astro +1 -1
- package/src/pages/settings/[slug].astro +19 -1
- package/src/styles/main.css +50 -38
|
@@ -5,7 +5,6 @@ import { Spinner } from "./ui/Spinner";
|
|
|
5
5
|
import { Shimmer } from "./ui/Shimmer";
|
|
6
6
|
import { SlidePanel } from "./ui/SlidePanel";
|
|
7
7
|
import { Modal } from "./ui/Modal";
|
|
8
|
-
import { Pagination } from "./ui/Pagination";
|
|
9
8
|
import { Badge } from "./ui/Badge";
|
|
10
9
|
import { Folder } from "./ui/icons";
|
|
11
10
|
|
|
@@ -26,13 +25,7 @@ import {
|
|
|
26
25
|
Archive,
|
|
27
26
|
} from "./ui/icons";
|
|
28
27
|
import { PromptModal } from "./ui/PromptModal";
|
|
29
|
-
import
|
|
30
|
-
type Crop,
|
|
31
|
-
centerCrop,
|
|
32
|
-
makeAspectCrop,
|
|
33
|
-
convertToPixelCrop,
|
|
34
|
-
} from "react-image-crop";
|
|
35
|
-
import "react-image-crop/dist/ReactCrop.css";
|
|
28
|
+
import { ImageFocalEditor } from "./ui/ImageFocalEditor";
|
|
36
29
|
import {
|
|
37
30
|
apiGet,
|
|
38
31
|
apiPost,
|
|
@@ -56,6 +49,11 @@ interface MediaItem {
|
|
|
56
49
|
folder?: string;
|
|
57
50
|
alt?: string;
|
|
58
51
|
caption?: string;
|
|
52
|
+
metadata?: {
|
|
53
|
+
crop?: { x: number; y: number; width: number; height: number };
|
|
54
|
+
hotspot?: { x: number; y: number; width: number; height: number };
|
|
55
|
+
[key: string]: any;
|
|
56
|
+
};
|
|
59
57
|
createdAt: string;
|
|
60
58
|
updatedAt?: string;
|
|
61
59
|
}
|
|
@@ -133,6 +131,7 @@ export function MediaGallery({
|
|
|
133
131
|
|
|
134
132
|
const [items, setItems] = useState<MediaItem[]>([]);
|
|
135
133
|
const [loading, setLoading] = useState(true);
|
|
134
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
136
135
|
const [folders, setFolders] = useState<string[]>([]);
|
|
137
136
|
const [currentFolder, setCurrentFolder] = useState<string>("");
|
|
138
137
|
const [search, setSearch] = useState("");
|
|
@@ -160,14 +159,14 @@ export function MediaGallery({
|
|
|
160
159
|
const [isDragging, setIsDragging] = useState(false);
|
|
161
160
|
|
|
162
161
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
163
|
-
const [crop, setCrop] = useState<Crop>();
|
|
164
|
-
const imgRef = useRef<HTMLImageElement>(null);
|
|
165
162
|
|
|
166
|
-
const loadMedia = useCallback(async () => {
|
|
167
|
-
setLoading(true);
|
|
163
|
+
const loadMedia = useCallback(async (pageNum: number) => {
|
|
164
|
+
if (pageNum === 1) setLoading(true);
|
|
165
|
+
else setLoadingMore(true);
|
|
166
|
+
|
|
168
167
|
try {
|
|
169
168
|
const params = new URLSearchParams({
|
|
170
|
-
page:
|
|
169
|
+
page: pageNum.toString(),
|
|
171
170
|
limit: limit.toString(),
|
|
172
171
|
});
|
|
173
172
|
if (currentFolder) params.append("folder", currentFolder);
|
|
@@ -175,18 +174,21 @@ export function MediaGallery({
|
|
|
175
174
|
if (filter !== "all") params.append("type", filter);
|
|
176
175
|
|
|
177
176
|
const result = await apiGet(withCacheBust(`/api/media?${params}`));
|
|
178
|
-
|
|
177
|
+
const newItems = (result.docs || []).map((doc: Record<string, unknown>) => ({
|
|
179
178
|
...doc,
|
|
180
|
-
type: getFileType(doc.mimeType),
|
|
181
|
-
}))
|
|
179
|
+
type: getFileType(doc.mimeType as string),
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
setItems(prev => pageNum === 1 ? newItems : [...prev, ...newItems]);
|
|
182
183
|
setTotal(result.totalDocs || 0);
|
|
183
184
|
setTotalPages(result.totalPages || 1);
|
|
184
185
|
} catch (error) {
|
|
185
186
|
console.error("Failed to load media:", error);
|
|
186
187
|
} finally {
|
|
187
188
|
setLoading(false);
|
|
189
|
+
setLoadingMore(false);
|
|
188
190
|
}
|
|
189
|
-
}, [
|
|
191
|
+
}, [currentFolder, search, filter, limit]);
|
|
190
192
|
|
|
191
193
|
const loadFolders = useCallback(async () => {
|
|
192
194
|
try {
|
|
@@ -220,8 +222,16 @@ export function MediaGallery({
|
|
|
220
222
|
}, [pickerMode, storageConfigured, storageChecked]);
|
|
221
223
|
|
|
222
224
|
useEffect(() => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
+
setPage(1);
|
|
226
|
+
loadMedia(1);
|
|
227
|
+
}, [currentFolder, search, filter, loadMedia]);
|
|
228
|
+
|
|
229
|
+
const handleLoadMore = () => {
|
|
230
|
+
if (loadingMore || page >= totalPages) return;
|
|
231
|
+
const nextPage = page + 1;
|
|
232
|
+
setPage(nextPage);
|
|
233
|
+
loadMedia(nextPage);
|
|
234
|
+
};
|
|
225
235
|
|
|
226
236
|
useEffect(() => {
|
|
227
237
|
loadFolders();
|
|
@@ -271,7 +281,8 @@ export function MediaGallery({
|
|
|
271
281
|
|
|
272
282
|
setUploading(false);
|
|
273
283
|
setUploadProgress({});
|
|
274
|
-
|
|
284
|
+
setPage(1);
|
|
285
|
+
loadMedia(1);
|
|
275
286
|
loadFolders();
|
|
276
287
|
if (failCount > 0) {
|
|
277
288
|
toast.error(`${failCount} file(s) failed to upload`);
|
|
@@ -292,7 +303,8 @@ export function MediaGallery({
|
|
|
292
303
|
await apiDelete(`/api/media/${id}`);
|
|
293
304
|
}
|
|
294
305
|
setSelectedIds(new Set());
|
|
295
|
-
|
|
306
|
+
setPage(1);
|
|
307
|
+
loadMedia(1);
|
|
296
308
|
toast.success(`${selectedIds.size} item(s) deleted`);
|
|
297
309
|
} catch (error) {
|
|
298
310
|
console.error("Bulk delete failed:", error);
|
|
@@ -337,7 +349,8 @@ export function MediaGallery({
|
|
|
337
349
|
await apiDelete(`/api/media/folders?path=${encodeURIComponent(folder)}`);
|
|
338
350
|
if (currentFolder === folder) setCurrentFolder("");
|
|
339
351
|
loadFolders();
|
|
340
|
-
|
|
352
|
+
setPage(1);
|
|
353
|
+
loadMedia(1);
|
|
341
354
|
toast.success(`Folder "${folder}" deleted`);
|
|
342
355
|
} catch (error) {
|
|
343
356
|
console.error("Failed to delete folder:", error);
|
|
@@ -361,66 +374,24 @@ export function MediaGallery({
|
|
|
361
374
|
}
|
|
362
375
|
};
|
|
363
376
|
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
const initialCrop = centerCrop(
|
|
367
|
-
makeAspectCrop({ unit: "%", width: 90 }, 1, width, height),
|
|
368
|
-
width,
|
|
369
|
-
height,
|
|
370
|
-
);
|
|
371
|
-
setCrop(initialCrop);
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
const onCropComplete = async () => {
|
|
375
|
-
if (!crop || !imgRef.current || !panelItem) return;
|
|
376
|
-
|
|
377
|
-
setUploading(true);
|
|
377
|
+
const handleSaveCropHotspot = async (cropData: any, hotspotData: any) => {
|
|
378
|
+
if (!panelItem) return;
|
|
378
379
|
try {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const ctx = canvas.getContext("2d");
|
|
392
|
-
|
|
393
|
-
if (ctx) {
|
|
394
|
-
ctx.drawImage(
|
|
395
|
-
imgRef.current,
|
|
396
|
-
pixelCrop.x * scaleX,
|
|
397
|
-
pixelCrop.y * scaleY,
|
|
398
|
-
pixelCrop.width * scaleX,
|
|
399
|
-
pixelCrop.height * scaleY,
|
|
400
|
-
0,
|
|
401
|
-
0,
|
|
402
|
-
pixelCrop.width,
|
|
403
|
-
pixelCrop.height,
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
const blob = await new Promise<Blob | null>((resolve) =>
|
|
407
|
-
canvas.toBlob(resolve, panelItem.mimeType),
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
if (blob) {
|
|
411
|
-
const formData = new FormData();
|
|
412
|
-
formData.append("file", blob, `cropped-${panelItem.filename}`);
|
|
413
|
-
if (currentFolder) formData.append("folder", currentFolder);
|
|
414
|
-
|
|
415
|
-
await apiUpload("/api/media", formData);
|
|
416
|
-
loadMedia();
|
|
417
|
-
setShowCrop(false);
|
|
418
|
-
toast.success("Cropped image saved");
|
|
419
|
-
}
|
|
420
|
-
}
|
|
380
|
+
setUploading(true);
|
|
381
|
+
const metadata = {
|
|
382
|
+
...panelItem.metadata,
|
|
383
|
+
crop: cropData,
|
|
384
|
+
hotspot: hotspotData
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const result = await apiPatch(`/api/media/${panelItem.id}`, { metadata });
|
|
388
|
+
setItems(prev => prev.map(item => item.id === panelItem.id ? result.doc : item));
|
|
389
|
+
setPanelItem(result.doc);
|
|
390
|
+
setShowCrop(false);
|
|
391
|
+
toast.success("Focal metadata saved");
|
|
421
392
|
} catch (err) {
|
|
422
|
-
console.error("
|
|
423
|
-
toast.error("
|
|
393
|
+
console.error("Save failed:", err);
|
|
394
|
+
toast.error("Failed to save focal metadata");
|
|
424
395
|
} finally {
|
|
425
396
|
setUploading(false);
|
|
426
397
|
}
|
|
@@ -598,7 +569,7 @@ export function MediaGallery({
|
|
|
598
569
|
)}
|
|
599
570
|
|
|
600
571
|
{/* Main Content Area */}
|
|
601
|
-
<div className="flex-1 flex flex-col min-h-0 bg-[var(--kyro-bg)]">
|
|
572
|
+
<div className="flex-1 flex flex-col min-h-0 min-w-0 bg-[var(--kyro-bg)]">
|
|
602
573
|
<div className={`flex-1 overflow-y-auto custom-scrollbar ${pickerMode ? "px-2 py-4" : "py-4 px-2 md:py-8 md:px-4"}`}>
|
|
603
574
|
{loading ? (
|
|
604
575
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
|
@@ -782,13 +753,26 @@ export function MediaGallery({
|
|
|
782
753
|
</table>
|
|
783
754
|
</div>
|
|
784
755
|
)}
|
|
785
|
-
</div>
|
|
786
756
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
757
|
+
{/* Load More Button */}
|
|
758
|
+
{!loading && page < totalPages && (
|
|
759
|
+
<div className="flex justify-center mt-8 pb-8">
|
|
760
|
+
<button
|
|
761
|
+
onClick={handleLoadMore}
|
|
762
|
+
disabled={loadingMore}
|
|
763
|
+
className="px-8 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-xs hover:scale-105 active:scale-95 transition-all shadow-xl disabled:opacity-50 flex items-center gap-2"
|
|
764
|
+
>
|
|
765
|
+
{loadingMore ? (
|
|
766
|
+
<>
|
|
767
|
+
<Spinner size="sm" /> Loading...
|
|
768
|
+
</>
|
|
769
|
+
) : (
|
|
770
|
+
"Load More"
|
|
771
|
+
)}
|
|
772
|
+
</button>
|
|
773
|
+
</div>
|
|
774
|
+
)}
|
|
775
|
+
</div>
|
|
792
776
|
</div>
|
|
793
777
|
</div>
|
|
794
778
|
|
|
@@ -1191,7 +1175,7 @@ export function MediaGallery({
|
|
|
1191
1175
|
</Modal>
|
|
1192
1176
|
)}
|
|
1193
1177
|
|
|
1194
|
-
{/*
|
|
1178
|
+
{/* Focal Editor Modal */}
|
|
1195
1179
|
{!pickerMode && showCrop && panelItem && (
|
|
1196
1180
|
<Modal
|
|
1197
1181
|
open={showCrop}
|
|
@@ -1200,41 +1184,14 @@ export function MediaGallery({
|
|
|
1200
1184
|
size="full"
|
|
1201
1185
|
variant="lightbox"
|
|
1202
1186
|
>
|
|
1203
|
-
<
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
onClick={() => setShowCrop(false)}
|
|
1212
|
-
className="px-4 py-2 border border-white/20 text-white/80 hover:bg-white/10 rounded-lg font-bold text-sm transition-colors"
|
|
1213
|
-
>
|
|
1214
|
-
Cancel
|
|
1215
|
-
</button>
|
|
1216
|
-
<button
|
|
1217
|
-
type="button"
|
|
1218
|
-
disabled={uploading}
|
|
1219
|
-
onClick={onCropComplete}
|
|
1220
|
-
className="px-4 py-2 bg-[var(--kyro-sidebar-active)] hover:opacity-90 text-[var(--kyro-sidebar-text-active)] rounded-lg font-bold text-sm transition-colors"
|
|
1221
|
-
>
|
|
1222
|
-
{uploading ? "Saving..." : "Save Crop"}
|
|
1223
|
-
</button>
|
|
1224
|
-
</div>
|
|
1225
|
-
</div>
|
|
1226
|
-
<div className="flex-1 w-full flex items-center justify-center overflow-auto">
|
|
1227
|
-
<ReactCrop crop={crop} onChange={(c) => setCrop(c)}>
|
|
1228
|
-
<img
|
|
1229
|
-
ref={imgRef}
|
|
1230
|
-
src={panelItem.url}
|
|
1231
|
-
alt="Crop preview"
|
|
1232
|
-
className="max-h-[70vh] object-contain"
|
|
1233
|
-
onLoad={onImageLoad}
|
|
1234
|
-
/>
|
|
1235
|
-
</ReactCrop>
|
|
1236
|
-
</div>
|
|
1237
|
-
</div>
|
|
1187
|
+
<ImageFocalEditor
|
|
1188
|
+
url={getAbsoluteUrl(panelItem.url)}
|
|
1189
|
+
initialCrop={panelItem.metadata?.crop}
|
|
1190
|
+
initialHotspot={panelItem.metadata?.hotspot}
|
|
1191
|
+
onSave={handleSaveCropHotspot}
|
|
1192
|
+
onCancel={() => setShowCrop(false)}
|
|
1193
|
+
isSaving={uploading}
|
|
1194
|
+
/>
|
|
1238
1195
|
</Modal>
|
|
1239
1196
|
)}
|
|
1240
1197
|
{!pickerMode && (
|
|
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
|
|
|
2
2
|
import { apiGet, apiDelete } from "../lib/api";
|
|
3
3
|
import { Shield, Monitor, Trash2, Clock, AlertTriangle, Info, LogOut, Globe, Activity, RefreshCcw, Smartphone, Laptop } from "./ui/icons";
|
|
4
4
|
import { PageHeader } from "./ui/PageHeader";
|
|
5
|
+
import toast from "react-hot-toast";
|
|
5
6
|
import { Badge } from "./ui/Badge";
|
|
6
7
|
|
|
7
8
|
interface Session {
|
|
@@ -53,8 +54,10 @@ export function SessionsManager() {
|
|
|
53
54
|
try {
|
|
54
55
|
await apiDelete(`/api/auth/sessions/${id}`);
|
|
55
56
|
setSessions((p) => p.filter((s) => s.id !== id));
|
|
57
|
+
toast.success("Session revoked");
|
|
56
58
|
} catch {
|
|
57
59
|
setError("Failed to revoke session");
|
|
60
|
+
toast.error("Failed to revoke session");
|
|
58
61
|
} finally {
|
|
59
62
|
setRevokingId(null);
|
|
60
63
|
}
|
|
@@ -65,8 +68,10 @@ export function SessionsManager() {
|
|
|
65
68
|
try {
|
|
66
69
|
await apiDelete("/api/auth/sessions");
|
|
67
70
|
setSessions((p) => p.filter((s) => s.currentSession));
|
|
71
|
+
toast.success("All other sessions revoked");
|
|
68
72
|
} catch {
|
|
69
73
|
setError("Failed to revoke sessions");
|
|
74
|
+
toast.error("Failed to revoke sessions");
|
|
70
75
|
} finally {
|
|
71
76
|
setRevokingAll(false);
|
|
72
77
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import "../styles/main.css";
|
|
3
3
|
import { nonAuthCollections } from "../lib/config";
|
|
4
4
|
import { adminPath } from "../lib/paths";
|
|
5
|
-
import { getSiteSettings } from "../lib/globals";
|
|
5
|
+
import { getSiteSettings, getBrandSettings } from "../lib/globals";
|
|
6
6
|
import * as Icons from "lucide-react";
|
|
7
7
|
import { UserMenu } from "./UserMenu";
|
|
8
8
|
|
|
@@ -13,11 +13,14 @@ interface NavItem {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const siteSettings = await getSiteSettings({ request: Astro.request });
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
16
|
+
const brandSettings = await getBrandSettings({ request: Astro.request });
|
|
17
|
+
|
|
18
|
+
const siteName = siteSettings?.siteName || brandSettings?.companyInfo?.companyName || "KYRO.";
|
|
19
|
+
const siteLogo = brandSettings?.identity?.primaryLogo;
|
|
20
|
+
const darkLogo = brandSettings?.identity?.darkLogo;
|
|
21
|
+
const logoWidth = siteLogo?.width || darkLogo?.width;
|
|
22
|
+
const logoHeight = siteLogo?.height || darkLogo?.height;
|
|
23
|
+
const logoAlt = siteLogo?.altText || darkLogo?.altText || siteName;
|
|
21
24
|
|
|
22
25
|
interface Props {
|
|
23
26
|
title: string;
|
|
@@ -97,17 +100,33 @@ function isActive(item: NavItem): boolean {
|
|
|
97
100
|
>
|
|
98
101
|
<div class="px-6 md:px-8 py-6 md:py-8 flex items-center justify-between gap-3">
|
|
99
102
|
{
|
|
100
|
-
siteLogo ? (
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
siteLogo || darkLogo ? (
|
|
104
|
+
<div class="flex items-center">
|
|
105
|
+
{siteLogo && (
|
|
106
|
+
<img
|
|
107
|
+
src={siteLogo.url}
|
|
108
|
+
alt={logoAlt}
|
|
109
|
+
style={{
|
|
110
|
+
width: logoWidth ? `${logoWidth}px` : "auto",
|
|
111
|
+
height: logoHeight ? `${logoHeight}px` : "32px",
|
|
112
|
+
objectFit: "contain",
|
|
113
|
+
}}
|
|
114
|
+
class={`rounded-lg ${darkLogo ? "block dark:hidden" : ""}`}
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
117
|
+
{darkLogo && (
|
|
118
|
+
<img
|
|
119
|
+
src={darkLogo.url}
|
|
120
|
+
alt={logoAlt}
|
|
121
|
+
style={{
|
|
122
|
+
width: logoWidth ? `${logoWidth}px` : "auto",
|
|
123
|
+
height: logoHeight ? `${logoHeight}px` : "32px",
|
|
124
|
+
objectFit: "contain",
|
|
125
|
+
}}
|
|
126
|
+
class={`rounded-lg ${siteLogo ? "hidden dark:block" : ""}`}
|
|
127
|
+
/>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
111
130
|
) : (
|
|
112
131
|
<span class="text-2xl font-black tracking-tighter text-[var(--kyro-text-primary)] ">
|
|
113
132
|
{siteName}
|
|
@@ -14,6 +14,7 @@ interface AutoFormHeaderProps {
|
|
|
14
14
|
documentStatus: string;
|
|
15
15
|
hasUnpublishedChanges: boolean;
|
|
16
16
|
localSaveStatus: "idle" | "saving" | "saved" | "error";
|
|
17
|
+
isDuplicating?: boolean;
|
|
17
18
|
handleCreateNew: () => void;
|
|
18
19
|
handleDuplicate: () => void;
|
|
19
20
|
handleUnpublish: () => void;
|
|
@@ -28,6 +29,7 @@ export function AutoFormHeader({
|
|
|
28
29
|
documentStatus,
|
|
29
30
|
hasUnpublishedChanges,
|
|
30
31
|
localSaveStatus,
|
|
32
|
+
isDuplicating,
|
|
31
33
|
handleCreateNew,
|
|
32
34
|
handleDuplicate,
|
|
33
35
|
handleUnpublish,
|
|
@@ -221,6 +223,7 @@ export function AutoFormHeader({
|
|
|
221
223
|
{!globalSlug && (
|
|
222
224
|
<DropdownItem
|
|
223
225
|
onClick={handleDuplicate}
|
|
226
|
+
disabled={isDuplicating}
|
|
224
227
|
icon={
|
|
225
228
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
226
229
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
@@ -228,7 +231,7 @@ export function AutoFormHeader({
|
|
|
228
231
|
</svg>
|
|
229
232
|
}
|
|
230
233
|
>
|
|
231
|
-
Duplicate
|
|
234
|
+
{isDuplicating ? "Duplicating..." : "Duplicate"}
|
|
232
235
|
</DropdownItem>
|
|
233
236
|
)}
|
|
234
237
|
<DropdownItem
|
|
@@ -388,7 +391,7 @@ export function AutoFormHeader({
|
|
|
388
391
|
</header>
|
|
389
392
|
|
|
390
393
|
{/* DESKTOP HEADER */}
|
|
391
|
-
<header className="hidden md:flex surface-tile px-8 py-6 items-center justify-between sticky top-0 border-b border-[var(--kyro-border)] mb-8 bg-[var(--kyro-surface)] backdrop-blur-md">
|
|
394
|
+
<header className="hidden md:flex surface-tile px-8 py-6 items-center justify-between sticky top-0 border-b border-[var(--kyro-border)] mb-8 bg-[var(--kyro-surface)] z-50 backdrop-blur-md">
|
|
392
395
|
<div className="flex flex-col gap-2 min-w-0">
|
|
393
396
|
<div className="flex items-center gap-3 flex-wrap min-w-0">
|
|
394
397
|
<a
|
|
@@ -52,7 +52,7 @@ export function TabsLayout({
|
|
|
52
52
|
)}
|
|
53
53
|
</div>
|
|
54
54
|
|
|
55
|
-
{currentTab?.label === "SEO" && (
|
|
55
|
+
{currentTab?.label === "SEO Settings" && (
|
|
56
56
|
<div className="mt-12 pt-8 border-t border-[var(--kyro-border)]">
|
|
57
57
|
<h4 className="text-[10px] font-bold text-[var(--kyro-text-secondary)] tracking-[0.2em] mb-6 opacity-50">
|
|
58
58
|
Live Google Preview
|
|
@@ -33,9 +33,8 @@ export function CommandPaletteWrapper({ collections, globals }: Props) {
|
|
|
33
33
|
const handleClose = () => setIsOpen(false);
|
|
34
34
|
|
|
35
35
|
const handleLogoutConfirm = () => {
|
|
36
|
-
|
|
37
|
-
(
|
|
38
|
-
window.location.href = "/login";
|
|
36
|
+
document.getElementById("logout-btn")?.click();
|
|
37
|
+
setShowLogoutConfirm(false);
|
|
39
38
|
};
|
|
40
39
|
|
|
41
40
|
const handleNavigate = (view: string, collection?: string, id?: string) => {
|