(null);
// Element selections = entries with `id` (data-cd-id) set. Pure artboard
// selections (artboardId-only, no id) and empty sets fall through.
const elementSelections = selected.filter((s) => !!s.id);
const visible = elementSelections.length > 0;
useEffect(() => {
const div = ref.current;
if (!div) return;
if (!visible) {
div.style.display = 'none';
div.setAttribute('data-on', 'false');
return;
}
const tick = () => {
rafRef.current = null;
// Dogfood 2026-07-07 — "hrozně zavaší ten toolbar" during a reorder drag:
// the floating pill has nothing useful to say mid-drag anyway (Inspect /
// Copy CSS / Copy ID on an element that's about to move), and its own
// per-frame resolveSelectionEl + getBoundingClientRect work was
// compounding with the drag's own DOM churn + the resize/spacing
// overlays' rAF loops. Hide it for the duration, like Figma/Webflow do.
if (isElementDragActive()) {
div.style.display = 'none';
div.setAttribute('data-on', 'false');
rafRef.current = requestAnimationFrame(tick);
return;
}
let xMin = Number.POSITIVE_INFINITY;
let yMin = Number.POSITIVE_INFINITY;
let xMax = Number.NEGATIVE_INFINITY;
let yMax = Number.NEGATIVE_INFINITY;
let any = false;
for (const sel of elementSelections) {
const node = resolveSelectionEl(document, sel);
if (!node) continue;
const r = (node as HTMLElement).getBoundingClientRect();
if (r.width === 0 && r.height === 0) continue;
any = true;
if (r.left < xMin) xMin = r.left;
if (r.top < yMin) yMin = r.top;
if (r.right > xMax) xMax = r.right;
if (r.bottom > yMax) yMax = r.bottom;
}
if (!any) {
div.style.display = 'none';
div.setAttribute('data-on', 'false');
rafRef.current = requestAnimationFrame(tick);
return;
}
div.style.display = 'flex';
const tw = div.offsetWidth || 0;
const centerX = (xMin + xMax) / 2;
const top = yMin;
const gap = 14;
let anchorY = top - div.offsetHeight - gap;
if (anchorY < 60) anchorY = yMax + gap;
div.style.left = `${Math.round(centerX - tw / 2)}px`;
div.style.top = `${Math.round(anchorY)}px`;
div.setAttribute('data-on', 'true');
rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
return () => {
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
};
}, [visible, elementSelections]);
if (!visible) {
return ;
}
// For multi-selection, Copy actions operate on the FIRST element — same
// convention as the right-click menu (single-target). Future polish: a
// mini-menu that lets the user pick which selection's CSS to copy.
const primary = elementSelections[0];
const count = elementSelections.length;
// Task L5 — align (≥2) / distribute (≥3) / tidy-up (≥2) for a multi-element
// selection. Button enablement is count-gated (matching MultiArtboardToolbar's
// G7 cluster); the absolute/fixed-positioning eligibility check happens lazily
// on click via `resolveAlignTargets` (a silent no-op otherwise, rather than a
// per-frame DOM scan just to grey out a button).
const alignOk = count >= 2;
const distributeOk = count >= 3;
const runAlign = (mode: AlignAxisMode) => {
const targets = resolveAlignTargets(elementSelections);
if (!targets) return;
commitMoves(targets, computeAlign(toKeyedRects(targets), mode));
};
const runDistribute = (axis: SpacingAxis) => {
const targets = resolveAlignTargets(elementSelections);
if (!targets) return;
commitMoves(targets, computeDistribute(toKeyedRects(targets), axis));
};
const runTidyUp = () => {
const targets = resolveAlignTargets(elementSelections);
if (!targets) return;
commitMoves(targets, computeTidyGrid(toKeyedRects(targets)));
};
return (
{count === 1 ? '1 element' : `${count} elements`}
{alignOk && (
<>
runAlign('left')}>
runAlign('center-x')}
>
runAlign('right')}>
runAlign('top')}>
runAlign('center-y')}
>
runAlign('bottom')}
>
runDistribute('x')}
>
runDistribute('y')}
>
>
)}
);
}