Launch OpenGPEX

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-0 only for fixed-height rows; if a section can grow during a tab switch, prefer layout="position" instead.
  • Avoid flex-1 on 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


Last updated: 2026-10-12