diff --git a/packages/app/src/web/shell.tsx b/packages/app/src/web/shell.tsx index 1ba146e99..13e34ee75 100644 --- a/packages/app/src/web/shell.tsx +++ b/packages/app/src/web/shell.tsx @@ -152,7 +152,12 @@ function IntegrationList(props: { pathname: string; onNavigate?: () => void }) { // ── SidebarContent ─────────────────────────────────────────────────────── -function SidebarContent(props: { pathname: string; onNavigate?: () => void; showBrand?: boolean }) { +function SidebarContent(props: { + pathname: string; + onNavigate?: () => void; + showBrand?: boolean; + onOpenCommands: () => void; +}) { const isHome = props.pathname === "/"; const isSecrets = props.pathname === "/secrets"; const isPolicies = props.pathname === "/policies"; @@ -212,6 +217,15 @@ function SidebarContent(props: { pathname: string; onNavigate?: () => void; show {/* Footer */}
+ {/* oxlint-disable-next-line react/forbid-elements */} + - + {/* Desktop sidebar */} {/* Mobile sidebar overlay */} @@ -330,6 +345,10 @@ export function Shell() { pathname={pathname} onNavigate={() => setMobileSidebarOpen(false)} showBrand={false} + onOpenCommands={() => { + setMobileSidebarOpen(false); + setCommandPaletteOpen(true); + }} />
diff --git a/packages/react/src/components/command-palette.tsx b/packages/react/src/components/command-palette.tsx index c2c10239d..ae8c75db6 100644 --- a/packages/react/src/components/command-palette.tsx +++ b/packages/react/src/components/command-palette.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { useNavigate } from "@tanstack/react-router"; import { useAtomValue } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; @@ -28,9 +28,9 @@ import { // 3. Popular integrations (plugin presets) // --------------------------------------------------------------------------- -export function CommandPalette() { +export function CommandPalette(props: { open: boolean; onOpenChange: (open: boolean) => void }) { + const { open, onOpenChange } = props; const integrationPlugins = useIntegrationPlugins(); - const [open, setOpen] = useState(false); const navigate = useNavigate(); const integrationsResult = useAtomValue(integrationsOptimisticAtom); @@ -39,12 +39,12 @@ export function CommandPalette() { const onKeyDown = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); - setOpen((o) => !o); + onOpenChange(!open); } }; document.addEventListener("keydown", onKeyDown); return () => document.removeEventListener("keydown", onKeyDown); - }, []); + }, [onOpenChange, open]); const connectedSources = useMemo( () => @@ -88,7 +88,7 @@ export function CommandPalette() { return entries; }, [integrationPlugins]); - const close = useCallback(() => setOpen(false), []); + const close = useCallback(() => onOpenChange(false), [onOpenChange]); const goToIntegration = useCallback( (id: string) => { @@ -132,8 +132,10 @@ export function CommandPalette() { [close, navigate], ); + if (!open) return null; + return ( - + No results found. diff --git a/packages/react/src/multiplayer/shell.tsx b/packages/react/src/multiplayer/shell.tsx index 439739ea0..0e00b1987 100644 --- a/packages/react/src/multiplayer/shell.tsx +++ b/packages/react/src/multiplayer/shell.tsx @@ -2,7 +2,7 @@ import { Link, Outlet, useLocation, useParams } from "@tanstack/react-router"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { useAtomValue } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; -import { BookOpen, ExternalLink } from "lucide-react"; +import { BookOpen, Command, ExternalLink } from "lucide-react"; import type { Integration } from "@executor-js/sdk/shared"; import { integrationsOptimisticAtom } from "../api/atoms"; import { trackEvent } from "../api/analytics"; @@ -163,6 +163,21 @@ function DocsLink(props: { href: string; onNavigate?: () => void }) { ); } +function CommandsButton(props: { onOpen: () => void }) { + return ( + // oxlint-disable-next-line react/forbid-elements + + ); +} + // ── IntegrationList ─────────────────────────────────────────────────────────── // `pathname` is scope-relative (org-slug prefix already stripped). @@ -326,7 +341,12 @@ function UserFooter(props: Pick) { // ── SidebarContent ─────────────────────────────────────────────────────── function SidebarContent( - props: ShellProps & { pathname: string; onNavigate?: () => void; showBrand?: boolean }, + props: ShellProps & { + pathname: string; + onNavigate?: () => void; + showBrand?: boolean; + onOpenCommands: () => void; + }, ) { const plugins = useClientPlugins(); const pluginNavItems = plugins.flatMap((plugin) => @@ -371,6 +391,7 @@ function SidebarContent(
+ {APP_VERSION && (

@@ -392,6 +413,7 @@ export function Shell(props: ShellProps) { const pathname = useScopeRelativePathname(); const lastPathname = useRef(pathname); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); + const [commandPaletteOpen, setCommandPaletteOpen] = useState(false); if (lastPathname.current !== pathname) { lastPathname.current = pathname; if (mobileSidebarOpen) setMobileSidebarOpen(false); @@ -408,10 +430,14 @@ export function Shell(props: ShellProps) { return (

- + {/* Desktop sidebar */} {/* Mobile sidebar overlay */} @@ -450,6 +476,10 @@ export function Shell(props: ShellProps) { pathname={pathname} onNavigate={() => setMobileSidebarOpen(false)} showBrand={false} + onOpenCommands={() => { + setMobileSidebarOpen(false); + setCommandPaletteOpen(true); + }} />