@kyro-cms/admin 0.1.6 → 0.1.8
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/README.md +149 -51
- package/package.json +54 -5
- package/src/collections/auth/index.ts +2 -2
- package/src/collections/portfolio/index.ts +343 -0
- package/src/components/ActionBar.tsx +153 -16
- package/src/components/Admin.tsx +137 -28
- package/src/components/ApiExplorer.tsx +325 -0
- package/src/components/ApiKeysManager.tsx +563 -0
- package/src/components/AuditLogsPage.tsx +664 -0
- package/src/components/AutoForm.tsx +2155 -770
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +4 -4
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +200 -58
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +890 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +192 -54
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +206 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- package/src/components/ThemeProvider.tsx +8 -2
- package/src/components/UserManagement.tsx +204 -0
- package/src/components/VersionHistoryPanel.tsx +3 -3
- package/src/components/WebhookManager.tsx +608 -0
- package/src/components/blocks/AccordionBlock.tsx +65 -0
- package/src/components/blocks/ArrayBlock.tsx +84 -0
- package/src/components/blocks/BlockEditModal.tsx +363 -0
- package/src/components/blocks/ButtonBlock.tsx +64 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +114 -0
- package/src/components/blocks/ColumnsBlock.tsx +93 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +63 -0
- package/src/components/blocks/HeadingBlock.tsx +59 -0
- package/src/components/blocks/HeroBlock.tsx +99 -0
- package/src/components/blocks/ImageBlock.tsx +82 -0
- package/src/components/blocks/LinkBlock.tsx +65 -0
- package/src/components/blocks/ListBlock.tsx +60 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +72 -0
- package/src/components/blocks/RichTextBlock.tsx +66 -0
- package/src/components/blocks/VStackBlock.tsx +61 -0
- package/src/components/blocks/VideoBlock.tsx +65 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/AccordionField.tsx +213 -0
- package/src/components/fields/ArrayField.tsx +241 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/ButtonField.tsx +53 -0
- package/src/components/fields/CheckboxField.tsx +18 -8
- package/src/components/fields/ChildrenField.tsx +48 -0
- package/src/components/fields/CodeField.tsx +294 -0
- package/src/components/fields/ColumnsField.tsx +137 -0
- package/src/components/fields/DateField.tsx +24 -12
- package/src/components/fields/EditorClient.tsx +537 -0
- package/src/components/fields/HeadingField.tsx +31 -0
- package/src/components/fields/HeroField.tsx +101 -0
- package/src/components/fields/JSONField.tsx +341 -0
- package/src/components/fields/LinkField.tsx +81 -0
- package/src/components/fields/ListField.tsx +74 -0
- package/src/components/fields/MarkdownField.tsx +260 -0
- package/src/components/fields/NumberField.tsx +25 -13
- package/src/components/fields/PortableTextField.tsx +155 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipBlockField.tsx +233 -0
- package/src/components/fields/RelationshipField.tsx +278 -60
- package/src/components/fields/SelectField.tsx +28 -16
- package/src/components/fields/TextField.tsx +31 -15
- package/src/components/fields/UploadField.tsx +613 -0
- package/src/components/fields/VideoField.tsx +73 -0
- package/src/components/fields/extensions/blockComponents.tsx +247 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +24 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +3 -3
- package/src/components/ui/Badge.tsx +9 -4
- package/src/components/ui/BlockDrawer.tsx +79 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +362 -0
- package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
- package/src/components/ui/Dropdown.tsx +1 -1
- package/src/components/ui/Modal.tsx +37 -12
- package/src/components/ui/PromptModal.tsx +94 -0
- package/src/components/ui/SlidePanel.tsx +43 -16
- package/src/components/ui/Toast.tsx +80 -14
- package/src/env.d.ts +16 -0
- package/src/env.ts +20 -0
- package/src/index.ts +0 -1
- package/src/layouts/AdminLayout.astro +164 -170
- package/src/layouts/AuthLayout.astro +23 -6
- package/src/lib/MediaService.ts +541 -0
- package/src/lib/api.ts +163 -0
- package/src/lib/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +23 -7
- package/src/lib/dataStore.ts +188 -73
- package/src/lib/date-utils.ts +69 -0
- package/src/lib/db/adapter.ts +54 -0
- package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
- package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
- package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
- package/src/lib/db/index.ts +449 -0
- package/src/lib/db/mongodb-adapter.ts +207 -0
- package/src/lib/db/mongodb-auth-adapter.ts +305 -0
- package/src/lib/db/schema/mysql-auth.ts +113 -0
- package/src/lib/db/schema/mysql-content.ts +20 -0
- package/src/lib/db/schema/postgres-auth.ts +116 -0
- package/src/lib/db/schema/postgres-content.ts +35 -0
- package/src/lib/db/schema/postgres-media.ts +52 -0
- package/src/lib/db/schema/postgres-settings.ts +11 -0
- package/src/lib/db/schema/sqlite-auth.ts +112 -0
- package/src/lib/db/schema/sqlite-content.ts +20 -0
- package/src/lib/db/version-adapter.ts +248 -0
- package/src/lib/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/i18n.tsx +353 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/slugify.ts +15 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -0
- package/src/lib/validation.ts +250 -0
- package/src/middleware.ts +70 -11
- package/src/pages/[collection]/[id].astro +178 -122
- package/src/pages/[collection]/index.astro +24 -156
- package/src/pages/admin/api-explorer.astro +98 -0
- package/src/pages/admin/graphql-explorer.astro +40 -0
- package/src/pages/admin/graphql.astro +97 -0
- package/src/pages/admin/index.astro +200 -139
- package/src/pages/admin/keys.astro +8 -0
- package/src/pages/admin/rest-playground.astro +44 -0
- package/src/pages/admin/webhooks.astro +8 -0
- package/src/pages/api/[collection]/[id]/publish.ts +52 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +66 -0
- package/src/pages/api/[collection]/[id].ts +114 -159
- package/src/pages/api/[collection]/index.ts +150 -230
- package/src/pages/api/auth/[id].ts +48 -69
- package/src/pages/api/auth/audit-logs.ts +20 -43
- package/src/pages/api/auth/login.ts +159 -45
- package/src/pages/api/auth/logout.ts +42 -24
- package/src/pages/api/auth/refresh.ts +119 -0
- package/src/pages/api/auth/register.ts +110 -40
- package/src/pages/api/auth/users.ts +22 -97
- package/src/pages/api/collections.ts +59 -0
- package/src/pages/api/globals/[slug]/test.ts +172 -0
- package/src/pages/api/globals/[slug].ts +42 -0
- package/src/pages/api/graphql.ts +90 -0
- package/src/pages/api/health.ts +417 -40
- package/src/pages/api/keys/[id].ts +26 -0
- package/src/pages/api/keys/index.ts +75 -0
- package/src/pages/api/media/[id].ts +309 -0
- package/src/pages/api/media/folders.ts +609 -0
- package/src/pages/api/media/index.ts +146 -0
- package/src/pages/api/media/resize.ts +267 -0
- package/src/pages/api/search.ts +82 -0
- package/src/pages/api/slug-availability.ts +70 -0
- package/src/pages/api/storage-config.ts +20 -0
- package/src/pages/api/storage-status.ts +206 -0
- package/src/pages/api/upload.ts +334 -0
- package/src/pages/api/webhooks/index.ts +71 -0
- package/src/pages/audit/index.astro +2 -104
- package/src/pages/login.astro +11 -11
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +13 -13
- package/src/pages/roles/index.astro +21 -21
- package/src/pages/settings/[slug].astro +162 -0
- package/src/pages/settings/index.astro +9 -0
- package/src/pages/users/[id].astro +29 -21
- package/src/pages/users/index.astro +22 -17
- package/src/pages/users/new.astro +18 -17
- package/src/styles/main.css +563 -128
- package/src/components/layout/Sidebar.tsx +0 -497
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import { useDraggable } from "@dnd-kit/core";
|
|
3
|
+
import { SlidePanel } from "./SlidePanel";
|
|
4
|
+
|
|
5
|
+
interface BlockDrawerProps {
|
|
6
|
+
open: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
onSelect: (blockType: string) => void;
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function BlockDrawer({
|
|
13
|
+
open,
|
|
14
|
+
onClose,
|
|
15
|
+
onSelect,
|
|
16
|
+
children,
|
|
17
|
+
}: BlockDrawerProps) {
|
|
18
|
+
if (!open) return null;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<SlidePanel open={open} onClose={onClose} title="Insert Block" width="md">
|
|
22
|
+
<p className="text-sm text-[var(--kyro-text-muted)] mb-4">
|
|
23
|
+
Drag blocks into the editor or click to insert
|
|
24
|
+
</p>
|
|
25
|
+
{children}
|
|
26
|
+
</SlidePanel>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Draggable wrapper for block types in the drawer
|
|
31
|
+
export function DraggableBlockType({
|
|
32
|
+
block,
|
|
33
|
+
onSelect,
|
|
34
|
+
children,
|
|
35
|
+
}: {
|
|
36
|
+
block: { type: string; label: string; icon: any; description: string };
|
|
37
|
+
onSelect: (type: string) => void;
|
|
38
|
+
children?: ReactNode;
|
|
39
|
+
}) {
|
|
40
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
41
|
+
id: `drawer-${block.type}`,
|
|
42
|
+
data: { source: "drawer", blockType: block.type },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
ref={setNodeRef}
|
|
48
|
+
{...listeners}
|
|
49
|
+
{...attributes}
|
|
50
|
+
onClick={() => onSelect(block.type)}
|
|
51
|
+
className={`flex flex-col items-center text-center gap-1 p-2 rounded-md border border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/60 hover:bg-[var(--kyro-surface-accent)]/30 transition-all cursor-pointer group ${
|
|
52
|
+
isDragging ? "opacity-50 border-[var(--kyro-primary)]" : ""
|
|
53
|
+
}`}
|
|
54
|
+
style={{ opacity: isDragging ? 0.5 : 1 }}
|
|
55
|
+
>
|
|
56
|
+
<div className="w-6 h-6 flex items-center justify-center rounded group-hover:bg-[var(--kyro-primary)]/10 group-hover:text-[var(--kyro-primary)] transition-all duration-300">
|
|
57
|
+
{children || (
|
|
58
|
+
<span className="text-[var(--kyro-text-muted)]">
|
|
59
|
+
{/* Default icon */}
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
<div className="flex-1 min-w-0">
|
|
64
|
+
<div className="text-xs font-medium uppercase tracking-tight text-[var(--kyro-text-primary)] leading-tight">
|
|
65
|
+
{block.label}
|
|
66
|
+
</div>
|
|
67
|
+
<div className="text-[10px] text-[var(--kyro-text-muted)] mt-0.5 leading-tight">
|
|
68
|
+
{block.description}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface BlockType {
|
|
76
|
+
type: string;
|
|
77
|
+
label: string;
|
|
78
|
+
icon: string;
|
|
79
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Search,
|
|
4
|
+
FileText,
|
|
5
|
+
Image as ImageIcon,
|
|
6
|
+
Settings,
|
|
7
|
+
Plus,
|
|
8
|
+
ArrowRight,
|
|
9
|
+
Clock,
|
|
10
|
+
Loader2,
|
|
11
|
+
File,
|
|
12
|
+
Moon,
|
|
13
|
+
Sun,
|
|
14
|
+
LogOut,
|
|
15
|
+
Shield,
|
|
16
|
+
Code,
|
|
17
|
+
Database,
|
|
18
|
+
Network,
|
|
19
|
+
Hexagon,
|
|
20
|
+
} from "lucide-react";
|
|
21
|
+
|
|
22
|
+
interface SearchResult {
|
|
23
|
+
collection: string;
|
|
24
|
+
label: string;
|
|
25
|
+
id: string;
|
|
26
|
+
title: string;
|
|
27
|
+
doc?: any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CommandPaletteProps {
|
|
31
|
+
isOpen: boolean;
|
|
32
|
+
onClose: () => void;
|
|
33
|
+
collections: any;
|
|
34
|
+
globals: any;
|
|
35
|
+
onNavigate: (view: any, collection?: string, id?: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function CommandPalette({
|
|
39
|
+
isOpen,
|
|
40
|
+
onClose,
|
|
41
|
+
collections,
|
|
42
|
+
globals,
|
|
43
|
+
onNavigate,
|
|
44
|
+
}: CommandPaletteProps) {
|
|
45
|
+
const [query, setQuery] = useState("");
|
|
46
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
47
|
+
const [loading, setLoading] = useState(false);
|
|
48
|
+
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
|
49
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
50
|
+
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (isOpen) {
|
|
54
|
+
setQuery("");
|
|
55
|
+
setSelectedIndex(0);
|
|
56
|
+
setSearchResults([]);
|
|
57
|
+
setLoading(false);
|
|
58
|
+
setTimeout(() => inputRef.current?.focus(), 100);
|
|
59
|
+
}
|
|
60
|
+
}, [isOpen]);
|
|
61
|
+
|
|
62
|
+
const performSearch = useCallback(async (searchQuery: string) => {
|
|
63
|
+
if (!searchQuery || searchQuery.length < 2) {
|
|
64
|
+
setSearchResults([]);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setLoading(true);
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(
|
|
71
|
+
`/api/search?q=${encodeURIComponent(searchQuery)}&limit=15`,
|
|
72
|
+
);
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
if (data.results) {
|
|
75
|
+
setSearchResults(data.results);
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error("Search error:", err);
|
|
79
|
+
setSearchResults([]);
|
|
80
|
+
} finally {
|
|
81
|
+
setLoading(false);
|
|
82
|
+
}
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (debounceRef.current) {
|
|
87
|
+
clearTimeout(debounceRef.current);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (query.length >= 2) {
|
|
91
|
+
debounceRef.current = setTimeout(() => performSearch(query), 300);
|
|
92
|
+
} else {
|
|
93
|
+
setSearchResults([]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
if (debounceRef.current) {
|
|
98
|
+
clearTimeout(debounceRef.current);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}, [query, performSearch]);
|
|
102
|
+
|
|
103
|
+
if (!isOpen) return null;
|
|
104
|
+
|
|
105
|
+
const collectionItems = Object.entries(collections).map(
|
|
106
|
+
([slug, config]: [string, any]) => ({
|
|
107
|
+
id: `col-${slug}`,
|
|
108
|
+
label: config.label || slug,
|
|
109
|
+
type: "collection",
|
|
110
|
+
slug,
|
|
111
|
+
icon: FileText,
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const globalItems = Object.entries(globals).map(
|
|
116
|
+
([slug, config]: [string, any]) => ({
|
|
117
|
+
id: `global-${slug}`,
|
|
118
|
+
label: config.label || slug,
|
|
119
|
+
type: "global",
|
|
120
|
+
slug,
|
|
121
|
+
icon: Settings,
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const isDark =
|
|
126
|
+
typeof document !== "undefined" &&
|
|
127
|
+
document.documentElement.classList.contains("dark");
|
|
128
|
+
|
|
129
|
+
const actionItems = [
|
|
130
|
+
{
|
|
131
|
+
id: "action-media",
|
|
132
|
+
label: "Media Gallery",
|
|
133
|
+
type: "action",
|
|
134
|
+
view: "media",
|
|
135
|
+
icon: ImageIcon,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "action-users",
|
|
139
|
+
label: "Team Management",
|
|
140
|
+
type: "action",
|
|
141
|
+
view: "users",
|
|
142
|
+
icon: Clock,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "action-audit",
|
|
146
|
+
label: "Audit Logs",
|
|
147
|
+
type: "action",
|
|
148
|
+
view: "audit",
|
|
149
|
+
icon: File,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "action-roles",
|
|
153
|
+
label: "Roles & Permissions",
|
|
154
|
+
type: "action",
|
|
155
|
+
view: "roles",
|
|
156
|
+
icon: Shield,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "action-api",
|
|
160
|
+
label: "REST API Explorer",
|
|
161
|
+
type: "action",
|
|
162
|
+
view: "api-explorer",
|
|
163
|
+
icon: Database,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "action-graphql",
|
|
167
|
+
label: "GraphQL Playground",
|
|
168
|
+
type: "action",
|
|
169
|
+
view: "graphql",
|
|
170
|
+
icon: Hexagon,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: "action-rest",
|
|
174
|
+
label: "REST Playground",
|
|
175
|
+
type: "action",
|
|
176
|
+
view: "rest",
|
|
177
|
+
icon: Network,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "action-theme",
|
|
181
|
+
label: isDark ? "Switch to Light Mode" : "Switch to Dark Mode",
|
|
182
|
+
type: "action",
|
|
183
|
+
view: "theme",
|
|
184
|
+
icon: isDark ? Sun : Moon,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "action-logout",
|
|
188
|
+
label: "Sign Out",
|
|
189
|
+
type: "action",
|
|
190
|
+
view: "logout",
|
|
191
|
+
icon: LogOut,
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const docResultItems: any[] = searchResults.map((result, idx) => ({
|
|
196
|
+
id: `doc-${result.collection}-${result.id}`,
|
|
197
|
+
label: result.title,
|
|
198
|
+
type: "document",
|
|
199
|
+
collection: result.collection,
|
|
200
|
+
label2: result.label,
|
|
201
|
+
docId: result.id,
|
|
202
|
+
icon: File,
|
|
203
|
+
doc: result.doc,
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
const allItems =
|
|
207
|
+
query.length >= 2
|
|
208
|
+
? [...actionItems, ...collectionItems, ...globalItems, ...docResultItems]
|
|
209
|
+
: [...actionItems, ...collectionItems, ...globalItems];
|
|
210
|
+
|
|
211
|
+
const filteredItems =
|
|
212
|
+
query === ""
|
|
213
|
+
? allItems
|
|
214
|
+
: allItems.filter((item) =>
|
|
215
|
+
item.label.toLowerCase().includes(query.toLowerCase()),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
219
|
+
if (e.key === "ArrowDown") {
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
setSelectedIndex((i) => (i + 1) % filteredItems.length);
|
|
222
|
+
} else if (e.key === "ArrowUp") {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
setSelectedIndex(
|
|
225
|
+
(i) => (i - 1 + filteredItems.length) % filteredItems.length,
|
|
226
|
+
);
|
|
227
|
+
} else if (e.key === "Enter") {
|
|
228
|
+
const item = filteredItems[selectedIndex];
|
|
229
|
+
if (item) handleSelect(item);
|
|
230
|
+
} else if (e.key === "Escape") {
|
|
231
|
+
onClose();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const handleSelect = (item: any) => {
|
|
236
|
+
if (item.type === "collection") {
|
|
237
|
+
onNavigate("list", item.slug);
|
|
238
|
+
} else if (item.type === "global") {
|
|
239
|
+
onNavigate("settings", item.slug);
|
|
240
|
+
} else if (item.type === "document") {
|
|
241
|
+
onNavigate("edit", item.collection, item.docId);
|
|
242
|
+
} else if (item.type === "action") {
|
|
243
|
+
if (item.view === "users") {
|
|
244
|
+
onNavigate(item.view, item.view);
|
|
245
|
+
} else if (item.view === "media") {
|
|
246
|
+
onNavigate("media", item.view);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
onClose();
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const getSectionLabel = () => {
|
|
253
|
+
if (query === "") return "Quick Actions & Collections";
|
|
254
|
+
if (searchResults.length > 0) return "Documents";
|
|
255
|
+
if (loading) return "Searching...";
|
|
256
|
+
return "Search Results";
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<div className="fixed inset-0 z-[1000] flex items-start justify-center pt-[15vh] px-4">
|
|
261
|
+
{/* Backdrop */}
|
|
262
|
+
<div
|
|
263
|
+
className="absolute inset-0 bg-black/60 backdrop-blur-sm animate-in fade-in duration-300"
|
|
264
|
+
onClick={onClose}
|
|
265
|
+
/>
|
|
266
|
+
|
|
267
|
+
{/* Palette Body */}
|
|
268
|
+
<div className="relative w-full max-w-2xl bg-[var(--kyro-surface)] rounded-3xl shadow-2xl overflow-hidden animate-in zoom-in-95 fade-in duration-300 ring-1 ring-white/10 border border-white/5">
|
|
269
|
+
<div className="flex items-center px-6 py-5 border-b border-[var(--kyro-border)]">
|
|
270
|
+
{loading ? (
|
|
271
|
+
<Loader2 className="w-5 h-5 text-[var(--kyro-text-secondary)] opacity-50 mr-4 animate-spin" />
|
|
272
|
+
) : (
|
|
273
|
+
<Search className="w-5 h-5 text-[var(--kyro-text-secondary)] opacity-50 mr-4" />
|
|
274
|
+
)}
|
|
275
|
+
<input
|
|
276
|
+
ref={inputRef}
|
|
277
|
+
placeholder="Search anything..."
|
|
278
|
+
className="flex-1 bg-transparent border-none focus:outline-none text-lg font-medium text-[var(--kyro-text-primary)] placeholder:text-[var(--kyro-text-muted)]"
|
|
279
|
+
value={query}
|
|
280
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
281
|
+
onKeyDown={handleKeyDown}
|
|
282
|
+
/>
|
|
283
|
+
<div className="flex items-center gap-2 px-2 py-1 bg-[var(--kyro-bg-secondary)] rounded-lg border border-[var(--kyro-border)]">
|
|
284
|
+
<span className="text-[10px] font-black opacity-40 uppercase tracking-widest">
|
|
285
|
+
ESC
|
|
286
|
+
</span>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<div className="max-h-[400px] overflow-y-auto py-4">
|
|
291
|
+
{filteredItems.length > 0 ? (
|
|
292
|
+
<div className="space-y-1 px-4">
|
|
293
|
+
<p className="px-4 text-[10px] font-black uppercase tracking-[0.2em] opacity-40 mb-4">
|
|
294
|
+
{getSectionLabel()}
|
|
295
|
+
</p>
|
|
296
|
+
{filteredItems.map((item, index) => (
|
|
297
|
+
<div
|
|
298
|
+
key={item.id}
|
|
299
|
+
onClick={() => handleSelect(item)}
|
|
300
|
+
onMouseEnter={() => setSelectedIndex(index)}
|
|
301
|
+
className={`flex items-center justify-between px-4 py-4 rounded-2xl cursor-pointer transition-all ${
|
|
302
|
+
index === selectedIndex
|
|
303
|
+
? "bg-[var(--kyro-primary)] text-[var(--kyro-sidebar-text-active)] shadow-xl shadow-[var(--kyro-primary)]"
|
|
304
|
+
: "hover:bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)]"
|
|
305
|
+
}`}
|
|
306
|
+
>
|
|
307
|
+
<div className="flex items-center gap-4">
|
|
308
|
+
<div
|
|
309
|
+
className={`p-2 rounded-xl ${index === selectedIndex ? "bg-white/20" : "bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)]"}`}
|
|
310
|
+
>
|
|
311
|
+
<item.icon className="w-4 h-4" />
|
|
312
|
+
</div>
|
|
313
|
+
<div className="flex flex-col">
|
|
314
|
+
<span className="font-bold text-sm">{item.label}</span>
|
|
315
|
+
{item.type === "document" && item.label2 && (
|
|
316
|
+
<span
|
|
317
|
+
className={`text-[10px] font-black uppercase tracking-widest ${index === selectedIndex ? "text-[var(--kyro-sidebar-text-active)]/60" : "opacity-40"}`}
|
|
318
|
+
>
|
|
319
|
+
{item.label2}
|
|
320
|
+
</span>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
<div className="flex items-center gap-2">
|
|
325
|
+
<span
|
|
326
|
+
className={`text-[10px] font-black uppercase tracking-widest opacity-40 ${index === selectedIndex ? "text-[var(--kyro-sidebar-text-active)] p-1" : ""}`}
|
|
327
|
+
>
|
|
328
|
+
{item.type}
|
|
329
|
+
</span>
|
|
330
|
+
{index === selectedIndex && (
|
|
331
|
+
<ArrowRight className="w-4 h-4 mr-2" />
|
|
332
|
+
)}
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
))}
|
|
336
|
+
</div>
|
|
337
|
+
) : (
|
|
338
|
+
<div className="py-12 text-center">
|
|
339
|
+
<p className="text-[var(--kyro-text-secondary)] italic opacity-60">
|
|
340
|
+
{query.length >= 2 && !loading
|
|
341
|
+
? `No results found for "${query}"`
|
|
342
|
+
: "Start typing to search..."}
|
|
343
|
+
</p>
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<div className="px-8 py-4 bg-[var(--kyro-bg-secondary)] border-t border-[var(--kyro-border)] flex items-center justify-between text-[10px] font-black uppercase tracking-widest text-[var(--kyro-text-secondary)] opacity-60">
|
|
349
|
+
<div className="flex gap-6">
|
|
350
|
+
<span className="flex items-center gap-2 underline underline-offset-4 decoration-2 decoration-[var(--kyro-primary)]">
|
|
351
|
+
↑↓ Navigate
|
|
352
|
+
</span>
|
|
353
|
+
<span className="flex items-center gap-2 underline underline-offset-4 decoration-2 decoration-[var(--kyro-primary)]">
|
|
354
|
+
⏎ Select
|
|
355
|
+
</span>
|
|
356
|
+
</div>
|
|
357
|
+
<div>Kyro Universal Search</div>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { CommandPalette } from "./CommandPalette";
|
|
3
|
+
import { ConfirmModal } from "./Modal";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
collections: any;
|
|
7
|
+
globals: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function CommandPaletteWrapper({ collections, globals }: Props) {
|
|
11
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
12
|
+
const [showLogoutConfirm, setShowLogoutConfirm] = useState(false);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
16
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
setIsOpen((prev) => !prev);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
(window as any).openCommandPalette = () => setIsOpen(true);
|
|
23
|
+
|
|
24
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
28
|
+
delete (window as any).openCommandPalette;
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const handleClose = () => setIsOpen(false);
|
|
33
|
+
|
|
34
|
+
const handleLogoutConfirm = () => {
|
|
35
|
+
localStorage.removeItem("kyro_token");
|
|
36
|
+
localStorage.removeItem("kyro_user");
|
|
37
|
+
window.location.href = "/login";
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleNavigate = (view: string, collection?: string, id?: string) => {
|
|
41
|
+
if (view === "list" && collection) {
|
|
42
|
+
window.location.href = `/${collection}`;
|
|
43
|
+
} else if (view === "edit" && collection && id) {
|
|
44
|
+
window.location.href = `/${collection}/${id}`;
|
|
45
|
+
} else if (view === "create" && collection) {
|
|
46
|
+
window.location.href = `/${collection}/new`;
|
|
47
|
+
} else if (view === "settings" && collection) {
|
|
48
|
+
window.location.href = `/settings/${collection}`;
|
|
49
|
+
} else if (view === "media") {
|
|
50
|
+
window.location.href = `/media`;
|
|
51
|
+
} else if (view === "users") {
|
|
52
|
+
window.location.href = `/users`;
|
|
53
|
+
} else if (view === "audit") {
|
|
54
|
+
window.location.href = `/audit`;
|
|
55
|
+
} else if (view === "roles") {
|
|
56
|
+
window.location.href = `/roles`;
|
|
57
|
+
} else if (view === "api-explorer") {
|
|
58
|
+
window.location.href = `/admin/api-explorer`;
|
|
59
|
+
} else if (view === "graphql") {
|
|
60
|
+
window.location.href = `/admin/graphql`;
|
|
61
|
+
} else if (view === "rest") {
|
|
62
|
+
window.location.href = `/admin/rest-playground`;
|
|
63
|
+
} else if (view === "theme") {
|
|
64
|
+
const isDark = document.documentElement.classList.contains("dark");
|
|
65
|
+
if (isDark) {
|
|
66
|
+
document.documentElement.classList.remove("dark");
|
|
67
|
+
localStorage.setItem("theme", "light");
|
|
68
|
+
} else {
|
|
69
|
+
document.documentElement.classList.add("dark");
|
|
70
|
+
localStorage.setItem("theme", "dark");
|
|
71
|
+
}
|
|
72
|
+
} else if (view === "logout") {
|
|
73
|
+
setShowLogoutConfirm(true);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
<CommandPalette
|
|
80
|
+
isOpen={isOpen}
|
|
81
|
+
onClose={handleClose}
|
|
82
|
+
collections={collections}
|
|
83
|
+
globals={globals}
|
|
84
|
+
onNavigate={handleNavigate}
|
|
85
|
+
/>
|
|
86
|
+
<ConfirmModal
|
|
87
|
+
open={showLogoutConfirm}
|
|
88
|
+
onClose={() => setShowLogoutConfirm(false)}
|
|
89
|
+
onConfirm={handleLogoutConfirm}
|
|
90
|
+
title="Sign Out"
|
|
91
|
+
message="Are you sure you want to sign out?"
|
|
92
|
+
confirmLabel="Sign Out"
|
|
93
|
+
variant="danger"
|
|
94
|
+
/>
|
|
95
|
+
</>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import React, { useEffect, type ReactNode } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
|
|
4
|
+
export function ModalContent({ children }: { children: ReactNode }) {
|
|
5
|
+
return <div className="text-[var(--kyro-text-secondary)]">{children}</div>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function ModalActions({ children }: { children: ReactNode }) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex items-center justify-end gap-3 mt-6">{children}</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
2
13
|
|
|
3
14
|
interface ModalProps {
|
|
4
15
|
open: boolean;
|
|
@@ -7,6 +18,7 @@ interface ModalProps {
|
|
|
7
18
|
children: ReactNode;
|
|
8
19
|
footer?: ReactNode;
|
|
9
20
|
size?: "sm" | "md" | "lg";
|
|
21
|
+
variant?: "default" | "danger";
|
|
10
22
|
}
|
|
11
23
|
|
|
12
24
|
export function Modal({
|
|
@@ -16,6 +28,7 @@ export function Modal({
|
|
|
16
28
|
children,
|
|
17
29
|
footer,
|
|
18
30
|
size = "md",
|
|
31
|
+
variant = "default",
|
|
19
32
|
}: ModalProps) {
|
|
20
33
|
useEffect(() => {
|
|
21
34
|
const handleEscape = (e: KeyboardEvent) => {
|
|
@@ -41,20 +54,27 @@ export function Modal({
|
|
|
41
54
|
lg: "max-w-2xl",
|
|
42
55
|
};
|
|
43
56
|
|
|
44
|
-
return (
|
|
45
|
-
<div className="fixed inset-0 z-
|
|
57
|
+
return createPortal(
|
|
58
|
+
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
|
|
46
59
|
<div
|
|
47
60
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
|
48
61
|
onClick={onClose}
|
|
49
62
|
/>
|
|
50
63
|
<div
|
|
51
|
-
className={`relative w-full ${sizeClasses[size]} mx-4 bg-
|
|
64
|
+
className={`relative w-full ${sizeClasses[size]} mx-4 bg-[var(--kyro-surface)] rounded-lg shadow-2xl animate-in fade-in zoom-in-95 duration-200 border ${
|
|
65
|
+
variant === "danger"
|
|
66
|
+
? "border-red-500/20"
|
|
67
|
+
: "border-[var(--kyro-border)]"
|
|
68
|
+
}`}
|
|
52
69
|
>
|
|
53
|
-
<div className="flex items-center justify-between px-6 py-4 border-b border-
|
|
54
|
-
<h2 className="text-lg font-semibold text-
|
|
70
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--kyro-border)]">
|
|
71
|
+
<h2 className="text-lg font-semibold text-[var(--kyro-text-primary)]">
|
|
72
|
+
{title}
|
|
73
|
+
</h2>
|
|
55
74
|
<button
|
|
75
|
+
type="button"
|
|
56
76
|
onClick={onClose}
|
|
57
|
-
className="p-1 text-
|
|
77
|
+
className="p-1 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-colors"
|
|
58
78
|
>
|
|
59
79
|
<svg
|
|
60
80
|
width="20"
|
|
@@ -70,12 +90,13 @@ export function Modal({
|
|
|
70
90
|
</div>
|
|
71
91
|
<div className="px-6 py-4">{children}</div>
|
|
72
92
|
{footer && (
|
|
73
|
-
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-
|
|
93
|
+
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] rounded-b-lg">
|
|
74
94
|
{footer}
|
|
75
95
|
</div>
|
|
76
96
|
)}
|
|
77
97
|
</div>
|
|
78
|
-
</div
|
|
98
|
+
</div>,
|
|
99
|
+
document.body,
|
|
79
100
|
);
|
|
80
101
|
}
|
|
81
102
|
|
|
@@ -111,17 +132,21 @@ export function ConfirmModal({
|
|
|
111
132
|
footer={
|
|
112
133
|
<>
|
|
113
134
|
<button
|
|
135
|
+
type="button"
|
|
114
136
|
onClick={onClose}
|
|
115
137
|
disabled={loading}
|
|
116
|
-
className="kyro-
|
|
138
|
+
className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
117
139
|
>
|
|
118
140
|
{cancelLabel}
|
|
119
141
|
</button>
|
|
120
142
|
<button
|
|
143
|
+
type="button"
|
|
121
144
|
onClick={onConfirm}
|
|
122
145
|
disabled={loading}
|
|
123
|
-
className={`
|
|
124
|
-
variant === "danger"
|
|
146
|
+
className={`px-4 py-2 rounded-lg font-medium text-sm transition-colors ${
|
|
147
|
+
variant === "danger"
|
|
148
|
+
? "bg-red-500 text-white hover:bg-red-600"
|
|
149
|
+
: "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90"
|
|
125
150
|
}`}
|
|
126
151
|
>
|
|
127
152
|
{loading ? "Loading..." : confirmLabel}
|
|
@@ -129,7 +154,7 @@ export function ConfirmModal({
|
|
|
129
154
|
</>
|
|
130
155
|
}
|
|
131
156
|
>
|
|
132
|
-
<p className="text-
|
|
157
|
+
<p className="text-[var(--kyro-text-secondary)]">{message}</p>
|
|
133
158
|
</Modal>
|
|
134
159
|
);
|
|
135
160
|
}
|