Add multi-file tabs

Open multiple documents in tabs within a single window, like Figma.

- Tab bar appears when 2+ documents are open, with close (×) and new (+) buttons
- ⌘N / ⌘T create a new empty tab
- ⌘W closes the active tab (last tab closes and reopens as Untitled)
- ⌘O opens .fig files in a new tab (reuses current tab if Untitled with no changes)
- Middle-click closes a tab
- Each tab has its own SceneGraph, UndoManager, viewport, and selection state
- Switching tabs remounts the canvas to rebind the Skia renderer to the active store
- Keyboard shortcuts, menu actions, and AI tools all operate on the active tab
This commit is contained in:
Danila Poyarkov 2026-03-02 18:08:03 +03:00
parent 7d98201548
commit 1b4dc08c54
8 changed files with 347 additions and 131 deletions

View file

@ -54,7 +54,12 @@ interface MenuItem {
}
const fileMenu: MenuItem[] = [
{ label: 'Open…', shortcut: `${mod}O`, action: () => openFileDialog(store) },
{
label: 'New',
shortcut: `${mod}N`,
action: () => import('@/stores/tabs').then((m) => m.createTab())
},
{ label: 'Open…', shortcut: `${mod}O`, action: () => openFileDialog() },
{ separator: true },
{ label: 'Save', shortcut: `${mod}S`, action: () => store.saveFigFile() },
{ label: 'Save as…', shortcut: `${mod}⇧S`, action: () => store.saveFigFileAs() },
@ -159,7 +164,8 @@ const topMenus = [
v-else
class="min-w-0 flex-1 cursor-default truncate rounded px-1 py-0.5 text-xs text-surface hover:bg-hover"
@dblclick="startRename"
>{{ store.state.documentName }}</span>
>{{ store.state.documentName }}</span
>
<button
class="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded text-muted transition-colors hover:bg-hover hover:text-surface"
title="Toggle UI (⌘\)"
@ -177,58 +183,60 @@ const topMenus = [
{{ menu.label }}
</MenubarTrigger>
<MenubarPortal>
<MenubarContent
:side-offset="4"
align="start"
class="min-w-52 rounded-lg border border-border bg-panel p-1 shadow-lg"
>
<template v-for="(item, i) in menu.items" :key="i">
<MenubarSeparator v-if="item.separator" class="mx-1 my-1 h-px bg-border" />
<MenubarSub v-else-if="item.sub">
<MenubarSubTrigger
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs text-surface outline-none select-none hover:bg-hover"
<MenubarPortal>
<MenubarContent
:side-offset="4"
align="start"
class="min-w-52 rounded-lg border border-border bg-panel p-1 shadow-lg"
>
<template v-for="(item, i) in menu.items" :key="i">
<MenubarSeparator v-if="item.separator" class="mx-1 my-1 h-px bg-border" />
<MenubarSub v-else-if="item.sub">
<MenubarSubTrigger
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs text-surface outline-none select-none hover:bg-hover"
>
<span class="flex-1">{{ item.label }}</span>
<IconChevronRight class="size-3 text-muted" />
</MenubarSubTrigger>
<MenubarPortal>
<MenubarSubContent
:side-offset="4"
class="min-w-44 rounded-lg border border-border bg-panel p-1 shadow-lg"
>
<template v-for="(sub, j) in item.sub" :key="j">
<MenubarSeparator v-if="sub.separator" class="mx-1 my-1 h-px bg-border" />
<MenubarItem
v-else
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs outline-none select-none"
:class="sub.disabled ? 'text-muted/50' : 'text-surface hover:bg-hover'"
:disabled="sub.disabled"
@select="sub.action?.()"
>
<span class="flex-1">{{ sub.label }}</span>
<span v-if="sub.shortcut" class="text-[11px] text-muted">{{
sub.shortcut
}}</span>
</MenubarItem>
</template>
</MenubarSubContent>
</MenubarPortal>
</MenubarSub>
<MenubarItem
v-else
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs outline-none select-none"
:class="item.disabled ? 'text-muted/50' : 'text-surface hover:bg-hover'"
:disabled="item.disabled"
@select="item.action?.()"
>
<span class="flex-1">{{ item.label }}</span>
<IconChevronRight class="size-3 text-muted" />
</MenubarSubTrigger>
<MenubarPortal>
<MenubarSubContent
:side-offset="4"
class="min-w-44 rounded-lg border border-border bg-panel p-1 shadow-lg"
>
<template v-for="(sub, j) in item.sub" :key="j">
<MenubarSeparator v-if="sub.separator" class="mx-1 my-1 h-px bg-border" />
<MenubarItem
v-else
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs outline-none select-none"
:class="sub.disabled ? 'text-muted/50' : 'text-surface hover:bg-hover'"
:disabled="sub.disabled"
@select="sub.action?.()"
>
<span class="flex-1">{{ sub.label }}</span>
<span v-if="sub.shortcut" class="text-[11px] text-muted">{{
sub.shortcut
}}</span>
</MenubarItem>
</template>
</MenubarSubContent>
</MenubarPortal>
</MenubarSub>
<MenubarItem
v-else
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs outline-none select-none"
:class="item.disabled ? 'text-muted/50' : 'text-surface hover:bg-hover'"
:disabled="item.disabled"
@select="item.action?.()"
>
<span class="flex-1">{{ item.label }}</span>
<span v-if="item.shortcut" class="text-[11px] text-muted">{{ item.shortcut }}</span>
</MenubarItem>
</template>
</MenubarContent>
</MenubarPortal>
</MenubarMenu>
<span v-if="item.shortcut" class="text-[11px] text-muted">{{
item.shortcut
}}</span>
</MenubarItem>
</template>
</MenubarContent>
</MenubarPortal>
</MenubarMenu>
</MenubarRoot>
</div>
</div>

55
src/components/TabBar.vue Normal file
View file

@ -0,0 +1,55 @@
<script setup lang="ts">
import { useTabsStore, createTab } from '@/stores/tabs'
const { tabs, switchTab, closeTab } = useTabsStore()
function onMiddleClick(e: MouseEvent, tabId: string) {
if (e.button === 1) {
e.preventDefault()
closeTab(tabId)
}
}
function onClose(e: MouseEvent, tabId: string) {
e.stopPropagation()
closeTab(tabId)
}
</script>
<template>
<div
v-if="tabs.length > 1"
class="flex h-9 shrink-0 items-end gap-0 overflow-x-auto border-b border-border bg-panel scrollbar-none"
>
<div
v-for="tab in tabs"
:key="tab.id"
class="group/tab flex h-full max-w-48 min-w-0 cursor-pointer items-center gap-1.5 border-r border-border px-3 text-xs transition-colors select-none"
:class="
tab.isActive
? 'bg-surface text-surface'
: 'bg-panel text-muted hover:bg-hover hover:text-surface'
"
@click="switchTab(tab.id)"
@mousedown="onMiddleClick($event, tab.id)"
>
<icon-lucide-file class="size-3 shrink-0 opacity-50" />
<span class="min-w-0 flex-1 truncate">{{ tab.name }}</span>
<button
class="flex size-4 shrink-0 cursor-pointer items-center justify-center rounded opacity-0 transition-opacity hover:bg-hover group-hover/tab:opacity-100"
:class="tab.isActive ? 'opacity-100' : ''"
title="Close tab"
@click="onClose($event, tab.id)"
>
<icon-lucide-x class="size-3" />
</button>
</div>
<button
class="flex size-9 shrink-0 cursor-pointer items-center justify-center text-muted transition-colors hover:bg-hover hover:text-surface"
title="New tab"
@click="createTab()"
>
<icon-lucide-plus class="size-3.5" />
</button>
</div>
</template>

View file

@ -6,6 +6,7 @@ import dedent from 'dedent'
import { computed, ref, watch } from 'vue'
import { createAITools } from '@/ai/tools'
import { useEditorStore } from '@/stores/editor'
import type { EditorStore } from '@/stores/editor'
import type { UIMessage } from 'ai'
@ -65,7 +66,13 @@ function createTransport() {
}
})
const tools = editorStore ? createAITools(editorStore) : {}
let currentStore: EditorStore | null = editorStore
try {
currentStore = useEditorStore()
} catch {
// store not yet available
}
const tools = currentStore ? createAITools(currentStore) : {}
const agent = new ToolLoopAgent({
model: openrouter(modelId.value),

View file

@ -1,56 +1,61 @@
import { useEventListener } from '@vueuse/core'
import { useAIChat } from '@/composables/use-chat'
import { TOOL_SHORTCUTS } from '@/stores/editor'
import { TOOL_SHORTCUTS, useEditorStore } from '@/stores/editor'
import { closeTab, createTab, activeTab as activeTabRef } from '@/stores/tabs'
import { openFileDialog } from './use-menu'
import type { EditorStore } from '@/stores/editor'
function isEditing(e: Event) {
return e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement
}
export function useKeyboard(store: EditorStore) {
export function useKeyboard() {
const { activeTab } = useAIChat()
function store() {
return useEditorStore()
}
useEventListener(window, 'copy', (e: ClipboardEvent) => {
if (isEditing(e)) return
e.preventDefault()
if (e.clipboardData) store.writeCopyData(e.clipboardData)
if (e.clipboardData) store().writeCopyData(e.clipboardData)
})
useEventListener(window, 'cut', (e: ClipboardEvent) => {
if (isEditing(e)) return
e.preventDefault()
if (e.clipboardData) store.writeCopyData(e.clipboardData)
store.deleteSelected()
if (e.clipboardData) store().writeCopyData(e.clipboardData)
store().deleteSelected()
})
useEventListener(window, 'paste', (e: ClipboardEvent) => {
if (isEditing(e)) return
e.preventDefault()
const html = e.clipboardData?.getData('text/html') ?? ''
if (html) store.pasteFromHTML(html)
if (html) store().pasteFromHTML(html)
})
useEventListener(window, 'keydown', (e: KeyboardEvent) => {
if (isEditing(e)) return
const s = store()
const tool = TOOL_SHORTCUTS[e.key.toLowerCase()]
if (tool) {
store.setTool(tool)
s.setTool(tool)
return
}
if ((e.metaKey || e.ctrlKey) && e.altKey) {
if (e.code === 'KeyK') {
e.preventDefault()
store.createComponentFromSelection()
s.createComponentFromSelection()
return
}
if (e.code === 'KeyB') {
e.preventDefault()
store.detachInstance()
s.detachInstance()
return
}
}
@ -58,23 +63,23 @@ export function useKeyboard(store: EditorStore) {
if ((e.metaKey || e.ctrlKey) && e.shiftKey) {
if (e.code === 'KeyK') {
e.preventDefault()
store.createComponentSetFromComponents()
s.createComponentSetFromComponents()
return
}
if (e.code === 'KeyH') {
e.preventDefault()
store.toggleVisibility()
s.toggleVisibility()
return
}
if (e.code === 'KeyL') {
e.preventDefault()
store.toggleLock()
s.toggleLock()
return
}
if (e.code === 'KeyE') {
e.preventDefault()
if (store.state.selectedIds.size > 0) {
store.exportSelection(1, 'PNG')
if (s.state.selectedIds.size > 0) {
s.exportSelection(1, 'PNG')
}
return
}
@ -83,7 +88,7 @@ export function useKeyboard(store: EditorStore) {
if (e.metaKey || e.ctrlKey) {
if (e.code === 'Backslash') {
e.preventDefault()
store.state.showUI = !store.state.showUI
s.state.showUI = !s.state.showUI
return
}
if (e.code === 'KeyJ') {
@ -91,78 +96,88 @@ export function useKeyboard(store: EditorStore) {
activeTab.value = activeTab.value === 'ai' ? 'design' : 'ai'
return
}
if (e.key === 'w') {
e.preventDefault()
if (activeTabRef.value) closeTab(activeTabRef.value.id)
return
}
if (e.key === 'n' || e.key === 't') {
e.preventDefault()
createTab()
return
}
if (e.key === 'z' && !e.shiftKey) {
e.preventDefault()
store.undoAction()
s.undoAction()
} else if ((e.key === 'z' && e.shiftKey) || e.key === 'y') {
e.preventDefault()
store.redoAction()
s.redoAction()
} else if (e.key === '0') {
e.preventDefault()
store.zoomToFit()
s.zoomToFit()
} else if (e.key === 'd') {
e.preventDefault()
store.duplicateSelected()
s.duplicateSelected()
} else if (e.key === 'a') {
e.preventDefault()
store.selectAll()
s.selectAll()
} else if (e.key === 's' && e.shiftKey) {
e.preventDefault()
store.saveFigFileAs()
s.saveFigFileAs()
} else if (e.key === 's') {
e.preventDefault()
store.saveFigFile()
s.saveFigFile()
} else if (e.key === 'o') {
e.preventDefault()
openFileDialog(store)
openFileDialog()
} else if (e.key === 'g' && !e.shiftKey) {
e.preventDefault()
store.groupSelected()
s.groupSelected()
} else if (e.key === 'g' && e.shiftKey) {
e.preventDefault()
store.ungroupSelected()
s.ungroupSelected()
}
}
if (e.shiftKey && e.key === 'A') {
e.preventDefault()
const node = store.selectedNode.value
if (node && node.type === 'FRAME' && store.selectedNodes.value.length === 1) {
store.setLayoutMode(node.id, node.layoutMode === 'NONE' ? 'VERTICAL' : 'NONE')
} else if (store.selectedNodes.value.length > 0) {
store.wrapInAutoLayout()
const node = s.selectedNode.value
if (node && node.type === 'FRAME' && s.selectedNodes.value.length === 1) {
s.setLayoutMode(node.id, node.layoutMode === 'NONE' ? 'VERTICAL' : 'NONE')
} else if (s.selectedNodes.value.length > 0) {
s.wrapInAutoLayout()
}
return
}
if (e.key === ']') {
e.preventDefault()
store.bringToFront()
s.bringToFront()
return
}
if (e.key === '[') {
e.preventDefault()
store.sendToBack()
s.sendToBack()
return
}
if (e.key === 'Backspace' || e.key === 'Delete') {
store.deleteSelected()
s.deleteSelected()
}
if (e.key === 'Enter' && store.state.penState) {
if (e.key === 'Enter' && s.state.penState) {
e.preventDefault()
store.penCommit(false)
s.penCommit(false)
return
}
if (e.key === 'Escape') {
if (store.state.penState) {
store.penCancel()
if (s.state.penState) {
s.penCancel()
return
}
store.clearSelection()
store.setTool('SELECT')
s.clearSelection()
s.setTool('SELECT')
}
})
}

View file

@ -1,10 +1,10 @@
import { onUnmounted } from 'vue'
import { IS_TAURI } from '@/constants'
import { useEditorStore } from '@/stores/editor'
import { openFileInNewTab, createTab, closeTab, activeTab } from '@/stores/tabs'
import type { EditorStore } from '@/stores/editor'
export async function openFileDialog(store: EditorStore) {
export async function openFileDialog() {
if (IS_TAURI) {
const { open } = await import('@tauri-apps/plugin-dialog')
const { readFile } = await import('@tauri-apps/plugin-fs')
@ -15,7 +15,7 @@ export async function openFileDialog(store: EditorStore) {
if (!path) return
const bytes = await readFile(path as string)
const file = new File([bytes], (path as string).split('/').pop() ?? 'file.fig')
await store.openFigFile(file, undefined, path as string)
await openFileInNewTab(file, undefined, path as string)
return
}
@ -30,7 +30,7 @@ export async function openFileDialog(store: EditorStore) {
]
})
const file = await handle.getFile()
await store.openFigFile(file, handle)
await openFileInNewTab(file, handle)
return
} catch (e) {
if ((e as Error).name === 'AbortError') return
@ -42,29 +42,38 @@ export async function openFileDialog(store: EditorStore) {
input.accept = '.fig'
input.addEventListener('change', () => {
const file = input.files?.[0]
if (file) store.openFigFile(file)
if (file) openFileInNewTab(file)
})
input.click()
}
const MENU_ACTIONS: Record<string, (store: EditorStore) => void> = {
open: (store) => openFileDialog(store),
save: (store) => store.saveFigFile(),
'save-as': (store) => store.saveFigFileAs(),
duplicate: (store) => store.duplicateSelected(),
delete: (store) => store.deleteSelected(),
group: (store) => store.groupSelected(),
ungroup: (store) => store.ungroupSelected(),
'create-component': (store) => store.createComponentFromSelection(),
'create-component-set': (store) => store.createComponentSetFromComponents(),
'detach-instance': (store) => store.detachInstance(),
'zoom-fit': (store) => store.zoomToFit(),
export: (store) => {
if (store.state.selectedIds.size > 0) store.exportSelection(1, 'PNG')
function getStore() {
return useEditorStore()
}
const MENU_ACTIONS: Record<string, () => void> = {
open: () => openFileDialog(),
save: () => getStore().saveFigFile(),
'save-as': () => getStore().saveFigFileAs(),
duplicate: () => getStore().duplicateSelected(),
delete: () => getStore().deleteSelected(),
group: () => getStore().groupSelected(),
ungroup: () => getStore().ungroupSelected(),
'create-component': () => getStore().createComponentFromSelection(),
'create-component-set': () => getStore().createComponentSetFromComponents(),
'detach-instance': () => getStore().detachInstance(),
'zoom-fit': () => getStore().zoomToFit(),
export: () => {
const s = getStore()
if (s.state.selectedIds.size > 0) s.exportSelection(1, 'PNG')
},
'new-tab': () => createTab(),
'close-tab': () => {
if (activeTab.value) closeTab(activeTab.value.id)
}
}
export function useMenu(store: EditorStore) {
export function useMenu() {
if (!IS_TAURI) return
let unlisten: (() => void) | undefined
@ -72,7 +81,7 @@ export function useMenu(store: EditorStore) {
import('@tauri-apps/api/event').then(({ listen }) => {
listen<string>('menu-event', (event) => {
const action = MENU_ACTIONS[event.payload]
if (action) action(store)
if (action) action()
}).then((fn) => {
unlisten = fn
})

View file

@ -618,7 +618,11 @@ export function createEditorStore() {
if (!path) return
filePath = path
fileHandle = null
state.documentName = path.split('/').pop()?.replace(/\.fig$/i, '') ?? 'Untitled'
state.documentName =
path
.split('/')
.pop()
?.replace(/\.fig$/i, '') ?? 'Untitled'
await writeFile(data)
return
}
@ -1595,8 +1599,8 @@ export function createEditorStore() {
parseFigmaClipboard(html).then((figma) => {
if (figma) {
const bounds = figmaNodesBounds(figma.nodes)
const viewCenterX = (-state.panX + (window.innerWidth / 2)) / state.zoom
const viewCenterY = (-state.panY + (window.innerHeight / 2)) / state.zoom
const viewCenterX = (-state.panX + window.innerWidth / 2) / state.zoom
const viewCenterY = (-state.panY + window.innerHeight / 2) / state.zoom
const offsetX = bounds ? viewCenterX - (bounds.x + bounds.w / 2) : 0
const offsetY = bounds ? viewCenterY - (bounds.y + bounds.h / 2) : 0
@ -1965,6 +1969,10 @@ export type EditorStore = ReturnType<typeof createEditorStore>
const storeRef = shallowRef<EditorStore>()
export function setActiveEditorStore(store: EditorStore) {
storeRef.value = store
}
export function provideEditorStore(): EditorStore {
const store = createEditorStore()
storeRef.value = store

108
src/stores/tabs.ts Normal file
View file

@ -0,0 +1,108 @@
import { shallowRef, computed } from 'vue'
import { createEditorStore, setActiveEditorStore } from './editor'
import type { EditorStore } from './editor'
export interface Tab {
id: string
store: EditorStore
}
let nextTabId = 1
function generateTabId(): string {
return `tab-${nextTabId++}`
}
const tabsRef = shallowRef<Tab[]>([])
const activeTabId = shallowRef('')
export const activeTab = computed(() => tabsRef.value.find((t) => t.id === activeTabId.value))
export const allTabs = computed(() =>
tabsRef.value.map((t) => ({
id: t.id,
name: t.store.state.documentName,
isActive: t.id === activeTabId.value
}))
)
export function getActiveStore(): EditorStore {
const tab = tabsRef.value.find((t) => t.id === activeTabId.value)
if (!tab) throw new Error('No active tab')
return tab.store
}
export function createTab(store?: EditorStore): Tab {
const s = store ?? createEditorStore()
const tab: Tab = { id: generateTabId(), store: s }
tabsRef.value = [...tabsRef.value, tab]
activateTab(tab)
return tab
}
function activateTab(tab: Tab) {
activeTabId.value = tab.id
setActiveEditorStore(tab.store)
window.__OPEN_PENCIL_STORE__ = tab.store
}
export function switchTab(tabId: string) {
const tab = tabsRef.value.find((t) => t.id === tabId)
if (!tab) return
activateTab(tab)
}
export function closeTab(tabId: string) {
const idx = tabsRef.value.findIndex((t) => t.id === tabId)
if (idx < 0) return
const wasActive = activeTabId.value === tabId
tabsRef.value = tabsRef.value.filter((t) => t.id !== tabId)
if (tabsRef.value.length === 0) {
createTab()
return
}
if (wasActive) {
const newIdx = Math.min(idx, tabsRef.value.length - 1)
activateTab(tabsRef.value[newIdx])
}
}
export async function openFileInNewTab(
file: File,
handle?: FileSystemFileHandle,
path?: string
): Promise<void> {
const current = activeTab.value
const isUntouched =
current && current.store.state.documentName === 'Untitled' && !current.store.undo.canUndo
if (isUntouched) {
await current.store.openFigFile(file, handle, path)
} else {
const store = createEditorStore()
createTab(store)
await store.openFigFile(file, handle, path)
}
}
export function tabCount(): number {
return tabsRef.value.length
}
export function useTabsStore() {
return {
tabs: allTabs,
activeTabId,
createTab,
switchTab,
closeTab,
openFileInNewTab,
getActiveStore,
tabCount
}
}

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { provide } from 'vue'
import { computed, provide } from 'vue'
import { useEventListener, useUrlSearchParams } from '@vueuse/core'
import { useRoute, useRouter } from 'vue-router'
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from 'reka-ui'
@ -9,24 +9,26 @@ import { useMenu } from '@/composables/use-menu'
import { useCollab, COLLAB_KEY } from '@/composables/use-collab'
import { toast } from '@/composables/use-toast'
import { createDemoShapes } from '@/demo'
import { provideEditorStore } from '@/stores/editor'
import { useEditorStore } from '@/stores/editor'
import { createTab, activeTab } from '@/stores/tabs'
import CollabPanel from '@/components/CollabPanel.vue'
import EditorCanvas from '@/components/EditorCanvas.vue'
import LayersPanel from '@/components/LayersPanel.vue'
import PropertiesPanel from '@/components/PropertiesPanel.vue'
import SafariBanner from '@/components/SafariBanner.vue'
import TabBar from '@/components/TabBar.vue'
import Toolbar from '@/components/Toolbar.vue'
const route = useRoute()
const router = useRouter()
const store = provideEditorStore()
useKeyboard(store)
useMenu(store)
const collab = useCollab(store)
const firstTab = createTab()
const store = computed(() => useEditorStore())
useKeyboard()
useMenu()
const collab = useCollab(firstTab.store)
provide(COLLAB_KEY, collab)
window.__OPEN_PENCIL_STORE__ = store
useEventListener(
document,
@ -37,12 +39,10 @@ useEventListener(
{ passive: false }
)
const params = useUrlSearchParams('history')
const showChrome = !('no-chrome' in params)
if (!('test' in params)) {
createDemoShapes(store)
createDemoShapes(firstTab.store)
}
const pendingRoomId = (route.params.roomId as string) || null
@ -68,8 +68,10 @@ function onDisconnect() {
<template>
<div class="flex h-screen w-screen flex-col">
<SafariBanner />
<TabBar />
<SplitterGroup
v-if="showChrome && store.state.showUI"
:key="activeTab?.id"
direction="horizontal"
class="flex-1 overflow-hidden"
auto-save-id="editor-layout"
@ -108,7 +110,11 @@ function onDisconnect() {
<PropertiesPanel />
</SplitterPanel>
</SplitterGroup>
<div v-else-if="showChrome" class="flex flex-1 overflow-hidden">
<div
v-else-if="showChrome"
:key="'collapsed-' + activeTab?.id"
class="flex flex-1 overflow-hidden"
>
<div class="relative flex min-w-0 flex-1">
<EditorCanvas />
<Toolbar />
@ -127,7 +133,7 @@ function onDisconnect() {
</div>
</div>
</div>
<div v-else class="flex flex-1 overflow-hidden">
<div v-else :key="'bare-' + activeTab?.id" class="flex flex-1 overflow-hidden">
<div class="relative flex min-w-0 flex-1">
<EditorCanvas />
</div>