i18n: English-only UI; hitboxes colored (corners violet, edges per-side) and edges span full side
This commit is contained in:
1 parent
c799c22889
commit
17dd61ae0f
1 file changed
+85
-83
+85
-83
@@ -16,13 +16,13 @@ import { h, Fragment } from 'preact'
|
|||||||
import { useState, useEffect, useRef } from 'preact/hooks'
|
import { useState, useEffect, useRef } from 'preact/hooks'
|
||||||
|
|
||||||
const MIN_WINDOW_SIZE = { width: 320, height: 200 }
|
const MIN_WINDOW_SIZE = { width: 320, height: 200 }
|
||||||
// Статичного максимума нет: "развернуть" целится в весь viewport (maxViewport из main),
|
// No static max: "maximize" targets the whole viewport (maxViewport from main),
|
||||||
// а верхняя граница drag-ресайза тоже приходит оттуда. useWindowResize ставим очень
|
// and the upper bound of drag-resize also comes from there. useWindowResize max is set
|
||||||
// большой max — реальный кламп делает main через clampWindowSize (bounds*zoom).
|
// very high — actual clamp is done by main via clampWindowSize (bounds*zoom).
|
||||||
|
|
||||||
// Одна строка отладочного вывода: имя переменной из кода (как в main.ts),
|
// One debug output row: variable name from code (as in main.ts),
|
||||||
// опционально — формула расчёта, опционально — подстановка значений в формулу,
|
// optional — calculation formula, optional — value substitution into the formula,
|
||||||
// и само значение (моноширинное, по правому краю).
|
// and the value itself (monospace, right-aligned).
|
||||||
function Field (props: { name: string; formula?: string; subst?: string; value: string | number }) {
|
function Field (props: { name: string; formula?: string; subst?: string; value: string | number }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', columnGap: '10px', rowGap: '1px', alignItems: 'baseline' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', columnGap: '10px', rowGap: '1px', alignItems: 'baseline' }}>
|
||||||
@@ -38,7 +38,7 @@ function Field (props: { name: string; formula?: string; subst?: string; value:
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Секция-карточка: заголовок + список Field.
|
// Section card: title + list of Fields.
|
||||||
function Section (props: { title: string; children: preact.ComponentChildren }) {
|
function Section (props: { title: string; children: preact.ComponentChildren }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: '8px', padding: '8px 10px', border: '1px solid var(--figma-color-border)', borderRadius: '4px', background: 'var(--figma-color-bg-secondary)' }}>
|
<div style={{ marginTop: '8px', padding: '8px 10px', border: '1px solid var(--figma-color-border)', borderRadius: '4px', background: 'var(--figma-color-bg-secondary)' }}>
|
||||||
@@ -51,10 +51,10 @@ function Section (props: { title: string; children: preact.ComponentChildren })
|
|||||||
function Plugin () {
|
function Plugin () {
|
||||||
const [currentWindowSize, setCurrentWindowSize] = useState(MIN_WINDOW_SIZE)
|
const [currentWindowSize, setCurrentWindowSize] = useState(MIN_WINDOW_SIZE)
|
||||||
const resizeRef = useRef({ active: false, dir: '', startX: 0, startY: 0, startW: 0, startH: 0, fixedAxis: 'none' as 'none' | 'x' | 'y' })
|
const resizeRef = useRef({ active: false, dir: '', startX: 0, startY: 0, startW: 0, startH: 0, fixedAxis: 'none' as 'none' | 'x' | 'y' })
|
||||||
// Текущая док-позиция (для детекта "окно в доке у края" в UI). Центр и углы НЕ пишем —
|
// Current dock position (for "window is edge-docked" detection in UI). Center and
|
||||||
// они не считаются доком (окно там просто лежит, не растянуто).
|
// corners are NOT written here — they are not considered docked (window just lies there).
|
||||||
const dockedPosRef = useRef<DockPosition | null>(null)
|
const dockedPosRef = useRef<DockPosition | null>(null)
|
||||||
// Флаг: ресайз выводит окно из дока (на отпускании не ре-докать, очистить dock-состояние).
|
// Flag: resize pulls the window out of dock (on release, do not re-dock, clear dock state).
|
||||||
const exitingDockRef = useRef(false)
|
const exitingDockRef = useRef(false)
|
||||||
const [windowInfo, setWindowInfo] = useState<{
|
const [windowInfo, setWindowInfo] = useState<{
|
||||||
x: number; y: number; width: number; height: number
|
x: number; y: number; width: number; height: number
|
||||||
@@ -67,9 +67,9 @@ function Plugin () {
|
|||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
// Кастомный ресайз по ВСЕМУ периметру (8 невидимых ручек: 4 края + 4 угла).
|
// Custom resize over the WHOLE perimeter (8 handles: 4 edges + 4 corners).
|
||||||
// Левый/верхний края двигают окно (main двигает через reposition), правый/нижний — только resize.
|
// Left/top edges move the window (main moves via reposition), right/bottom — resize only.
|
||||||
// Live-обновление: RESIZE_WINDOW_DRAG во время drag'а, финальный RESIZE_WINDOW по pointerup.
|
// Live update: RESIZE_WINDOW_DRAG during drag, final RESIZE_WINDOW on pointerup.
|
||||||
function startResize (e: PointerEvent, dir: string, fixedAxis: 'none' | 'x' | 'y' = 'none') {
|
function startResize (e: PointerEvent, dir: string, fixedAxis: 'none' | 'x' | 'y' = 'none') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -80,8 +80,8 @@ function Plugin () {
|
|||||||
resizeRef.current.startY = e.clientY
|
resizeRef.current.startY = e.clientY
|
||||||
resizeRef.current.startW = window.innerWidth
|
resizeRef.current.startW = window.innerWidth
|
||||||
resizeRef.current.startH = window.innerHeight
|
resizeRef.current.startH = window.innerHeight
|
||||||
// Сообщаем main зафиксировать якорь (старт-позиция + старт-размер) ДО первого движения.
|
// Tell main to fix the anchor (start position + start size) BEFORE the first move.
|
||||||
// Размер берём из window (UI-поток), т.к. в main-потоке window нет.
|
// Size comes from window (UI thread) since main thread has no window.
|
||||||
emit('RESIZE_START', { dir: dir, startW: window.innerWidth, startH: window.innerHeight, fixedAxis: fixedAxis })
|
emit('RESIZE_START', { dir: dir, startW: window.innerWidth, startH: window.innerHeight, fixedAxis: fixedAxis })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,9 +100,9 @@ function Plugin () {
|
|||||||
const h = window.innerHeight
|
const h = window.innerHeight
|
||||||
setCurrentWindowSize({ width: w, height: h })
|
setCurrentWindowSize({ width: w, height: h })
|
||||||
emit('RESIZE_WINDOW', { width: w, height: h })
|
emit('RESIZE_WINDOW', { width: w, height: h })
|
||||||
// Если ресайз выводил окно из дока — больше не держим док-состояние (чтобы
|
// If resize pulled the window out of dock — stop holding dock state (so polling
|
||||||
// поллинг не переприжимал и RESIZE_WINDOW не ре-докал). Обычный (не из дока) ресайз
|
// does not re-pin and RESIZE_WINDOW does not re-dock). A normal (non-dock) resize
|
||||||
// сам держит centerLocked/dock через applyDock в main.
|
// holds centerLocked/dock itself via applyDock in main.
|
||||||
if (exitingDockRef.current) {
|
if (exitingDockRef.current) {
|
||||||
exitingDockRef.current = false
|
exitingDockRef.current = false
|
||||||
dockedPosRef.current = null
|
dockedPosRef.current = null
|
||||||
@@ -111,15 +111,15 @@ function Plugin () {
|
|||||||
refreshInfo()
|
refreshInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Клик-или-драг на периметральной ручке:
|
// Click-or-drag on a perimeter handle:
|
||||||
// - смещение курсора > CLICK_THRESHOLD px → ресайз (startResize + live drag через глобальный onPointerMove)
|
// - cursor move > CLICK_THRESHOLD px → resize (startResize + live drag via global onPointerMove)
|
||||||
// - иначе (чистый клик) → зависит от позиции:
|
// - otherwise (clean click) → depends on position:
|
||||||
// * угол (top-left/right, bottom-left/right):
|
// * corner (top-left/right, bottom-left/right):
|
||||||
// - окно НЕ в доке у края (dockedPosRef == null) → телепорт в угол (dockWindow)
|
// - window NOT edge-docked (dockedPosRef == null) → teleport to corner (dockWindow)
|
||||||
// - окно В ДОКЕ у края → старт single-axis live-ресайза сжатой оси
|
// - window IS edge-docked → start single-axis live resize of the compressed axis
|
||||||
// (окно "выезжает" в угол, выход из дока), dir = угол, fixedAxis = сжатая ось
|
// (window "slides out" to corner, exits dock), dir = corner, fixedAxis = compressed axis
|
||||||
// * край (top/bottom-center, center-left/right) → телепорт в край (dockWindow)
|
// * edge (top/bottom-center, center-left/right) → teleport to edge (dockWindow)
|
||||||
// Центр НЕ входит в периметр — только кнопкой.
|
// Center is NOT in perimeter — button only.
|
||||||
const CLICK_THRESHOLD = 4
|
const CLICK_THRESHOLD = 4
|
||||||
const isCornerPos = (p: DockPosition) =>
|
const isCornerPos = (p: DockPosition) =>
|
||||||
p === 'top-left' || p === 'top-right' || p === 'bottom-left' || p === 'bottom-right'
|
p === 'top-left' || p === 'top-right' || p === 'bottom-left' || p === 'bottom-right'
|
||||||
@@ -132,7 +132,7 @@ function Plugin () {
|
|||||||
let started = false
|
let started = false
|
||||||
const isCorner = isCornerPos(dockPos)
|
const isCorner = isCornerPos(dockPos)
|
||||||
const docked = dockedPosRef.current !== null
|
const docked = dockedPosRef.current !== null
|
||||||
// Угол в доке: drag = single-axis ресайз сжатой оси (выход из дока). Обычный/не-док — просто ресайз.
|
// Corner in dock: drag = single-axis resize of compressed axis (exit dock). Normal/non-dock = plain resize.
|
||||||
const fixed = (isCorner && docked) ? (window.innerHeight <= 324 ? 'y' : 'x') : 'none'
|
const fixed = (isCorner && docked) ? (window.innerHeight <= 324 ? 'y' : 'x') : 'none'
|
||||||
function move (ev: PointerEvent) {
|
function move (ev: PointerEvent) {
|
||||||
if (started) return
|
if (started) return
|
||||||
@@ -142,10 +142,10 @@ function Plugin () {
|
|||||||
started = true
|
started = true
|
||||||
exitingDockRef.current = isCorner && docked
|
exitingDockRef.current = isCorner && docked
|
||||||
startResize(ev, dir, fixed)
|
startResize(ev, dir, fixed)
|
||||||
// Считаем ресайз от исходной точки (до порога), а не от текущей (>порога).
|
// Count resize from the original point (before threshold), not current (>threshold).
|
||||||
resizeRef.current.startX = startX
|
resizeRef.current.startX = startX
|
||||||
resizeRef.current.startY = startY
|
resizeRef.current.startY = startY
|
||||||
// Дальше drag ведёт глобальный onPointerMove (resizeRef.active уже true).
|
// Drag continues via global onPointerMove (resizeRef.active already true).
|
||||||
}
|
}
|
||||||
function up () {
|
function up () {
|
||||||
window.removeEventListener('pointermove', move)
|
window.removeEventListener('pointermove', move)
|
||||||
@@ -153,15 +153,15 @@ function Plugin () {
|
|||||||
if (started) {
|
if (started) {
|
||||||
onPointerUpResize()
|
onPointerUpResize()
|
||||||
} else {
|
} else {
|
||||||
// Чистый клик.
|
// Clean click.
|
||||||
if (isCorner && docked) {
|
if (isCorner && docked) {
|
||||||
// Угол в доке: свободная ось -> 50% viewport, прижим в угол (выход из дока).
|
// Corner in dock: free axis -> 50% viewport, pin to corner (exit dock).
|
||||||
// Тот же механизм изменения размера, что и у центра (DOCK_HALF в main.ts).
|
// Same resize mechanism as center (DOCK_HALF in main.ts).
|
||||||
emit('DOCK_HALF', { position: dockPos, width: window.innerWidth, height: window.innerHeight })
|
emit('DOCK_HALF', { position: dockPos, width: window.innerWidth, height: window.innerHeight })
|
||||||
dockedPosRef.current = null
|
dockedPosRef.current = null
|
||||||
requestAnimationFrame(function () { requestAnimationFrame(refreshInfo) })
|
requestAnimationFrame(function () { requestAnimationFrame(refreshInfo) })
|
||||||
} else {
|
} else {
|
||||||
// Не-док угол / любой край: телепорт в позицию.
|
// Non-dock corner / any edge: teleport to position.
|
||||||
dockWindow(dockPos)
|
dockWindow(dockPos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ function Plugin () {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Слушаем ответ от main с актуальными x/y (canvas-space) и размерами окна
|
// Listen for the response from main with actual x/y (canvas-space) and window sizes
|
||||||
useEffect(function () {
|
useEffect(function () {
|
||||||
const off = on('WINDOW_INFO', function (info: {
|
const off = on('WINDOW_INFO', function (info: {
|
||||||
x: number; y: number; width: number; height: number
|
x: number; y: number; width: number; height: number
|
||||||
@@ -195,9 +195,9 @@ function Plugin () {
|
|||||||
return function () { off() }
|
return function () { off() }
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Запрашиваем свежие данные об окне у main (после любого действия/старта).
|
// Request fresh window data from main (after any action / on start).
|
||||||
// Передаём РЕАЛЬНЫЙ размер iframe (window.innerWidth/innerHeight) — это то, что
|
// Pass the REAL iframe size (window.innerWidth/innerHeight) — what Figma actually
|
||||||
// Figma реально отрисовала, а не запрошенный lastSize. Иначе gaps будут тавтологией.
|
// rendered, not the requested lastSize. Otherwise gaps would be tautological.
|
||||||
function refreshInfo () {
|
function refreshInfo () {
|
||||||
emit('GET_WINDOW_INFO', {
|
emit('GET_WINDOW_INFO', {
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
@@ -205,9 +205,9 @@ function Plugin () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Копирует все debug-значения (RAW + расчёт) в буфер обмена как текст.
|
// Copies all debug values (RAW + calc) to the clipboard as text.
|
||||||
// Важно: в iframe плагина Figma navigator.clipboard часто недоступен (или его
|
// Important: in the Figma plugin iframe navigator.clipboard is often unavailable (or its
|
||||||
// промис тихо reject'ится), поэтому обязателен fallback через document.execCommand('copy').
|
// promise silently rejects), so a fallback via document.execCommand('copy') is mandatory.
|
||||||
function copyInfo () {
|
function copyInfo () {
|
||||||
if (windowInfo == null) return
|
if (windowInfo == null) return
|
||||||
const i = windowInfo
|
const i = windowInfo
|
||||||
@@ -222,7 +222,7 @@ function Plugin () {
|
|||||||
`relX = ${i.relX} (x − bounds.x)`,
|
`relX = ${i.relX} (x − bounds.x)`,
|
||||||
`relY = ${i.relY} (y − bounds.y)`,
|
`relY = ${i.relY} (y − bounds.y)`,
|
||||||
'',
|
'',
|
||||||
'== РАСЧЁТ · в main.ts ==',
|
'== CALC · in main.ts ==',
|
||||||
`gapL = ${i.gapL} (x − bounds.x)`,
|
`gapL = ${i.gapL} (x − bounds.x)`,
|
||||||
`gapR = ${i.gapR} (bounds.x + bounds.width − (x + w))`,
|
`gapR = ${i.gapR} (bounds.x + bounds.width − (x + w))`,
|
||||||
`gapT = ${i.gapT} (y − bounds.y)`,
|
`gapT = ${i.gapT} (y − bounds.y)`,
|
||||||
@@ -237,23 +237,23 @@ function Plugin () {
|
|||||||
setTimeout(function () { setCopied(false) }, 1200)
|
setTimeout(function () { setCopied(false) }, 1200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Путь 1: современный async Clipboard API (если доступен и сработал)
|
// Path 1: modern async Clipboard API (if available and worked)
|
||||||
if (navigator.clipboard != null && navigator.clipboard.writeText != null) {
|
if (navigator.clipboard != null && navigator.clipboard.writeText != null) {
|
||||||
const apiResult = navigator.clipboard.writeText(text)
|
const apiResult = navigator.clipboard.writeText(text)
|
||||||
if (apiResult != null && typeof apiResult.then === 'function') {
|
if (apiResult != null && typeof apiResult.then === 'function') {
|
||||||
apiResult.then(function () { markCopied() }).catch(function () {
|
apiResult.then(function () { markCopied() }).catch(function () {
|
||||||
// тихо — переходим к fallback
|
// silent — fall back
|
||||||
fallbackCopy(text, markCopied)
|
fallbackCopy(text, markCopied)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Путь 2: синхронный fallback для iframe/небезопасных контекстов
|
// Path 2: synchronous fallback for iframe/non-secure contexts
|
||||||
fallbackCopy(text, markCopied)
|
fallbackCopy(text, markCopied)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback-копирование через временный <textarea> + document.execCommand('copy').
|
// Fallback copy via a temp <textarea> + document.execCommand('copy').
|
||||||
// Работает там, где navigator.clipboard недоступен (iframe Figma, file://, не HTTPS).
|
// Works where navigator.clipboard is unavailable (Figma iframe, file://, non-HTTPS).
|
||||||
function fallbackCopy (text: string, onDone: () => void) {
|
function fallbackCopy (text: string, onDone: () => void) {
|
||||||
try {
|
try {
|
||||||
const ta = document.createElement('textarea')
|
const ta = document.createElement('textarea')
|
||||||
@@ -270,12 +270,12 @@ function Plugin () {
|
|||||||
onDone()
|
onDone()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// оба пути не сработали — ничего не делаем (индикации нет)
|
// both paths failed — do nothing (no indication)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем цифры размера и инфо ТОЛЬКО после окончания drag'а (отпускание кнопки).
|
// Update size numbers and info ONLY after the drag ends (button release).
|
||||||
// К моменту pointerup Figma уже применила новый размер окна.
|
// By pointerup Figma has already applied the new window size.
|
||||||
useEffect(function () {
|
useEffect(function () {
|
||||||
const handlePointerUp = function () {
|
const handlePointerUp = function () {
|
||||||
setCurrentWindowSize({
|
setCurrentWindowSize({
|
||||||
@@ -290,16 +290,16 @@ function Plugin () {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Первичное заполнение инфо при старте плагина.
|
// Initial info fill on plugin start.
|
||||||
useEffect(function () {
|
useEffect(function () {
|
||||||
refreshInfo()
|
refreshInfo()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
function toggleWindowSize () {
|
function toggleWindowSize () {
|
||||||
// "Развернуть" целится в геометрию видимого viewport (maxViewport = bounds*zoom,
|
// "Maximize" targets the visible viewport geometry (maxViewport = bounds*zoom,
|
||||||
// без вычетов шапки/рамок). Figma сама клампнет запрос до своего реального лимита
|
// without subtracting header/frame). Figma clamps the request to its real window
|
||||||
// окна плагина (зависит от того, спрятан ли UI Figma). Реальный размер потом
|
// limit (depends on whether Figma UI is hidden). The real size is then measured
|
||||||
// измеряется через iframe (window.innerWidth/Height) и показывается как lastSize/gaps.
|
// via the iframe (window.innerWidth/Height) and shown as lastSize/gaps.
|
||||||
const max = windowInfo != null ? windowInfo.maxViewport : { width: 1920, height: 1080 }
|
const max = windowInfo != null ? windowInfo.maxViewport : { width: 1920, height: 1080 }
|
||||||
const isMaximized =
|
const isMaximized =
|
||||||
currentWindowSize.width >= max.width && currentWindowSize.height >= max.height
|
currentWindowSize.width >= max.width && currentWindowSize.height >= max.height
|
||||||
@@ -309,29 +309,29 @@ function Plugin () {
|
|||||||
refreshInfo()
|
refreshInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9 позиций докинга: 4 угла + 4 края + центр.
|
// 9 dock positions: 4 corners + 4 edges + center.
|
||||||
type DockPosition =
|
type DockPosition =
|
||||||
| 'top-left' | 'top-center' | 'top-right'
|
| 'top-left' | 'top-center' | 'top-right'
|
||||||
| 'center-left' | 'center' | 'center-right'
|
| 'center-left' | 'center' | 'center-right'
|
||||||
| 'bottom-left' | 'bottom-center' | 'bottom-right'
|
| 'bottom-left' | 'bottom-center' | 'bottom-right'
|
||||||
|
|
||||||
function dockWindow (position: DockPosition) {
|
function dockWindow (position: DockPosition) {
|
||||||
// Запоминаем док-позицию только для КРАЁВ (растянуто у края). Углы и центр — не док
|
// Remember dock position only for EDGES (stretched at edge). Corners and center are
|
||||||
// (окно там просто лежит), поэтому dockedPosRef оставляем как есть / не трогаем.
|
// not dock (window just lies there), so leave dockedPosRef as-is / untouched.
|
||||||
const isEdge = position === 'top-center' || position === 'bottom-center' ||
|
const isEdge = position === 'top-center' || position === 'bottom-center' ||
|
||||||
position === 'center-left' || position === 'center-right'
|
position === 'center-left' || position === 'center-right'
|
||||||
if (isEdge) dockedPosRef.current = position
|
if (isEdge) dockedPosRef.current = position
|
||||||
else if (position !== 'center') dockedPosRef.current = null
|
else if (position !== 'center') dockedPosRef.current = null
|
||||||
// Этап 1: просим main растянуть (попытка до канона) и запомнить режим.
|
// Stage 1: ask main to stretch (attempt to canon) and remember the mode.
|
||||||
// main НЕ позиционирует сразу — позиция считается на этапе 2 от РЕАЛЬНОГО
|
// main does NOT position immediately — position is computed at stage 2 from the REAL
|
||||||
// размера, который Figma отрисует после клампа (видимый UI режет запрос).
|
// size Figma renders after clamp (visible UI cuts the request).
|
||||||
emit('DOCK_WINDOW', {
|
emit('DOCK_WINDOW', {
|
||||||
position,
|
position,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight
|
height: window.innerHeight
|
||||||
})
|
})
|
||||||
// Этап 2: через 2 кадра Figma уже применила resize и кламп — читаем реальный
|
// Stage 2: after 2 frames Figma already applied resize and clamp — read the real
|
||||||
// размер из iframe и шлём на точную прижимку (reposition от реального размера).
|
// size from iframe and send for precise pin (reposition from real size).
|
||||||
requestAnimationFrame(function () {
|
requestAnimationFrame(function () {
|
||||||
requestAnimationFrame(function () {
|
requestAnimationFrame(function () {
|
||||||
emit('DOCK_REFINE', {
|
emit('DOCK_REFINE', {
|
||||||
@@ -345,10 +345,10 @@ function Plugin () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const max = windowInfo != null ? windowInfo.maxViewport : null
|
const max = windowInfo != null ? windowInfo.maxViewport : null
|
||||||
// "Развернуто" считаем, если окно достигло (или превысило) геометрию viewport
|
// "Maximized" if the window reached (or exceeded) the viewport geometry
|
||||||
// (maxViewport = bounds*zoom). Figma клампнет запрос до своего лимита, поэтому
|
// (maxViewport = bounds*zoom). Figma clamps the request to its limit, so the real
|
||||||
// реальный lastSize может быть чуть меньше maxViewport — при достижении потолка
|
// lastSize may be slightly less than maxViewport — at the ceiling currentWindowSize
|
||||||
// currentWindowSize будет >= maxViewport и иконка покажет "свернуть".
|
// will be >= maxViewport and the icon will show "restore".
|
||||||
const isMaximized =
|
const isMaximized =
|
||||||
max != null &&
|
max != null &&
|
||||||
currentWindowSize.width >= max.width &&
|
currentWindowSize.width >= max.width &&
|
||||||
@@ -361,19 +361,19 @@ function Plugin () {
|
|||||||
<Muted>{currentWindowSize.width} × {currentWindowSize.height}</Muted>
|
<Muted>{currentWindowSize.width} × {currentWindowSize.height}</Muted>
|
||||||
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
title="Копировать значения переменных"
|
title="Copy variable values"
|
||||||
onClick={copyInfo}
|
onClick={copyInfo}
|
||||||
>
|
>
|
||||||
{copied ? <IconCheck24 /> : <IconCopySmall24 />}
|
{copied ? <IconCheck24 /> : <IconCopySmall24 />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
title="Центр окна (50% viewport)"
|
title="Center window (50% viewport)"
|
||||||
onClick={function () { dockWindow('center') }}
|
onClick={function () { dockWindow('center') }}
|
||||||
>
|
>
|
||||||
<IconDockToSide24 />
|
<IconDockToSide24 />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
title={isMaximized ? 'Свернуть окно' : 'Развернуть окно'}
|
title={isMaximized ? 'Restore window' : 'Maximize window'}
|
||||||
onClick={toggleWindowSize}
|
onClick={toggleWindowSize}
|
||||||
>
|
>
|
||||||
{isMaximized ? <IconCollapse24 /> : <IconExpand24 />}
|
{isMaximized ? <IconCollapse24 /> : <IconExpand24 />}
|
||||||
@@ -394,7 +394,7 @@ function Plugin () {
|
|||||||
<Muted>AI assistant (MVP) — bootstrap</Muted>
|
<Muted>AI assistant (MVP) — bootstrap</Muted>
|
||||||
{windowInfo != null && (
|
{windowInfo != null && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{/* RAW — значения, которые реально возвращает Figma API (без преобразований) */}
|
{/* RAW — values actually returned by the Figma API (no transformation) */}
|
||||||
<Section title="RAW · Figma API">
|
<Section title="RAW · Figma API">
|
||||||
<Field name="figma.ui.getPosition().canvasSpace.x" value={windowInfo.x} />
|
<Field name="figma.ui.getPosition().canvasSpace.x" value={windowInfo.x} />
|
||||||
<Field name="figma.ui.getPosition().canvasSpace.y" value={windowInfo.y} />
|
<Field name="figma.ui.getPosition().canvasSpace.y" value={windowInfo.y} />
|
||||||
@@ -403,11 +403,11 @@ function Plugin () {
|
|||||||
<Field name="lastSize.width" value={windowInfo.width} />
|
<Field name="lastSize.width" value={windowInfo.width} />
|
||||||
<Field name="lastSize.height" value={windowInfo.height} />
|
<Field name="lastSize.height" value={windowInfo.height} />
|
||||||
<Field name="maxViewport (bounds·zoom)" formula="bounds.width*zoom, bounds.height*zoom" value={`${windowInfo.maxViewport.width}×${windowInfo.maxViewport.height}`} />
|
<Field name="maxViewport (bounds·zoom)" formula="bounds.width*zoom, bounds.height*zoom" value={`${windowInfo.maxViewport.width}×${windowInfo.maxViewport.height}`} />
|
||||||
<Field name="relX (x − bounds.x)" formula="нормализованная X окна в viewport" value={windowInfo.relX} />
|
<Field name="relX (x − bounds.x)" formula="normalized X of window in viewport" value={windowInfo.relX} />
|
||||||
<Field name="relY (y − bounds.y)" formula="нормализованная Y окна в viewport" value={windowInfo.relY} />
|
<Field name="relY (y − bounds.y)" formula="normalized Y of window in viewport" value={windowInfo.relY} />
|
||||||
</Section>
|
</Section>
|
||||||
{/* Расчётные — посчитаны в main.ts из вышеперечисленных raw-значений */}
|
{/* Computed values — calculated in main.ts from the raw values above */}
|
||||||
<Section title="РАСЧЁТ · в main.ts">
|
<Section title="CALC · in main.ts">
|
||||||
<Field
|
<Field
|
||||||
name="gapL"
|
name="gapL"
|
||||||
formula="x − bounds.x"
|
formula="x − bounds.x"
|
||||||
@@ -449,8 +449,8 @@ function Plugin () {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
{/* Декоративная иконка drag-хендла в правом нижнем углу (как в старом плагине).
|
{/* Decorative drag-handle icon in the bottom-right corner (as in the old plugin).
|
||||||
pointerEvents:none — сам drag ловит невидимый div от useWindowResize. */}
|
pointerEvents:none — the actual drag is caught by the invisible corner div. */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
@@ -471,15 +471,17 @@ 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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{/* Ручки по периметру (4 края + 4 угла) — клик = док в эту позицию.
|
{/* Perimeter hitboxes (4 edges + 4 corners). Visible for debugging.
|
||||||
ВИДИМЫЕ для отладки (фиолетовые). Центр НЕ входит в периметр — только кнопкой. */}
|
Corners = violet. Edges colored per side: top=red, bottom=green, left=blue, right=orange.
|
||||||
|
Edge handles span the FULL side length (left:0;right:0 / top:0;bottom:0).
|
||||||
|
Center is button-only (not in perimeter). */}
|
||||||
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'top-left', 'top-left') }} style={{ position: 'fixed', top: -3, left: -3, width: 12, height: 12, zIndex: 3, background: 'rgba(211,168,255,0.6)', cursor: 'nwse-resize' }} aria-hidden="true" />
|
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'top-left', 'top-left') }} style={{ position: 'fixed', top: -3, left: -3, width: 12, height: 12, zIndex: 3, background: 'rgba(211,168,255,0.6)', cursor: 'nwse-resize' }} aria-hidden="true" />
|
||||||
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'top', 'top-center') }} style={{ position: 'fixed', top: -3, left: '50%', transform: 'translateX(-50%)', width: 32, height: 10, zIndex: 3, background: 'rgba(211,168,255,0.6)', cursor: 'ns-resize' }} aria-hidden="true" />
|
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'top', 'top-center') }} style={{ position: 'fixed', top: -3, left: 0, right: 0, height: 8, zIndex: 3, background: 'rgba(255,99,99,0.5)', cursor: 'ns-resize' }} aria-hidden="true" />
|
||||||
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'top-right', 'top-right') }} style={{ position: 'fixed', top: -3, right: -3, width: 12, height: 12, zIndex: 3, background: 'rgba(211,168,255,0.6)', cursor: 'nesw-resize' }} aria-hidden="true" />
|
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'top-right', 'top-right') }} style={{ position: 'fixed', top: -3, right: -3, width: 12, height: 12, zIndex: 3, background: 'rgba(211,168,255,0.6)', cursor: 'nesw-resize' }} aria-hidden="true" />
|
||||||
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'left', 'center-left') }} style={{ position: 'fixed', top: '50%', left: -3, transform: 'translateY(-50%)', width: 10, height: 32, zIndex: 3, background: 'rgba(211,168,255,0.6)', cursor: 'ew-resize' }} aria-hidden="true" />
|
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'left', 'center-left') }} style={{ position: 'fixed', top: 0, bottom: 0, left: -3, width: 8, zIndex: 3, background: 'rgba(99,149,255,0.5)', cursor: 'ew-resize' }} aria-hidden="true" />
|
||||||
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'right', 'center-right') }} style={{ position: 'fixed', top: '50%', right: -3, transform: 'translateY(-50%)', width: 10, height: 32, zIndex: 3, background: 'rgba(211,168,255,0.6)', cursor: 'ew-resize' }} aria-hidden="true" />
|
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'right', 'center-right') }} style={{ position: 'fixed', top: 0, bottom: 0, right: -3, width: 8, zIndex: 3, background: 'rgba(255,165,0,0.5)', cursor: 'ew-resize' }} aria-hidden="true" />
|
||||||
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'bottom-left', 'bottom-left') }} style={{ position: 'fixed', bottom: -3, left: -3, width: 12, height: 12, zIndex: 3, background: 'rgba(211,168,255,0.6)', cursor: 'nesw-resize' }} aria-hidden="true" />
|
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'bottom-left', 'bottom-left') }} style={{ position: 'fixed', bottom: -3, left: -3, width: 12, height: 12, zIndex: 3, background: 'rgba(211,168,255,0.6)', cursor: 'nesw-resize' }} aria-hidden="true" />
|
||||||
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'bottom', 'bottom-center') }} style={{ position: 'fixed', bottom: -3, left: '50%', transform: 'translateX(-50%)', width: 32, height: 10, zIndex: 3, background: 'rgba(211,168,255,0.6)', cursor: 'ns-resize' }} aria-hidden="true" />
|
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'bottom', 'bottom-center') }} style={{ position: 'fixed', bottom: -3, left: 0, right: 0, height: 8, zIndex: 3, background: 'rgba(99,199,99,0.5)', cursor: 'ns-resize' }} aria-hidden="true" />
|
||||||
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'bottom-right', 'bottom-right') }} style={{ position: 'fixed', bottom: -3, right: -3, width: 12, height: 12, zIndex: 3, background: 'rgba(211,168,255,0.6)', cursor: 'nwse-resize' }} aria-hidden="true" />
|
<div onPointerDown={function (e: PointerEvent) { onHandleDown(e, 'bottom-right', 'bottom-right') }} style={{ position: 'fixed', bottom: -3, right: -3, width: 12, height: 12, zIndex: 3, background: 'rgba(211,168,255,0.6)', cursor: 'nwse-resize' }} aria-hidden="true" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user