Move, resize, and rotate components
Manipulation writes ordinary application signals. Keep one writable box with
left, top, width, and height; use a separate writable radians signal
when rotation is enabled. ManipulationFrame binds that geometry and lets
children use its finite local allocation.
import { DragHandle, ManipulationFrame, Rectangle, ResizeHandles, RotationHandle, Text, degrees, useManipulation, useSignal,} from "@pibbl/core";
function Card() { const box = useSignal({ left: 96, top: 120, width: 320, height: 224 }); const rotation = useSignal(0); const manipulation = useManipulation({ box, rotation, move: { snap: { step: 16, when: "shift" } }, resize: { minWidth: 224, minHeight: 176, snap: { step: 8, when: "shift" }, aspectRatio: { value: "initial", when: "alt" }, }, rotate: { snap: { step: degrees(15), when: "shift" } }, });
return ( <ManipulationFrame {...manipulation.frameProps}> <Rectangle style={{ width: 320, height: 224, fill: "#e2e8f0" }} /> <DragHandle controller={manipulation} style={{ readout: true }}> <Rectangle keyboardFocusable style={{ width: 320, height: 36, fill: "#dbeafe", cursor: "grab" }} /> <Text pointerEvents="none" style={{ left: 16, top: 10, fill: "#0f172a" }} > Drag this title </Text> </DragHandle> <ResizeHandles controller={manipulation} /> <RotationHandle controller={manipulation} /> </ManipulationFrame> );}Drag the title, resize a corner, or use the top control to rotate. Its Snap control switches snapping between holding Shift and always-on. Hold Alt while resizing to preserve the initial aspect ratio. Reset geometry cancels an active gesture and restores the example’s original box, rotation, and completed change count. The live manipulation example also displays the current geometry, rotation, operation, and completed changes.
Placement and layout
Section titled “Placement and layout”The box is free placement in the frame parent’s coordinates. Use left and
top; Rectangle, Text, and Image no longer accept positioning-style
x/y. A ManipulationFrame may contain Flex, Flow, or Grid; omitted
container dimensions consume its current allocation and reflow when resizing.
Do not attach a freely positioned frame directly to Flex, Flow, or Grid.
Those parents own direct-child placement and ignore left/top in production
while development and test execution report the ignored fields. Put the frame
under a free-placement parent or make it content inside a separately positioned
component.
Custom handles and cancellation
Section titled “Custom handles and cancellation”Use useManipulation with ManipulationFrame for a custom composition.
DragHandle listens through child hit areas, ResizeHandle accepts a position,
and RotationHandle is explicit. Resize and rotation controls have a 24 CSS
pixel hit target by default, including under zoom and device-density changes.
Custom children of those handles use CSS-pixel local coordinates at the handle
anchor.
Pointer capture keeps an active gesture routed to its handle outside its
original geometry. Movement below three CSS pixels remains an ordinary press.
Call manipulation.cancel() to restore the starting owned geometry. Escape
cancels when focus management is installed and the resize/rotation handle or a
drag handle’s descendant is focused. A newer
external write ends the gesture and remains authoritative, so cancellation does
not overwrite application state.
Snapping and bounds
Section titled “Snapping and bounds”Move snaps left/top on a parent-space lattice. Size resize snaps width and
height. Use target: "grid" when the moving resize edge must land on a
parent-space grid; grid resize requires an unrotated frame and aligned parent
mapping. Bounds, minimum/maximum dimensions, and an active aspect ratio take
priority over snapping, so a legal constrained result can be off-grid.
Rotation values are radians and stay continuous across turns. Use degrees()
for readable option values. Existing Group.rotationDegrees remains degrees.
Handle guides and readouts
Section titled “Handle guides and readouts”Handles rotate with the box while keeping their CSS-pixel size. Rotation controls
include a perpendicular connector by default; style.connector: false removes
it. Set style.arc: true for the default blue-to-transparent guide, or provide
color, sweep (radians), strokeWidth and opacity.
Readout presentation is local to style.readout. A readout is opt-in and defaults
to visible only during an active gesture. For example:
<RotationHandle controller={manipulation} readoutUnit="degrees" style={{ arc: { color: "#2563eb", sweep: degrees(60) }, readout: { font: "12px sans-serif", visible: false, fill: "#64748b", focused: { visible: true }, active: { visible: true, fill: "#ff0000" }, }, }}/>Resolve base style, inactive when no manipulation is active, focused, then active. Later matching overrides win. This is a typed local readout convention, not a CSS selector system. State changes are immediate; automatic transitions and arbitrary label placement are not included.
readoutUnit="radians" changes the default rotation label. formatReadout
replaces default formatting and receives raw radians or logical width/height.
For document units, supply your application’s units-per-inch conversion:
<ResizeHandles controller={manipulation} formatReadout={({ width, height }) => `${(width / 96).toFixed(2)} × ${(height / 96).toFixed(2)} in` } style={{ readout: { active: { visible: true } } }}/>This example explicitly assumes 96 document units per inch; it does not infer
printer resolution or convert device pixels. The collection displays one size
label. style.readout: false disables it even when a formatter is supplied.
Resize labels stay upright and sit outside the rotated box, 5 CSS pixels from
the selected edges by default. Set style.readout.gap to change that spacing
(for example, { readout: { gap: 10 } }). The gap also accepts state overrides.
Canvas containment takes priority: labels clamp at the canvas edge, where overlap
may be unavoidable; oversized formatted text is ellipsized and
clipped as needed. Labels never intercept input or change gesture geometry.
Manipulable forwards these options through resizeHandles={{ style: { readout: true }, formatReadout }} and rotationHandle={{ style: { arc: true, readout: true }, readoutUnit: "radians" }}. The existing resize-position shorthand remains supported.
DragHandle listens to its descendants rather than adding an invisible box. Wrap the actual title bar, grip, or content that should start movement. Child shapes keep their normal hit testing, including gaps and clipping. An empty handle cannot be dragged. Its position readout uses left/top in parent-local logical units; a custom formatReadout receives those raw values. Manipulable accepts the same readout options through dragHandle.
Drag position labels default to a 40 CSS-pixel gap above the displayed top edge
to clear the standard rotation control. Adjust it with
style={{ readout: { gap: 48 } }} on DragHandle, or through
Manipulable’s dragHandle.style.readout.gap. State-specific gap overrides
work the same way as other readout styles. Labels remain clamped inside the
canvas; the gap is preferred spacing when there is room.
Implementation guidance for agents
Section titled “Implementation guidance for agents”Read the Input, focus, and native HTML companion for ownership, adaptation, failure modes, and verification. Agent start provides the version-selection workflow.
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.