Add ⌘\ to toggle side panels, header with logo and editable filename

Header row with app icon, double-click-to-rename document name, and
sidebar toggle button. Shows in both Tauri and browser.
When panels are hidden, a floating bar on the canvas provides the same
controls to restore the UI.
This commit is contained in:
Danila Poyarkov 2026-03-01 20:14:39 +03:00
parent 575914b194
commit ab658ac33f
5 changed files with 90 additions and 13 deletions

2
components.d.ts vendored
View file

@ -63,6 +63,7 @@ declare module 'vue' {
IconLucideLink: typeof import('~icons/lucide/link')['default']
IconLucideLoaderCircle: typeof import('~icons/lucide/loader-circle')['default']
IconLucideMessageCircle: typeof import('~icons/lucide/message-circle')['default']
IconLucidePanelLeft: typeof import('~icons/lucide/panel-left')['default']
IconLucidePlus: typeof import('~icons/lucide/plus')['default']
IconLucideRadius: typeof import('~icons/lucide/radius')['default']
IconLucideRotateCcw: typeof import('~icons/lucide/rotate-ccw')['default']
@ -71,6 +72,7 @@ declare module 'vue' {
IconLucideSend: typeof import('~icons/lucide/send')['default']
IconLucideSettings2: typeof import('~icons/lucide/settings2')['default']
IconLucideShare2: typeof import('~icons/lucide/share2')['default']
IconLucideSidebar: typeof import('~icons/lucide/sidebar')['default']
IconLucideSparkles: typeof import('~icons/lucide/sparkles')['default']
IconLucideSquare: typeof import('~icons/lucide/square')['default']
IconLucideStrikethrough: typeof import('~icons/lucide/strikethrough')['default']

View file

@ -14,12 +14,33 @@ import {
import IconChevronRight from '~icons/lucide/chevron-right'
import { ref, nextTick } from 'vue'
import { IS_TAURI } from '@/constants'
import { openFileDialog } from '@/composables/use-menu'
import { useEditorStore } from '@/stores/editor'
const store = useEditorStore()
const editingName = ref(false)
function startRename() {
editingName.value = true
nextTick(() => {
const input = document.querySelector<HTMLInputElement>('[data-doc-name-edit]')
input?.focus()
input?.select()
})
}
function commitRename(input: HTMLInputElement) {
const value = input.value.trim()
if (value) {
store.state.documentName = value
}
editingName.value = false
}
const isMac = navigator.platform.includes('Mac')
const mod = isMac ? '⌘' : 'Ctrl+'
@ -122,17 +143,39 @@ const topMenus = [
</script>
<template>
<div
v-if="!IS_TAURI"
class="shrink-0 overflow-x-auto border-b border-border px-1 py-1 scrollbar-none"
>
<MenubarRoot class="flex w-max items-center gap-0.5">
<MenubarMenu v-for="menu in topMenus" :key="menu.label">
<MenubarTrigger
class="flex cursor-pointer items-center rounded px-2 py-1 text-xs text-muted transition-colors select-none hover:bg-hover hover:text-surface data-[state=open]:bg-hover data-[state=open]:text-surface"
>
{{ menu.label }}
</MenubarTrigger>
<div class="shrink-0 border-b border-border">
<div class="flex items-center gap-2 px-2 py-1.5">
<img src="/favicon-32.png" class="size-4" alt="OpenPencil" />
<input
v-if="editingName"
data-doc-name-edit
class="min-w-0 flex-1 rounded border border-accent bg-input px-1 py-0.5 text-xs text-surface outline-none"
:value="store.state.documentName"
@blur="commitRename($event.target as HTMLInputElement)"
@keydown.enter="($event.target as HTMLInputElement).blur()"
@keydown.escape="editingName = false"
/>
<span
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>
<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 (⌘\)"
@click="store.state.showUI = !store.state.showUI"
>
<icon-lucide-sidebar class="size-3.5" />
</button>
</div>
<div v-if="!IS_TAURI" class="flex items-center px-1 pb-1">
<MenubarRoot class="flex items-center gap-0.5 overflow-x-auto scrollbar-none">
<MenubarMenu v-for="menu in topMenus" :key="menu.label">
<MenubarTrigger
class="flex cursor-pointer items-center rounded px-2 py-1 text-xs text-muted transition-colors select-none hover:bg-hover hover:text-surface data-[state=open]:bg-hover data-[state=open]:text-surface"
>
{{ menu.label }}
</MenubarTrigger>
<MenubarPortal>
<MenubarContent
@ -186,6 +229,7 @@ const topMenus = [
</MenubarContent>
</MenubarPortal>
</MenubarMenu>
</MenubarRoot>
</MenubarRoot>
</div>
</div>
</template>

View file

@ -81,6 +81,11 @@ export function useKeyboard(store: EditorStore) {
}
if (e.metaKey || e.ctrlKey) {
if (e.code === 'Backslash') {
e.preventDefault()
store.state.showUI = !store.state.showUI
return
}
if (e.code === 'KeyJ') {
e.preventDefault()
activeTab.value = activeTab.value === 'ai' ? 'design' : 'ai'

View file

@ -174,6 +174,8 @@ export function createEditorStore() {
y: number
selection?: string[]
}>,
showUI: true,
documentName: 'Untitled' as string,
panX: 0,
pageColor: { ...CANVAS_BG_COLOR } as Color,
panY: 0,
@ -565,6 +567,8 @@ export function createEditorStore() {
pageViewports.clear()
fileHandle = handle ?? null
filePath = path ?? null
state.documentName = file.name.replace(/\.fig$/i, '')
downloadName = file.name
state.selectedIds = new Set()
const firstPage = graph.getPages()[0]
state.currentPageId = firstPage?.id ?? graph.rootId
@ -613,6 +617,7 @@ export function createEditorStore() {
if (!path) return
filePath = path
fileHandle = null
state.documentName = path.split('/').pop()?.replace(/\.fig$/i, '') ?? 'Untitled'
await writeFile(data)
return
}
@ -630,6 +635,7 @@ export function createEditorStore() {
})
fileHandle = handle
filePath = null
state.documentName = handle.name.replace(/\.fig$/i, '')
await writeFile(data)
return
} catch (e) {
@ -640,6 +646,7 @@ export function createEditorStore() {
const filename = prompt('Save as:', downloadName ?? 'Untitled.fig')
if (!filename) return
downloadName = filename
state.documentName = filename.replace(/\.fig$/i, '')
downloadBlob(new Uint8Array(data), filename, 'application/octet-stream')
}

View file

@ -72,7 +72,7 @@ function onDisconnect() {
<div class="flex h-screen w-screen flex-col">
<SafariBanner />
<SplitterGroup
v-if="showChrome"
v-if="showChrome && store.state.showUI"
direction="horizontal"
class="flex-1 overflow-hidden"
auto-save-id="editor-layout"
@ -111,6 +111,25 @@ function onDisconnect() {
<PropertiesPanel />
</SplitterPanel>
</SplitterGroup>
<div v-else-if="showChrome" class="flex flex-1 overflow-hidden">
<div class="relative flex min-w-0 flex-1">
<EditorCanvas />
<Toolbar />
<div
class="absolute left-7 top-7 z-10 flex items-center gap-2 rounded-lg border border-border bg-panel px-2 py-1 shadow-sm"
>
<img src="/favicon-32.png" class="size-4" alt="OpenPencil" />
<span class="text-xs text-surface">{{ store.state.documentName }}</span>
<button
class="ml-1 flex size-6 cursor-pointer items-center justify-center rounded text-muted transition-colors hover:bg-hover hover:text-surface"
title="Show UI (⌘\)"
@click="store.state.showUI = true"
>
<icon-lucide-sidebar class="size-3.5" />
</button>
</div>
</div>
</div>
<div v-else class="flex flex-1 overflow-hidden">
<div class="relative flex min-w-0 flex-1">
<EditorCanvas />