Add app menu bar for browser mode (File, Edit, View, Object, Text, Arrange)
Reka-ui Menubar in the left panel header, hidden when running in Tauri (native menu handles it). Includes Open, Save, Save As, Export, Undo/Redo, Zoom, Group/Ungroup, Component creation, Bring to front/Send to back, Auto layout, Text formatting, Alignment.
This commit is contained in:
parent
3849872b32
commit
7809fb60f9
1
components.d.ts
vendored
1
components.d.ts
vendored
|
|
@ -14,6 +14,7 @@ declare module 'vue' {
|
|||
ApiKeySetup: typeof import('./src/components/chat/ApiKeySetup.vue')['default']
|
||||
APIKeySetup: typeof import('./src/components/chat/APIKeySetup.vue')['default']
|
||||
AppearanceSection: typeof import('./src/components/properties/AppearanceSection.vue')['default']
|
||||
AppMenu: typeof import('./src/components/AppMenu.vue')['default']
|
||||
CanvasContextMenu: typeof import('./src/components/CanvasContextMenu.vue')['default']
|
||||
ChatInput: typeof import('./src/components/chat/ChatInput.vue')['default']
|
||||
ChatMessage: typeof import('./src/components/chat/ChatMessage.vue')['default']
|
||||
|
|
|
|||
196
src/components/AppMenu.vue
Normal file
196
src/components/AppMenu.vue
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarMenu,
|
||||
MenubarPortal,
|
||||
MenubarRoot,
|
||||
MenubarSeparator,
|
||||
MenubarSub,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarTrigger
|
||||
} from 'reka-ui'
|
||||
|
||||
import IconChevronRight from '~icons/lucide/chevron-right'
|
||||
|
||||
import { IS_TAURI } from '@/constants'
|
||||
import { openFileDialog } from '@/composables/use-menu'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
|
||||
const store = useEditorStore()
|
||||
|
||||
const isMac = navigator.platform.includes('Mac')
|
||||
const mod = isMac ? '⌘' : 'Ctrl+'
|
||||
|
||||
interface MenuItem {
|
||||
label: string
|
||||
shortcut?: string
|
||||
action?: () => void
|
||||
separator?: boolean
|
||||
disabled?: boolean
|
||||
sub?: MenuItem[]
|
||||
}
|
||||
|
||||
const fileMenu: MenuItem[] = [
|
||||
{ label: 'Open…', shortcut: `${mod}O`, action: () => openFileDialog(store) },
|
||||
{ separator: true },
|
||||
{ label: 'Save', shortcut: `${mod}S`, action: () => store.saveFigFile() },
|
||||
{ label: 'Save as…', shortcut: `${mod}⇧S`, action: () => store.saveFigFileAs() },
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Export selection…',
|
||||
shortcut: `${mod}⇧E`,
|
||||
action: () => {
|
||||
if (store.state.selectedIds.size > 0) store.exportSelection(1, 'PNG')
|
||||
},
|
||||
disabled: store.state.selectedIds.size === 0
|
||||
}
|
||||
]
|
||||
|
||||
const editMenu: MenuItem[] = [
|
||||
{ label: 'Undo', shortcut: `${mod}Z`, action: () => store.undoAction() },
|
||||
{ label: 'Redo', shortcut: `${mod}⇧Z`, action: () => store.redoAction() },
|
||||
{ separator: true },
|
||||
{ label: 'Copy', shortcut: `${mod}C` },
|
||||
{ label: 'Paste', shortcut: `${mod}V` },
|
||||
{ label: 'Duplicate', shortcut: `${mod}D`, action: () => store.duplicateSelected() },
|
||||
{ label: 'Delete', shortcut: '⌫', action: () => store.deleteSelected() },
|
||||
{ separator: true },
|
||||
{ label: 'Select all', shortcut: `${mod}A`, action: () => store.selectAll() }
|
||||
]
|
||||
|
||||
const viewMenu: MenuItem[] = [
|
||||
{ label: 'Zoom to fit', shortcut: '⇧1', action: () => store.zoomToFit() },
|
||||
{ label: 'Zoom in', shortcut: `${mod}=`, action: () => store.applyZoom(-100, window.innerWidth / 2, window.innerHeight / 2) },
|
||||
{ label: 'Zoom out', shortcut: `${mod}-`, action: () => store.applyZoom(100, window.innerWidth / 2, window.innerHeight / 2) }
|
||||
]
|
||||
|
||||
const objectMenu: MenuItem[] = [
|
||||
{ label: 'Group', shortcut: `${mod}G`, action: () => store.groupSelected() },
|
||||
{ label: 'Ungroup', shortcut: `${mod}⇧G`, action: () => store.ungroupSelected() },
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Create component',
|
||||
shortcut: `${mod}⌥K`,
|
||||
action: () => store.createComponentFromSelection()
|
||||
},
|
||||
{
|
||||
label: 'Create component set',
|
||||
action: () => store.createComponentSetFromComponents()
|
||||
},
|
||||
{ label: 'Detach instance', action: () => store.detachInstance() },
|
||||
{ separator: true },
|
||||
{ label: 'Bring to front', shortcut: ']', action: () => store.bringToFront() },
|
||||
{ label: 'Send to back', shortcut: '[', action: () => store.sendToBack() }
|
||||
]
|
||||
|
||||
const textMenu: MenuItem[] = [
|
||||
{ label: 'Bold', shortcut: `${mod}B` },
|
||||
{ label: 'Italic', shortcut: `${mod}I` },
|
||||
{ label: 'Underline', shortcut: `${mod}U` }
|
||||
]
|
||||
|
||||
const arrangeMenu: MenuItem[] = [
|
||||
{ label: 'Add auto layout', shortcut: '⇧A', action: () => store.wrapInAutoLayout() },
|
||||
{ separator: true },
|
||||
{ label: 'Align left', shortcut: '⌥A' },
|
||||
{ label: 'Align center', shortcut: '⌥H' },
|
||||
{ label: 'Align right', shortcut: '⌥D' },
|
||||
{ separator: true },
|
||||
{ label: 'Align top', shortcut: '⌥W' },
|
||||
{ label: 'Align middle', shortcut: '⌥V' },
|
||||
{ label: 'Align bottom', shortcut: '⌥S' }
|
||||
]
|
||||
|
||||
const topMenus = [
|
||||
{ label: 'File', items: fileMenu },
|
||||
{ label: 'Edit', items: editMenu },
|
||||
{ label: 'View', items: viewMenu },
|
||||
{ label: 'Object', items: objectMenu },
|
||||
{ label: 'Text', items: textMenu },
|
||||
{ label: 'Arrange', items: arrangeMenu }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!IS_TAURI" class="border-b border-border px-1 py-1">
|
||||
<MenubarRoot class="flex 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>
|
||||
|
||||
<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>
|
||||
<span v-if="item.shortcut" class="text-[11px] text-muted">{{
|
||||
item.shortcut
|
||||
}}</span>
|
||||
</MenubarItem>
|
||||
</template>
|
||||
</MenubarContent>
|
||||
</MenubarPortal>
|
||||
</MenubarMenu>
|
||||
</MenubarRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -15,6 +15,7 @@ import IconSection from '~icons/lucide/layout-grid'
|
|||
import IconSquare from '~icons/lucide/square'
|
||||
import IconType from '~icons/lucide/type'
|
||||
|
||||
import AppMenu from './AppMenu.vue'
|
||||
import PagesPanel from './PagesPanel.vue'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
|
||||
|
|
@ -225,6 +226,7 @@ function updateDropTarget(ev: PointerEvent) {
|
|||
class="flex min-w-0 flex-1 flex-col overflow-y-auto border-r border-border bg-panel"
|
||||
style="contain: paint layout style"
|
||||
>
|
||||
<AppMenu />
|
||||
<PagesPanel />
|
||||
<header class="shrink-0 px-3 py-2 text-[11px] uppercase tracking-wider text-muted">
|
||||
Layers
|
||||
|
|
|
|||
Loading…
Reference in a new issue