feat: Add canvas gallery with soft delete, snapshots and components

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
This commit is contained in:
2026-02-15 01:57:04 -06:00
parent 9a636e26a7
commit d5ee533db9
16 changed files with 1055 additions and 653 deletions

View File

@@ -1,9 +1,11 @@
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
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
@@ -12,6 +14,9 @@ function handleLoadComponent(e: Event) {
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()
@@ -32,26 +37,42 @@ function handleLoadComponent(e: Event) {
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 class="canvas-placeholder">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M3 9h18"/>
<path d="M9 21V9"/>
</svg>
<p>Canvas listo</p>
<span>Haz clic en el cuadrado azul (abajo derecha) para conectar con Claude Code</span>
<div v-if="showGallery" class="canvas-placeholder">
<CanvasGallery @snapshot-restored="showGallery = false" @component-loaded="showGallery = false" />
</div>
</div>
</div>
@@ -75,28 +96,7 @@ onUnmounted(() => {
}
.canvas-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 400px;
color: var(--text-muted);
text-align: center;
}
.canvas-placeholder svg {
margin-bottom: 1rem;
opacity: 0.5;
}
.canvas-placeholder p {
font-size: 1.25rem;
margin: 0 0 0.5rem 0;
}
.canvas-placeholder span {
font-size: 0.875rem;
opacity: 0.7;
}
</style>