* feat: refresh branding with generated platform icons Keep the approved mark and optical micro master as the source of truth. Generate web, documentation, and native assets at their owning build boundaries instead of storing raster variants or linking web icons to desktop output. * fix: refine small brand marks and loading artwork * feat: adopt blue editing-handle brand mark * feat: refine teal branding for light and dark themes * feat: apply ivory tiles across brand surfaces Use edge-to-edge web tiles with a larger mark and optical micro favicons. Preserve native spacing and platform-owned maskable/touch cropping. Address review feedback on story props, native dimensions, and brand test type checking. * test: allow cold npm startup in packaging fixture CI hit Bun's five-second default while running npm pack --dry-run. Give this integration test a scoped 30-second budget without changing production timeouts or other tests.
182 lines
6.9 KiB
Vue
182 lines
6.9 KiB
Vue
<script setup lang="ts">
|
|
import { templateRef } from '@vueuse/core'
|
|
import {
|
|
MenubarCheckboxItem,
|
|
MenubarContent,
|
|
MenubarItem,
|
|
MenubarItemIndicator,
|
|
MenubarMenu,
|
|
MenubarPortal,
|
|
MenubarRoot,
|
|
MenubarSeparator,
|
|
MenubarSub,
|
|
MenubarSubContent,
|
|
MenubarSubTrigger,
|
|
MenubarTrigger
|
|
} from 'reka-ui'
|
|
import { watch } from 'vue'
|
|
import IconChevronRight from '~icons/lucide/chevron-right'
|
|
|
|
import { vTestId, useI18n } from '@open-pencil/vue'
|
|
|
|
import { useEditorStore } from '@/app/editor/active-store'
|
|
import { openSettingsDialog } from '@/app/settings/dialog'
|
|
import { useAppMenu } from '@/app/shell/menu/app-menu'
|
|
import { useDocumentNameRename } from '@/app/shell/menu/document-name'
|
|
import {
|
|
hasMenuSubItems,
|
|
isMenuCheckbox,
|
|
isMenuSeparator,
|
|
menuChecked,
|
|
menuDisabled,
|
|
menuLabel,
|
|
menuShortcut,
|
|
menuSubItems,
|
|
runMenuAction,
|
|
updateMenuChecked
|
|
} from '@/app/shell/menu/entry'
|
|
import { appMenuShortcutLabel } from '@/app/shell/menu/shortcut'
|
|
import { resolvedAppTheme } from '@/app/shell/theme'
|
|
import BrandMark from '@/components/brand/BrandMark.vue'
|
|
import IconButton from '@/components/ui/button/IconButton.vue'
|
|
import AppShortcutText from '@/components/ui/menu/AppShortcutText.vue'
|
|
import { useMenuUI } from '@/components/ui/menu/menu'
|
|
import { IS_TAURI } from '@/constants'
|
|
|
|
const store = useEditorStore()
|
|
|
|
const { rename, editingName, startRename, commitRename } = useDocumentNameRename(store)
|
|
const nameInput = templateRef<HTMLInputElement>('nameInput')
|
|
|
|
watch(nameInput, (input) => {
|
|
if (input) void rename.focusInput(input)
|
|
})
|
|
|
|
const { menu: t, settings } = useI18n()
|
|
|
|
const { topMenus } = useAppMenu()
|
|
const menuCls = useMenuUI()
|
|
const mainMenuCls = useMenuUI({ content: 'min-w-52' })
|
|
const subMenuCls = useMenuUI({ content: 'min-w-44' })
|
|
</script>
|
|
|
|
<template>
|
|
<div class="shrink-0 border-b border-border">
|
|
<div class="flex items-center gap-2 px-2 py-1.5">
|
|
<BrandMark
|
|
data-test-id="app-logo"
|
|
variant="app-icon"
|
|
:appearance="resolvedAppTheme"
|
|
class="size-6"
|
|
/>
|
|
<input
|
|
v-if="editingName"
|
|
ref="nameInput"
|
|
data-test-id="app-document-name-input"
|
|
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)"
|
|
@keydown="rename.onKeydown"
|
|
/>
|
|
<span
|
|
v-else
|
|
data-test-id="app-document-name"
|
|
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
|
|
>
|
|
<IconButton
|
|
:label="settings.title"
|
|
data-test-id="app-settings-trigger"
|
|
@click="openSettingsDialog()"
|
|
>
|
|
<icon-lucide-settings class="size-3.5" />
|
|
</IconButton>
|
|
<IconButton
|
|
:label="`${t.toggleUI} (${appMenuShortcutLabel('toggle-ui')})`"
|
|
data-test-id="app-toggle-ui"
|
|
@click="store.state.showUI = !store.state.showUI"
|
|
>
|
|
<icon-lucide-sidebar class="size-3.5" />
|
|
</IconButton>
|
|
</div>
|
|
<div v-if="!IS_TAURI" class="flex items-center px-1 pb-1">
|
|
<MenubarRoot class="scrollbar-none flex items-center gap-0.5 overflow-x-auto">
|
|
<MenubarMenu v-for="menu in topMenus" :key="menu.label">
|
|
<MenubarTrigger
|
|
v-test-id="`menubar-${menu.label.toLowerCase()}`"
|
|
class="flex cursor-pointer items-center rounded px-2 py-1 text-[11px] text-surface/80 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="mainMenuCls.content">
|
|
<template v-for="(item, i) in menu.items" :key="i">
|
|
<MenubarSeparator v-if="isMenuSeparator(item)" :class="menuCls.separator" />
|
|
<MenubarSub v-else-if="hasMenuSubItems(item)">
|
|
<MenubarSubTrigger :class="menuCls.item" :disabled="menuDisabled(item)">
|
|
<span class="flex-1">{{ menuLabel(item) }}</span>
|
|
<IconChevronRight class="size-3 text-muted" />
|
|
</MenubarSubTrigger>
|
|
<MenubarPortal>
|
|
<MenubarSubContent :side-offset="4" :class="subMenuCls.content">
|
|
<template v-for="(sub, j) in menuSubItems(item)" :key="j">
|
|
<MenubarSeparator v-if="isMenuSeparator(sub)" :class="menuCls.separator" />
|
|
<MenubarCheckboxItem
|
|
v-else-if="isMenuCheckbox(sub)"
|
|
:model-value="menuChecked(sub)"
|
|
:class="menuCls.item"
|
|
@update:model-value="updateMenuChecked(sub, $event as boolean)"
|
|
>
|
|
<span class="flex-1">{{ menuLabel(sub) }}</span>
|
|
<MenubarItemIndicator class="text-surface">
|
|
<icon-lucide-check class="size-3.5" />
|
|
</MenubarItemIndicator>
|
|
</MenubarCheckboxItem>
|
|
<MenubarItem
|
|
v-else
|
|
:class="menuCls.item"
|
|
:disabled="menuDisabled(sub)"
|
|
@select="runMenuAction(sub)"
|
|
>
|
|
<span class="flex-1">{{ menuLabel(sub) }}</span>
|
|
<AppShortcutText v-if="menuShortcut(sub)">{{
|
|
menuShortcut(sub)
|
|
}}</AppShortcutText>
|
|
</MenubarItem>
|
|
</template>
|
|
</MenubarSubContent>
|
|
</MenubarPortal>
|
|
</MenubarSub>
|
|
<MenubarCheckboxItem
|
|
v-else-if="isMenuCheckbox(item)"
|
|
:model-value="menuChecked(item)"
|
|
:class="menuCls.item"
|
|
@update:model-value="updateMenuChecked(item, $event as boolean)"
|
|
>
|
|
<span class="flex-1">{{ menuLabel(item) }}</span>
|
|
<MenubarItemIndicator class="text-surface">
|
|
<icon-lucide-check class="size-3.5" />
|
|
</MenubarItemIndicator>
|
|
</MenubarCheckboxItem>
|
|
<MenubarItem
|
|
v-else
|
|
:class="menuCls.item"
|
|
:disabled="menuDisabled(item)"
|
|
@select="runMenuAction(item)"
|
|
>
|
|
<span class="flex-1">{{ menuLabel(item) }}</span>
|
|
<AppShortcutText v-if="menuShortcut(item)">{{
|
|
menuShortcut(item)
|
|
}}</AppShortcutText>
|
|
</MenubarItem>
|
|
</template>
|
|
</MenubarContent>
|
|
</MenubarPortal>
|
|
</MenubarMenu>
|
|
</MenubarRoot>
|
|
</div>
|
|
</div>
|
|
</template>
|