Slots & UI
Plugins render into predefined slot regions within the editor workspace. This page covers slot types, visibility policies, Z-index conventions, contributions, drawer panel design rules, and drawer auto-reveal.
Available Slots
| Slot Constant | Location / Context | Typical Plugins |
|---|---|---|
SIDE_BAR |
Sidebar drawers (left & right) | Adjustment, Craft, AI Bridge, Image Info |
OPTION_BAR |
Top strip (below header) | Tool option panels (ClipOptions, ColorOptions) |
DOCK |
Bottom tab panel area | Layer panel, History panel |
TL / TR / BL / BR |
Viewport corner widgets | Anchored HUD actions and status widgets |
VIEWPORT_OVERLAY |
Over the canvas (pointer-interactive) | Crop handles, selection outlines |
STAGE_OVERLAY |
Stage-level layer (non-interactive) | Pixel grid, rulers, smart guide lines |
STAGE_GIZMOS |
Interactive transform & crop tools | SmartGuides, transform handles |
ROOT_OVERLAY |
App-level absolute layers | Global notifications, dialogs |
TOOL_MENU |
Left vertical toolbar | Tool selection menu |
HIDDEN |
No visual UI | TimeTraveler, background services |
Show Policies
| Policy | Behavior |
|---|---|
'always-show' |
Visible whenever the editor is open |
'frame-required' |
Visible only when a document/frame is loaded |
Most document-dependent tools use 'frame-required'.
Z-Index Protocol
Z-index values come from the shared EDITOR_Z_INDEX constant (@opengpex/editor/core/helpers/config). Never hardcode raw numbers — import the constant:
| Constant | Value | Content |
|---|---|---|
STAGE.BACKDROP |
0 | Stage backdrop |
STAGE.CONTENT |
100 | Layer stack start |
STAGE.GIZMOS |
1000 | Basic interactive entities (guide lines, anchor points) |
STAGE.SYSTEM_TOOLS |
2000 | Core tools (selection box, crop box) |
STAGE.DEVELOPER_ZONE |
3000 | Third-party safety zone |
UI.WORKSPACE_BASE |
2000 | Sidebar, top bar, action bar |
UI.OVERLAY |
4000 | Global overlay plugins |
UI.POPOVER |
5000 | Popovers, dropdowns (requires Portal) |
UI.MODAL |
6000 | Modal dialogs and warnings |
UI.TOOLTIP |
10100 | Tooltips — must float above popovers |
Within a slot, plugins order by the order property (lower renders first). PluginSlot also inserts a logical divider whenever order crosses a multiple of 100 between adjacent items.
Contributions System
A plugin can donate UI to additional slots via contributions:
export const plugin: EditorPlugin = {
slot: 'SIDE_BAR',
component: MyDrawer,
contributions: [
{ slot: 'OPTION_BAR', component: MyOptionsBar },
],
};
This lets a drawer also contribute an options strip without creating a second plugin.
Drawer Panel Design Rules
All SIDE_BAR panels follow a shared visual contract so drawers behave as one system.
Standard container and padding
<div className="flex flex-col gap-2 px-2 pt-1 pb-1 overflow-hidden">
<motion.div layout="position" className="flex justify-between items-center shrink-0">
{/* Header: title left, actions right */}
</motion.div>
{/* Content sections — no extra top margin; gap-2 handles spacing */}
</div>
Height-transition safety
DrawerBar animates panel height with framer-motion. To prevent children from stretching or spilling during the transition:
- Wrap the header and any section that mounts/unmounts or resizes in
<motion.div layout="position">— this animates position only, never size. - Use
shrink-0only for fixed-height rows; if a section can grow during a tab switch, preferlayout="position"instead. - Avoid
flex-1on conditionally mounted content.
Section dividers
Internal border-t dividers must declare a dark-mode override so they read as a subtle line in both themes:
<div className="border-t border-[var(--border-subtle)] dark:border-white/10" />
Header tool selection
| Pattern | Widget | When |
|---|---|---|
| A: dropdown | ActionDropdown |
2+ text-labeled items, possibly dynamic (providers, models) |
| B: icon toggles | ActionGroup |
Fixed set of 2–5 icon-representable modes |
<ActionDropdown
options={providers.map(p => ({ label: p.name, value: p.id }))}
onSelect={(val) => setActiveProvider(val)}
trigger={(isOpen) => (
<div className="flex items-center gap-1 group cursor-pointer">
<span className="text-[10px] font-black uppercase tracking-[0.15em]">
{activeProvider.name}
</span>
<ChevronDown size={10} className={isOpen ? 'rotate-180' : ''} />
</div>
)}
/>
Do not introduce @headlessui/react for local menus — use the editor widget library, which is Portal-based and immune to overflow: hidden clipping.
Options-bar plugins (XxxOptions)
Options plugins pack tightly into a shared flex strip. Standard root:
<div className="flex items-center gap-1 -mr-1 animate-in fade-in slide-in-from-left-2 duration-300">
<div className="flex items-center">{/* header label */}</div>
<div className="mx-1 w-px h-5 bg-[var(--border-subtle)]" />
<div className="flex items-center gap-0.5">{/* controls */}</div>
</div>
The trailing -mr-1 swallows visual dead space so adjacent options plugins read as a single toolbar. Compress FancyButton to shape="rect" iconOnly with className="w-6 h-6", variant="ghost", and set tooltipPosition="right" (the convention used by ClipOptions) so tooltips open away from the header.
Drawer Auto-Reveal
SIDE_BAR plugins can declare an autoReveal rule. The engine watches the predicate and reacts only to edges — false → true opens the drawer; true → false behavior depends on collapseWhenFalse:
| Mode | collapseWhenFalse |
Behavior |
|---|---|---|
| Sticky | unset (default) | Opens on rising edge; user closes manually |
| Restore | 'restore' |
Opens on rising edge; on falling edge restores the pre-activation open state |
| Collapse | 'collapse' |
Opens on rising edge; always closes on falling edge |
// CraftDrawer — restore mode: open while a craft tool is active
import * as P from './protocols';
autoReveal: {
when: (state) => state.interaction.signals[P.CraftDrawerAPI.signals.activeCraft] != null,
collapseWhenFalse: 'restore',
priority: 200,
},
// AIToolsDrawer — restore mode: open while the SAM tool is active;
// on exit it closes again unless the user engaged with the drawer
// (the reveal engine's markUserEngaged upgrades the restore snapshot
// on any pointer/key event inside the panel)
autoReveal: {
when: (state) => {
if (state.interaction.interactionMode !== 'clip') return false;
const frame = state.activeFrameId ? state.frames.byId[state.activeFrameId] : null;
return frame?.latestClipTool === 'sam';
},
collapseWhenFalse: 'restore',
priority: 150,
},
Rules for when: pure function, O(1), no deep traversal. First render is seed-immune — persisted signals do not trigger a spurious reveal on refresh. When multiple rules fire, priority (higher wins) resolves contention.
Not every drawer should auto-reveal. AdjustmentDrawer deliberately omits it: layer selection has too many unrelated causes, and collapse-on-deselect would make the panel bounce.
Next Steps
- Signals & Communication — driving UI from signals
- API Reference — hook behavior and re-render costs
- Your First Plugin — a drawer built to this spec
Last updated: 2026-10-12