feat: persist window placement across restarts via figma.clientStorage (dock|free, default=center)

This commit is contained in:
Hermes Agent committed 2026-09-11 13:46:02 +02:00
1 parent fda4be1765
commit ce20357098
1 file changed
+113 -6
+113 -6
View File
@@ -35,6 +35,24 @@ export default function () {
// bottom panel/toolbar stays visible instead of being covered by the plugin window.
const DOCK_SIDE_BOTTOM_INSET = 70
let dockEdgeSize = DEFAULT_DOCK_EDGE // редактируемая фикс-сторона для edge-позиций (320 по умолч.)
// Persistent window placement across plugin restarts.
// Stored on the Figma file via clientStorage (survives close/reopen). Saved after every
// action and periodically (to catch the native Figma drag-by-titlebar the plugin can't see).
const PLACEMENT_KEY = 'figma-ai-plugin:placement'
type Placement =
| { mode: 'dock'; position: DockPosition }
| { mode: 'free'; x: number; y: number; w: number; h: number }
let saveTimer: ReturnType<typeof setTimeout> | null = null
function savePlacement (p: Placement) {
try {
figma.clientStorage.setAsync(PLACEMENT_KEY, p)
} catch (e) { /* storage unavailable — ignore */ }
}
function savePlacementDebounced (p: Placement) {
if (saveTimer != null) clearTimeout(saveTimer)
saveTimer = setTimeout(function () { savePlacement(p) }, 400)
}
// computeDock считает целевой размер+позицию для док-позиции.
// canonical=true — НАЖАТИЕ кнопки дока: задаём канонический размер
// (edge: фикс-сторона dockEdgeSize, другая = max viewport; center: 50%).
@@ -124,14 +142,34 @@ export default function () {
// События viewportchange в Figma API НЕТ (нельзя подписаться через figma.on),
// поэтому опрашиваем bounds и пере-применяем док только когда он реально изменился.
let lastBoundsKey = ''
let lastNativePosKey = '' // canvas-space key of last native-drag position (to detect titlebar drags)
const dockInterval = setInterval(function () {
if (dockedPosition === null) return
const b = figma.viewport.bounds
const key = `${b.x}|${b.y}|${b.width}|${b.height}`
if (key !== lastBoundsKey) {
lastBoundsKey = key
applyDock(dockedPosition, lastSize.width, lastSize.height, false)
if (dockedPosition !== null) {
const b = figma.viewport.bounds
const key = `${b.x}|${b.y}|${b.width}|${b.height}`
if (key !== lastBoundsKey) {
lastBoundsKey = key
applyDock(dockedPosition, lastSize.width, lastSize.height, false)
}
return
}
// Not docked: track native drag (moving the window by its Figma titlebar is invisible
// to our pointer handlers). When the canvas-space position changes, persist it as a
// free placement so it survives the next plugin restart.
try {
const pos = (figma.ui as any).getPosition()
const key = `${Math.round(pos.canvasSpace.x)}|${Math.round(pos.canvasSpace.y)}|${lastSize.width}|${lastSize.height}`
if (key !== lastNativePosKey) {
lastNativePosKey = key
savePlacementDebounced({
mode: 'free',
x: Math.round(pos.canvasSpace.x),
y: Math.round(pos.canvasSpace.y),
w: lastSize.width,
h: lastSize.height
})
}
} catch (e) { /* getPosition unavailable — skip */ }
}, 250)
// Кламп размера окна: не меньше минимума и не больше видимого viewport.
@@ -220,6 +258,19 @@ export default function () {
}
if (dockedPosition !== null) {
applyDock(dockedPosition, width, height, false)
savePlacement({ mode: 'dock', position: dockedPosition })
} else {
// Free (or center one-shot): save the actual canvas-space position + size.
try {
const pos = (figma.ui as any).getPosition()
savePlacement({
mode: 'free',
x: Math.round(pos.canvasSpace.x),
y: Math.round(pos.canvasSpace.y),
w: width,
h: height
})
} catch (e) { /* getPosition unavailable — skip */ }
}
})
@@ -254,6 +305,8 @@ export default function () {
figma.ui.resize(state.width, state.height)
lastSize = { width: clampWindowSize(state).width, height: clampWindowSize(state).height }
// reposition откладываем на DOCK_REFINE (UI пришлёт реальный размер после клампа).
// Remember placement (center is one-shot but we still persist it as a logical position).
savePlacement({ mode: 'dock', position: position })
})
// Точная прижимка от РЕАЛЬНОГО размера окна (после клампа Figma).
@@ -286,6 +339,17 @@ export default function () {
lastSize = { width: size.width, height: size.height }
// Прижимаем в угол от реального размера (computeDock для угла не меняет размер).
applyDock(position, size.width, size.height, false)
// Exit from dock → free placement at the corner, persisted.
try {
const pos = (figma.ui as any).getPosition()
savePlacement({
mode: 'free',
x: Math.round(pos.canvasSpace.x),
y: Math.round(pos.canvasSpace.y),
w: size.width,
h: size.height
})
} catch (e) { /* getPosition unavailable — skip */ }
})
// Останавливаем поллинг при закрытии плагина, чтобы не висел в памяти.
@@ -368,4 +432,47 @@ export default function () {
})
showUI({ width: 320, height: 200, themeColors: true, title: 'AI assistant (MVP)' })
// Restore persisted window placement (saved in clientStorage on the Figma file).
// Default when nothing is stored: behave like the Center button (50% viewport, centered).
try {
figma.clientStorage.getAsync(PLACEMENT_KEY).then(function (saved: Placement | undefined) {
if (saved == null) {
// No saved placement → default to center (one-shot, not held).
const b = figma.viewport.bounds
const zoom = figma.viewport.zoom || 1
const maxW = Math.round(b.width * zoom)
const maxH = Math.round(b.height * zoom) - PLUGIN_HEADER_PX
const cw = Math.round(maxW / 2)
const ch = Math.round(maxH / 2)
figma.ui.resize(cw, ch)
lastSize = { width: cw, height: ch }
requestAnimationFrame(function () {
requestAnimationFrame(function () {
applyDock('center', cw, ch, false)
})
})
return
}
if (saved.mode === 'dock') {
// Dock mode (incl. center): stretch to its canonical size and pin.
// For center the window is NOT held (one-shot); for edges it stays docked.
dockedPosition = saved.position === 'center' ? null : saved.position
const state = computeDock(saved.position, 320, 200, true)
figma.ui.resize(state.width, state.height)
lastSize = { width: clampWindowSize(state).width, height: clampWindowSize(state).height }
requestAnimationFrame(function () {
requestAnimationFrame(function () {
applyDock(saved.position, lastSize.width, lastSize.height, false)
lastBoundsKey = '' // force dock poll to keep it pinned on zoom/pan
})
})
} else {
// Free placement: restore exact canvas-space position + size.
figma.ui.resize(saved.w, saved.h)
figma.ui.reposition(saved.x, saved.y)
lastSize = { width: saved.w, height: saved.h }
}
})
} catch (e) { /* storage unavailable → default center below */ }
}