Skip to content

Commit 194ef7a

Browse files
committed
feat: implement Figma availability check and improve layout readiness logic
1 parent 28033bd commit 194ef7a

File tree

7 files changed

+104
-104
lines changed

7 files changed

+104
-104
lines changed

packages/extension/components/Panel.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ const restrictedPosition = computed(() => {
128128
}
129129
130130
const panelPixelWidth = panelWidth.value
131-
const { offsetHeight: headerHeight } = header.value
131+
const headerHeight = header.value.offsetHeight - 1
132132
133133
const xMin = -panelPixelWidth / 2
134134
const xMax = windowWidth.value - panelPixelWidth / 2
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { useDocumentVisibility, useIntervalFn } from '@vueuse/core'
2+
import waitFor from 'p-wait-for'
3+
import { computed, watch } from 'vue'
4+
5+
import { layoutReady, runtimeMode } from '@/ui/state'
6+
import { getCanvas, getLeftPanel } from '@/utils'
7+
import { logger } from '@/utils/log'
8+
9+
const LAYOUT_CHECK_INTERVAL = 500
10+
const FIGMA_READY_TIMEOUT = 1000
11+
const FIGMA_RECOVER_INTERVAL = 3000
12+
13+
function isLayoutReady() {
14+
return getCanvas() != null && getLeftPanel() != null
15+
}
16+
17+
export function useFigmaAvailability() {
18+
const visibility = useDocumentVisibility()
19+
const canCheck = computed(() => layoutReady.value && visibility.value === 'visible')
20+
const isFigmaReady = () => window.figma?.currentPage != null
21+
let checkToken = 0
22+
23+
const setMode = (mode: 'standard' | 'unavailable') => {
24+
const modeChanged = runtimeMode.value !== mode
25+
runtimeMode.value = mode
26+
if (!modeChanged) return
27+
if (mode === 'standard') {
28+
logger.log('`window.figma` is now available. TemPad Dev is ready.')
29+
} else {
30+
logger.log('`window.figma` is not available. TemPad Dev is currently unavailable.')
31+
}
32+
}
33+
34+
useIntervalFn(
35+
() => {
36+
const ready = isLayoutReady()
37+
if (ready !== layoutReady.value) {
38+
layoutReady.value = ready
39+
}
40+
},
41+
LAYOUT_CHECK_INTERVAL,
42+
{ immediate: true }
43+
)
44+
45+
const { pause: stopRecover, resume: startRecover } = useIntervalFn(
46+
() => {
47+
if (!canCheck.value) {
48+
stopRecover()
49+
return
50+
}
51+
const ok = isFigmaReady()
52+
setMode(ok ? 'standard' : 'unavailable')
53+
if (ok) stopRecover()
54+
},
55+
FIGMA_RECOVER_INTERVAL,
56+
{ immediate: false }
57+
)
58+
59+
const runCheck = async () => {
60+
const token = (checkToken += 1)
61+
if (!canCheck.value) return
62+
const ok = await waitFor(isFigmaReady, { timeout: FIGMA_READY_TIMEOUT }).then(
63+
() => true,
64+
() => false
65+
)
66+
if (token !== checkToken || !canCheck.value) return
67+
setMode(ok ? 'standard' : 'unavailable')
68+
if (ok) stopRecover()
69+
else startRecover()
70+
}
71+
72+
watch(
73+
canCheck,
74+
(ready) => {
75+
if (!ready) {
76+
checkToken += 1
77+
stopRecover()
78+
return
79+
}
80+
void runCheck()
81+
},
82+
{ immediate: true }
83+
)
84+
}

packages/extension/composables/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './copy'
22
export * from './deep-link'
33
export * from './dev-resources'
4+
export * from './availability'
45
export * from './input'
56
export * from './key-lock'
67
export * from './mcp'

packages/extension/entrypoints/ui/App.vue

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<script setup lang="ts">
22
import { useIdle, useIntervalFn, useTimeoutFn } from '@vueuse/core'
3-
import waitFor from 'p-wait-for'
43
54
import Badge from '@/components/Badge.vue'
65
import IconButton from '@/components/IconButton.vue'
@@ -12,36 +11,22 @@ import CodeSection from '@/components/sections/CodeSection.vue'
1211
import ErrorSection from '@/components/sections/ErrorSection.vue'
1312
import MetaSection from '@/components/sections/MetaSection.vue'
1413
import PrefSection from '@/components/sections/PrefSection.vue'
15-
import { syncSelection, useKeyLock, useSelection } from '@/composables'
16-
import { useMcp } from '@/composables'
14+
import {
15+
syncSelection,
16+
useFigmaAvailability,
17+
useKeyLock,
18+
useMcp,
19+
useSelection
20+
} from '@/composables'
1721
import { layoutReady, options, runtimeMode, selection } from '@/ui/state'
18-
import { getCanvas, getLeftPanel } from '@/utils'
22+
import { getCanvas } from '@/utils'
1923
2024
useSelection()
2125
useKeyLock()
2226
23-
const LAYOUT_CHECK_INTERVAL = 500
27+
const HINT_CHECK_INTERVAL = 500
2428
25-
function isLayoutReady() {
26-
return getCanvas() != null && getLeftPanel() != null
27-
}
28-
29-
useIntervalFn(
30-
() => {
31-
const ready = isLayoutReady()
32-
if (ready !== layoutReady.value) {
33-
layoutReady.value = ready
34-
}
35-
},
36-
LAYOUT_CHECK_INTERVAL,
37-
{ immediate: true }
38-
)
39-
40-
onMounted(() => {
41-
waitFor(isLayoutReady).then(() => {
42-
layoutReady.value = true
43-
})
44-
})
29+
useFigmaAvailability()
4530
4631
const HINT_IDLE_MS = 10000
4732
@@ -120,7 +105,7 @@ function updateHintOverlap() {
120105
lowVisibility.value = overlapWidth < panelRect.width / 3
121106
}
122107
123-
useIntervalFn(updateHintOverlap, LAYOUT_CHECK_INTERVAL, { immediate: true })
108+
useIntervalFn(updateHintOverlap, HINT_CHECK_INTERVAL, { immediate: true })
124109
125110
useTimeoutFn(() => {
126111
initialLock.value = false
@@ -147,6 +132,7 @@ function activateMcp() {
147132

148133
<template>
149134
<Panel
135+
v-show="layoutReady"
150136
class="tp-main"
151137
:class="{ 'tp-main-minimized': options.minimized, 'tp-main-hint': showHint }"
152138
>
@@ -193,6 +179,7 @@ function activateMcp() {
193179

194180
<style scoped>
195181
.tp-main {
182+
overflow: hidden;
196183
transition:
197184
height 0.2s cubic-bezier(0.87, 0, 0.13, 1),
198185
box-shadow 0.2s cubic-bezier(0.87, 0, 0.13, 1);

packages/extension/entrypoints/ui/index.ts

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,10 @@
11
import 'overlayscrollbars/styles/overlayscrollbars.css'
2-
import waitFor from 'p-wait-for'
3-
4-
import { runtimeMode } from '@/ui/state'
5-
import { logger } from '@/utils/log'
62

73
import './style.css'
84

95
export default defineUnlistedScript(async () => {
106
import('./prism')
117

12-
const FIGMA_READY_TIMEOUT = 1000
13-
const FIGMA_RECOVER_INTERVAL = 3000
14-
15-
let announcedUnavailable = false
16-
17-
const announceUnavailable = () => {
18-
if (announcedUnavailable) return
19-
if (document.visibilityState === 'hidden') return
20-
runtimeMode.value = 'unavailable'
21-
announcedUnavailable = true
22-
logger.log('`window.figma` is not available. TemPad Dev is currently unavailable.')
23-
}
24-
25-
async function ensureFigmaReady(timeout?: number): Promise<boolean> {
26-
try {
27-
await waitFor(() => window.figma?.currentPage != null, timeout ? { timeout } : undefined)
28-
return true
29-
} catch {
30-
return false
31-
}
32-
}
33-
34-
const ready = await ensureFigmaReady(FIGMA_READY_TIMEOUT)
35-
if (!ready) {
36-
announceUnavailable()
37-
const panelEl = () => document.getElementById('tempad')
38-
39-
const tryRecover = async () => {
40-
const el = panelEl()
41-
const available = await ensureFigmaReady()
42-
if (available) {
43-
runtimeMode.value = 'standard'
44-
if (el) el.style.display = ''
45-
logger.log('`window.figma` is now available. TemPad Dev is ready.')
46-
return true
47-
}
48-
if (el && document.visibilityState === 'hidden') el.style.display = 'none'
49-
else announceUnavailable()
50-
return false
51-
}
52-
53-
const recovery = window.setInterval(async () => {
54-
const ok = await tryRecover()
55-
if (ok) clearInterval(recovery)
56-
}, FIGMA_RECOVER_INTERVAL)
57-
58-
const handleVisibility = async () => {
59-
const el = panelEl()
60-
if (!el) return
61-
if (document.visibilityState === 'hidden') {
62-
if (runtimeMode.value === 'unavailable') el.style.display = 'none'
63-
return
64-
}
65-
el.style.display = ''
66-
const ok = await tryRecover()
67-
if (!ok) announceUnavailable()
68-
}
69-
70-
const handleFocus = async () => {
71-
await tryRecover()
72-
}
73-
74-
document.addEventListener('visibilitychange', handleVisibility)
75-
window.addEventListener('focus', handleFocus)
76-
}
77-
788
const App = (await import('./App.vue')).default
799

8010
createApp(App).mount('tempad')

packages/extension/entrypoints/ui/style.css

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,6 @@ tempad input[type="number"]:focus-visible {
125125
outline: unset;
126126
}
127127

128-
#react-page:not(:has(#fullscreen-root .gpu-view-content canvas):has(#left-panel-container))
129-
~ tempad {
130-
display: none;
131-
}
132-
133128
.os-scrollbar {
134129
--os-track-bg-hover: var(--color-scrollbartrackhover);
135130
--os-track-bg-active: var(--color-scrollbartrackdrag);

packages/extension/utils/figma.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ export function getCanvas() {
22
// Need to ensure the whole plugin is rendered after canvas is ready
33
// so that we can cast the result to HTMLElement here.
44
// The layout readiness check lives in App.vue.
5-
return document.querySelector('#fullscreen-root .gpu-view-content canvas') as HTMLElement
5+
return document.querySelector<HTMLElement>('#fullscreen-root .gpu-view-content canvas')
66
}
77

88
export function getLeftPanel() {
99
// Similar to `getCanvas()`.
10-
return document.querySelector('#left-panel-container') as HTMLElement
10+
return (
11+
document.querySelector<HTMLElement>('#left-panel-container') ||
12+
document.querySelector<HTMLElement>('[class*="left_panel_island_container"]')
13+
)
1114
}

0 commit comments

Comments
 (0)