import { render, Container, Text, Muted, VerticalSpace, Button, IconButton, IconExpand24, IconCollapse24, IconCheck24, IconCopySmall24, useWindowResize } from '@create-figma-plugin/ui' import { emit, on } from '@create-figma-plugin/utilities' import { h, Fragment } from 'preact' import { useState, useEffect } from 'preact/hooks' const MIN_WINDOW_SIZE = { width: 320, height: 200 } // Статичного максимума нет: "развернуть" целится в весь viewport (maxViewport из main), // а верхняя граница drag-ресайза тоже приходит оттуда. useWindowResize ставим очень // большой max — реальный кламп делает main через clampWindowSize (bounds*zoom). // Одна строка отладочного вывода: имя переменной из кода (как в main.ts), // опционально — формула расчёта, и само значение (моноширинное, по правому краю). function Field (props: { name: string; formula?: string; value: string | number }) { return (
{props.name} {props.value} {props.formula != null && ( {props.formula} )}
) } // Секция-карточка: заголовок + список Field. function Section (props: { title: string; children: preact.ComponentChildren }) { return (
{props.title}
{props.children}
) } function Plugin () { const [currentWindowSize, setCurrentWindowSize] = useState(MIN_WINDOW_SIZE) const [windowInfo, setWindowInfo] = useState<{ x: number; y: number; width: number; height: number zoom: number viewport: { x: number; y: number; width: number; height: number } maxViewport: { width: number; height: number } gapL: number; gapR: number; gapT: number; gapB: number center: { x: number; y: number } } | 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 }) // Слушаем ответ от main с актуальными x/y (canvas-space) и размерами окна useEffect(function () { const off = on('WINDOW_INFO', function (info: { x: number; y: number; width: number; height: number zoom: number viewport: { x: number; y: number; width: number; height: number } maxViewport: { width: number; height: number } gapL: number; gapR: number; gapT: number; gapB: number center: { x: number; y: number } }) { setWindowInfo(info) }) return function () { off() } }, []) // Запрашиваем свежие данные об окне у main (после любого действия/старта). // Передаём РЕАЛЬНЫЙ размер iframe (window.innerWidth/innerHeight) — это то, что // Figma реально отрисовала, а не запрошенный lastSize. Иначе gaps будут тавтологией. function refreshInfo () { emit('GET_WINDOW_INFO', { width: window.innerWidth, height: window.innerHeight }) } // Копирует все debug-значения (RAW + расчёт) в буфер обмена как текст. // Важно: в iframe плагина Figma navigator.clipboard часто недоступен (или его // промис тихо reject'ится), поэтому обязателен fallback через document.execCommand('copy'). function copyInfo () { if (windowInfo == null) return const i = windowInfo const lines = [ '== RAW · Figma API ==', `figma.ui.getPosition().canvasSpace.x = ${i.x}`, `figma.ui.getPosition().canvasSpace.y = ${i.y}`, `figma.viewport.zoom = ${i.zoom}`, `figma.viewport.bounds = ${i.viewport.x},${i.viewport.y} · ${i.viewport.width}×${i.viewport.height}`, `lastSize.width = ${i.width}`, `lastSize.height = ${i.height}`, '', '== РАСЧЁТ · в main.ts ==', `gapL = ${i.gapL} (x − bounds.x)`, `gapR = ${i.gapR} (bounds.x + bounds.width − (x + w))`, `gapT = ${i.gapT} (y − bounds.y)`, `gapB = ${i.gapB} (bounds.y + bounds.height − (y + h))`, `center.x = ${i.center.x} (x + w / 2)`, `center.y = ${i.center.y} (y + h / 2)` ] const text = lines.join('\n') function markCopied () { setCopied(true) setTimeout(function () { setCopied(false) }, 1200) } // Путь 1: современный async Clipboard API (если доступен и сработал) if (navigator.clipboard != null && navigator.clipboard.writeText != null) { const apiResult = navigator.clipboard.writeText(text) if (apiResult != null && typeof apiResult.then === 'function') { apiResult.then(function () { markCopied() }).catch(function () { // тихо — переходим к fallback fallbackCopy(text, markCopied) }) return } } // Путь 2: синхронный fallback для iframe/небезопасных контекстов fallbackCopy(text, markCopied) } // Fallback-копирование через временный