Fix duplicate :class, rename race, useFileDialog leak, restore clip/wrap

This commit is contained in:
Danila Poyarkov 2026-03-07 14:27:17 +03:00
parent 3ed9ccbe3f
commit 3684caea87
12 changed files with 110 additions and 42 deletions

View file

@ -27,15 +27,17 @@ import { useEditorStore } from '@/stores/editor'
const store = useEditorStore()
const DOCUMENT_NAME_ID = 'document-name'
const nameInputRef = ref<HTMLInputElement | null>(null)
const rename = useInlineRename<'document-name'>((_id, name) => {
store.state.documentName = name
})
const editingName = computed(() => rename.editingId.value === DOCUMENT_NAME_ID)
function setNameInputRef(el: HTMLInputElement | null) {
if (el) void rename.focusInput(el)
}
function startRename() {
rename.start(DOCUMENT_NAME_ID, store.state.documentName)
void rename.focusInput(nameInputRef.value)
}
function commitRename(input: HTMLInputElement) {
@ -176,7 +178,7 @@ const topMenus = [
<img data-test-id="app-logo" src="/favicon-32.png" class="size-4" alt="OpenPencil" />
<input
v-if="editingName"
ref="nameInputRef"
:ref="(el) => setNameInputRef(el as HTMLInputElement | null)"
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"
@ -219,17 +221,12 @@ const topMenus = [
<template v-for="(item, i) in menu.items" :key="i">
<MenubarSeparator v-if="item.separator" :class="menuSeparator()" />
<MenubarSub v-else-if="item.sub">
<MenubarSubTrigger
:class="menuItem()"
>
<MenubarSubTrigger :class="menuItem()">
<span class="flex-1">{{ item.label }}</span>
<IconChevronRight class="size-3 text-muted" />
</MenubarSubTrigger>
<MenubarPortal>
<MenubarSubContent
:side-offset="4"
:class="menuContent({ class: 'min-w-44' })"
>
<MenubarSubContent :side-offset="4" :class="menuContent({ class: 'min-w-44' })">
<template v-for="(sub, j) in item.sub" :key="j">
<MenubarSeparator v-if="sub.separator" :class="menuSeparator()" />
<MenubarItem

View file

@ -63,7 +63,13 @@ function onSelect(val: string) {
<PopoverContent
:side-offset="2"
align="start"
:class="panelSurface({ radius: 'md', padding: 'none', class: 'z-50 flex min-w-56 w-[var(--reka-popper-anchor-width)] flex-col overflow-hidden' })"
:class="
panelSurface({
radius: 'md',
padding: 'none',
class: 'z-50 flex min-w-56 w-[var(--reka-popper-anchor-width)] flex-col overflow-hidden'
})
"
@open-auto-focus.prevent
>
<ListboxRoot :model-value="modelValue" @update:model-value="onSelect">

View file

@ -222,7 +222,13 @@ const onlineCount = computed(() => props.collabPeers.length + 1)
<DropdownMenuItem
v-for="item in menuItems"
:key="item.label"
:class="menuItem({ justify: 'start', class: 'w-full gap-2.5 rounded-lg border-none bg-transparent px-2.5 py-2 active: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" />

View file

@ -102,7 +102,9 @@ function copyAsJSX() {
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 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>

View file

@ -181,8 +181,11 @@ function goNext() {
v-for="sub in tool.flyout"
:key="sub"
:data-test-id="`toolbar-flyout-item-${sub.toLowerCase()}`"
:class="menuItem()"
:class="store.state.activeTool === sub ? 'bg-accent text-white' : undefined"
:class="
menuItem({
class: store.state.activeTool === sub ? 'bg-accent text-white' : undefined
})
"
@select="store.setTool(sub)"
>
<component :is="toolIcons[sub]" class="size-3.5" />
@ -294,11 +297,10 @@ function goNext() {
v-for="sub in tool.flyout"
:key="sub"
:data-test-id="`mobile-toolbar-flyout-item-${sub.toLowerCase()}`"
:class="menuItem()"
:class="
store.state.activeTool === sub
? 'bg-accent text-white'
: 'text-surface hover:bg-hover'
menuItem({
class: store.state.activeTool === sub ? 'bg-accent text-white' : undefined
})
"
@select="store.setTool(sub)"
>

View file

@ -54,7 +54,12 @@ function handleSubmit(e: Event) {
<SelectRoot v-model="modelId">
<SelectTrigger
data-test-id="chat-model-selector"
:class="selectTrigger({ class: 'gap-1 rounded border-none bg-transparent px-1.5 py-0.5 text-[10px] text-muted' })"
:class="
selectTrigger({
class:
'gap-1 rounded border-none bg-transparent px-1.5 py-0.5 text-[10px] text-muted'
})
"
>
<icon-lucide-bot class="size-3" />
{{ selectedModelName }}
@ -65,7 +70,9 @@ function handleSubmit(e: Event) {
position="popper"
side="top"
:side-offset="4"
:class="selectContent({ radius: 'lg', padding: 'md', class: 'max-h-60 overflow-y-auto' })"
:class="
selectContent({ radius: 'lg', padding: 'md', class: 'max-h-60 overflow-y-auto' })
"
>
<SelectViewport>
<SelectItem
@ -103,7 +110,14 @@ function handleSubmit(e: Event) {
<button
type="button"
data-test-id="chat-stop-button"
:class="uiButton({ tone: 'ghost', shape: 'rounded', size: 'sm', class: 'shrink-0 border border-border px-2 py-1.5' })"
:class="
uiButton({
tone: 'ghost',
shape: 'rounded',
size: 'sm',
class: 'shrink-0 border border-border px-2 py-1.5'
})
"
@click="emit('stop')"
>
<icon-lucide-square class="size-3" />
@ -124,7 +138,14 @@ function handleSubmit(e: Event) {
<button
type="submit"
data-test-id="chat-send-button"
:class="uiButton({ tone: 'accent', shape: 'rounded', size: 'sm', class: 'shrink-0 px-2.5 py-1.5 font-medium' })"
:class="
uiButton({
tone: 'accent',
shape: 'rounded',
size: 'sm',
class: 'shrink-0 px-2.5 py-1.5 font-medium'
})
"
:disabled="!input.trim()"
>
<icon-lucide-send class="size-3" />

View file

@ -188,27 +188,44 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
<template v-if="node.layoutMode !== 'NONE'">
<div class="mt-1.5 flex gap-0.5">
<button
data-test-id="layout-direction-horizontal"
class="flex cursor-pointer items-center justify-center rounded border px-2 py-1"
:class="
node.layoutMode === 'HORIZONTAL'
? 'border-accent bg-accent/10 text-accent'
: 'border-border text-muted hover:bg-hover hover:text-surface'
"
title="Horizontal layout"
@click="store.setLayoutMode(node.id, 'HORIZONTAL')"
>
<icon-lucide-arrow-right class="size-3.5" />
</button>
<button
data-test-id="layout-direction-vertical"
class="flex cursor-pointer items-center justify-center rounded border px-2 py-1"
:class="
node.layoutMode === 'VERTICAL'
? 'border-accent bg-accent/10 text-accent'
: 'border-border text-muted hover:bg-hover hover:text-surface'
"
title="Vertical layout"
@click="store.setLayoutMode(node.id, 'VERTICAL')"
>
<icon-lucide-arrow-down class="size-3.5" />
</button>
<button
data-test-id="layout-direction-wrap"
class="flex cursor-pointer items-center justify-center rounded border px-2 py-1"
:class="
node.layoutWrap === 'WRAP'
? 'border-accent bg-accent/10 text-accent'
: 'border-border text-muted hover:bg-hover hover:text-surface'
"
title="Wrap"
@click="updateProp('layoutWrap', node.layoutWrap === 'WRAP' ? 'NO_WRAP' : 'WRAP')"
>
<icon-lucide-wrap-text class="size-3.5" />
</button>
</div>
<div class="mt-2 grid grid-cols-2 gap-1.5">
@ -237,7 +254,10 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
</button>
</div>
<div v-if="showIndividualPadding || !hasUniformPadding()" class="mt-1.5 grid grid-cols-2 gap-1.5">
<div
v-if="showIndividualPadding || !hasUniformPadding()"
class="mt-1.5 grid grid-cols-2 gap-1.5"
>
<ScrubInput
icon="Top"
:model-value="Math.round(node.paddingTop)"
@ -270,7 +290,7 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
<div class="mt-2">
<label class="mb-1 block text-[11px] text-muted">Alignment</label>
<div class="grid grid-cols-3 gap-1">
<div data-test-id="layout-alignment-grid" class="grid grid-cols-3 gap-1">
<button
v-for="cell in ALIGN_GRID"
:key="`${cell.primary}-${cell.counter}`"
@ -288,4 +308,22 @@ function setAlignment(primary: LayoutAlign, counter: LayoutCounterAlign) {
</div>
</template>
</div>
<div v-if="node.type === 'FRAME'" class="border-b border-border px-3 py-2">
<label class="flex cursor-pointer items-center gap-2 text-xs text-surface">
<input
type="checkbox"
class="accent-accent"
:checked="node.clipsContent"
@change="
store.updateNodeWithUndo(
node.id,
{ clipsContent: !node.clipsContent },
'Toggle clip content'
)
"
/>
Clip content
</label>
</div>
</template>

View file

@ -1,5 +1,5 @@
import { tv } from 'tailwind-variants'
import { twMerge } from 'tailwind-merge'
import { tv } from 'tailwind-variants'
const button = tv({
base: 'inline-flex items-center justify-center transition-colors select-none',
@ -24,10 +24,6 @@ const button = tv({
bordered: {
true: 'border border-white/10',
false: ''
},
active: {
true: '',
false: ''
}
},
compoundVariants: [
@ -43,8 +39,7 @@ const button = tv({
tone: 'ghost',
shape: 'rounded',
size: 'sm',
bordered: false,
active: false
bordered: false
}
})
@ -53,7 +48,6 @@ export function uiButton(options?: {
shape?: 'square' | 'rounded' | 'pill'
size?: 'sm' | 'md' | 'icon' | 'iconSm'
bordered?: boolean
active?: boolean
class?: string
}) {
return twMerge(button(options), options?.class)

View file

@ -1,5 +1,5 @@
import { tv } from 'tailwind-variants'
import { twMerge } from 'tailwind-merge'
import { tv } from 'tailwind-variants'
const content = tv({
base: 'z-50 rounded-lg border border-border bg-panel p-1 shadow-lg'
@ -10,7 +10,8 @@ const item = tv({
variants: {
tone: {
default: 'text-surface',
component: 'text-[#9747ff] data-[highlighted]:bg-[#9747ff]/12 data-[disabled]:text-[#9747ff]/40'
component:
'text-[#9747ff] data-[highlighted]:bg-[#9747ff]/12 data-[disabled]:text-[#9747ff]/40'
},
justify: {
between: 'justify-between gap-6',

View file

@ -1,5 +1,5 @@
import { tv } from 'tailwind-variants'
import { twMerge } from 'tailwind-merge'
import { tv } from 'tailwind-variants'
const trigger = tv({
base: 'flex items-center justify-between border border-border bg-input text-surface outline-none hover:bg-hover'

View file

@ -1,5 +1,5 @@
import { tv } from 'tailwind-variants'
import { twMerge } from 'tailwind-merge'
import { tv } from 'tailwind-variants'
const surface = tv({
base: 'border border-border bg-panel',

View file

@ -1,5 +1,4 @@
import { onUnmounted } from 'vue'
import { useFileDialog } from '@vueuse/core'
import { IS_TAURI } from '@/constants'
import { useEditorStore } from '@/stores/editor'
@ -38,12 +37,14 @@ export async function openFileDialog() {
}
}
const { open, onChange } = useFileDialog({ accept: '.fig', multiple: false })
onChange((files) => {
const file = files?.[0]
const input = document.createElement('input')
input.type = 'file'
input.accept = '.fig'
input.addEventListener('change', () => {
const file = input.files?.[0]
if (file) void openFileInNewTab(file)
})
open()
input.click()
}
const store = useEditorStore()