Extract menu styling with tailwind-variants
This commit is contained in:
parent
a334a7f63c
commit
35dc5bd2a5
6
bun.lock
6
bun.lock
|
|
@ -28,6 +28,8 @@
|
|||
"motion-v": "^2.0.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"reka-ui": "^2.9.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"trystero": "^0.22.0",
|
||||
"valibot": "^1.2.0",
|
||||
|
|
@ -2163,6 +2165,10 @@
|
|||
|
||||
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
||||
|
||||
"tailwind-variants": ["tailwind-variants@3.2.2", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge"] }, "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
|
||||
|
||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@
|
|||
"motion-v": "^2.0.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"reka-ui": "^2.9.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"trystero": "^0.22.0",
|
||||
"valibot": "^1.2.0",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import IconChevronRight from '~icons/lucide/chevron-right'
|
|||
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { menuContent, menuItem, menuSeparator } from '@/components/ui/menu'
|
||||
import { IS_TAURI } from '@/constants'
|
||||
import { openFileDialog } from '@/composables/use-menu'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
|
|
@ -214,13 +215,13 @@ const topMenus = [
|
|||
<MenubarContent
|
||||
:side-offset="4"
|
||||
align="start"
|
||||
class="min-w-52 rounded-lg border border-border bg-panel p-1 shadow-lg"
|
||||
:class="menuContent({ class: 'min-w-52' })"
|
||||
>
|
||||
<template v-for="(item, i) in menu.items" :key="i">
|
||||
<MenubarSeparator v-if="item.separator" class="mx-1 my-1 h-px bg-border" />
|
||||
<MenubarSeparator v-if="item.separator" :class="menuSeparator()" />
|
||||
<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"
|
||||
:class="menuItem()"
|
||||
>
|
||||
<span class="flex-1">{{ item.label }}</span>
|
||||
<IconChevronRight class="size-3 text-muted" />
|
||||
|
|
@ -228,14 +229,13 @@ const topMenus = [
|
|||
<MenubarPortal>
|
||||
<MenubarSubContent
|
||||
:side-offset="4"
|
||||
class="min-w-44 rounded-lg border border-border bg-panel p-1 shadow-lg"
|
||||
:class="menuContent({ class: 'min-w-44' })"
|
||||
>
|
||||
<template v-for="(sub, j) in item.sub" :key="j">
|
||||
<MenubarSeparator v-if="sub.separator" class="mx-1 my-1 h-px bg-border" />
|
||||
<MenubarSeparator v-if="sub.separator" :class="menuSeparator()" />
|
||||
<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'"
|
||||
:class="menuItem()"
|
||||
:disabled="sub.disabled"
|
||||
@select="sub.action?.()"
|
||||
>
|
||||
|
|
@ -251,7 +251,7 @@ const topMenus = [
|
|||
<MenubarCheckboxItem
|
||||
v-else-if="item.onCheckedChange"
|
||||
:model-value="item.checked"
|
||||
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"
|
||||
:class="menuItem()"
|
||||
@update:model-value="item.onCheckedChange?.($event as boolean)"
|
||||
>
|
||||
<span class="flex-1">{{ item.label }}</span>
|
||||
|
|
@ -261,8 +261,7 @@ const topMenus = [
|
|||
</MenubarCheckboxItem>
|
||||
<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'"
|
||||
:class="menuItem()"
|
||||
:disabled="item.disabled"
|
||||
@select="item.action?.()"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import IconImageDown from '~icons/lucide/image-down'
|
|||
import IconSave from '~icons/lucide/save'
|
||||
import IconZoomIn from '~icons/lucide/zoom-in'
|
||||
|
||||
import { menuContent, menuItem } from '@/components/ui/menu'
|
||||
import { openFileDialog } from '@/composables/use-menu'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { colorToCSS } from '@open-pencil/core'
|
||||
|
|
@ -216,12 +217,12 @@ const onlineCount = computed(() => props.collabPeers.length + 1)
|
|||
:side-offset="8"
|
||||
side="bottom"
|
||||
align="end"
|
||||
class="z-50 w-48 rounded-xl border border-border bg-panel p-1.5 shadow-xl"
|
||||
:class="menuContent({ class: 'w-48 rounded-xl p-1.5 shadow-xl' })"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
v-for="item in menuItems"
|
||||
:key="item.label"
|
||||
class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-none bg-transparent px-2.5 py-2 text-xs text-surface outline-none select-none active:bg-hover data-[highlighted]:bg-hover"
|
||||
:class="menuItem({ justify: 'start', class: 'w-full gap-2.5 rounded-lg border-none bg-transparent px-2.5 py-2 active:bg-hover' })"
|
||||
@click="item.action()"
|
||||
>
|
||||
<component :is="item.icon" class="size-4 text-muted" />
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
import { selectionToJSX, renderNodesToSVG } from '@open-pencil/core'
|
||||
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { menuContent, menuItem, menuSeparator } from '@/components/ui/menu'
|
||||
import { toast } from '@/composables/use-toast'
|
||||
|
||||
const store = useEditorStore()
|
||||
|
|
@ -99,12 +100,10 @@ function copyAsJSX() {
|
|||
toast.show('Copied as JSX')
|
||||
}
|
||||
|
||||
const itemClass =
|
||||
'flex w-full cursor-pointer select-none items-center justify-between gap-6 rounded px-2 py-1.5 text-xs text-surface outline-none data-[highlighted]:bg-hover data-[disabled]:cursor-default data-[disabled]:text-muted'
|
||||
const componentItemClass =
|
||||
'flex w-full cursor-pointer select-none items-center justify-between gap-6 rounded px-2 py-1.5 text-xs text-[#9747ff] outline-none data-[highlighted]:bg-[#9747ff]/12 data-[disabled]:cursor-default data-[disabled]:text-[#9747ff]/40'
|
||||
const menuClass =
|
||||
'z-50 min-w-56 rounded-lg border border-border bg-panel p-1 shadow-[0_8px_30px_rgb(0_0_0/0.4)] animate-in fade-in zoom-in-95'
|
||||
const itemClass = menuItem()
|
||||
const componentItemClass = menuItem({ tone: 'component' })
|
||||
const menuClass = menuContent({ class: 'min-w-56 shadow-[0_8px_30px_rgb(0_0_0/0.4)] animate-in fade-in zoom-in-95' })
|
||||
const separatorClass = menuSeparator({ class: 'my-1' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -154,7 +153,7 @@ const menuClass =
|
|||
<span class="text-[11px] text-muted">⌫</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator class="my-1 h-px bg-border" />
|
||||
<ContextMenuSeparator :class="separatorClass" />
|
||||
|
||||
<ContextMenuSub v-if="otherPages.length > 0 && hasSelection">
|
||||
<ContextMenuSubTrigger data-test-id="context-move-to-page" :class="itemClass">
|
||||
|
|
@ -194,7 +193,7 @@ const menuClass =
|
|||
<span class="text-[11px] text-muted">[</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator class="my-1 h-px bg-border" />
|
||||
<ContextMenuSeparator :class="separatorClass" />
|
||||
|
||||
<ContextMenuItem
|
||||
data-test-id="context-group"
|
||||
|
|
@ -224,7 +223,7 @@ const menuClass =
|
|||
<span class="text-[11px] text-muted">⇧A</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator class="my-1 h-px bg-border" />
|
||||
<ContextMenuSeparator :class="separatorClass" />
|
||||
|
||||
<ContextMenuItem
|
||||
data-test-id="context-create-component"
|
||||
|
|
@ -271,7 +270,7 @@ const menuClass =
|
|||
</ContextMenuItem>
|
||||
|
||||
<template v-if="hasSelection">
|
||||
<ContextMenuSeparator class="my-1 h-px bg-border" />
|
||||
<ContextMenuSeparator :class="separatorClass" />
|
||||
|
||||
<ContextMenuItem
|
||||
data-test-id="context-toggle-visibility"
|
||||
|
|
@ -290,7 +289,7 @@ const menuClass =
|
|||
<span class="text-[11px] text-muted">⇧⌘L</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator class="my-1 h-px bg-border" />
|
||||
<ContextMenuSeparator :class="separatorClass" />
|
||||
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger data-test-id="context-copy-paste-as" :class="itemClass">
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import IconGroup from '~icons/lucide/group'
|
|||
import IconUngroup from '~icons/lucide/ungroup'
|
||||
import IconLock from '~icons/lucide/lock'
|
||||
|
||||
import { menuContent, menuItem } from '@/components/ui/menu'
|
||||
import { ACTION_TOAST_DURATION } from '@/constants'
|
||||
import { TOOLS, useEditorStore } from '@/stores/editor'
|
||||
import { toolIcons } from '@/utils/tools'
|
||||
|
|
@ -174,18 +175,14 @@ function goNext() {
|
|||
side="top"
|
||||
:side-offset="8"
|
||||
align="start"
|
||||
class="min-w-32 rounded-lg border border-border bg-panel p-1 shadow-lg"
|
||||
:class="menuContent({ class: 'min-w-32' })"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
v-for="sub in tool.flyout"
|
||||
:key="sub"
|
||||
:data-test-id="`toolbar-flyout-item-${sub.toLowerCase()}`"
|
||||
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs outline-none transition-colors"
|
||||
:class="
|
||||
store.state.activeTool === sub
|
||||
? 'bg-accent text-white'
|
||||
: 'text-surface hover:bg-hover'
|
||||
"
|
||||
:class="menuItem()"
|
||||
:class="store.state.activeTool === sub ? 'bg-accent text-white' : undefined"
|
||||
@select="store.setTool(sub)"
|
||||
>
|
||||
<component :is="toolIcons[sub]" class="size-3.5" />
|
||||
|
|
@ -291,13 +288,13 @@ function goNext() {
|
|||
side="top"
|
||||
:side-offset="8"
|
||||
align="start"
|
||||
class="min-w-32 rounded-lg border border-border bg-panel p-1 shadow-lg"
|
||||
:class="menuContent({ class: 'min-w-32' })"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
v-for="sub in tool.flyout"
|
||||
:key="sub"
|
||||
:data-test-id="`mobile-toolbar-flyout-item-${sub.toLowerCase()}`"
|
||||
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs outline-none transition-colors"
|
||||
:class="menuItem()"
|
||||
:class="
|
||||
store.state.activeTool === sub
|
||||
? 'bg-accent text-white'
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { computed, ref } from 'vue'
|
||||
|
||||
import AppSelect from '@/components/AppSelect.vue'
|
||||
import { menuContent, menuItem } from '@/components/ui/menu'
|
||||
import ColorInput from '@/components/ColorInput.vue'
|
||||
import ScrubInput from '@/components/ScrubInput.vue'
|
||||
import { useNodeProps } from '@/composables/use-node-props'
|
||||
|
|
@ -322,12 +323,12 @@ const borderWeights = computed(() => {
|
|||
<DropdownMenuContent
|
||||
:side-offset="4"
|
||||
align="end"
|
||||
class="z-50 min-w-[140px] rounded-md border border-border bg-panel p-0.5 shadow-lg"
|
||||
:class="menuContent({ class: 'min-w-[140px] rounded-md p-0.5' })"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
v-for="opt in SIDE_OPTIONS"
|
||||
:key="opt.value"
|
||||
class="relative flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-xs text-surface outline-none data-[highlighted]:bg-hover"
|
||||
:class="menuItem({ justify: 'start', class: 'relative px-2' })"
|
||||
@click="selectSide(opt.value)"
|
||||
>
|
||||
<icon-lucide-check
|
||||
|
|
|
|||
44
src/components/ui/menu.ts
Normal file
44
src/components/ui/menu.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { tv } from 'tailwind-variants'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
const content = tv({
|
||||
base: 'z-50 rounded-lg border border-border bg-panel p-1 shadow-lg'
|
||||
})
|
||||
|
||||
const item = tv({
|
||||
base: 'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs outline-none select-none data-[highlighted]:bg-hover data-[disabled]:cursor-default data-[disabled]:text-muted/50',
|
||||
variants: {
|
||||
tone: {
|
||||
default: 'text-surface',
|
||||
component: 'text-[#9747ff] data-[highlighted]:bg-[#9747ff]/12 data-[disabled]:text-[#9747ff]/40'
|
||||
},
|
||||
justify: {
|
||||
between: 'justify-between gap-6',
|
||||
start: 'justify-start'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
tone: 'default',
|
||||
justify: 'between'
|
||||
}
|
||||
})
|
||||
|
||||
const separator = tv({
|
||||
base: 'mx-1 my-1 h-px bg-border'
|
||||
})
|
||||
|
||||
export function menuContent(options?: { class?: string }) {
|
||||
return twMerge(content(), options?.class)
|
||||
}
|
||||
|
||||
export function menuItem(options?: {
|
||||
tone?: 'default' | 'component'
|
||||
justify?: 'between' | 'start'
|
||||
class?: string
|
||||
}) {
|
||||
return twMerge(item(options), options?.class)
|
||||
}
|
||||
|
||||
export function menuSeparator(options?: { class?: string }) {
|
||||
return twMerge(separator(), options?.class)
|
||||
}
|
||||
Loading…
Reference in a new issue