diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 67faee65..76db3391 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -14,6 +14,7 @@ import { DevRestartBanner } from "./DevRestartBanner"; import { IconRail } from "./frame/IconRail"; import { MobileTabBar } from "./frame/MobileTabBar"; import { TopStrip } from "./frame/TopStrip"; +import { useCommandPalette } from "../context/CommandPaletteContext"; import { useDialog } from "../context/DialogContext"; import { GeneralSettingsProvider } from "../context/GeneralSettingsContext"; import { useCompany } from "../context/CompanyContext"; @@ -30,6 +31,7 @@ import { NotFoundPage } from "../pages/NotFound"; export function Layout() { const { isMobile } = useSidebar(); const { openNewIssue, openOnboarding } = useDialog(); + const commandPalette = useCommandPalette(); const { companies, loading: companiesLoading, @@ -129,9 +131,10 @@ export function Layout() { enabled: keyboardShortcutsEnabled, onNewIssue: () => openNewIssue(), onSearch: () => { - // Phase 8: open the command palette via synthetic keydown, mirroring - // the CmdKButton shim. Phase 14 replaces with a real palette context. - document.dispatchEvent(new KeyboardEvent("keydown", { key: "k", metaKey: true, bubbles: true })); + // Phase 14: the CommandPalette is state-managed via a real context + // now, so we can open it directly instead of dispatching a synthetic + // Cmd+K keydown. The old shim is gone. + commandPalette.setOpen(true); }, }); diff --git a/ui/src/components/assistant/PromoteTransition.tsx b/ui/src/components/assistant/PromoteTransition.tsx index 7da81132..5e462a50 100644 --- a/ui/src/components/assistant/PromoteTransition.tsx +++ b/ui/src/components/assistant/PromoteTransition.tsx @@ -79,6 +79,22 @@ const TRANSITION_STYLE = ` transition: none !important; } } +/* Phase 15: on mobile the brainstormer completely covers the chat + instead of rendering a 30/70 split. The ribbon and source label are + hidden; the panel takes the full viewport height. Single 768px + breakpoint matches the rest of the Nexus frame. */ +@media (max-width: 767px) { + .nx-promote-ribbon[data-pstate="prompting"], + .nx-promote-ribbon[data-pstate="creating"] { + max-height: 0; + box-shadow: none; + overflow: hidden; + } + .nx-promote-label[data-pstate="prompting"], + .nx-promote-label[data-pstate="creating"] { + display: none; + } +} `; export function PromoteTransition({ diff --git a/ui/src/components/frame/TopStrip.test.tsx b/ui/src/components/frame/TopStrip.test.tsx index 3c6d06dd..a24e41e6 100644 --- a/ui/src/components/frame/TopStrip.test.tsx +++ b/ui/src/components/frame/TopStrip.test.tsx @@ -22,6 +22,32 @@ vi.mock("@/context/CompanyContext", () => ({ }), })); +// Phase 14 wired CmdKButton to CommandPaletteContext. Without a provider +// the hook throws at render time. Stub both of the Phase 14 contexts at +// module scope so the TopStrip renders in isolation. +vi.mock("@/context/CommandPaletteContext", () => ({ + useCommandPalette: () => ({ + open: false, + setOpen: () => {}, + toggle: () => {}, + }), + CommandPaletteProvider: ({ children }: { children: React.ReactNode }) => children, +})); + +vi.mock("@/context/VoiceContext", () => ({ + useVoice: () => ({ + state: "idle" as const, + mediaStream: null, + hasQueuedVoice: false, + queueCount: 0, + startListening: () => {}, + stopListening: () => {}, + toggleListening: () => {}, + drainQueue: () => [], + }), + VoiceProvider: ({ children }: { children: React.ReactNode }) => children, +})); + describe("TopStrip", () => { let container: HTMLDivElement; let root: ReturnType | null = null;