Toolbar inputs

Reusable toolbar input components — color, font, slider, layout, shorthand, modifiers, action, media. Lives in packages/sdk/src/chrome/toolbar/inputs/.

Reusable toolbar input components — color, font, slider, layout, shorthand, modifiers, action, media. Lives in packages/sdk/src/chrome/toolbar/inputs/.

These are the building blocks the property registry's PropertyRenderer dispatches to. Custom MainTabs compose them directly.

Subdirectories

  • color/ — color picker, TokenPicker (palette + styleGuide tokens)
  • typography/ — font family, weight, size, alignment, and the text-styles picker (TextStylePicker + lazy TextStylePickerPanel / TextStyleEditorPanel — see text-styles)
  • layout/ — flex/grid/direction inputs, LayoutIconGenerator.tsx
  • shorthand/ — uniform/split-mode inputs (padding, margin, border, radius)
  • modifiers/ — modifier class chips (hover:, md:, dark:)
  • action/ — list-style chip-list (ActionsAddPicker + ActionsInput + ActionChipRow + ActionEditorPanel) for the registry-driven Action section, plus the flat ActionInput body still used by NavMainTab / ButtonListMainTab / ButtonItem. HandlersInput lives inside ActionsInput so per-node DOM event handlers stay grouped under the Action section.
  • media/ — media manager modal, MediaInput, grid, preview, edit
  • preset/ — preset picker (theme presets), ListEditor + CraftListEditor (see List editors)
  • bundle/ — Border / Ring / Outline / Divide bundles
  • advanced/ — less-common inputs
  • universal-input/ — single input that adapts to the property type
  • Notice.tsx — inline info / warning banner
  • useContainerLayoutManager.tsx — flex direction / wrap / gap controller

Color inputs

TokenPicker is the canonical color picker. Used in:

  • ColorPicker dialog
  • InlineEditToolbar (text color, bg color)
  • Border / Ring / Outline bundle inputs
  • Any property whose type is "color"

It surfaces palette slots (CSS vars from Background.props.pallet), styleGuide tokens, and free-form hex/rgba.

Shorthand inputs

For four-axis values (padding, margin, border-width, border-radius) the shorthand input toggles between uniform mode (one value for all sides) and split mode (independent T/R/B/L). Behavior controlled via shorthandMode on the property def.

Media input

<MediaInput propKey typeKey contentKey kindFilter? defaultTypeValue?> — picker for components (Image, Video). kindFilter pre-applies the chip in the modal; defaultTypeValue is what's written on clear.

Action input

The Action section in the unified-settings sidebar is a list-style section (see editor-popover-pattern.md §8). One pinned body chip-list (ActionsInput) renders one Chip (mode="popover") per actions[] entry; a non-pinned popover-mode header + (ActionsAddPicker) opens a SearchableMenuPopover of action types and appends to the array. Each chip lazy-loads ActionEditorPanel on click — that panel hosts the type dropdown + per-type sub-form for ONE action. Chaining = adding more chips via the header + (the in-panel "Chain Another Action" button is gone). HandlersInput renders below the chip-list inside the same body so per-node DOM event handlers stay grouped under Action; the body's isActive OR-gates on actions[].length > 0 OR a populated handlers map, so a node with only handlers still expands the section.

The flat (in-section) ActionInput body — still used directly by NavMainTab / ButtonListMainTab / ButtonItem because they live inside their own dialogs — renders an empty-state LabeledAddChip (Action: + Add...) instead of a heavy dashed CTA. Same for HandlersInput empty state. The labeled-chip pattern matches IconInput's empty state so all three rows in a Button detail panel read as one consistent shape.

List editors

Two-layer architecture for editing a list of items with an inline detail panel per row.

  • ListEditor (packages/sdk/src/chrome/toolbar/inputs/preset/ListEditor.tsx) — pure-React primitive. Renders a slim row per item (drag handle + label + extras + delete + chevron), an inline DetailPanel when a row is active, and a list-level + Add (uses ToolbarDashedButton for visual consistency with the empty-state CTAs inside the detail panel). Click zones are explicit: [drag] never toggles, [label] toggles, [actions] never toggle, [arrow] toggles — no row-level click target.

    • Drag-to-reorder is opt-in via onReorder?: (from, to) => void. Implemented with the canonical window-listener gesture pattern (per .claude/rules/drag-listeners.md) — pointerdown on the grip handle attaches pointermove / pointerup / pointercancel / blur to window, detaches on end, also detaches on component unmount. Survives mid-drag re-renders, overlay traversal, and missed pointerup (buttons === 0 mid-move triggers implicit end).
    • Visual feedback during drag: source row goes opacity-40, the row under the cursor renders a 2px primary drop line at its top edge when dragging up (source > over) or bottom edge when dragging down. Same-row hover renders nothing.
  • CraftListEditor (packages/sdk/src/chrome/toolbar/inputs/preset/CraftListEditor.tsx) — high-level wrapper for the common case: editing CraftJS child nodes of a parent. Owns the five things every consumer used to reimplement (filter+map child nodes, delete via actions.delete, add via BatchOperationAtom + addNodeTree, reorder via actions.move(id, parent, parentNodesIndex) translating the visible-list index to the parent's full data.nodes index, and the standard "edit" pencil that calls actions.selectNode). Consumers declare only their node-type specifics: childTypeName, optional filterChild, mapItem, addLabel, onAdd factory, renderLabel, renderPopover.

    <CraftListEditor
      parentId={id}
      childTypeName="Button"
      filterChild={node => !isHamburger(node)}
      mapItem={node => ({ text: node.data.props.text || "Button" })}
      activeIndex={activeIndex}
      setActiveIndex={setActiveIndex}
      addLabel="Add Button"
      editTooltip="Edit button"
      renderLabel={item => item.text}
      onAdd={({ query, addNode }) => {
        const Button = query.getOptions().resolver.Button;
        if (Button) addNode(<Button text="New Button" />);
      }}
      renderPopover={item => (
        <NodeProvider id={item.id}>
          <ActionInput />
          <IconInput propKey="icon" propType="component" label="Icon" />
        </NodeProvider>
      )}
    />
    

    addNode(element) returns the new tree's rootNodeId for cases that need follow-up (e.g. ButtonListMainTab runs applyPeerClassInherit on the new id). Pass editTooltip={null} to suppress the edit pencil.

    Consumers today: ButtonListMainTab, NavMainTab, ImageListMainTab, MapMainTab, TableSectionMainTab, TableRowMainTab. Use CraftListEditor for any new MainTab that edits CraftJS child nodes — do not reach for raw ListEditor unless the data isn't CraftJS-backed.

Related