MenuBar
Source:
src/components/layout/MenuBar.tsx
Purpose & UX role
MenuBar is the 32-pixel-high menu strip at the top of WorkspaceLayout. It has four blocks (left → right):
- Logo + app name —
Apollo Map Studiotext plus a gradient badge icon. - Menus (File / Edit / View / Tools / Help) — fully driven by the Action Registry; nothing is hardcoded inside this component.
- Spacer (
flex-1). - ModeToggle — a drawing/scene segmented control bound to
useUIStore.appMode.
Together with ToolStrip and CommandPalette, MenuBar is one of three UI outlets of the Action Registry (see the architecture's Action Registry section).
Composition tree
Props
ts
export interface MenuBarProps {
onExecute: (actionId: ActionId) => void;
getToggleState: (actionId: ActionId) => boolean;
}1
2
3
4
2
3
4
| Prop | Type | Default | Description |
|---|---|---|---|
onExecute | (actionId: ActionId) => void | — | Called when the user picks a menu item — typically useActionDispatcher().execute |
getToggleState | (actionId: ActionId) => boolean | — | Returns the current state for actions whose isToggle is true (e.g. toggleGrid); used to render the checkmark |
Subcomponents
Menu
ts
function Menu(props: {
label: string;
actions: ActionDef[];
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
onExecute: (id: ActionId) => void;
getToggleState: (id: ActionId) => boolean;
}): JSX.Element;1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Behavior:
- Clicking the button toggles open/closed.
- While open, mounts a global
mousedownlistener — clicking outside closes the menu (MenuBar.tsx:32-41). - Groups actions by
menuOrderand inserts a divider every 10-step boundary (MenuBar.tsx:44-53). - Renders a checkmark with
getToggleState(item.id) ? '✓' : ''.
ModeToggle
ts
function ModeToggle(): JSX.Element;1
- Reads
appModeandsetAppModedirectly fromuseUIStore. - Two buttons (
drawing/scene), each 11px text, 3px padding; active state isbg-cyan-500/20 text-cyan-300.
Internal state
| Hook | Purpose |
|---|---|
useState<string | null>(null) | openMenu — name of currently-open menu (mutually exclusive) |
useUIStore(s.appMode) | Current app mode (drawing/scene) |
useUIStore(s.setAppMode) | Switch mode |
Side effects
- Click outside: each
Menumounts amousedownlistener whileisOpenis true. Any click outside the menu closes it. The listener must be removed on cleanup; otherwise multi-menu use leaks listeners. - Item execution:
onExecute(item.id)is handled by the parent'suseActionDispatcher, including the R1 undo CANCEL fix.
Render anatomy
jsx
<div className="h-8 bg-zinc-950 border-b border-white/[0.07] flex items-center px-2 shrink-0">
<div className="flex items-center gap-2 mr-4">
<div className="w-4 h-4 rounded bg-gradient-to-br from-cyan-400 to-cyan-600" />
<span className="text-xs font-medium text-zinc-300 tracking-wide">Apollo Map Studio</span>
</div>
<div className="flex items-center">
{menuNames.map((name) => (
<Menu key={name} label={name} actions={getMenuActions(name)} … />
))}
</div>
<div className="flex-1" />
<ModeToggle />
</div>1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Dropdown panel:
jsx
<div className="absolute top-full left-0 mt-1 py-1 min-w-[200px] bg-zinc-900 border border-white/10 rounded-md shadow-xl z-50">
{/* each row: ✓ marker / label / shortcut */}
</div>1
2
3
2
3
Performance notes
getMenuNames()runs every render, but internally it is aMENU_ORDERconstant array +Map.has, O(N) and negligible. If the menu set grows, wrap withuseMemoin the parent.Menuis not memoized: the parent'sonExecute/getToggleStatecome fromuseActionDispatcherand are fresh references each render. The dropdown panel is unmounted whenisOpen=false, so the cost is irrelevant.- Keyboard shortcuts are NOT handled here:
useActionDispatchermounts a globalkeydownlistener atWorkspaceLayout;MenuBarmerely displaysformatShortcut(item.shortcut).
Source map
| Concern | File location |
|---|---|
| MenuBar body | MenuBar.tsx:142-177 |
Menu subcomponent | MenuBar.tsx:13-97 |
| Click-outside listener | MenuBar.tsx:32-41 |
| Menu divider insertion | MenuBar.tsx:44-53 |
| ModeToggle | MenuBar.tsx:108-140 |
| Action Registry entry | src/core/actions/registry.ts (getMenuNames, getMenuActions, formatShortcut) |
Cross-references
- WorkspaceLayout — parent
- ToolStrip / CommandPalette — sibling Action Registry outlets
- Action Registry →
src/core/actions/registry.ts useActionDispatcher→/en/api/hooks- Mode toggle →
uiStore.appMode