@kyro-cms/admin 0.1.5 → 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.
Files changed (164) hide show
  1. package/README.md +149 -51
  2. package/package.json +52 -5
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +136 -27
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +1417 -661
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +3 -3
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +199 -57
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +786 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +191 -53
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +149 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/UserManagement.tsx +204 -0
  26. package/src/components/VersionHistoryPanel.tsx +3 -3
  27. package/src/components/WebhookManager.tsx +608 -0
  28. package/src/components/blocks/AccordionBlock.tsx +97 -0
  29. package/src/components/blocks/ArrayBlock.tsx +75 -0
  30. package/src/components/blocks/BlockEditModal.MARKER +12 -0
  31. package/src/components/blocks/BlockEditModal.tsx +774 -0
  32. package/src/components/blocks/ButtonBlock.tsx +165 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +66 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +151 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +64 -0
  38. package/src/components/blocks/HeadingBlock.tsx +81 -0
  39. package/src/components/blocks/HeroBlock.tsx +157 -0
  40. package/src/components/blocks/ImageBlock.tsx +83 -0
  41. package/src/components/blocks/LinkBlock.tsx +71 -0
  42. package/src/components/blocks/ListBlock.tsx +39 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +279 -0
  45. package/src/components/blocks/VStackBlock.tsx +75 -0
  46. package/src/components/blocks/VideoBlock.tsx +45 -0
  47. package/src/components/blocks/index.ts +10 -0
  48. package/src/components/fields/BlocksField.tsx +323 -0
  49. package/src/components/fields/CheckboxField.tsx +15 -9
  50. package/src/components/fields/CodeField.tsx +234 -0
  51. package/src/components/fields/DateField.tsx +38 -11
  52. package/src/components/fields/EditorClient.tsx +271 -0
  53. package/src/components/fields/FileField.tsx +390 -0
  54. package/src/components/fields/HybridContentField.tsx +109 -0
  55. package/src/components/fields/ImageField.tsx +429 -0
  56. package/src/components/fields/JSONField.tsx +361 -0
  57. package/src/components/fields/MarkdownField.tsx +282 -0
  58. package/src/components/fields/NumberField.tsx +42 -12
  59. package/src/components/fields/PortableTextField.tsx +143 -0
  60. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  61. package/src/components/fields/RelationshipField.tsx +231 -59
  62. package/src/components/fields/SelectField.tsx +25 -15
  63. package/src/components/fields/TextField.tsx +45 -14
  64. package/src/components/fields/extensions/blockComponents.tsx +237 -0
  65. package/src/components/fields/extensions/blocksStore.ts +273 -0
  66. package/src/components/fields/index.ts +13 -0
  67. package/src/components/index.ts +1 -2
  68. package/src/components/layout/Header.tsx +2 -2
  69. package/src/components/layout/Layout.tsx +2 -2
  70. package/src/components/ui/Badge.tsx +9 -4
  71. package/src/components/ui/BlockDrawer.tsx +79 -0
  72. package/src/components/ui/Button.tsx +1 -1
  73. package/src/components/ui/CommandPalette.tsx +362 -0
  74. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  75. package/src/components/ui/Dropdown.tsx +1 -1
  76. package/src/components/ui/Modal.tsx +37 -12
  77. package/src/components/ui/PromptModal.tsx +94 -0
  78. package/src/components/ui/SlidePanel.tsx +43 -16
  79. package/src/components/ui/Toast.tsx +80 -14
  80. package/src/env.d.ts +16 -0
  81. package/src/env.ts +20 -0
  82. package/src/index.ts +0 -1
  83. package/src/layouts/AdminLayout.astro +164 -170
  84. package/src/layouts/AuthLayout.astro +50 -0
  85. package/src/lib/MediaService.ts +541 -0
  86. package/src/lib/auth/sqlite-adapter.ts +319 -0
  87. package/src/lib/config.ts +22 -6
  88. package/src/lib/dataStore.ts +132 -74
  89. package/src/lib/db/adapter.ts +54 -0
  90. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  91. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  92. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  93. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  94. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  95. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  96. package/src/lib/db/index.ts +449 -0
  97. package/src/lib/db/mongodb-adapter.ts +207 -0
  98. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  99. package/src/lib/db/schema/mysql-auth.ts +113 -0
  100. package/src/lib/db/schema/mysql-content.ts +20 -0
  101. package/src/lib/db/schema/postgres-auth.ts +116 -0
  102. package/src/lib/db/schema/postgres-content.ts +35 -0
  103. package/src/lib/db/schema/postgres-media.ts +52 -0
  104. package/src/lib/db/schema/postgres-settings.ts +11 -0
  105. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  106. package/src/lib/db/schema/sqlite-content.ts +20 -0
  107. package/src/lib/graphql/index.ts +1 -0
  108. package/src/lib/graphql/schema.ts +443 -0
  109. package/src/lib/rate-limit.ts +267 -0
  110. package/src/lib/storage.ts +374 -0
  111. package/src/lib/store.ts +85 -0
  112. package/src/middleware.ts +116 -28
  113. package/src/pages/[collection]/[id].astro +178 -122
  114. package/src/pages/[collection]/index.astro +24 -156
  115. package/src/pages/admin/api-explorer.astro +98 -0
  116. package/src/pages/admin/graphql-explorer.astro +40 -0
  117. package/src/pages/admin/graphql.astro +97 -0
  118. package/src/pages/admin/index.astro +286 -0
  119. package/src/pages/admin/keys.astro +8 -0
  120. package/src/pages/admin/rest-playground.astro +44 -0
  121. package/src/pages/admin/webhooks.astro +8 -0
  122. package/src/pages/api/[collection]/[id]/publish.ts +44 -0
  123. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  124. package/src/pages/api/[collection]/[id]/versions.ts +36 -0
  125. package/src/pages/api/[collection]/[id].ts +102 -159
  126. package/src/pages/api/[collection]/index.ts +151 -230
  127. package/src/pages/api/auth/[id].ts +48 -69
  128. package/src/pages/api/auth/audit-logs.ts +20 -43
  129. package/src/pages/api/auth/login.ts +159 -45
  130. package/src/pages/api/auth/logout.ts +50 -20
  131. package/src/pages/api/auth/refresh.ts +119 -0
  132. package/src/pages/api/auth/register.ts +110 -40
  133. package/src/pages/api/auth/users.ts +22 -97
  134. package/src/pages/api/collections.ts +59 -0
  135. package/src/pages/api/globals/[slug]/test.ts +172 -0
  136. package/src/pages/api/globals/[slug].ts +42 -0
  137. package/src/pages/api/graphql.ts +90 -0
  138. package/src/pages/api/health.ts +417 -40
  139. package/src/pages/api/keys/[id].ts +26 -0
  140. package/src/pages/api/keys/index.ts +75 -0
  141. package/src/pages/api/media/[id].ts +309 -0
  142. package/src/pages/api/media/folders.ts +609 -0
  143. package/src/pages/api/media/index.ts +146 -0
  144. package/src/pages/api/media/resize.ts +267 -0
  145. package/src/pages/api/search.ts +82 -0
  146. package/src/pages/api/slug-availability.ts +70 -0
  147. package/src/pages/api/storage-config.ts +20 -0
  148. package/src/pages/api/storage-status.ts +206 -0
  149. package/src/pages/api/upload.ts +334 -0
  150. package/src/pages/api/webhooks/index.ts +71 -0
  151. package/src/pages/audit/index.astro +2 -104
  152. package/src/pages/login.astro +82 -0
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +102 -0
  156. package/src/pages/roles/index.astro +21 -21
  157. package/src/pages/settings/[slug].astro +162 -0
  158. package/src/pages/settings/index.astro +9 -0
  159. package/src/pages/users/[id].astro +29 -21
  160. package/src/pages/users/index.astro +22 -17
  161. package/src/pages/users/new.astro +18 -17
  162. package/src/styles/main.css +553 -128
  163. package/src/components/layout/Sidebar.tsx +0 -497
  164. package/src/pages/index.astro +0 -225
@@ -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 'react';
1
+ import type { ReactNode } from "react";
2
2
 
3
3
  interface BadgeProps {
4
- variant?: 'default' | 'success' | 'warning' | 'danger' | 'info';
4
+ variant?: "default" | "success" | "warning" | "danger" | "info";
5
+ className?: string;
5
6
  children: ReactNode;
6
7
  }
7
8
 
8
- export function Badge({ variant = 'default', children }: BadgeProps) {
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
+ }
@@ -21,7 +21,7 @@ export function Button({
21
21
  const sizeClass = `kyro-btn-${size}`;
22
22
 
23
23
  return (
24
- <button
24
+ <button type="button"
25
25
  className={`${baseClass} ${variantClass} ${sizeClass} ${className}`}
26
26
  disabled={disabled || loading}
27
27
  {...props}
@@ -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
+ }
@@ -62,7 +62,7 @@ export function DropdownItem({
62
62
  disabled,
63
63
  }: DropdownItemProps) {
64
64
  return (
65
- <button
65
+ <button type="button"
66
66
  onClick={onClick}
67
67
  disabled={disabled}
68
68
  className={`w-full flex items-center gap-3 px-3 py-2 text-sm text-left transition-colors ${