Appearance
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)
- Hover: Label gets
cursor: ew-resize+ dashed underline in accent color - Mousedown: Store start position and start value
- 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
- Mouseup: Cleanup listeners, emit final value
- 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
labelRefviaonMounted/ 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
scrubis true, applieslabelReffromuseScrubInputto the title element - Passes
onKeydowndown 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
scrubis true, bindsonKeydownhandler 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
| State | Text Color | Border | Cursor |
|---|---|---|---|
| Default | --text-muted | none | default |
| Hover | --text-secondary | 1px dashed accent-primary @ 0.35 | ew-resize |
| Dragging | --accent-primary | 1px solid accent-primary @ 0.50 | ew-resize |
Transitions: color + border-bottom at 120ms ease. Drag start is instant (no transition).
Input States (number inputs)
| State | Background | Border/Outline |
|---|---|---|
| Default | --input-bg | 1px bottom --border-default |
| Hover | slightly lighter | same |
| Focus | --input-bg | box-shadow: 0 0 0 1.5px accent-primary @ 0.45 inset |
| During label drag | --input-bg | outline: 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-primaryduring 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
- Build
useScrubInputcomposable + CSS classes - Integrate into
OptionRowandInputFieldwithscrubprop - Enable on a few key config rows first (Size, Padding, Offset, Gap)
- Verify in all 5 themes
- 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.)