refactor(nexus): wave 3a controller integration pass
Three coordinated fixes after phases 12, 13, 14, 15 all landed in
parallel on nexus/design-system-migration:
1. TopStrip.test.tsx provider stubs for phase 14 contexts.
Phase 14 wired CmdKButton to a new CommandPaletteContext via the
useCommandPalette() hook. TopStrip renders CmdKButton internally,
so its existing tests (written in phase 8 before the context
existed) throw "useCommandPalette must be used within a
CommandPaletteProvider" on every case. Same issue on GlobalMicButton
which now reads from VoiceContext.
Fix: add module-scope vi.mock() stubs for both contexts returning
minimal shapes, matching the existing CompanyContext mock pattern
already in the file. No test semantics change - the TopStrip is
still rendered in isolation, still verified to contain the three
children (ModeBreadcrumb, CmdKButton, GlobalMicButton) and the
header landmark. All 4 TopStrip tests pass again.
Phase 15's report flagged this breakage explicitly and confirmed
via git stash that it was pre-existing on phase 14's HEAD, not
introduced by phase 15.
2. PromoteTransition mobile variant.
Phase 15 deferred the mobile variant of the promote-to-project
transition because PromoteTransition.tsx did not yet exist when
phase 15 started (phase 12 created it mid-dispatch). The defer
was correct per explicit instructions.
Fix: add a CSS media query inside the existing scoped <style>
block in PromoteTransition.tsx:
@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;
}
}
On mobile the brainstormer completely covers the chat thread
instead of sharing a 30/70 split, per spec section 9.1. The panel
itself already takes the remaining viewport height via flex, so
once the ribbon collapses to zero the brainstormer naturally
fills the whole area.
Pure CSS - no JS media query, no useMediaQuery call, no test
changes needed. Single 768px breakpoint matches the rest of the
Nexus frame.
3. Layout.tsx onSearch now calls useCommandPalette().setOpen(true)
directly instead of dispatching a synthetic Cmd+K keydown.
Phase 14 installed a real command palette context with a provider
mounted in main.tsx, but left Layout.tsx's onSearch callback using
the old synthetic keydown shim because Layout is phase 15-owned
(parallel dispatch rules). The synthetic dispatch worked end-to-end
because the provider's global keydown listener catches it, but it's
a code smell: a Layout-level callback generating a synthetic event
for a listener the Layout also owns.
Fix: import useCommandPalette, hold a commandPalette ref in the
Layout body, and replace the synthetic keydown dispatch with
commandPalette.setOpen(true). Drop the Phase 8 shim comment; leave
a single-line Phase 14 explanation.
All 294 tests pass across 37 test files (frame, assistant, studio,
projects, settings, Voice/CommandPalette contexts, home-status and
gate-indicator and promote-to-project hooks, StudioWorkshopDetail
page). Typecheck clean across every wave 1-3A file plus App.tsx
and Layout.tsx.
Known deferrals not addressed in this commit:
- MobileTabBar does not port MobileBottomNav's "new issue" FAB or
inbox badge count. Spec section 9.1 says 4 destinations only;
requires user decision on whether to restore as a separate mobile
affordance or route through the command palette.
- VoiceMicButton.tsx and useVadRecorder.ts are now dead code after
phase 14's ChatInput migration. Both are in the phase 16 cleanup
plan's deletion list.
- Destination regexes are duplicated between IconRail and
MobileTabBar. Phase 16 DRY target via a shared frame/destinations
module.
- Phase 13 left ui/src/lib/instance-settings.ts whitelisting
/heartbeats and /experimental paths. Redirect routes catch these,
so the whitelist is cosmetic; phase 16 cleanup target.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1a0d611cb1
commit
fb76b5eeef
3 changed files with 48 additions and 3 deletions
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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<typeof createRoot> | null = null;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue