feat: persist window placement across restarts via figma.clientStorage (dock|free, default=center)
This commit is contained in:
1 parent
fda4be1765
commit
ce20357098
1 file changed
+113
-6
+113
-6
@@ -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 */ }
|
||||
}
|
||||
Reference in new issue
Block a user