feat: 8 invisible resize handles on whole perimeter (edges + corners)

This commit is contained in:
Hermes Agent committed 2026-09-09 19:19:26 +02:00
1 parent c0e2fe0358
commit 8a0bf4d8f3
2 files changed
+109 -36

No files matched your search

+81 -36
View File
@@ -9,12 +9,11 @@ import {
IconExpand24,
IconCollapse24,
IconCheck24,
IconCopySmall24,
useWindowResize
IconCopySmall24
} from '@create-figma-plugin/ui'
import { emit, on } from '@create-figma-plugin/utilities'
import { h, Fragment } from 'preact'
import { useState, useEffect } from 'preact/hooks'
import { useState, useEffect, useRef } from 'preact/hooks'
const MIN_WINDOW_SIZE = { width: 320, height: 200 }
// Статичного максимума нет: "развернуть" целится в весь viewport (maxViewport из main),
@@ -51,6 +50,7 @@ function Section (props: { title: string; children: preact.ComponentChildren })
function Plugin () {
const [currentWindowSize, setCurrentWindowSize] = useState(MIN_WINDOW_SIZE)
const resizeRef = useRef({ active: false, dir: '', startX: 0, startY: 0, startW: 0, startH: 0 })
const [windowInfo, setWindowInfo] = useState<{
x: number; y: number; width: number; height: number
zoom: number
@@ -62,22 +62,56 @@ function Plugin () {
} | null>(null)
const [copied, setCopied] = useState(false)
// Нативный drag-ресайз окна Figma (тянуть за правый нижний угол).
// Опции min/max обязательны: без них хук берёт базу из window.innerWidth на момент
// маунта (окно ещё не приняло размер от showUI) → первый drag не работает.
// В колбэке шлём размер в main (figma.ui.resize), чтобы окно тянулось вживую.
// State с цифрами НЕ трогаем здесь — обновляем только после отпускания (pointerup).
useWindowResize(function (windowSize: { width: number; height: number }) {
const size = { width: windowSize.width, height: windowSize.height }
emit('RESIZE_WINDOW', size)
}, {
minWidth: 320,
minHeight: 200,
// Верхний предел = 2000 (как запрос разворачивания). Реальный кламп до доступного
// Figma делает в main через clampWindowSize (bounds * zoom) — здесь просто потолок drag.
maxWidth: 2000,
maxHeight: 2000
})
// Кастомный ресайз по ВСЕМУ периметру (8 ручек: 4 края + 4 угла), а не только
// снизу-справа (что даёт нативный useWindowResize). Левый/верхний края двигают окно
// через figma.ui.reposition, чтобы окно не "уезжало" при расширении влево/вверх.
// В процессе драга шлём RESIZE_WINDOW_DRAG (live), по pointerup — финальный RESIZE_WINDOW.
function startResize (e: PointerEvent, dir: string) {
e.preventDefault()
e.stopPropagation()
resizeRef.current.active = true
resizeRef.current.dir = dir
resizeRef.current.startX = e.clientX
resizeRef.current.startY = e.clientY
resizeRef.current.startW = window.innerWidth
resizeRef.current.startH = window.innerHeight
}
function onPointerMove (e: PointerEvent) {
if (!resizeRef.current.active) return
const dir = resizeRef.current.dir
const dx = e.clientX - resizeRef.current.startX
const dy = e.clientY - resizeRef.current.startY
let w = resizeRef.current.startW
let h = resizeRef.current.startH
if (dir.includes('right')) w = resizeRef.current.startW + dx
if (dir.includes('left')) w = resizeRef.current.startW - dx
if (dir.includes('bottom')) h = resizeRef.current.startH + dy
if (dir.includes('top')) h = resizeRef.current.startH - dy
w = Math.max(320, w)
h = Math.max(200, h)
setCurrentWindowSize({ width: w, height: h })
emit('RESIZE_WINDOW_DRAG', { width: w, height: h, dx, dy, dir })
}
function onPointerUpResize () {
if (!resizeRef.current.active) return
resizeRef.current.active = false
const w = window.innerWidth
const h = window.innerHeight
setCurrentWindowSize({ width: w, height: h })
emit('RESIZE_WINDOW', { width: w, height: h })
refreshInfo()
}
useEffect(function () {
window.addEventListener('pointermove', onPointerMove)
window.addEventListener('pointerup', onPointerUpResize)
return function () {
window.removeEventListener('pointermove', onPointerMove)
window.removeEventListener('pointerup', onPointerUpResize)
}
}, [])
// Слушаем ответ от main с актуальными x/y (canvas-space) и размерами окна
useEffect(function () {
@@ -174,22 +208,6 @@ function Plugin () {
}
}
// Обновляем цифры размера и инфо ТОЛЬКО после окончания drag'а (отпускание кнопки).
// К моменту pointerup Figma уже применила новый размер окна.
useEffect(function () {
const handlePointerUp = function () {
setCurrentWindowSize({
width: window.innerWidth,
height: window.innerHeight
})
refreshInfo()
}
window.addEventListener('pointerup', handlePointerUp)
return function () {
window.removeEventListener('pointerup', handlePointerUp)
}
}, [])
// Первичное заполнение инфо при старте плагина.
useEffect(function () {
refreshInfo()
@@ -335,7 +353,7 @@ function Plugin () {
</div>
</Container>
{/* Декоративная иконка drag-хендла в правом нижнем углу (как в старом плагине).
pointerEvents:none — сам drag ловит невидимый div от useWindowResize. */}
pointerEvents:none — она не ловит события. */}
<div
style={{
position: 'fixed',
@@ -356,6 +374,33 @@ 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>
{/* Невидимые интерактивные области ресайза по всему периметру (8 ручек: 4 края + 4 угла).
Нативный useWindowResize даёт только bottom-right; эти div-ы ловят pointerdown и шлют
dir + dx/dy в main, для left/top main двигает окно через reposition. Сами ручки прозрачные. */}
{[
{ dir: 'top-left', style: { top: -3, left: -3, width: 12, height: 12, cursor: 'nwse-resize' } },
{ dir: 'top', style: { top: -3, left: '50%', transform: 'translateX(-50%)', width: 32, height: 10, cursor: 'ns-resize' } },
{ dir: 'top-right', style: { top: -3, right: -3, width: 12, height: 12, cursor: 'nesw-resize' } },
{ dir: 'left', style: { top: '50%', left: -3, transform: 'translateY(-50%)', width: 10, height: 32, cursor: 'ew-resize' } },
{ dir: 'right', style: { top: '50%', right: -3, transform: 'translateY(-50%)', width: 10, height: 32, cursor: 'ew-resize' } },
{ dir: 'bottom-left', style: { bottom: -3, left: -3, width: 12, height: 12, cursor: 'nesw-resize' } },
{ dir: 'bottom', style: { bottom: -3, left: '50%', transform: 'translateX(-50%)', width: 32, height: 10, cursor: 'ns-resize' } },
{ dir: 'bottom-right', style: { bottom: -3, right: -3, width: 12, height: 12, cursor: 'nwse-resize' } }
].map(function (h) {
return (
<div
key={h.dir}
onPointerDown={function (e: PointerEvent) { startResize(e, h.dir) }}
style={{
position: 'fixed',
zIndex: 3,
background: 'transparent',
...h.style
}}
aria-hidden="true"
/>
)
})}
</Fragment>
)
}