@checkmate-monitor/command-frontend 0.0.2 → 0.1.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @checkmate-monitor/command-frontend
2
2
 
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ae33df2: Move command palette from dashboard to centered navbar position
8
+
9
+ - Converted `command-frontend` into a plugin with `NavbarCenterSlot` extension
10
+ - Added compact `NavbarSearch` component with responsive search trigger
11
+ - Moved `SearchDialog` from dashboard-frontend to command-frontend
12
+ - Keyboard shortcut (⌘K / Ctrl+K) now works on every page
13
+ - Renamed navbar slots for clarity:
14
+ - `NavbarSlot` → `NavbarRightSlot`
15
+ - `NavbarMainSlot` → `NavbarLeftSlot`
16
+ - Added new `NavbarCenterSlot` for centered content
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies [52231ef]
21
+ - Updated dependencies [b0124ef]
22
+ - Updated dependencies [54cc787]
23
+ - Updated dependencies [a65e002]
24
+ - Updated dependencies [ae33df2]
25
+ - Updated dependencies [32ea706]
26
+ - @checkmate-monitor/ui@0.1.2
27
+ - @checkmate-monitor/common@0.2.0
28
+ - @checkmate-monitor/frontend-api@0.1.0
29
+ - @checkmate-monitor/command-common@0.0.3
30
+
31
+ ## 0.0.3
32
+
33
+ ### Patch Changes
34
+
35
+ - Updated dependencies [0f8cc7d]
36
+ - @checkmate-monitor/frontend-api@0.0.3
37
+
3
38
  ## 0.0.2
4
39
 
5
40
  ### Patch Changes
package/package.json CHANGED
@@ -1,17 +1,20 @@
1
1
  {
2
2
  "name": "@checkmate-monitor/command-frontend",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
7
- "import": "./src/index.ts"
7
+ "import": "./src/index.tsx"
8
8
  }
9
9
  },
10
10
  "dependencies": {
11
11
  "@checkmate-monitor/command-common": "workspace:*",
12
12
  "@checkmate-monitor/common": "workspace:*",
13
13
  "@checkmate-monitor/frontend-api": "workspace:*",
14
- "react": "^18.2.0"
14
+ "@checkmate-monitor/ui": "workspace:*",
15
+ "lucide-react": "^0.468.0",
16
+ "react": "^18.2.0",
17
+ "react-router-dom": "^7.1.1"
15
18
  },
