diff --git a/src/main.ts b/src/main.ts index dbc6338..163119c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -121,6 +121,34 @@ export default function () { } } + // Live-ресайз во время drag'а (кастомные ручки по периметру в UI). + // dir + dx/dy (смещение курсора в px). Для right/bottom — только resize. + // Для left/top — resize + сдвиг окна через reposition (иначе окно "уедет" вправо/вниз). + function getPos () { + try { + const p = (figma.ui as any).getPosition() + return { x: Math.round(p.canvasSpace.x), y: Math.round(p.canvasSpace.y) } + } catch (e) { + return { x: 0, y: 0 } + } + } + on('RESIZE_WINDOW_DRAG', function (payload: { width: number; height: number; dx: number; dy: number; dir: string }) { + const { width, height, dx, dy, dir } = payload + const size = clampWindowSize({ width, height }) + figma.ui.resize(size.width, size.height) + lastSize = { width: size.width, height: size.height } + if (dir.includes('left') || dir.includes('top')) { + const zoom = figma.viewport.zoom || 1 + const p = getPos() + let nx = p.x + let ny = p.y + if (dir.includes('left')) nx = p.x - dx / zoom + if (dir.includes('top')) ny = p.y - dy / zoom + figma.ui.reposition(nx, ny) + } + // Док-режим во время live-ресайза не трогаем (финальный RESIZE_WINDOW пересчитает). + }) + // Ресайз окна плагина (drag за угол в UI -> useWindowResize -> RESIZE_WINDOW) on('RESIZE_WINDOW', function (windowSize: { width: number; height: number }) { const { width, height } = clampWindowSize(windowSize) diff --git a/src/ui.tsx b/src/ui.tsx index 5d07041..53310e9 100644 --- a/src/ui.tsx +++ b/src/ui.tsx @@ -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 () { {/* Декоративная иконка drag-хендла в правом нижнем углу (как в старом плагине). - pointerEvents:none — сам drag ловит невидимый div от useWindowResize. */} + pointerEvents:none — она не ловит события. */}
+ {/* Невидимые интерактивные области ресайза по всему периметру (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 ( + + ) + })} ) }