feat: 8 invisible resize handles on whole perimeter (edges + corners)
This commit is contained in:
1 parent
c0e2fe0358
commit
8a0bf4d8f3
2 files changed
+109
-36
No files matched your search
+81
-36
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user