ui: fix invisible handles — drive visibility via React state, not broken CSS class

This commit is contained in:
Hermes Agent committed 2026-09-16 14:47:33 +02:00
1 parent 6328989608
commit aa261ed1e8
1 file changed
+32 -33
+32 -33
View File
@@ -276,32 +276,33 @@ function Plugin () {
}
const [isMaxToggled, setIsMaxToggled] = useState(false)
// Resize-handle visibility is state-driven (not CSS :hover/:has) for Figma webview
// reliability: show all handles when the window or any handle is hovered; hoveredHandle
// tracks the one under the cursor, rendered brighter.
const [rootHover, setRootHover] = useState(false)
const [hoveredHandle, setHoveredHandle] = useState<string | null>(null)
const handleVisible = rootHover || hoveredHandle !== null
function hitboxStyle (dir: string, pos: { [k: string]: number | string }) {
const active = hoveredHandle === dir
return {
...pos,
position: 'fixed',
zIndex: 3,
opacity: handleVisible ? 1 : 0,
background: active ? 'rgba(255,255,255,0.95)' : 'rgba(255,255,255,0.5)',
border: '1.5px solid ' + (active ? 'rgba(80,80,80,1)' : 'rgba(110,110,110,0.9)'),
borderRadius: 4,
transition: 'opacity 120ms ease, background 120ms ease, border-color 120ms ease'
} as any
}
// "Maximized" flag drives the icon (expand/collapse). Real size after maximize is
// clamped by Figma, so we track it explicitly rather than comparing sizes.
const isMaximized = isMaxToggled
return (
<div className="plugin-root" style={{ width: '100%', height: '100%' }}>
<style>{`
.plugin-root { width: 100%; height: 100%; }
.resize-hitbox {
opacity: 0;
transition: opacity 120ms ease, background 120ms ease, border-color 120ms ease;
/* Light fill (visible on dark themes) + mid-gray border (visible on light
themes) so the handles read on either Figma theme. */
background: rgba(255, 255, 255, 0.5);
border: 1.5px solid rgba(110, 110, 110, 0.9);
border-radius: 4px;
}
/* Hovering any handle (or anywhere in the window) reveals all handles. */
.plugin-root:hover .resize-hitbox,
.plugin-root:has(.resize-hitbox:hover) .resize-hitbox { opacity: 1; }
/* The specifically hovered handle is brighter. */
.resize-hitbox:hover {
background: rgba(255, 255, 255, 0.95);
border-color: rgba(80, 80, 80, 1);
}
`}</style>
<div className="plugin-root" style={{ width: '100%', height: '100%' }}
onPointerEnter={function () { setRootHover(true) }}
onPointerLeave={function () { setRootHover(false) }}>
<Container space="medium" style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
@@ -349,18 +350,16 @@ function Plugin () {
<path d="M10 1C10.5523 1 11 1.44772 11 2C11 2.55228 10.5523 3 10 3C9.44772 3 9 2.55228 9 2C9 1.44772 9.44772 1 10 1Z" fill="currentColor" />
</svg>
</div>
{/* Perimeter hitboxes (4 edges + 4 corners). Visible for debugging.
Corners = violet. Edges colored per side: top=red, bottom=green, left=blue, right=orange.
Edge handles span the FULL side length (left:0;right:0 / top:0;bottom:0).
Center is button-only (not in perimeter). */}
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'top-left', 'top-left') }} style={{ position: 'fixed', top: -3, left: -3, width: 12, height: 12, zIndex: 3, className: 'resize-hitbox', cursor: 'nwse-resize' }} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'top', 'top-center') }} style={{ position: 'fixed', top: -3, left: 16, right: 16, height: 8, zIndex: 3, className: 'resize-hitbox', cursor: 'ns-resize' }} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'top-right', 'top-right') }} style={{ position: 'fixed', top: -3, right: -3, width: 12, height: 12, zIndex: 3, className: 'resize-hitbox', cursor: 'nesw-resize' }} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'left', 'center-left') }} style={{ position: 'fixed', top: 16, bottom: 16, left: -3, width: 8, zIndex: 3, className: 'resize-hitbox', cursor: 'ew-resize' }} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'right', 'center-right') }} style={{ position: 'fixed', top: 16, bottom: 16, right: -3, width: 8, zIndex: 3, className: 'resize-hitbox', cursor: 'ew-resize' }} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'bottom-left', 'bottom-left') }} style={{ position: 'fixed', bottom: -3, left: -3, width: 12, height: 12, zIndex: 3, className: 'resize-hitbox', cursor: 'nesw-resize' }} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'bottom', 'bottom-center') }} style={{ position: 'fixed', bottom: -3, left: 16, right: 16, height: 8, zIndex: 3, className: 'resize-hitbox', cursor: 'ns-resize' }} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'bottom-right', 'bottom-right') }} style={{ position: 'fixed', bottom: -3, right: -3, width: 12, height: 12, zIndex: 3, className: 'resize-hitbox', cursor: 'nwse-resize' }} aria-hidden="true" />
{/* Perimeter hitboxes (4 edges + 4 corners). Hidden until the window or any handle is
hovered; the handle under the cursor is rendered brighter. */}
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'top-left', 'top-left') }} onPointerEnter={function () { setHoveredHandle('top-left') }} onPointerLeave={function () { setHoveredHandle(null) }} style={hitboxStyle('top-left', { top: -3, left: -3, width: 12, height: 12, cursor: 'nwse-resize' })} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'top', 'top-center') }} onPointerEnter={function () { setHoveredHandle('top') }} onPointerLeave={function () { setHoveredHandle(null) }} style={hitboxStyle('top', { top: -3, left: 16, right: 16, height: 8, cursor: 'ns-resize' })} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'top-right', 'top-right') }} onPointerEnter={function () { setHoveredHandle('top-right') }} onPointerLeave={function () { setHoveredHandle(null) }} style={hitboxStyle('top-right', { top: -3, right: -3, width: 12, height: 12, cursor: 'nesw-resize' })} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'left', 'center-left') }} onPointerEnter={function () { setHoveredHandle('left') }} onPointerLeave={function () { setHoveredHandle(null) }} style={hitboxStyle('left', { top: 16, bottom: 16, left: -3, width: 8, cursor: 'ew-resize' })} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'right', 'center-right') }} onPointerEnter={function () { setHoveredHandle('right') }} onPointerLeave={function () { setHoveredHandle(null) }} style={hitboxStyle('right', { top: 16, bottom: 16, right: -3, width: 8, cursor: 'ew-resize' })} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'bottom-left', 'bottom-left') }} onPointerEnter={function () { setHoveredHandle('bottom-left') }} onPointerLeave={function () { setHoveredHandle(null) }} style={hitboxStyle('bottom-left', { bottom: -3, left: -3, width: 12, height: 12, cursor: 'nesw-resize' })} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'bottom', 'bottom-center') }} onPointerEnter={function () { setHoveredHandle('bottom') }} onPointerLeave={function () { setHoveredHandle(null) }} style={hitboxStyle('bottom', { bottom: -3, left: 16, right: 16, height: 8, cursor: 'ns-resize' })} aria-hidden="true" />
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'bottom-right', 'bottom-right') }} onPointerEnter={function () { setHoveredHandle('bottom-right') }} onPointerLeave={function () { setHoveredHandle(null) }} style={hitboxStyle('bottom-right', { bottom: -3, right: -3, width: 12, height: 12, cursor: 'nwse-resize' })} aria-hidden="true" />
</div>
)
}