Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.14.0

- Added MCP server support to let agents/IDEs pull code, structure, and screenshots from your current Figma selection.
- The inspect panel can be resized horizontally by dragging either its left or right edge. (Suggested and implemented by @molinla at [#34](https://github.com/ecomfe/tempad-dev/pull/34))

## 0.13.1

Expand Down
168 changes: 154 additions & 14 deletions components/Panel.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { useDraggable, useWindowSize, watchDebounced } from '@vueuse/core'
import { useDraggable, useEventListener, useWindowSize, watchDebounced } from '@vueuse/core'

import { useScrollbar } from '@/composables/scrollbar'
import { ui } from '@/ui/figma'
Expand All @@ -21,7 +21,7 @@ useScrollbar(main, {
})

const position = options.value.panelPosition
const { x, y } = useDraggable(panel, {
const { x, y, isDragging } = useDraggable(panel, {
initialValue: {
x: position ? position.left : 0,
y: position ? position.top : 0
Expand All @@ -31,16 +31,107 @@ const { x, y } = useDraggable(panel, {

const { width: windowWidth, height: windowHeight } = useWindowSize()

const panelWidth = ref(position?.width ?? ui.tempadPanelWidth)

let resizeState: {
direction: 'left' | 'right'
startX: number
startWidth: number
target: HTMLElement
pointerId: number
} | null = null

function clampWidth(value: number): number {
return Math.max(ui.tempadPanelWidth, Math.min(ui.tempadPanelMaxWidth, value))
}

function startResize(e: PointerEvent, direction: 'left' | 'right') {
e.preventDefault()
e.stopPropagation()

const target = e.currentTarget as HTMLElement
if (!target) return

target.setPointerCapture(e.pointerId)

resizeState = {
direction,
startX: e.clientX,
startWidth: panelWidth.value,
target,
pointerId: e.pointerId
}
}

function onPointerMove(e: PointerEvent) {
if (!resizeState) return

if (e.pointerId !== resizeState.pointerId) return

if (e.buttons === 0) {
endResize(e)
return
}

const deltaX = e.clientX - resizeState.startX
const newWidth =
resizeState.direction === 'right'
? clampWidth(resizeState.startWidth + deltaX)
: clampWidth(resizeState.startWidth - deltaX)

if (resizeState.direction === 'left') {
const positionDelta = panelWidth.value - newWidth
x.value += positionDelta
}

panelWidth.value = newWidth
}

function endResize(e: PointerEvent) {
if (!resizeState || e.pointerId !== resizeState.pointerId) return

resizeState.target.releasePointerCapture(resizeState.pointerId)

if (position) {
position.width = panelWidth.value
}

resizeState = null
}

function resetWidth(direction: 'left' | 'right') {
const newWidth = ui.tempadPanelWidth
const positionDelta = panelWidth.value - newWidth

// Keep the edge under the double-clicked handle stationary
if (direction === 'left' && positionDelta !== 0) {
x.value += positionDelta
}

panelWidth.value = newWidth

if (position) {
delete position.width
}
}

useEventListener('pointermove', onPointerMove)
useEventListener('pointerup', endResize)
useEventListener('pointercancel', endResize)

const isAtMinWidth = computed(() => panelWidth.value <= ui.tempadPanelWidth)
const isAtMaxWidth = computed(() => panelWidth.value >= ui.tempadPanelMaxWidth)

const restrictedPosition = computed(() => {
if (!panel.value || !header.value) {
if (!header.value) {
return { top: x.value, left: y.value }
}

const { offsetWidth: panelWidth } = panel.value
const panelPixelWidth = panelWidth.value
const { offsetHeight: headerHeight } = header.value

const xMin = -panelWidth / 2
const xMax = windowWidth.value - panelWidth / 2
const xMin = -panelPixelWidth / 2
const xMax = windowWidth.value - panelPixelWidth / 2
const yMin = ui.topBoundary
const yMax = windowHeight.value - headerHeight

Expand All @@ -54,6 +145,8 @@ const panelMaxHeight = computed(
() => `${windowHeight.value - restrictedPosition.value.top - ui.bottomBoundary}px`
)

const panelWidthPx = computed(() => `${panelWidth.value}px`)

const positionStyle = computed(() => {
const p = restrictedPosition.value
return `top: ${p.top}px; left: ${p.left}px`
Expand All @@ -73,15 +166,45 @@ if (position) {
function toggleMinimized() {
options.value.minimized = !options.value.minimized
}

function getResizeCursor(direction: 'left' | 'right'): 'e-resize' | 'w-resize' | 'ew-resize' {
const atMin = isAtMinWidth.value
const atMax = isAtMaxWidth.value

if (direction === 'left') {
if (atMax) return 'e-resize'
if (atMin) return 'w-resize'
} else {
if (atMax) return 'w-resize'
if (atMin) return 'e-resize'
}
return 'ew-resize'
}

const leftHandleCursor = computed(() => getResizeCursor('left'))
const rightHandleCursor = computed(() => getResizeCursor('right'))
</script>

<template>
<article
ref="panel"
class="tp-panel"
:class="{ 'tp-panel-minimized': options.minimized }"
:class="{
'tp-panel-minimized': options.minimized,
'tp-panel-dragging': isDragging
}"
:style="positionStyle"
>
<div
class="tp-panel-resize-handle tp-panel-resize-handle-left"
@pointerdown="startResize($event, 'left')"
@dblclick="resetWidth('left')"
/>
<div
class="tp-panel-resize-handle tp-panel-resize-handle-right"
@pointerdown="startResize($event, 'right')"
@dblclick="resetWidth('right')"
/>
<header ref="header" class="tp-row tp-row-justify tp-panel-header" @dblclick="toggleMinimized">
<slot name="header" />
</header>
Expand All @@ -97,10 +220,32 @@ function toggleMinimized() {
z-index: 6;
display: flex;
flex-direction: column;
width: v-bind(panelWidthPx);
max-height: v-bind(panelMaxHeight);
background-color: var(--color-bg);
border-radius: 2px;
box-shadow: var(--elevation-500-modal-window);
border-radius: var(--radius-large);
box-shadow: var(--elevation-100);
}

.tp-panel-resize-handle {
position: absolute;
top: 0;
bottom: 0;
width: 8px;
z-index: 10;
transition: background-color 0.2s ease;
touch-action: none;
user-select: none;
}

.tp-panel-resize-handle-left {
left: -8px;
cursor: v-bind(leftHandleCursor);
}

.tp-panel-resize-handle-right {
right: -8px;
cursor: v-bind(rightHandleCursor);
}

.tp-panel-header {
Expand All @@ -126,9 +271,4 @@ function toggleMinimized() {
width: auto;
height: 32px;
}

[data-fpl-version='ui3'] .tp-panel {
box-shadow: var(--elevation-100);
border-radius: var(--radius-large);
}
</style>
13 changes: 6 additions & 7 deletions entrypoints/ui/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ function toggleMinimized() {
options.value.minimized = !options.value.minimized
}

const panelWidth = `${ui.tempadPanelWidth}px`

const { status, selfActive, count, activate } = useMcp()

const isMcpConnected = computed(() => status.value === 'connected')
Expand Down Expand Up @@ -111,18 +109,19 @@ function activateMcp() {

<style scoped>
.tp-main {
width: v-bind(panelWidth);
transition: width, height;
transition-duration: 0.2s;
transition-timing-function: cubic-bezier(0.87, 0, 0.13, 1);
overflow: hidden;
transition: height 0.2s cubic-bezier(0.87, 0, 0.13, 1);
}

.tp-main-minimized {
height: 41px;
border-bottom-width: 0;
}

.tp-main.tp-panel-dragging,
.tp-main.tp-panel-resizing {
transition: none;
}

.tp-mcp-badge {
gap: 4px;
}
Expand Down
5 changes: 5 additions & 0 deletions ui/figma.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const NATIVE_PANEL_WIDTH = 241
const TEMPAD_PANEL_WIDTH = 240
const TEMPAD_PANEL_MAX_WIDTH = 500

const ui = reactive({
isUi3: false,
Expand All @@ -12,6 +13,10 @@ const ui = reactive({
return TEMPAD_PANEL_WIDTH
},

get tempadPanelMaxWidth() {
return TEMPAD_PANEL_MAX_WIDTH
},

get topBoundary() {
return sumLength(this.isUi3 ? 12 : '--toolbar-height', '--editor-banner-height')
},
Expand Down
4 changes: 3 additions & 1 deletion ui/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type Options = {
panelPosition: {
left: number
top: number
width?: number
}
prefOpen: boolean
deepSelectOn: boolean
Expand All @@ -33,7 +34,8 @@ export const options = useStorage<Options>('tempad-dev', {
minimized: false,
panelPosition: {
left: window.innerWidth - ui.nativePanelWidth - ui.tempadPanelWidth,
top: ui.topBoundary
top: ui.topBoundary,
width: ui.tempadPanelWidth
},
prefOpen: false,
deepSelectOn: false,
Expand Down