@kyro-cms/admin 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -51
- package/package.json +53 -6
- 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 +136 -27
- 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 +1417 -661
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +3 -3
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +199 -57
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +786 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +191 -53
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +149 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- 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 +97 -0
- package/src/components/blocks/ArrayBlock.tsx +75 -0
- package/src/components/blocks/BlockEditModal.MARKER +12 -0
- package/src/components/blocks/BlockEditModal.tsx +774 -0
- package/src/components/blocks/ButtonBlock.tsx +165 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +66 -0
- package/src/components/blocks/ColumnsBlock.tsx +151 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +64 -0
- package/src/components/blocks/HeadingBlock.tsx +81 -0
- package/src/components/blocks/HeroBlock.tsx +157 -0
- package/src/components/blocks/ImageBlock.tsx +83 -0
- package/src/components/blocks/LinkBlock.tsx +71 -0
- package/src/components/blocks/ListBlock.tsx +39 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +279 -0
- package/src/components/blocks/VStackBlock.tsx +75 -0
- package/src/components/blocks/VideoBlock.tsx +45 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/CheckboxField.tsx +15 -9
- package/src/components/fields/CodeField.tsx +234 -0
- package/src/components/fields/DateField.tsx +38 -11
- package/src/components/fields/EditorClient.tsx +271 -0
- package/src/components/fields/FileField.tsx +390 -0
- package/src/components/fields/HybridContentField.tsx +109 -0
- package/src/components/fields/ImageField.tsx +429 -0
- package/src/components/fields/JSONField.tsx +361 -0
- package/src/components/fields/MarkdownField.tsx +282 -0
- package/src/components/fields/NumberField.tsx +42 -12
- package/src/components/fields/PortableTextField.tsx +143 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipField.tsx +231 -59
- package/src/components/fields/SelectField.tsx +25 -15
- package/src/components/fields/TextField.tsx +45 -14
- package/src/components/fields/extensions/blockComponents.tsx +237 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +13 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +2 -2
- 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/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +22 -6
- package/src/lib/dataStore.ts +132 -74
- 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/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -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 +44 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +36 -0
- package/src/pages/api/[collection]/[id].ts +102 -159
- package/src/pages/api/[collection]/index.ts +151 -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 +553 -128
- package/src/components/layout/Sidebar.tsx +0 -497
package/src/components/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { Admin } from "./Admin";
|
|
|
2
2
|
export { ListView } from "./ListView";
|
|
3
3
|
export { DetailView } from "./DetailView";
|
|
4
4
|
export { CreateView } from "./CreateView";
|
|
5
|
+
export { Dashboard } from "./Dashboard";
|
|
5
6
|
export { AutoForm } from "./AutoForm";
|
|
6
7
|
export {
|
|
7
8
|
ActionBar,
|
|
@@ -19,8 +20,6 @@ export {
|
|
|
19
20
|
useTheme,
|
|
20
21
|
type ThemeMode,
|
|
21
22
|
} from "./ThemeProvider";
|
|
22
|
-
export * from "./layout/Header";
|
|
23
|
-
export * from "./layout/Sidebar";
|
|
24
23
|
export * from "./ui/Button";
|
|
25
24
|
export * from "./ui/Badge";
|
|
26
25
|
export * from "./ui/Spinner";
|
|
@@ -10,7 +10,7 @@ export function Header({ title, onMenuClick, actions }: HeaderProps) {
|
|
|
10
10
|
return (
|
|
11
11
|
<header className="kyro-header">
|
|
12
12
|
<div className="kyro-header-left">
|
|
13
|
-
<button
|
|
13
|
+
<button type="button"
|
|
14
14
|
className="kyro-header-menu"
|
|
15
15
|
onClick={onMenuClick}
|
|
16
16
|
aria-label="Toggle menu"
|
|
@@ -24,7 +24,7 @@ export function Header({ title, onMenuClick, actions }: HeaderProps) {
|
|
|
24
24
|
<div className="kyro-header-right">
|
|
25
25
|
{actions}
|
|
26
26
|
<div className="kyro-header-user">
|
|
27
|
-
<button className="kyro-header-user-btn">
|
|
27
|
+
<button type="button" className="kyro-header-user-btn">
|
|
28
28
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
29
29
|
<circle cx="12" cy="8" r="4" />
|
|
30
30
|
<path d="M4 20c0-4 4-6 8-6s8 2 8 6" />
|
|
@@ -14,10 +14,10 @@ export default function Layout({ children, collections = [], currentSlug }: Layo
|
|
|
14
14
|
<div id="sidebar-root" className="hidden lg:block">
|
|
15
15
|
{/* Sidebar rendered here */}
|
|
16
16
|
</div>
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
{/* Main content */}
|
|
19
19
|
<main className="flex-1 min-w-0">
|
|
20
|
-
{children}
|
|
20
|
+
{/* {children} */}
|
|
21
21
|
</main>
|
|
22
22
|
</div>
|
|
23
23
|
</div>
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import type { ReactNode } from
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
interface BadgeProps {
|
|
4
|
-
variant?:
|
|
4
|
+
variant?: "default" | "success" | "warning" | "danger" | "info";
|
|
5
|
+
className?: string;
|
|
5
6
|
children: ReactNode;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
export function Badge({
|
|
9
|
+
export function Badge({
|
|
10
|
+
variant = "default",
|
|
11
|
+
className = "",
|
|
12
|
+
children,
|
|
13
|
+
}: BadgeProps) {
|
|
9
14
|
return (
|
|
10
|
-
<span className={`kyro-badge kyro-badge-${variant}`}>
|
|
15
|
+
<span className={`kyro-badge kyro-badge-${variant} ${className}`}>
|
|
11
16
|
{children}
|
|
12
17
|
</span>
|
|
13
18
|
);
|
|
@@ -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
|
+
}
|