-
Notifications
You must be signed in to change notification settings - Fork 65
feat: added global modern Spotlight Search #367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Hmm I just realized that you haven't joined the apache organization on GitHub. Since you are a committer I think you can join by yourself via the process in the documentation: https://infra.apache.org/new-committers-guide.html#config-access |
sure i will do it |
PragmaTwice
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements a modern, context-aware spotlight search feature to improve navigation and accessibility across the web UI. The search supports keyboard shortcuts (Cmd+K on macOS, Ctrl+K on Windows) and provides intelligent filtering based on the current page context (namespace, cluster, shard, or node level).
Key Changes:
- Added a new
SpotlightSearchcomponent with context-aware search and keyboard navigation - Integrated search button with keyboard shortcut display in the application banner
- Added global CSS styling for keyboard shortcut indicators
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| webui/src/app/ui/spotlight-search.tsx | New spotlight search component with keyboard shortcuts, context-aware filtering, and hierarchical navigation for namespaces, clusters, shards, and nodes |
| webui/src/app/ui/banner.tsx | Added search button to the banner with keyboard shortcut indicator and event dispatcher |
| webui/src/app/layout.tsx | Integrated SpotlightSearch component into the root layout |
| webui/src/app/globals.css | Added styling for kbd elements to display keyboard shortcuts in light and dark modes |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }, [open, results, selectedIndex, allData.length, loadAllData]); | ||
|
|
||
| const handleSelect = (result: SearchResult) => { | ||
| router.push(result.path); | ||
| setOpen(false); | ||
| setQuery(""); | ||
| }; |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The handleSelect function is missing from the dependency array of this useEffect hook. This can lead to stale closures where the effect uses an outdated version of handleSelect. Add handleSelect to the dependency array or use useCallback to memoize the function.
| }, [open, results, selectedIndex, allData.length, loadAllData]); | |
| const handleSelect = (result: SearchResult) => { | |
| router.push(result.path); | |
| setOpen(false); | |
| setQuery(""); | |
| }; | |
| }, [open, results, selectedIndex, allData.length, loadAllData, handleSelect]); | |
| const handleSelect = useCallback((result: SearchResult) => { | |
| router.push(result.path); | |
| setOpen(false); | |
| setQuery(""); | |
| }, [router]); |
| const event = new KeyboardEvent("keydown", { | ||
| key: "k", | ||
| metaKey: true, | ||
| ctrlKey: true, |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The keyboard event dispatch is incorrect. Setting both metaKey: true and ctrlKey: true doesn't match the actual keyboard shortcut logic in spotlight-search.tsx (line 197), which uses || (either Cmd OR Ctrl, not both). This will trigger the search on any platform. Instead, dispatch the event with only one modifier based on the user's platform:
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const event = new KeyboardEvent("keydown", {
key: "k",
metaKey: isMac,
ctrlKey: !isMac,
});| const event = new KeyboardEvent("keydown", { | |
| key: "k", | |
| metaKey: true, | |
| ctrlKey: true, | |
| const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; | |
| const event = new KeyboardEvent("keydown", { | |
| key: "k", | |
| metaKey: isMac, | |
| ctrlKey: !isMac, |
| ⌘K | ||
| </Box> |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The search button only displays the macOS keyboard shortcut symbol "⌘K" regardless of the user's platform. Windows/Linux users see "⌘K" but should see "Ctrl+K". Update the button to display the correct shortcut based on the platform:
const isMac = typeof navigator !== 'undefined' && navigator.platform.toUpperCase().indexOf('MAC') >= 0;
// Then in the kbd element:
{isMac ? '⌘K' : 'Ctrl+K'}| const data: SearchResult[] = []; | ||
| const namespaces = await fetchNamespaces(); | ||
|
|
||
| for (const ns of namespaces) { | ||
| // Add namespace | ||
| data.push({ | ||
| type: "namespace", | ||
| title: ns, | ||
| path: `/namespaces/${ns}`, | ||
| namespace: ns, | ||
| }); | ||
|
|
||
| // Add clusters | ||
| const clusters = await fetchClusters(ns); | ||
| for (const cluster of clusters) { | ||
| data.push({ | ||
| type: "cluster", | ||
| title: cluster, | ||
| subtitle: `in ${ns}`, | ||
| path: `/namespaces/${ns}/clusters/${cluster}`, | ||
| namespace: ns, | ||
| cluster, | ||
| }); | ||
|
|
||
| // Add shards | ||
| const shards = await listShards(ns, cluster); | ||
| for (let i = 0; i < shards.length; i++) { | ||
| data.push({ | ||
| type: "shard", | ||
| title: `Shard ${i}`, | ||
| subtitle: `${cluster} / ${ns}`, | ||
| path: `/namespaces/${ns}/clusters/${cluster}/shards/${i}`, | ||
| namespace: ns, | ||
| cluster, | ||
| shard: String(i), | ||
| }); | ||
|
|
||
| // Add nodes | ||
| const nodes = await listNodes(ns, cluster, String(i)); | ||
| for (let nodeIndex = 0; nodeIndex < (nodes as any[]).length; nodeIndex++) { | ||
| const node = (nodes as any[])[nodeIndex]; | ||
| data.push({ | ||
| type: "node", | ||
| title: node.addr || node.id, | ||
| subtitle: `${node.role} in Shard ${i} / ${cluster}`, | ||
| path: `/namespaces/${ns}/clusters/${cluster}/shards/${i}/nodes/${nodeIndex}`, | ||
| namespace: ns, | ||
| cluster, | ||
| shard: String(i), | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loadAllData function makes multiple nested API calls sequentially (for each namespace -> for each cluster -> for each shard -> for each node), which can result in very poor performance and long loading times. Consider parallelizing these API calls using Promise.all() where possible:
const namespaces = await fetchNamespaces();
await Promise.all(namespaces.map(async (ns) => {
const clusters = await fetchClusters(ns);
await Promise.all(clusters.map(async (cluster) => {
// Similar pattern for shards and nodes
}));
}));This will significantly reduce the total loading time by making concurrent requests instead of waiting for each to complete sequentially.
| const data: SearchResult[] = []; | |
| const namespaces = await fetchNamespaces(); | |
| for (const ns of namespaces) { | |
| // Add namespace | |
| data.push({ | |
| type: "namespace", | |
| title: ns, | |
| path: `/namespaces/${ns}`, | |
| namespace: ns, | |
| }); | |
| // Add clusters | |
| const clusters = await fetchClusters(ns); | |
| for (const cluster of clusters) { | |
| data.push({ | |
| type: "cluster", | |
| title: cluster, | |
| subtitle: `in ${ns}`, | |
| path: `/namespaces/${ns}/clusters/${cluster}`, | |
| namespace: ns, | |
| cluster, | |
| }); | |
| // Add shards | |
| const shards = await listShards(ns, cluster); | |
| for (let i = 0; i < shards.length; i++) { | |
| data.push({ | |
| type: "shard", | |
| title: `Shard ${i}`, | |
| subtitle: `${cluster} / ${ns}`, | |
| path: `/namespaces/${ns}/clusters/${cluster}/shards/${i}`, | |
| namespace: ns, | |
| cluster, | |
| shard: String(i), | |
| }); | |
| // Add nodes | |
| const nodes = await listNodes(ns, cluster, String(i)); | |
| for (let nodeIndex = 0; nodeIndex < (nodes as any[]).length; nodeIndex++) { | |
| const node = (nodes as any[])[nodeIndex]; | |
| data.push({ | |
| type: "node", | |
| title: node.addr || node.id, | |
| subtitle: `${node.role} in Shard ${i} / ${cluster}`, | |
| path: `/namespaces/${ns}/clusters/${cluster}/shards/${i}/nodes/${nodeIndex}`, | |
| namespace: ns, | |
| cluster, | |
| shard: String(i), | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| const namespaces = await fetchNamespaces(); | |
| // For each namespace, fetch clusters in parallel | |
| const namespaceResults = await Promise.all( | |
| namespaces.map(async (ns) => { | |
| const results: SearchResult[] = []; | |
| // Add namespace | |
| results.push({ | |
| type: "namespace", | |
| title: ns, | |
| path: `/namespaces/${ns}`, | |
| namespace: ns, | |
| }); | |
| // Fetch clusters for this namespace | |
| const clusters = await fetchClusters(ns); | |
| const clusterResults = await Promise.all( | |
| clusters.map(async (cluster) => { | |
| const clusterData: SearchResult[] = []; | |
| // Add cluster | |
| clusterData.push({ | |
| type: "cluster", | |
| title: cluster, | |
| subtitle: `in ${ns}`, | |
| path: `/namespaces/${ns}/clusters/${cluster}`, | |
| namespace: ns, | |
| cluster, | |
| }); | |
| // Fetch shards for this cluster | |
| const shards = await listShards(ns, cluster); | |
| const shardResults = await Promise.all( | |
| shards.map(async (_shard, i) => { | |
| const shardData: SearchResult[] = []; | |
| // Add shard | |
| shardData.push({ | |
| type: "shard", | |
| title: `Shard ${i}`, | |
| subtitle: `${cluster} / ${ns}`, | |
| path: `/namespaces/${ns}/clusters/${cluster}/shards/${i}`, | |
| namespace: ns, | |
| cluster, | |
| shard: String(i), | |
| }); | |
| // Fetch nodes for this shard | |
| const nodes = await listNodes(ns, cluster, String(i)); | |
| if (Array.isArray(nodes)) { | |
| nodes.forEach((node: any, nodeIndex: number) => { | |
| shardData.push({ | |
| type: "node", | |
| title: node.addr || node.id, | |
| subtitle: `${node.role} in Shard ${i} / ${cluster}`, | |
| path: `/namespaces/${ns}/clusters/${cluster}/shards/${i}/nodes/${nodeIndex}`, | |
| namespace: ns, | |
| cluster, | |
| shard: String(i), | |
| }); | |
| }); | |
| } | |
| return shardData; | |
| }) | |
| ); | |
| // Flatten shardResults and add to clusterData | |
| clusterData.push(...shardResults.flat()); | |
| return clusterData; | |
| }) | |
| ); | |
| // Flatten clusterResults and add to results | |
| results.push(...clusterResults.flat()); | |
| return results; | |
| }) | |
| ); | |
| // Flatten all results | |
| const data: SearchResult[] = namespaceResults.flat(); |
| } catch (error) { | ||
| console.error("Failed to load search data:", error); |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error is logged to console but not displayed to the user. Users who trigger the search won't know if the data failed to load—they'll just see the loading indicator disappear without explanation. Consider setting an error state and displaying a user-friendly error message:
const [error, setError] = useState<string | null>(null);
// In catch block:
setError("Failed to load search data. Please try again.");
// Then render the error in the UI| <Dialog | ||
| open={open} | ||
| onClose={handleClose} | ||
| maxWidth="md" | ||
| fullWidth | ||
| PaperProps={{ | ||
| sx: { | ||
| position: "fixed", | ||
| top: "15%", | ||
| m: 0, | ||
| borderRadius: 4, | ||
| boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)", | ||
| overflow: "hidden", | ||
| backdropFilter: "blur(20px)", | ||
| background: (theme) => | ||
| theme.palette.mode === "dark" | ||
| ? "rgba(30, 30, 30, 0.95)" | ||
| : "rgba(255, 255, 255, 0.95)", | ||
| }, | ||
| }} | ||
| slotProps={{ | ||
| backdrop: { | ||
| sx: { | ||
| backdropFilter: "blur(4px)", | ||
| backgroundColor: "rgba(0, 0, 0, 0.3)", | ||
| }, | ||
| }, | ||
| }} | ||
| > | ||
| <DialogContent sx={{ p: 0 }}> | ||
| <Box | ||
| sx={{ | ||
| p: 3, | ||
| borderBottom: 1, | ||
| borderColor: (theme) => | ||
| theme.palette.mode === "dark" | ||
| ? "rgba(255, 255, 255, 0.08)" | ||
| : "rgba(0, 0, 0, 0.08)", | ||
| }} | ||
| > | ||
| <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}> | ||
| <SearchIcon | ||
| sx={{ | ||
| fontSize: 24, | ||
| color: "text.secondary", | ||
| opacity: 0.6, | ||
| }} | ||
| /> | ||
| <TextField | ||
| fullWidth | ||
| autoFocus | ||
| placeholder={ | ||
| query | ||
| ? "Search everything..." | ||
| : "Search in current context (or type to search all)..." | ||
| } | ||
| value={query} | ||
| onChange={(e) => setQuery(e.target.value)} | ||
| variant="standard" | ||
| InputProps={{ | ||
| disableUnderline: true, | ||
| sx: { | ||
| fontSize: "1.125rem", | ||
| fontWeight: 400, | ||
| "& input::placeholder": { | ||
| opacity: 0.6, | ||
| }, | ||
| }, | ||
| }} | ||
| /> | ||
| </Box> | ||
| </Box> | ||
|
|
||
| {loading ? ( | ||
| <Box sx={{ p: 8, textAlign: "center" }}> | ||
| <Typography color="text.secondary" sx={{ opacity: 0.7 }}> | ||
| Loading resources... | ||
| </Typography> | ||
| </Box> | ||
| ) : results.length > 0 ? ( | ||
| <List sx={{ maxHeight: 420, overflow: "auto", p: 2 }}> | ||
| {results.map((result, index) => ( | ||
| <ListItem | ||
| key={`${result.type}-${result.path}`} | ||
| disablePadding | ||
| sx={{ mb: 1 }} | ||
| > | ||
| <ListItemButton | ||
| selected={index === selectedIndex} | ||
| onClick={() => handleSelect(result)} | ||
| sx={{ | ||
| borderRadius: 3, | ||
| px: 2.5, | ||
| py: 1.5, | ||
| transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)", | ||
| "&:hover": { | ||
| transform: "translateX(4px)", | ||
| bgcolor: (theme) => | ||
| theme.palette.mode === "dark" | ||
| ? alpha(getTypeColor(result.type), 0.15) | ||
| : alpha(getTypeColor(result.type), 0.08), | ||
| }, | ||
| "&.Mui-selected": { | ||
| bgcolor: (theme) => | ||
| theme.palette.mode === "dark" | ||
| ? alpha(getTypeColor(result.type), 0.2) | ||
| : alpha(getTypeColor(result.type), 0.12), | ||
| "&:hover": { | ||
| bgcolor: (theme) => | ||
| theme.palette.mode === "dark" | ||
| ? alpha(getTypeColor(result.type), 0.25) | ||
| : alpha(getTypeColor(result.type), 0.15), | ||
| }, | ||
| }, | ||
| }} | ||
| > | ||
| <Box | ||
| sx={{ | ||
| display: "flex", | ||
| alignItems: "center", | ||
| gap: 2, | ||
| width: "100%", | ||
| }} | ||
| > | ||
| <Box | ||
| sx={{ | ||
| display: "flex", | ||
| alignItems: "center", | ||
| justifyContent: "center", | ||
| width: 40, | ||
| height: 40, | ||
| borderRadius: 2.5, | ||
| bgcolor: (theme) => | ||
| theme.palette.mode === "dark" | ||
| ? alpha(getTypeColor(result.type), 0.2) | ||
| : alpha(getTypeColor(result.type), 0.12), | ||
| color: getTypeColor(result.type), | ||
| flexShrink: 0, | ||
| }} | ||
| > | ||
| {getTypeIcon(result.type)} | ||
| </Box> | ||
| <Box sx={{ flex: 1, minWidth: 0 }}> | ||
| <Typography | ||
| variant="body1" | ||
| sx={{ | ||
| fontWeight: 500, | ||
| fontSize: "0.95rem", | ||
| mb: result.subtitle ? 0.25 : 0, | ||
| overflow: "hidden", | ||
| textOverflow: "ellipsis", | ||
| whiteSpace: "nowrap", | ||
| }} | ||
| > | ||
| {result.title} | ||
| </Typography> | ||
| {result.subtitle && ( | ||
| <Typography | ||
| variant="caption" | ||
| sx={{ | ||
| color: "text.secondary", | ||
| fontSize: "0.8rem", | ||
| opacity: 0.7, | ||
| overflow: "hidden", | ||
| textOverflow: "ellipsis", | ||
| whiteSpace: "nowrap", | ||
| display: "block", | ||
| }} | ||
| > | ||
| {result.subtitle} | ||
| </Typography> | ||
| )} | ||
| </Box> | ||
| <Chip | ||
| label={result.type} | ||
| size="small" | ||
| sx={{ | ||
| height: 24, | ||
| fontSize: "0.7rem", | ||
| fontWeight: 600, | ||
| borderRadius: 2, | ||
| bgcolor: (theme) => | ||
| theme.palette.mode === "dark" | ||
| ? alpha(getTypeColor(result.type), 0.25) | ||
| : alpha(getTypeColor(result.type), 0.15), | ||
| color: getTypeColor(result.type), | ||
| border: "none", | ||
| textTransform: "capitalize", | ||
| }} | ||
| /> | ||
| </Box> | ||
| </ListItemButton> | ||
| </ListItem> | ||
| ))} | ||
| </List> | ||
| ) : ( | ||
| <Box sx={{ p: 8, textAlign: "center" }}> | ||
| <SearchIcon | ||
| sx={{ | ||
| fontSize: 48, | ||
| color: "text.secondary", | ||
| opacity: 0.3, | ||
| mb: 2, | ||
| }} | ||
| /> | ||
| <Typography | ||
| color="text.secondary" | ||
| sx={{ opacity: 0.7, fontSize: "0.95rem" }} | ||
| > | ||
| {query ? "No results found" : "Start typing to search all resources..."} | ||
| </Typography> | ||
| </Box> | ||
| )} | ||
|
|
||
| <Box | ||
| sx={{ | ||
| px: 3, | ||
| py: 2, | ||
| borderTop: 1, | ||
| borderColor: (theme) => | ||
| theme.palette.mode === "dark" | ||
| ? "rgba(255, 255, 255, 0.08)" | ||
| : "rgba(0, 0, 0, 0.08)", | ||
| display: "flex", | ||
| gap: 3, | ||
| justifyContent: "flex-end", | ||
| bgcolor: (theme) => | ||
| theme.palette.mode === "dark" | ||
| ? "rgba(0, 0, 0, 0.2)" | ||
| : "rgba(0, 0, 0, 0.02)", | ||
| }} | ||
| > | ||
| <Box sx={{ display: "flex", alignItems: "center", gap: 0.75 }}> | ||
| <Typography | ||
| variant="caption" | ||
| sx={{ color: "text.secondary", fontSize: "0.75rem", opacity: 0.7 }} | ||
| > | ||
| <kbd>↑↓</kbd> Navigate | ||
| </Typography> | ||
| </Box> | ||
| <Box sx={{ display: "flex", alignItems: "center", gap: 0.75 }}> | ||
| <Typography | ||
| variant="caption" | ||
| sx={{ color: "text.secondary", fontSize: "0.75rem", opacity: 0.7 }} | ||
| > | ||
| <kbd>↵</kbd> Select | ||
| </Typography> | ||
| </Box> | ||
| <Box sx={{ display: "flex", alignItems: "center", gap: 0.75 }}> | ||
| <Typography | ||
| variant="caption" | ||
| sx={{ color: "text.secondary", fontSize: "0.75rem", opacity: 0.7 }} | ||
| > | ||
| <kbd>Esc</kbd> Close | ||
| </Typography> | ||
| </Box> | ||
| </Box> | ||
| </DialogContent> | ||
| </Dialog> |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SpotlightSearch component lacks ARIA attributes for accessibility. Screen reader users won't understand the purpose of the dialog or how to navigate the search results. Add appropriate ARIA attributes:
- Add
role="combobox"andaria-expandedto the search input - Add
role="listbox"to the results list - Add
role="option"andaria-selectedto each result item - Add
aria-labeloraria-labelledbyto describe the dialog purpose - Consider adding
aria-activedescendantto track the selected item
| } else if (e.key === "ArrowUp") { | ||
| e.preventDefault(); | ||
| setSelectedIndex((prev) => Math.max(prev - 1, 0)); | ||
| } else if (e.key === "Enter" && results[selectedIndex]) { |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no null/bounds check before accessing results[selectedIndex] in the Enter key handler. If selectedIndex is out of bounds (e.g., due to race conditions when results update), this could cause issues. While there's a check for results[selectedIndex] existence, the arrow key handlers on lines 215 and 218 could set selectedIndex to -1 or beyond array bounds if results.length becomes 0 after being set. Add a more robust check:
else if (e.key === "Enter" && results.length > 0 && selectedIndex >= 0 && selectedIndex < results.length) {
e.preventDefault();
handleSelect(results[selectedIndex]);
}| } else if (e.key === "Enter" && results[selectedIndex]) { | |
| } else if ( | |
| e.key === "Enter" && | |
| results.length > 0 && | |
| selectedIndex >= 0 && | |
| selectedIndex < results.length | |
| ) { |
| if ((e.metaKey || e.ctrlKey) && e.key === "k") { | ||
| e.preventDefault(); | ||
| setOpen(true); | ||
| if (!allData.length) { |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The data loading is triggered every time Cmd+K is pressed if allData is empty, but there's no mechanism to prevent duplicate concurrent API calls if the user presses Cmd+K multiple times quickly while data is still loading. This could result in multiple identical API calls running in parallel. Add a check for the loading state:
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
setOpen(true);
if (!allData.length && !loading) {
loadAllData();
}
}| if (!allData.length) { | |
| if (!allData.length && !loading) { |
| kbd { | ||
| display: inline-block; | ||
| padding: 3px 8px; | ||
| font-family: | ||
| ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; | ||
| font-size: 0.7rem; | ||
| line-height: 1.2; | ||
| font-weight: 600; | ||
| color: #555; | ||
| background: linear-gradient(180deg, #fafafa 0%, #f0f0f0 100%); | ||
| border: 1px solid #d0d0d0; | ||
| border-radius: 6px; | ||
| box-shadow: | ||
| 0 1px 2px rgba(0, 0, 0, 0.1), | ||
| 0 0 0 1px rgba(255, 255, 255, 0.8) inset, | ||
| 0 -1px 0 rgba(0, 0, 0, 0.05) inset; | ||
| text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); | ||
| } |
Copilot
AI
Nov 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The kbd styling uses fixed color values that may not provide sufficient contrast in all scenarios. The light mode kbd element uses color: #555 on a background that could vary, potentially violating WCAG contrast requirements. Consider using theme-aware colors or CSS variables that adapt to the theme context to ensure adequate contrast ratios (at least 4.5:1 for text).
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## unstable #367 +/- ##
============================================
+ Coverage 43.38% 49.06% +5.67%
============================================
Files 37 45 +8
Lines 2971 3783 +812
============================================
+ Hits 1289 1856 +567
- Misses 1544 1719 +175
- Partials 138 208 +70
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Implemented a modern fast Spotlight Search to improve accessibility and navigation for users.... Includes a context aware results, keyboard shortcuts cmd + k(mac) / Ctrl+K(windows) and correct routing for namespaces, clusters, shards, and nodes....
Monosnap.screencast.2025-11-19.16-36-43.mp4