Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions packages/app/src/web/shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -212,6 +217,15 @@ function SidebarContent(props: { pathname: string; onNavigate?: () => void; show
{/* Footer */}
<div className="shrink-0 border-t border-sidebar-border px-4 py-2.5">
<div className="flex flex-col gap-1.5 text-xs leading-none">
{/* oxlint-disable-next-line react/forbid-elements */}
<button
type="button"
onClick={props.onOpenCommands}
className="flex items-center justify-between text-left text-muted-foreground transition-colors hover:text-foreground"
>
<span>Commands</span>
<span className="font-mono text-[11px]">⌘K</span>
Comment on lines +226 to +227

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The ⌘K label is Mac-only. The keyboard handler in CommandPalette also matches Ctrl+K, so Windows/Linux users pressing Ctrl+K will open the palette but see a symbol that doesn't match their keyboard. A simple platform sniff (navigator.platform or UA Mac) at render time would let you show Ctrl+K on non-Mac.

Suggested change
<span>Commands</span>
<span className="font-mono text-[11px]">⌘K</span>
<span>Commands</span>
<span className="font-mono text-[11px]">
{typeof navigator !== "undefined" && /Mac/.test(navigator.platform) ? "⌘K" : "Ctrl+K"}
</span>

</button>
<a
href="https://executor.sh/docs"
target="_blank"
Expand Down Expand Up @@ -254,6 +268,7 @@ export function Shell() {
const refreshTools = useAtomRefresh(toolsAllAtom);
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);
Expand Down Expand Up @@ -288,10 +303,10 @@ export function Shell() {

return (
<div className="flex h-screen overflow-hidden">
<CommandPalette />
<CommandPalette open={commandPaletteOpen} onOpenChange={setCommandPaletteOpen} />
{/* Desktop sidebar */}
<aside className="desktop-macos-sidebar hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56">
<SidebarContent pathname={pathname} />
<SidebarContent pathname={pathname} onOpenCommands={() => setCommandPaletteOpen(true)} />
</aside>

{/* Mobile sidebar overlay */}
Expand Down Expand Up @@ -330,6 +345,10 @@ export function Shell() {
pathname={pathname}
onNavigate={() => setMobileSidebarOpen(false)}
showBrand={false}
onOpenCommands={() => {
setMobileSidebarOpen(false);
setCommandPaletteOpen(true);
}}
/>
</div>
</div>
Expand Down
16 changes: 9 additions & 7 deletions packages/react/src/components/command-palette.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);

Expand All @@ -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(
() =>
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -132,8 +132,10 @@ export function CommandPalette() {
[close, navigate],
);

if (!open) return null;

return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandDialog open={open} onOpenChange={onOpenChange}>
<CommandInput placeholder="Search integrations or jump to add…" />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
Expand Down
38 changes: 34 additions & 4 deletions packages/react/src/multiplayer/shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -163,6 +163,21 @@ function DocsLink(props: { href: string; onNavigate?: () => void }) {
);
}

function CommandsButton(props: { onOpen: () => void }) {
return (
// oxlint-disable-next-line react/forbid-elements
<button
type="button"
onClick={props.onOpen}
className="group flex w-full items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm text-sidebar-foreground transition-colors hover:bg-sidebar-active/60 hover:text-foreground"
>
<Command className="size-4 shrink-0" />
<span className="flex-1 text-left">Commands</span>
<span className="font-mono text-[11px] text-muted-foreground">⌘K</span>
</button>
);
}

// ── IntegrationList ───────────────────────────────────────────────────────────

// `pathname` is scope-relative (org-slug prefix already stripped).
Expand Down Expand Up @@ -326,7 +341,12 @@ function UserFooter(props: Pick<ShellProps, "onSignOut" | "orgMenuSlot">) {
// ── 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) =>
Expand Down Expand Up @@ -371,6 +391,7 @@ function SidebarContent(
<SidebarUpdateCard />

<div className="shrink-0 border-t border-sidebar-border p-2">
<CommandsButton onOpen={props.onOpenCommands} />
<DocsLink href={props.docsUrl ?? DEFAULT_DOCS_URL} onNavigate={props.onNavigate} />
{APP_VERSION && (
<p className="px-2.5 pt-1.5 font-mono text-[11px] text-muted-foreground tabular-nums">
Expand All @@ -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);
Expand All @@ -408,10 +430,14 @@ export function Shell(props: ShellProps) {

return (
<div className="flex h-screen overflow-hidden">
<CommandPalette />
<CommandPalette open={commandPaletteOpen} onOpenChange={setCommandPaletteOpen} />
{/* Desktop sidebar */}
<aside className="hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56">
<SidebarContent {...props} pathname={pathname} />
<SidebarContent
{...props}
pathname={pathname}
onOpenCommands={() => setCommandPaletteOpen(true)}
/>
</aside>

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