16
19
  "devDependencies": {
17
20
  "typescript": "^5.7.2",
@@ -0,0 +1,89 @@
1
+ import { useState, useEffect, useMemo } from "react";
2
+ import { cn } from "@checkmate-monitor/ui";
3
+ import { Search, Command } from "lucide-react";
4
+ import { SearchDialog } from "./SearchDialog";
5
+
6
+ /**
7
+ * NavbarSearch - Compact command palette trigger for the navbar.
8
+ * Displays a small search button that opens the global search dialog.
9
+ * Includes the ⌘K keyboard shortcut listener.
10
+ */
11
+ export const NavbarSearch = () => {
12
+ const [searchOpen, setSearchOpen] = useState(false);
13
+
14
+ // Detect Mac for keyboard shortcut display
15
+ const isMac = useMemo(
16
+ () =>
17
+ typeof navigator !== "undefined" &&
18
+ /Mac|iPhone|iPad/.test(navigator.userAgent),
19
+ []
20
+ );
21
+
22
+ // Global keyboard shortcut for search (⌘K / Ctrl+K)
23
+ useEffect(() => {
24
+ const handleKeyDown = (e: KeyboardEvent) => {
25
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
26
+ e.preventDefault();
27
+ setSearchOpen(true);
28
+ }
29
+ };
30
+
31
+ document.addEventListener("keydown", handleKeyDown);
32
+ return () => document.removeEventListener("keydown", handleKeyDown);
33
+ }, []);
34
+
35
+ return (
36
+ <>
37
+ {/* Search Dialog */}
38
+ <SearchDialog open={searchOpen} onOpenChange={setSearchOpen} />
39
+
40
+ {/* Compact trigger button */}
41
+ <button
42
+ onClick={() => setSearchOpen(true)}
43
+ className={cn(
44
+ // Base styles
45
+ "flex items-center gap-2 px-3 py-1.5 rounded-lg",
46
+ // Glassmorphism effect
47
+ "bg-muted/50 border border-primary/30",
48
+ // Subtle primary pulse animation
49
+ "animate-pulse-subtle ring-1 ring-primary/20",
50
+ // Hover state
51
+ "hover:bg-muted hover:border-primary/50 hover:ring-primary/40",
52
+ // Transition
53
+ "transition-all duration-200",
54
+ // Focus ring
55
+ "focus:outline-none focus:ring-2 focus:ring-primary/50 focus:ring-offset-2 focus:ring-offset-background",
56
+ // Cursor
57
+ "cursor-pointer"
58
+ )}
59
+ style={{
60
+ animation: "pulse-glow 3s ease-in-out infinite",
61
+ }}
62
+ aria-label="Open search"
63
+ >
64
+ <Search className="w-4 h-4 text-muted-foreground" />
65
+ {/* Show placeholder text only on larger screens */}
66
+ <span className="hidden md:inline text-sm text-muted-foreground">
67
+ Search...
68
+ </span>
69
+ {/* Keyboard shortcut badge - hidden on small screens */}
70
+ <kbd
71
+ className={cn(
72
+ "hidden sm:flex items-center gap-0.5 px-1.5 py-0.5 rounded",
73
+ "bg-background/50 border border-border/50",
74
+ "text-xs text-muted-foreground font-mono"
75
+ )}
76
+ >
77
+ {isMac ? (
78
+ <>
79
+ <Command className="w-3 h-3" />
80
+ <span>K</span>
81
+ </>
82
+ ) : (
83
+ <span>Ctrl+K</span>
84
+ )}
85
+ </kbd>
86
+ </button>
87
+ </>
88
+ );
89
+ };
@@ -0,0 +1,231 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ Input,
7
+ DynamicIcon,
8
+ type LucideIconName,
9
+ } from "@checkmate-monitor/ui";
10
+ import { useDebouncedSearch, useFormatShortcut } from "../index";
11
+ import type { SearchResult } from "@checkmate-monitor/command-common";
12
+ import { Search, ArrowUp, ArrowDown, CornerDownLeft } from "lucide-react";
13
+
14
+ interface SearchDialogProps {
15
+ open: boolean;
16
+ onOpenChange: (open: boolean) => void;
17
+ }
18
+
19
+ export const SearchDialog: React.FC<SearchDialogProps> = ({
20
+ open,
21
+ onOpenChange,
22
+ }) => {
23
+ const navigate = useNavigate();
24
+ const formatShortcut = useFormatShortcut();
25
+ const { results, loading, search, reset } = useDebouncedSearch(300);
26
+
27
+ const [query, setQuery] = useState("");
28
+ const [selectedIndex, setSelectedIndex] = useState(0);
29
+
30
+ const inputRef = useRef<HTMLInputElement>(null);
31
+
32
+ // Trigger search when dialog opens or query changes
33
+ useEffect(() => {
34
+ if (open) {
35
+ search(query);
36
+ // Focus input after dialog opens
37
+ setTimeout(() => inputRef.current?.focus(), 50);
38
+ } else {
39
+ // Reset state when dialog closes
40
+ setQuery("");
41
+ setSelectedIndex(0);
42
+ reset();
43
+ }
44
+ }, [open, query, search, reset]);
45
+
46
+ // Group results by category
47
+ const groupedResults: Record<string, SearchResult[]> = {};
48
+ for (const result of results) {
49
+ const category = result.category;
50
+ if (!groupedResults[category]) {
51
+ groupedResults[category] = [];
52
+ }
53
+ groupedResults[category].push(result);
54
+ }
55
+
56
+ // Flatten for navigation
57
+ const flatResults = Object.values(groupedResults).flat();
58
+
59
+ // Reset selection when results change
60
+ useEffect(() => {
61
+ setSelectedIndex(0);
62
+ }, [results]);
63
+
64
+ const handleSelect = useCallback(
65
+ (result: SearchResult) => {
66
+ onOpenChange(false);
67
+ if (result.route) {
68
+ navigate(result.route);
69
+ }
70
+ },
71
+ [navigate, onOpenChange]
72
+ );
73
+
74
+ // Keyboard navigation
75
+ const handleKeyDown = useCallback(
76
+ (e: React.KeyboardEvent) => {
77
+ switch (e.key) {
78
+ case "ArrowDown": {
79
+ e.preventDefault();
80
+ setSelectedIndex((prev) =>
81
+ Math.min(prev + 1, flatResults.length - 1)
82
+ );
83
+ break;
84
+ }
85
+ case "ArrowUp": {
86
+ e.preventDefault();
87
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
88
+ break;
89
+ }
90
+ case "Enter": {
91
+ e.preventDefault();
92
+ if (flatResults[selectedIndex]) {
93
+ handleSelect(flatResults[selectedIndex]);
94
+ }
95
+ break;
96
+ }
97
+ case "Escape": {
98
+ e.preventDefault();
99
+ onOpenChange(false);
100
+ break;
101
+ }
102
+ }
103
+ },
104
+ [flatResults, selectedIndex, handleSelect, onOpenChange]
105
+ );
106
+
107
+ // Render a single result item
108
+ const renderResult = (result: SearchResult, globalIndex: number) => {
109
+ const isSelected = globalIndex === selectedIndex;
110
+
111
+ return (
112
+ <button
113
+ key={result.id}
114
+ onClick={() => handleSelect(result)}
115
+ onMouseEnter={() => setSelectedIndex(globalIndex)}
116
+ className={`w-full flex items-center gap-3 px-4 py-2 text-left transition-colors ${
117
+ isSelected
118
+ ? "bg-primary/10 text-foreground"
119
+ : "text-muted-foreground hover:bg-muted/50"
120
+ }`}
121
+ >
122
+ <DynamicIcon
123
+ name={result.iconName as LucideIconName}
124
+ className="w-4 h-4 flex-shrink-0"
125
+ />
126
+ <div className="flex-1 min-w-0">
127
+ <span className="block truncate">{result.title}</span>
128
+ {result.subtitle && (
129
+ <span className="block text-xs text-muted-foreground truncate">
130
+ {result.subtitle}
131
+ </span>
132
+ )}
133
+ </div>
134
+ {/* Show shortcuts for commands */}
135
+ {result.type === "command" &&
136
+ result.shortcuts &&
137
+ result.shortcuts.length > 0 && (
138
+ <div className="flex gap-1">
139
+ {result.shortcuts.slice(0, 1).map((shortcut) => (
140
+ <kbd
141
+ key={shortcut}
142
+ className="px-1.5 py-0.5 text-xs rounded bg-muted border border-border font-mono"
143
+ >
144
+ {formatShortcut(shortcut)}
145
+ </kbd>
146
+ ))}
147
+ </div>
148
+ )}
149
+ {isSelected && (
150
+ <CornerDownLeft className="w-4 h-4 text-muted-foreground flex-shrink-0" />
151
+ )}
152
+ </button>
153
+ );
154
+ };
155
+
156
+ // Track global index for selection
157
+ let globalIndex = 0;
158
+
159
+ return (
160
+ <Dialog open={open} onOpenChange={onOpenChange}>
161
+ <DialogContent
162
+ size="lg"
163
+ className="p-0 gap-0 overflow-hidden"
164
+ onKeyDown={handleKeyDown}
165
+ >
166
+ {/* Search input */}
167
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-border">
168
+ <Search className="w-5 h-5 text-muted-foreground flex-shrink-0" />
169
+ <Input
170
+ ref={inputRef}
171
+ value={query}
172
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
173
+ setQuery(e.target.value)
174
+ }
175
+ placeholder="Search commands and systems..."
176
+ className="border-0 bg-transparent focus-visible:ring-0 px-0 text-base"
177
+ />
178
+ </div>
179
+
180
+ {/* Results */}
181
+ <div className="max-h-[300px] overflow-y-auto py-2">
182
+ {loading ? (
183
+ <div className="px-4 py-8 text-center text-muted-foreground">
184
+ Searching...
185
+ </div>
186
+ ) : flatResults.length === 0 ? (
187
+ <div className="px-4 py-8 text-center text-muted-foreground">
188
+ {query ? "No results found" : "Start typing to search..."}
189
+ </div>
190
+ ) : (
191
+ Object.entries(groupedResults).map(
192
+ ([category, categoryResults]) => (
193
+ <div key={category}>
194
+ {/* Category header */}
195
+ <div className="px-4 py-1.5 text-xs font-medium text-muted-foreground uppercase tracking-wider bg-muted/30">
196
+ {category} ({categoryResults.length})
197
+ </div>
198
+ {/* Category results */}
199
+ {categoryResults.map((result) => {
200
+ const element = renderResult(result, globalIndex);
201
+ globalIndex++;
202
+ return element;
203
+ })}
204
+ </div>
205
+ )
206
+ )
207
+ )}
208
+ </div>
209
+
210
+ {/* Footer with keyboard hints */}
211
+ <div className="flex items-center gap-4 px-4 py-2 border-t border-border bg-muted/30 text-xs text-muted-foreground">
212
+ <div className="flex items-center gap-1">
213
+ <ArrowUp className="w-3 h-3" />
214
+ <ArrowDown className="w-3 h-3" />
215
+ <span>Navigate</span>
216
+ </div>
217
+ <div className="flex items-center gap-1">
218
+ <CornerDownLeft className="w-3 h-3" />
219
+ <span>Select</span>
220
+ </div>
221
+ <div className="flex items-center gap-1">
222
+ <kbd className="px-1 rounded bg-muted border border-border font-mono">
223
+ esc
224
+ </kbd>
225
+ <span>Close</span>
226
+ </div>
227
+ </div>
228
+ </DialogContent>
229
+ </Dialog>
230
+ );
231
+ };
@@ -3,12 +3,32 @@ import {
3
3
  useApi,
4
4
  rpcApiRef,
5
5
  createApiRef,
6
+ createFrontendPlugin,
7
+ NavbarCenterSlot,
6
8
  } from "@checkmate-monitor/frontend-api";
7
9
  import {
8
10
  CommandApi,
11
+ pluginMetadata,
9
12
  type SearchResult,
10
13
  } from "@checkmate-monitor/command-common";
11
14
  import type { InferClient } from "@checkmate-monitor/common";
15
+ import { NavbarSearch } from "./components/NavbarSearch";
16
+
17
+ // =============================================================================
18
+ // PLUGIN
19
+ // =============================================================================
20
+
21
+ export const commandPlugin = createFrontendPlugin({
22
+ metadata: pluginMetadata,
23
+ routes: [],
24
+ extensions: [
25
+ {
26
+ id: "command.navbar.search",
27
+ slot: NavbarCenterSlot,
28
+ component: NavbarSearch,
29
+ },
30
+ ],
31
+ });
12
32
 
13
33
  // =============================================================================
14
34
  // API REF