Skip to content

Figma-like Inputs — Design Spec

Branch: theming-system-redesign (or new branch off it) Scope: Builder sidebar number inputs only Approach: Composable + visual polish (Option B)

Goal

Make builder sidebar number inputs feel like Figma: draggable labels, keyboard modifiers, and polished hover/focus/drag states. Build on existing InputField, OptionRow, and MultiPostfixInput — no new components.

1. useScrubInput Composable

New file: src/composables/useScrubInput.ts

API

ts
const { labelRef, onKeydown } = useScrubInput({
  value: () => props.value,         // reactive getter
  onChange: (v) => emit('input', v), // callback on change
  min: 0,                            // optional min
  max: 100,                          // optional max
  step: 1,                           // default step (default: 1)
  shiftStep: 10,                     // step with Shift held (default: 10)
  altStep: 0.1,                      // step with Alt held (default: 0.1)
  axis: 'x',                         // drag direction (default: 'x')
  sensitivity: 1,                    // px per step (default: 1)
})

Returns:

  • labelRef — template ref for the label element (drag target)
  • onKeydown — event handler for the input element

Drag-scrubbing (on label)

  1. Hover: Label gets cursor: ew-resize + dashed underline in accent color
  2. Mousedown: Store start position and start value
  3. Mousemove: Calculate delta (px / sensitivity) × step → new value
    • Shift held → uses shiftStep (10x)
    • Alt held → uses altStep (0.1x)
    • Clamp to min/max if defined
  4. Mouseup: Cleanup listeners, emit final value
  5. No pointer lock — standard cursor tracking for simplicity

Keyboard modifiers (on input)

When input is focused:

  • ↑ / ↓: ±step (default 1)
  • Shift + ↑/↓: ±shiftStep (default 10)
  • Alt + ↑/↓: ±altStep (default 0.1)
  • Enter: Blur input (confirm value)
  • Escape: Revert to previous value + blur

Prevents default on arrow keys to avoid cursor movement in the input field.

Lifecycle

  • Attaches mousedown listener on labelRef via onMounted / watches ref
  • Mousemove/mouseup added on document during drag (removed on mouseup)
  • Cleanup on onBeforeUnmount

2. Integration Points

OptionRow.vue

  • Accepts new prop: scrub: Boolean (default: false)
  • When scrub is true, applies labelRef from useScrubInput to the title element
  • Passes onKeydown down to child InputField via scoped slot or provide/inject
  • Auto-detects number input children where possible

InputField.vue

  • New prop: scrub: Boolean (default: false)
  • When scrub is true, binds onKeydown handler on the inner <input> element
  • Overrides native number input arrow key behavior with composable's keyboard modifiers

MultiPostfixInput.vue

  • Inherits scrub behavior through InputField — no changes needed

Config components

  • No changes needed initially — OptionRow handles everything
  • Can opt in per-row by passing :scrub="true" on OptionRow

3. Visual Polish

Label States

StateText ColorBorderCursor
Default--text-mutednonedefault
Hover--text-secondary1px dashed accent-primary @ 0.35ew-resize
Dragging--accent-primary1px solid accent-primary @ 0.50ew-resize

Transitions: color + border-bottom at 120ms ease. Drag start is instant (no transition).

Input States (number inputs)

StateBackgroundBorder/Outline
Default--input-bg1px bottom --border-default
Hoverslightly lightersame
Focus--input-bgbox-shadow: 0 0 0 1.5px accent-primary @ 0.45 inset
During label drag--input-bgoutline: 1px solid accent-primary @ 0.3

Value During Drag

  • Updates on requestAnimationFrame — no CSS transition on the value itself
  • Input text color changes to --accent-primary during drag

CSS Classes

scss
// On OptionRow label
.option-row-title--scrub {
  cursor: ew-resize;
  transition: color 120ms ease, border-bottom 120ms ease;
  border-bottom: 1px dashed transparent;
  padding-bottom: 1px;

  &:hover {
    color: var(--text-secondary);
    border-bottom-color: rgba(var(--accent-primary-rgb), 0.35);
  }

  &.is-scrubbing {
    color: var(--accent-primary);
    border-bottom-color: rgba(var(--accent-primary-rgb), 0.50);
    border-bottom-style: solid;
  }
}

New Tokens

Only one new token needed (per theme):

css
--accent-primary-rgb: <r>, <g>, <b>;  /* for rgba() usage */

All other visual states derive from existing tokens with opacity.

4. Rollout Strategy

  1. Build useScrubInput composable + CSS classes
  2. Integrate into OptionRow and InputField with scrub prop
  3. Enable on a few key config rows first (Size, Padding, Offset, Gap)
  4. Verify in all 5 themes
  5. Enable on remaining number input rows

5. Out of Scope

  • Math expressions in inputs
  • Mini-slider on hover
  • Drag overlay / preview tooltip
  • New ScrubInput component (decided against in approach selection)
  • Non-sidebar inputs (campaign settings, brand editor, etc.)

Internal documentation