Replace the empty dynamic canvas placeholder with a gallery showing saved canvases, snapshots and Vue components. Users can create new canvases, restore snapshots, load components, and manage canvas toolbar/archive settings from the gallery. - Backend: soft delete (archive) instead of hard delete, status column - Frontend: CanvasGallery component with grid, search, settings popover - Show canvas name in global header when viewing a project canvas - Remove ProjectsPage (replaced by gallery), clean all references - MCP tools: project category available on canvas page, update handlers
103 lines
2.8 KiB
Vue
103 lines
2.8 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
import { renderInlineComponent, type VueComponentDefinition } from '../services/dynamicComponents'
|
|
import { useCanvasStore } from '../stores/canvas'
|
|
import CanvasGallery from './CanvasGallery.vue'
|
|
|
|
const canvasStore = useCanvasStore()
|
|
const showGallery = ref(true)
|
|
|
|
function handleLoadComponent(e: Event) {
|
|
const detail = (e as CustomEvent).detail
|
|
if (!detail) return
|
|
|
|
const container = document.getElementById('canvas-content')
|
|
if (!container) return
|
|
|
|
// Hide gallery when MCP renders content
|
|
showGallery.value = false
|
|
|
|
const placeholder = container.querySelector('.canvas-placeholder')
|
|
if (placeholder) placeholder.remove()
|
|
|
|
const definition: VueComponentDefinition = {
|
|
id: detail.id,
|
|
name: detail.name,
|
|
template: detail.template,
|
|
setup: detail.setup,
|
|
style: detail.style,
|
|
props: detail.props,
|
|
imports: detail.imports || ['ref', 'reactive', 'computed']
|
|
}
|
|
|
|
const result = renderInlineComponent(definition, container, {}, false)
|
|
;(window as any).__vueComponentUnmount = result.unmount
|
|
|
|
window.dispatchEvent(new CustomEvent('vue-component-rendered', { detail }))
|
|
canvasStore.addToHistory({ tool: 'load_vue_component', args: detail, timestamp: Date.now() })
|
|
}
|
|
|
|
function handleContentRendered() {
|
|
showGallery.value = false
|
|
}
|
|
|
|
function handleClearCanvas() {
|
|
showGallery.value = true
|
|
const container = document.getElementById('canvas-content')
|
|
if (container) {
|
|
// Remove all non-gallery content
|
|
const children = Array.from(container.children)
|
|
for (const child of children) {
|
|
if (!(child as HTMLElement).classList?.contains('canvas-placeholder')) {
|
|
child.remove()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
window.addEventListener('load-vue-component', handleLoadComponent)
|
|
window.addEventListener('clear-canvas', handleClearCanvas)
|
|
window.addEventListener('canvas-content-rendered', handleContentRendered)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('load-vue-component', handleLoadComponent)
|
|
window.removeEventListener('clear-canvas', handleClearCanvas)
|
|
window.removeEventListener('canvas-content-rendered', handleContentRendered)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="canvas-container">
|
|
<div id="canvas-content" class="canvas-content">
|
|
<div v-if="showGallery" class="canvas-placeholder">
|
|
<CanvasGallery @snapshot-restored="showGallery = false" @component-loaded="showGallery = false" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.canvas-container {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--bg-primary);
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.canvas-content {
|
|
flex: 1;
|
|
position: relative;
|
|
min-height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.canvas-placeholder {
|
|
height: 100%;
|
|
min-height: 400px;
|
|
}
|
|
</style>
|