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-копирование через временный