mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-14 05:13:47 +00:00
fix(studio): redesign off-screen indicators as edge-clamped pills
Replace full-size dotted rectangle indicators with small pill chips pinned to the composition border. Each pill shows the element name and a directional arrow. Edge clamping computed from element center relative to composition bounds. Click to select, drag to move.
This commit is contained in:
@@ -525,19 +525,23 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
compRect.width > 0 &&
|
||||
offScreenIndicators.map((ind) => {
|
||||
const isSelected = selection?.id === ind.elementId;
|
||||
const isVerticalEdge = ind.edge === "left" || ind.edge === "right";
|
||||
return (
|
||||
<div
|
||||
key={`offscreen-${ind.key}`}
|
||||
className={`absolute rounded-sm ${isSelected ? "pointer-events-none" : "cursor-grab"}`}
|
||||
className={`absolute flex items-center justify-center transition-opacity duration-150 ${
|
||||
isSelected
|
||||
? "pointer-events-none opacity-20"
|
||||
: "cursor-grab opacity-40 hover:opacity-90 group"
|
||||
}`}
|
||||
style={{
|
||||
left: ind.left,
|
||||
top: ind.top,
|
||||
width: ind.width,
|
||||
height: ind.height,
|
||||
border: `1.5px dashed var(--panel-accent, #34d399)`,
|
||||
opacity: isSelected ? 0.3 : 0.5,
|
||||
zIndex: isSelected ? 1 : 5,
|
||||
left: ind.edgeX,
|
||||
top: ind.edgeY,
|
||||
width: isVerticalEdge ? 16 : 28,
|
||||
height: isVerticalEdge ? 28 : 16,
|
||||
zIndex: isSelected ? 1 : 10,
|
||||
}}
|
||||
title={ind.name}
|
||||
onPointerDown={
|
||||
isSelected
|
||||
? undefined
|
||||
@@ -575,8 +579,27 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
||||
el.addEventListener("pointerup", onUp);
|
||||
}
|
||||
}
|
||||
title={isSelected ? undefined : `Drag #${ind.elementId}`}
|
||||
/>
|
||||
>
|
||||
<svg
|
||||
width={isVerticalEdge ? 8 : 12}
|
||||
height={isVerticalEdge ? 12 : 8}
|
||||
viewBox={isVerticalEdge ? "0 0 8 12" : "0 0 12 8"}
|
||||
className="text-emerald-400"
|
||||
>
|
||||
{ind.edge === "left" && (
|
||||
<path d="M7 1L2 6L7 11" stroke="currentColor" strokeWidth="1.5" fill="none" />
|
||||
)}
|
||||
{ind.edge === "right" && (
|
||||
<path d="M1 1L6 6L1 11" stroke="currentColor" strokeWidth="1.5" fill="none" />
|
||||
)}
|
||||
{ind.edge === "top" && (
|
||||
<path d="M1 7L6 2L11 7" stroke="currentColor" strokeWidth="1.5" fill="none" />
|
||||
)}
|
||||
{ind.edge === "bottom" && (
|
||||
<path d="M1 1L6 6L11 1" stroke="currentColor" strokeWidth="1.5" fill="none" />
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<GridOverlay
|
||||
|
||||
@@ -5,13 +5,20 @@
|
||||
import { useRef, useState, type RefObject } from "react";
|
||||
import { useMountEffect } from "../../hooks/useMountEffect";
|
||||
|
||||
export type IndicatorEdge = "top" | "bottom" | "left" | "right";
|
||||
|
||||
export interface OffScreenIndicator {
|
||||
key: string;
|
||||
elementId: string;
|
||||
name: string;
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
edge: IndicatorEdge;
|
||||
edgeX: number;
|
||||
edgeY: number;
|
||||
arrow: string;
|
||||
}
|
||||
|
||||
interface CompRect {
|
||||
@@ -72,10 +79,9 @@ function indicatorsEqual(a: OffScreenIndicator[], b: OffScreenIndicator[]): bool
|
||||
const bi = b[i]!;
|
||||
if (
|
||||
ai.key !== bi.key ||
|
||||
Math.abs(ai.left - bi.left) > 0.5 ||
|
||||
Math.abs(ai.top - bi.top) > 0.5 ||
|
||||
Math.abs(ai.width - bi.width) > 0.5 ||
|
||||
Math.abs(ai.height - bi.height) > 0.5
|
||||
ai.edge !== bi.edge ||
|
||||
Math.abs(ai.edgeX - bi.edgeX) > 0.5 ||
|
||||
Math.abs(ai.edgeY - bi.edgeY) > 0.5
|
||||
)
|
||||
return false;
|
||||
}
|
||||
@@ -168,18 +174,59 @@ export function useOffScreenIndicators({
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only elements with a real id attribute can be selected via getElementById
|
||||
if (!el.id) continue;
|
||||
const name = el.getAttribute("data-name") || el.id;
|
||||
const count = keyCounts.get(el.id) ?? 0;
|
||||
keyCounts.set(el.id, count + 1);
|
||||
const key = count > 0 ? `${el.id}:${count}` : el.id;
|
||||
|
||||
const centerX = elLeft + elW / 2;
|
||||
const centerY = elTop + elH / 2;
|
||||
const compCenterX = (compLeft + compRight) / 2;
|
||||
const compCenterY = (compTop + compBottom) / 2;
|
||||
const dx = centerX - compCenterX;
|
||||
const dy = centerY - compCenterY;
|
||||
|
||||
let edge: IndicatorEdge;
|
||||
let arrow: string;
|
||||
if (Math.abs(dx) / cr.width > Math.abs(dy) / cr.height) {
|
||||
edge = dx > 0 ? "right" : "left";
|
||||
arrow = dx > 0 ? "→" : "←";
|
||||
} else {
|
||||
edge = dy > 0 ? "bottom" : "top";
|
||||
arrow = dy > 0 ? "↓" : "↑";
|
||||
}
|
||||
|
||||
let edgeX: number;
|
||||
let edgeY: number;
|
||||
const clampX = Math.max(compLeft + 4, Math.min(compRight - 80, centerX));
|
||||
const clampY = Math.max(compTop + 4, Math.min(compBottom - 24, centerY));
|
||||
if (edge === "top") {
|
||||
edgeX = clampX;
|
||||
edgeY = compTop;
|
||||
} else if (edge === "bottom") {
|
||||
edgeX = clampX;
|
||||
edgeY = compBottom - 24;
|
||||
} else if (edge === "left") {
|
||||
edgeX = compLeft;
|
||||
edgeY = clampY;
|
||||
} else {
|
||||
edgeX = compRight - 80;
|
||||
edgeY = clampY;
|
||||
}
|
||||
|
||||
next.push({
|
||||
key,
|
||||
elementId: el.id,
|
||||
name,
|
||||
left: elLeft,
|
||||
top: elTop,
|
||||
width: elW,
|
||||
height: elH,
|
||||
edge,
|
||||
edgeX,
|
||||
edgeY,
|
||||
arrow,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user