Guide for adding CoreUIBar menu buttons and wiring action handlers in the storyboard core package. Use when adding toolbar buttons, menu items, or action handlers to the CoreUIBar.
Guide for adding new menu buttons to the storyboard CoreUIBar — the floating toolbar at the bottom-right of every prototype page.
Everything added to storyboard must either be a new system or conform to an existing system. Do not hardcode behavior — use config declarations + registered action handlers. The CoreUIBar is config-driven end-to-end.
The CoreUIBar is a config-driven floating toolbar rendered by packages/core/src/CoreUIBar.svelte. All buttons are defined in packages/core/toolbar.config.json under the menus key. The toolbar reads this config at startup, filters menus by the current mode, and renders buttons in JSON key order (reversed, so top = rightmost after the command menu).
toolbar.config.json ← Menu declarations (icon, modes, behavior)
↓
CoreUIBar.svelte ← Reads config, renders buttons via {#each}
↓
├── TriggerButton + Icon ← Sidepanel buttons (docs, inspector)
├── ActionMenuButton.svelte ← Generic action-driven dropdown (config "action" key)
├── CreateMenuButton.svelte ← Workshop feature launcher (config "actions" + registry)
├── CommentsMenuButton.svelte ← Auth-aware comments toggle
└── CommandMenu.svelte ← Always rightmost, special handling
There are three patterns, from simplest to most custom:
For buttons that toggle a side panel. Just add a config entry — no Svelte component needed.
"docs": {
"ariaLabel": "Documentation",
"icon": "primer/book",
"modes": ["*"],
"sidepanel": "docs"
}
This is the preferred pattern for new dynamic menus. The config declares an "action" key pointing to a command action ID. CoreUIBar registers the handler in onMount. The generic ActionMenuButton.svelte renders the items.
"flows": {
"label": "Flows",
"ariaLabel": "Switch flow",
"icon": "feather/fast-forward",
"modes": ["*"],
"action": "core/flows"
}
The handler is registered in CoreUIBar's onMount:
registerCommandAction('core/flows', {
getChildren: () => {
// return array of { id, label, type, active?, execute }
return items.map(item => ({
id: item.key,
label: item.title,
type: 'radio', // or 'default', 'toggle'
active: item.isActive,
execute: () => { /* action */ },
}))
},
})
Supported child types: default (plain item), toggle (checkbox), radio (radio group with check indicator).
The button auto-hides when getChildren() returns an empty array.
Only use when the menu needs UI beyond what action items support (e.g., auth flows, overlay panels, feature registries). Must still be declared in config and dynamically imported.
"comments": {
"ariaLabel": "Comments",
"icon": "primer/comment",
"modes": ["*"]
}
Add an entry to packages/core/toolbar.config.json under menus. The key order determines position (top = leftmost, bottom = rightmost before command menu).
| Field | Required | Description |
|---|---|---|
ariaLabel | yes | Accessible label, also shown in tooltip |
icon | yes | Namespaced icon name (see Icon section) |
modes | yes | Array of mode names or ["*"] for all modes |
action | no | Command action ID — renders via generic ActionMenuButton.svelte (preferred for dynamic menus) |
meta | no | Passed as props to <Icon> (e.g. { "strokeWeight": 2, "scale": 1.1 }) |
sidepanel | no | If set, button toggles this side panel tab (no custom component needed) |
label | no | Display label (used in dropdown headers) |
actions | no | Array of static action items with feature references (used by create menu) |
excludeRoutes | no | Array of route patterns where this menu is hidden |
In CoreUIBar.svelte's onMount, register a handler for your action ID:
registerCommandAction('core/my-feature', {
getChildren: () => {
// Compute items dynamically based on current state
return items.map(item => ({
id: item.key,
label: item.title,
type: 'radio', // 'default', 'toggle', or 'radio'
active: item.isActive, // for radio/toggle: marks the selected item
execute: () => { /* what happens on click */ },
}))
},
})
That's it. The generic ActionMenuButton.svelte handles all rendering. It:
getActionChildren()RadioGroup for radio type, CheckboxItem for toggle, Item for defaultgetChildren() returns an empty arrayonOpenChangeOnly needed when the action system can't express the UI you need (auth flows, overlay panels, etc.).
1. Add state variable:
let MyFeatureButton: any = $state(null)
2. Add visibility filter in visibleMenus:
if (menu.key === 'my-feature') return !!MyFeatureButton
3. Dynamic import in onMount:
try {
const mod = await import('./MyFeatureButton.svelte')
MyFeatureButton = mod.default
} catch {}
4. Rendering branch in template:
{:else if menu.key === 'my-feature'}
<MyFeatureButton config={menu} {basePath} tabindex={getTabindex(i)} />
The storyboard data index (virtual:storyboard-data-index) is seeded by the React app, which initializes after the Svelte CoreUIBar mounts. If your handler reads from the data index (flows, objects, records), the action registry handles this naturally — getChildren() is called lazily when the menu opens, not at registration time.
The Icon.svelte component supports multiple icon sources via namespaced names:
| Prefix | Source | Style | Example |
|---|---|---|---|
primer/ | Primer Octicons | fill | primer/repo, primer/gear, primer/comment |
feather/ | Feather Icons | stroke | feather/fast-forward, feather/tablet |
iconoir/ | Iconoir (registered) | stroke | iconoir/plus-circle, iconoir/square-dashed |
| (none) | Custom overrides | fill | folder, folder-open |
Icon meta props: strokeWeight, scale, rotate, flipX, offsetX, offsetY.
Menus can be hidden via:
"modes": ["inspect"] — only visible in inspect mode"excludeRoutes": ["/viewfinder"] — hidden on specific routesstoryboard.config.json → ui.hide.menus: ["my-feature"]getChildren() returns empty arrayvisibleMenus derived block| Key | Type | Config | Behavior |
|---|---|---|---|
command | custom | trigger: "command" | Always rightmost, command palette |
create | custom | actions + feature refs | Workshop feature launcher, overlay panels |
flows | action | action: "core/flows" | Lists prototype flows, radio-select to switch |
comments | custom | (none) | Auth-aware comments toggle |
docs | sidepanel | sidepanel: "docs" | Toggles docs side panel |
inspector | sidepanel | sidepanel: "inspector" | Toggles inspector side panel |