2026-05-22 18:24:02 +00:00
|
|
|
import { computed } from 'vue'
|
2026-08-03 14:28:39 +00:00
|
|
|
import { useRouter } from 'vue-router'
|
2026-05-22 18:24:02 +00:00
|
|
|
|
|
|
|
|
import type { MenuEntry } from '@open-pencil/vue'
|
2026-06-30 07:25:52 +00:00
|
|
|
import { useEditorCommands, useI18n } from '@open-pencil/vue'
|
2026-05-22 18:24:02 +00:00
|
|
|
|
|
|
|
|
import { useEditorStore } from '@/app/editor/active-store'
|
feat(app): expand Figma menu and selection parity (#459)
- Add Figma-style object, component, selection, view, settings, and browser Move to Page actions
- Add undoable z-order, transformed distribution, inverse selection, and single/bulk rename behavior
- Keep browser and native menus, shortcuts, generated schema, localization, and documentation aligned
2026-08-02 21:23:05 +00:00
|
|
|
import { openSettingsDialog } from '@/app/settings/dialog'
|
2026-05-22 18:24:02 +00:00
|
|
|
import { createSharedEditorMenuActions } from '@/app/shell/menu/editor-actions'
|
2026-08-03 14:28:39 +00:00
|
|
|
import { openStorageWorkspace } from '@/app/shell/menu/navigation'
|
2026-05-22 18:24:02 +00:00
|
|
|
import type { AppMenuActionItem, AppMenuEntry, AppMenuGroupSchema } from '@/app/shell/menu/schema'
|
2026-06-30 07:25:52 +00:00
|
|
|
import { APP_MENU_SCHEMA } from '@/app/shell/menu/schema'
|
feat(app): expand Figma menu and selection parity (#459)
- Add Figma-style object, component, selection, view, settings, and browser Move to Page actions
- Add undoable z-order, transformed distribution, inverse selection, and single/bulk rename behavior
- Keep browser and native menus, shortcuts, generated schema, localization, and documentation aligned
2026-08-02 21:23:05 +00:00
|
|
|
import { createSelectionMenuActions } from '@/app/shell/menu/selection-actions'
|
2026-05-22 18:24:02 +00:00
|
|
|
import { appMenuShortcutLabel } from '@/app/shell/menu/shortcut'
|
|
|
|
|
import { openFileDialog } from '@/app/shell/menu/use'
|
|
|
|
|
import { useAppTheme } from '@/app/shell/theme'
|
feat(app): expand Figma menu and selection parity (#459)
- Add Figma-style object, component, selection, view, settings, and browser Move to Page actions
- Add undoable z-order, transformed distribution, inverse selection, and single/bulk rename behavior
- Keep browser and native menus, shortcuts, generated schema, localization, and documentation aligned
2026-08-02 21:23:05 +00:00
|
|
|
import { closeTab, activeTab } from '@/app/tabs'
|
Wire i18n into commands, menus, and add locale files
Commands and menu models now read labels from nanostores i18n
messages instead of hardcoded English strings. App menu rebuilt
to use i18n directly instead of matching on localized labels.
Added locale files: de, es, fr, it, pl (ru was added earlier).
All 6 translations plus English base cover menus, commands, tools,
panels, pages, and dialogs.
2026-03-25 14:05:11 +00:00
|
|
|
|
|
|
|
|
export interface AppMenuGroup {
|
2026-05-22 18:24:02 +00:00
|
|
|
label: string
|
|
|
|
|
items: MenuEntry[]
|
Wire i18n into commands, menus, and add locale files
Commands and menu models now read labels from nanostores i18n
messages instead of hardcoded English strings. App menu rebuilt
to use i18n directly instead of matching on localized labels.
Added locale files: de, es, fr, it, pl (ru was added earlier).
All 6 translations plus English base cover menus, commands, tools,
panels, pages, and dialogs.
2026-03-25 14:05:11 +00:00
|
|
|
}
|
2026-03-23 08:57:43 +00:00
|
|
|
|
2026-04-30 13:01:47 +00:00
|
|
|
function isVisible(entry: { target?: string }): boolean {
|
2026-05-22 18:24:02 +00:00
|
|
|
return entry.target !== 'native'
|
2026-04-30 13:01:47 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-22 18:24:02 +00:00
|
|
|
function isSeparator(entry: AppMenuEntry): entry is Extract<AppMenuEntry, { type: 'separator' }> {
|
|
|
|
|
return entry.type === 'separator'
|
2026-04-30 13:01:47 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-17 11:44:37 +00:00
|
|
|
export function useAppMenu() {
|
2026-05-22 18:24:02 +00:00
|
|
|
const store = useEditorStore()
|
2026-08-03 14:28:39 +00:00
|
|
|
const router = useRouter()
|
feat(app): expand Figma menu and selection parity (#459)
- Add Figma-style object, component, selection, view, settings, and browser Move to Page actions
- Add undoable z-order, transformed distribution, inverse selection, and single/bulk rename behavior
- Keep browser and native menus, shortcuts, generated schema, localization, and documentation aligned
2026-08-02 21:23:05 +00:00
|
|
|
const {
|
|
|
|
|
commands,
|
|
|
|
|
menuItem: commandMenuItem,
|
|
|
|
|
otherPages,
|
|
|
|
|
moveSelectionToPage
|
|
|
|
|
} = useEditorCommands()
|
2026-05-24 12:48:08 +00:00
|
|
|
const { menu, locale, availableLocales, localeLabels, setLocale } = useI18n()
|
2026-05-22 18:24:02 +00:00
|
|
|
const { theme, setTheme } = useAppTheme()
|
|
|
|
|
|
2026-05-24 12:48:08 +00:00
|
|
|
const translatedMenuItemLabels: Partial<Record<string, keyof typeof menu.value>> = {
|
|
|
|
|
new: 'new',
|
|
|
|
|
open: 'open',
|
2026-08-03 14:28:39 +00:00
|
|
|
'open-storage-workspace': 'openStorageWorkspace',
|
2026-05-24 12:48:08 +00:00
|
|
|
save: 'save',
|
|
|
|
|
'save-as': 'saveAs',
|
|
|
|
|
'export-selection': 'exportSelection',
|
|
|
|
|
autosave: 'autosave',
|
|
|
|
|
close: 'closeTab',
|
|
|
|
|
copy: 'copy',
|
|
|
|
|
cut: 'cut',
|
|
|
|
|
paste: 'paste',
|
|
|
|
|
'paste-to-replace': 'pasteToReplace',
|
feat(app): expand Figma menu and selection parity (#459)
- Add Figma-style object, component, selection, view, settings, and browser Move to Page actions
- Add undoable z-order, transformed distribution, inverse selection, and single/bulk rename behavior
- Keep browser and native menus, shortcuts, generated schema, localization, and documentation aligned
2026-08-02 21:23:05 +00:00
|
|
|
'selection.rename': 'renameSelection',
|
|
|
|
|
'selection.moveToPage': 'moveToPage',
|
2026-05-24 12:48:08 +00:00
|
|
|
language: 'language',
|
feat(app): expand Figma menu and selection parity (#459)
- Add Figma-style object, component, selection, view, settings, and browser Move to Page actions
- Add undoable z-order, transformed distribution, inverse selection, and single/bulk rename behavior
- Keep browser and native menus, shortcuts, generated schema, localization, and documentation aligned
2026-08-02 21:23:05 +00:00
|
|
|
settings: 'settings',
|
|
|
|
|
'view-rulers': 'rulers',
|
|
|
|
|
'view-multiplayer-cursors': 'multiplayerCursors',
|
2026-06-30 07:25:52 +00:00
|
|
|
profiler: 'profiler',
|
|
|
|
|
'toggle-ui': 'toggleUI',
|
|
|
|
|
theme: 'theme',
|
|
|
|
|
'theme-light': 'themeLight',
|
|
|
|
|
'theme-dark': 'themeDark',
|
|
|
|
|
'theme-auto': 'themeAuto',
|
|
|
|
|
'zoom-in': 'zoomIn',
|
|
|
|
|
'zoom-out': 'zoomOut',
|
|
|
|
|
'text.bold': 'bold',
|
|
|
|
|
'text.italic': 'italic',
|
|
|
|
|
'text.underline': 'underline',
|
|
|
|
|
'arrange.align-left': 'arrangeAlignLeft',
|
|
|
|
|
'arrange.align-center': 'arrangeAlignCenter',
|
|
|
|
|
'arrange.align-right': 'arrangeAlignRight',
|
|
|
|
|
'arrange.align-top': 'arrangeAlignTop',
|
|
|
|
|
'arrange.align-middle': 'arrangeAlignMiddle',
|
|
|
|
|
'arrange.align-bottom': 'arrangeAlignBottom'
|
2026-05-24 12:48:08 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-22 18:24:02 +00:00
|
|
|
const languageMenu = computed<MenuEntry[]>(() =>
|
|
|
|
|
availableLocales.map((code) => ({
|
|
|
|
|
label: localeLabels[code],
|
|
|
|
|
checked: locale.value === code,
|
|
|
|
|
onCheckedChange: (checked: boolean) => {
|
|
|
|
|
if (checked) setLocale(code)
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-28 07:12:41 +00:00
|
|
|
function exportSelection(format: 'png' | 'svg' | 'pptx' | 'fig') {
|
2026-05-22 18:24:02 +00:00
|
|
|
if (store.state.selectedIds.size > 0) void store.exportSelection(1, format)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const actions: Partial<Record<string, () => void>> = {
|
|
|
|
|
new: () => {
|
|
|
|
|
void import('@/app/tabs').then((m) => m.createTab())
|
|
|
|
|
},
|
|
|
|
|
open: () => void openFileDialog(),
|
2026-08-03 14:28:39 +00:00
|
|
|
'open-storage-workspace': () => openStorageWorkspace(router),
|
2026-05-22 18:24:02 +00:00
|
|
|
save: () => void store.saveFigFile(),
|
|
|
|
|
'save-as': () => void store.saveFigFileAs(),
|
|
|
|
|
'export-selection': () => exportSelection('png'),
|
feat(app): expand Figma menu and selection parity (#459)
- Add Figma-style object, component, selection, view, settings, and browser Move to Page actions
- Add undoable z-order, transformed distribution, inverse selection, and single/bulk rename behavior
- Keep browser and native menus, shortcuts, generated schema, localization, and documentation aligned
2026-08-02 21:23:05 +00:00
|
|
|
...createSelectionMenuActions(store),
|
|
|
|
|
close: () => {
|
|
|
|
|
if (activeTab.value) closeTab(activeTab.value.id)
|
|
|
|
|
},
|
|
|
|
|
settings: openSettingsDialog,
|
2026-05-22 18:24:02 +00:00
|
|
|
'export-png': () => exportSelection('png'),
|
|
|
|
|
'export-svg': () => exportSelection('svg'),
|
2026-07-28 07:12:41 +00:00
|
|
|
'export-pptx': () => exportSelection('pptx'),
|
2026-05-22 18:24:02 +00:00
|
|
|
'export-fig': () => exportSelection('fig'),
|
|
|
|
|
...createSharedEditorMenuActions(setTheme)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function itemAction(item: AppMenuActionItem): (() => void) | undefined {
|
|
|
|
|
return actions[item.id]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function checked(item: AppMenuActionItem): boolean | undefined {
|
|
|
|
|
switch (item.id) {
|
|
|
|
|
case 'autosave':
|
|
|
|
|
return store.state.autosaveEnabled
|
|
|
|
|
case 'profiler':
|
|
|
|
|
return store.renderer?.profiler.hudVisible ?? false
|
feat(app): expand Figma menu and selection parity (#459)
- Add Figma-style object, component, selection, view, settings, and browser Move to Page actions
- Add undoable z-order, transformed distribution, inverse selection, and single/bulk rename behavior
- Keep browser and native menus, shortcuts, generated schema, localization, and documentation aligned
2026-08-02 21:23:05 +00:00
|
|
|
case 'view-rulers':
|
|
|
|
|
return store.state.showRulers
|
|
|
|
|
case 'view-multiplayer-cursors':
|
|
|
|
|
return store.state.showRemoteCursors
|
2026-05-22 18:24:02 +00:00
|
|
|
case 'theme-light':
|
|
|
|
|
return theme.value === 'light'
|
|
|
|
|
case 'theme-dark':
|
|
|
|
|
return theme.value === 'dark'
|
|
|
|
|
case 'theme-auto':
|
|
|
|
|
return theme.value === 'auto'
|
|
|
|
|
default:
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onCheckedChange(item: AppMenuActionItem): ((checked: boolean) => void) | undefined {
|
|
|
|
|
switch (item.id) {
|
|
|
|
|
case 'autosave':
|
|
|
|
|
return (value: boolean) => {
|
|
|
|
|
store.state.autosaveEnabled = value
|
|
|
|
|
}
|
|
|
|
|
case 'profiler':
|
|
|
|
|
return () => store.toggleProfiler()
|
feat(app): expand Figma menu and selection parity (#459)
- Add Figma-style object, component, selection, view, settings, and browser Move to Page actions
- Add undoable z-order, transformed distribution, inverse selection, and single/bulk rename behavior
- Keep browser and native menus, shortcuts, generated schema, localization, and documentation aligned
2026-08-02 21:23:05 +00:00
|
|
|
case 'view-rulers':
|
|
|
|
|
return (value: boolean) => {
|
|
|
|
|
if (store.state.showRulers !== value) itemAction(item)?.()
|
|
|
|
|
}
|
|
|
|
|
case 'view-multiplayer-cursors':
|
|
|
|
|
return (value: boolean) => {
|
|
|
|
|
if (store.state.showRemoteCursors !== value) itemAction(item)?.()
|
|
|
|
|
}
|
2026-05-22 18:24:02 +00:00
|
|
|
case 'theme-light':
|
|
|
|
|
case 'theme-dark':
|
|
|
|
|
case 'theme-auto':
|
|
|
|
|
return (value: boolean) => {
|
|
|
|
|
if (value) itemAction(item)?.()
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 12:48:08 +00:00
|
|
|
function menuLabel(entry: AppMenuActionItem): string {
|
|
|
|
|
const key = translatedMenuItemLabels[entry.id]
|
|
|
|
|
return key ? menu.value[key] : entry.label
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 18:24:02 +00:00
|
|
|
function buildEntry(entry: AppMenuEntry): MenuEntry | null {
|
|
|
|
|
if (!isVisible(entry)) return null
|
|
|
|
|
if (isSeparator(entry)) return { separator: true }
|
|
|
|
|
|
|
|
|
|
if (entry.id === 'language') {
|
2026-05-24 12:48:08 +00:00
|
|
|
return { label: menuLabel(entry), sub: languageMenu.value }
|
2026-05-22 18:24:02 +00:00
|
|
|
}
|
feat(app): expand Figma menu and selection parity (#459)
- Add Figma-style object, component, selection, view, settings, and browser Move to Page actions
- Add undoable z-order, transformed distribution, inverse selection, and single/bulk rename behavior
- Keep browser and native menus, shortcuts, generated schema, localization, and documentation aligned
2026-08-02 21:23:05 +00:00
|
|
|
|
|
|
|
|
if (entry.id === 'selection.moveToPage') {
|
|
|
|
|
if (otherPages.value.length === 0) return null
|
|
|
|
|
const disabled = !commands['selection.moveToPage'].enabled.value
|
|
|
|
|
return {
|
|
|
|
|
label: menuLabel(entry),
|
|
|
|
|
disabled,
|
|
|
|
|
sub: otherPages.value.map((page) => ({
|
|
|
|
|
label: page.name,
|
|
|
|
|
disabled,
|
|
|
|
|
action: () => moveSelectionToPage(page.id)
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-22 18:24:02 +00:00
|
|
|
|
|
|
|
|
if (entry.command) {
|
|
|
|
|
return commandMenuItem(entry.command, appMenuShortcutLabel(entry.id))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
2026-05-24 12:48:08 +00:00
|
|
|
label: menuLabel(entry),
|
2026-05-22 18:24:02 +00:00
|
|
|
shortcut: appMenuShortcutLabel(entry.id),
|
|
|
|
|
action: itemAction(entry),
|
|
|
|
|
checked: checked(entry),
|
|
|
|
|
onCheckedChange: onCheckedChange(entry),
|
|
|
|
|
sub: entry.sub?.map(buildEntry).filter((item): item is MenuEntry => item !== null)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 12:48:08 +00:00
|
|
|
function groupLabel(group: AppMenuGroupSchema): string {
|
|
|
|
|
const key = group.label.toLowerCase() as keyof typeof menu.value
|
|
|
|
|
return menu.value[key] ?? group.label
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 18:24:02 +00:00
|
|
|
function buildGroup(group: AppMenuGroupSchema): AppMenuGroup | null {
|
|
|
|
|
if (!isVisible(group)) return null
|
|
|
|
|
return {
|
2026-05-24 12:48:08 +00:00
|
|
|
label: groupLabel(group),
|
2026-05-22 18:24:02 +00:00
|
|
|
items: group.items.map(buildEntry).filter((item): item is MenuEntry => item !== null)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const topMenus = computed<AppMenuGroup[]>(() =>
|
|
|
|
|
APP_MENU_SCHEMA.map(buildGroup).filter((group): group is AppMenuGroup => group !== null)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return { topMenus }
|
2026-03-23 08:57:43 +00:00
|
|
|
}
|