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.
TokenPicker (palette + styleGuide tokens)TextStylePicker + lazy TextStylePickerPanel / TextStyleEditorPanel — see text-styles)LayoutIconGenerator.tsxhover:, md:, dark:)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.MediaInput, grid, preview, editListEditor + CraftListEditor (see List editors)TokenPicker is the canonical color picker. Used in:
"color"It surfaces palette slots (CSS vars from Background.props.pallet), styleGuide tokens, and free-form hex/rgba.
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.
<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.
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.
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.
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).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.