Replace store thunks with a late-binding proxy

useEditorStore() now returns a Proxy that delegates to the current
active store at access time. No more store() wrapper functions,
try/catch guards, or passing stores through function parameters.
This commit is contained in:
Danila Poyarkov 2026-03-02 18:28:36 +03:00
parent a6634b9543
commit af9b9fd0fa
6 changed files with 65 additions and 80 deletions

View file

@ -9,7 +9,7 @@ import CodePanel from './CodePanel.vue'
import DesignPanel from './DesignPanel.vue'
const store = useEditorStore()
const { activeTab } = useAIChat(store)
const { activeTab } = useAIChat()
</script>
<template>

View file

@ -8,7 +8,6 @@ import { computed, ref, watch } from 'vue'
import { createAITools } from '@/ai/tools'
import { useEditorStore } from '@/stores/editor'
import type { EditorStore } from '@/stores/editor'
import type { UIMessage } from 'ai'
export { AI_MODELS as MODELS } from '@open-pencil/core'
@ -34,8 +33,6 @@ const apiKey = ref(localStorage.getItem(API_KEY_STORAGE) ?? '')
const modelId = ref(localStorage.getItem(MODEL_STORAGE) ?? DEFAULT_AI_MODEL)
const activeTab = ref<'design' | 'ai'>('design')
let editorStore: EditorStore | null = null
watch(apiKey, (key) => {
if (key) {
localStorage.setItem(API_KEY_STORAGE, key)
@ -66,13 +63,7 @@ function createTransport() {
}
})
let currentStore: EditorStore | null = editorStore
try {
currentStore = useEditorStore()
} catch {
// store not yet available
}
const tools = currentStore ? createAITools(currentStore) : {}
const tools = createAITools(useEditorStore())
const agent = new ToolLoopAgent({
model: openrouter(modelId.value),
@ -103,10 +94,7 @@ if (typeof window !== 'undefined') {
}
}
export function useAIChat(store?: EditorStore) {
if (store) {
editorStore = store
}
export function useAIChat() {
return {
apiKey,
modelId,

View file

@ -12,50 +12,46 @@ function isEditing(e: Event) {
export function useKeyboard() {
const { activeTab } = useAIChat()
function store() {
return useEditorStore()
}
const store = useEditorStore()
useEventListener(window, 'copy', (e: ClipboardEvent) => {
if (isEditing(e)) return
e.preventDefault()
if (e.clipboardData) store().writeCopyData(e.clipboardData)
if (e.clipboardData) store.writeCopyData(e.clipboardData)
})
useEventListener(window, 'cut', (e: ClipboardEvent) => {
if (isEditing(e)) return
e.preventDefault()
if (e.clipboardData) store().writeCopyData(e.clipboardData)
store().deleteSelected()
if (e.clipboardData) store.writeCopyData(e.clipboardData)
store.deleteSelected()
})
useEventListener(window, 'paste', (e: ClipboardEvent) => {
if (isEditing(e)) return
e.preventDefault()
const html = e.clipboardData?.getData('text/html') ?? ''
if (html) store().pasteFromHTML(html)
if (html) store.pasteFromHTML(html)
})
useEventListener(window, 'keydown', (e: KeyboardEvent) => {
if (isEditing(e)) return
const s = store()
const tool = TOOL_SHORTCUTS[e.key.toLowerCase()]
if (tool) {
s.setTool(tool)
store.setTool(tool)
return
}
if ((e.metaKey || e.ctrlKey) && e.altKey) {
if (e.code === 'KeyK') {
e.preventDefault()
s.createComponentFromSelection()
store.createComponentFromSelection()
return
}
if (e.code === 'KeyB') {
e.preventDefault()
s.detachInstance()
store.detachInstance()
return
}
}
@ -63,23 +59,23 @@ export function useKeyboard() {
if ((e.metaKey || e.ctrlKey) && e.shiftKey) {
if (e.code === 'KeyK') {
e.preventDefault()
s.createComponentSetFromComponents()
store.createComponentSetFromComponents()
return
}
if (e.code === 'KeyH') {
e.preventDefault()
s.toggleVisibility()
store.toggleVisibility()
return
}
if (e.code === 'KeyL') {
e.preventDefault()
s.toggleLock()
store.toggleLock()
return
}
if (e.code === 'KeyE') {
e.preventDefault()
if (s.state.selectedIds.size > 0) {
s.exportSelection(1, 'PNG')
if (store.state.selectedIds.size > 0) {
store.exportSelection(1, 'PNG')
}
return
}
@ -88,7 +84,7 @@ export function useKeyboard() {
if (e.metaKey || e.ctrlKey) {
if (e.code === 'Backslash') {
e.preventDefault()
s.state.showUI = !s.state.showUI
store.state.showUI = !store.state.showUI
return
}
if (e.code === 'KeyJ') {
@ -108,76 +104,76 @@ export function useKeyboard() {
}
if (e.key === 'z' && !e.shiftKey) {
e.preventDefault()
s.undoAction()
store.undoAction()
} else if ((e.key === 'z' && e.shiftKey) || e.key === 'y') {
e.preventDefault()
s.redoAction()
store.redoAction()
} else if (e.key === '0') {
e.preventDefault()
s.zoomToFit()
store.zoomToFit()
} else if (e.key === 'd') {
e.preventDefault()
s.duplicateSelected()
store.duplicateSelected()
} else if (e.key === 'a') {
e.preventDefault()
s.selectAll()
store.selectAll()
} else if (e.key === 's' && e.shiftKey) {
e.preventDefault()
s.saveFigFileAs()
store.saveFigFileAs()
} else if (e.key === 's') {
e.preventDefault()
s.saveFigFile()
store.saveFigFile()
} else if (e.key === 'o') {
e.preventDefault()
openFileDialog()
} else if (e.key === 'g' && !e.shiftKey) {
e.preventDefault()
s.groupSelected()
store.groupSelected()
} else if (e.key === 'g' && e.shiftKey) {
e.preventDefault()
s.ungroupSelected()
store.ungroupSelected()
}
}
if (e.shiftKey && e.key === 'A') {
e.preventDefault()
const node = s.selectedNode.value
if (node && node.type === 'FRAME' && s.selectedNodes.value.length === 1) {
s.setLayoutMode(node.id, node.layoutMode === 'NONE' ? 'VERTICAL' : 'NONE')
} else if (s.selectedNodes.value.length > 0) {
s.wrapInAutoLayout()
const node = store.selectedNode.value
if (node && node.type === 'FRAME' && store.selectedNodes.value.length === 1) {
store.setLayoutMode(node.id, node.layoutMode === 'NONE' ? 'VERTICAL' : 'NONE')
} else if (store.selectedNodes.value.length > 0) {
store.wrapInAutoLayout()
}
return
}
if (e.key === ']') {
e.preventDefault()
s.bringToFront()
store.bringToFront()
return
}
if (e.key === '[') {
e.preventDefault()
s.sendToBack()
store.sendToBack()
return
}
if (e.key === 'Backspace' || e.key === 'Delete') {
s.deleteSelected()
store.deleteSelected()
}
if (e.key === 'Enter' && s.state.penState) {
if (e.key === 'Enter' && store.state.penState) {
e.preventDefault()
s.penCommit(false)
store.penCommit(false)
return
}
if (e.key === 'Escape') {
if (s.state.penState) {
s.penCancel()
if (store.state.penState) {
store.penCancel()
return
}
s.clearSelection()
s.setTool('SELECT')
store.clearSelection()
store.setTool('SELECT')
}
})
}

View file

@ -47,9 +47,7 @@ export async function openFileDialog() {
input.click()
}
function getStore() {
return useEditorStore()
}
const store = useEditorStore()
const MENU_ACTIONS: Record<string, () => void> = {
new: () => createTab(),
@ -57,19 +55,18 @@ const MENU_ACTIONS: Record<string, () => void> = {
close: () => {
if (activeTab.value) closeTab(activeTab.value.id)
},
save: () => getStore().saveFigFile(),
'save-as': () => getStore().saveFigFileAs(),
duplicate: () => getStore().duplicateSelected(),
delete: () => getStore().deleteSelected(),
group: () => getStore().groupSelected(),
ungroup: () => getStore().ungroupSelected(),
'create-component': () => getStore().createComponentFromSelection(),
'create-component-set': () => getStore().createComponentSetFromComponents(),
'detach-instance': () => getStore().detachInstance(),
'zoom-fit': () => getStore().zoomToFit(),
save: () => store.saveFigFile(),
'save-as': () => store.saveFigFileAs(),
duplicate: () => store.duplicateSelected(),
delete: () => store.deleteSelected(),
group: () => store.groupSelected(),
ungroup: () => store.ungroupSelected(),
'create-component': () => store.createComponentFromSelection(),
'create-component-set': () => store.createComponentSetFromComponents(),
'detach-instance': () => store.detachInstance(),
'zoom-fit': () => store.zoomToFit(),
export: () => {
const s = getStore()
if (s.state.selectedIds.size > 0) s.exportSelection(1, 'PNG')
if (store.state.selectedIds.size > 0) store.exportSelection(1, 'PNG')
}
}

View file

@ -1973,13 +1973,17 @@ export function setActiveEditorStore(store: EditorStore) {
storeRef.value = store
}
export function provideEditorStore(): EditorStore {
const store = createEditorStore()
storeRef.value = store
return store
}
export function useEditorStore(): EditorStore {
export function getActiveEditorStore(): EditorStore {
if (!storeRef.value) throw new Error('Editor store not provided')
return storeRef.value
}
const storeProxy = new Proxy({} as EditorStore, {
get(_, prop) {
return Reflect.get(getActiveEditorStore(), prop)
}
})
export function useEditorStore(): EditorStore {
return storeProxy
}

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, provide } from 'vue'
import { provide } from 'vue'
import { useEventListener, useUrlSearchParams } from '@vueuse/core'
import { useRoute, useRouter } from 'vue-router'
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from 'reka-ui'
@ -24,7 +24,7 @@ const route = useRoute()
const router = useRouter()
const firstTab = createTab()
const store = computed(() => useEditorStore())
const store = useEditorStore()
useKeyboard()
useMenu()
const collab = useCollab(firstTab.store)