feat: Auto-save components, soft delete, tags, compact WCO header

- Auto-save rendered Vue components to DB on render_vue_component
- Soft delete (archive) instead of hard delete for components
- Tags support for component categorization
- Gallery limited to 10 most recent items per section
- Upsert with ON CONFLICT for component saves
- PUT endpoint for partial component updates
- Collapsible toolbar with animated toggle button
- Window Controls Overlay support for PWA titlebar
- Compact header mode (32px) with hidden dot toggle
- Dynamic theme-color meta sync for Windows titlebar
This commit is contained in:
2026-02-15 02:54:27 -06:00
parent 8154bac63f
commit 9f9f335439
10 changed files with 401 additions and 84 deletions

View File

@@ -6,6 +6,7 @@ import TorchButton from './components/TorchButton.vue'
import FloatingTerminal from './components/FloatingTerminal.vue'
import FloatingResponse from './components/FloatingResponse.vue'
import FloatingVoice from './components/FloatingVoice.vue'
import AgentBar from './components/AgentBar.vue'
import PwaInstallBanner from './components/PwaInstallBanner.vue'
import { initWebMCP, getWebMCP } from './services/webmcp'
import { initTorch, destroyTorch } from './services/torch'
@@ -21,6 +22,8 @@ const router = useRouter()
const showTerminal = ref(false)
const showVoice = ref(false)
const showDebugConsole = ref(false)
const toolbarVisible = ref(true)
const forceWco = ref(false)
const debugLogs = ref<Array<{ type: string; message: string; time: string }>>([])
// Intercept console.log for debug panel
@@ -242,7 +245,17 @@ function triggerToolFlash() {
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' | 'agents'
function syncThemeColor() {
const bg = getComputedStyle(document.documentElement).getPropertyValue('--bg-primary').trim()
if (bg) {
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', bg)
}
}
onMounted(async () => {
// Sync Windows titlebar color with CSS variable
syncThemeColor()
// Connect to WebSocket for Claude status updates
connectStatusWs()
@@ -348,10 +361,20 @@ watch(() => route.name, (newPage) => {
</script>
<template>
<div class="app-container">
<header class="app-header">
<div class="app-container" :class="{ wco: forceWco }">
<header class="app-header" :class="{ 'wco-header': forceWco }">
<div class="header-left">
<h1 class="logo">Agent UI</h1>
<button
class="toolbar-toggle"
:class="{ collapsed: !toolbarVisible }"
@click="toolbarVisible = !toolbarVisible"
:title="toolbarVisible ? 'Ocultar toolbar' : 'Mostrar toolbar'"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<line x1="9" y1="3" x2="9" y2="21"/>
</svg>
</button>
<template v-if="projectCanvasStore.activeCanvas && route.name === 'project-canvas'">
<span class="header-sep">/</span>
<span class="header-canvas-name">{{ projectCanvasStore.activeCanvas.name }}</span>
@@ -376,11 +399,12 @@ watch(() => route.name, (newPage) => {
<path d="M21 3v5h-5"/>
</svg>
</button>
<span class="wco-dot" :class="{ on: forceWco }" @click="forceWco = !forceWco"></span>
<TorchButton />
</div>
</header>
<main class="app-main">
<Toolbar />
<Toolbar :collapsed="!toolbarVisible" />
<RouterView v-slot="{ Component }">
<Transition name="page" mode="out-in">
<component :is="Component" />
@@ -490,6 +514,9 @@ watch(() => route.name, (newPage) => {
</svg>
</button>
<!-- Agent Bar (bottom pills) -->
<AgentBar />
<!-- Floating Terminal -->
<FloatingTerminal ref="terminalRef" v-model="showTerminal" />
@@ -544,51 +571,200 @@ watch(() => route.name, (newPage) => {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1.5rem;
padding-top: calc(0.75rem + env(safe-area-inset-top, 0px));
padding-left: calc(1.5rem + env(safe-area-inset-left, 0px));
padding-right: calc(1.5rem + env(safe-area-inset-right, 0px));
background: var(--bg-secondary);
padding: 0.5rem 1rem;
padding-top: calc(0.5rem + env(safe-area-inset-top, 0px));
padding-left: calc(1rem + env(safe-area-inset-left, 0px));
padding-right: calc(1rem + env(safe-area-inset-right, 0px));
background: var(--bg-primary);
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
min-height: 40px;
-webkit-app-region: drag;
app-region: drag;
}
.header-left {
display: flex;
align-items: center;
gap: 1.5rem;
gap: 0.75rem;
-webkit-app-region: no-drag;
app-region: no-drag;
}
.header-right {
display: flex;
align-items: center;
gap: 0.75rem;
gap: 0.5rem;
-webkit-app-region: no-drag;
app-region: no-drag;
}
.logo {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
/* ── Compact header (WCO + manual toggle) ── */
.wco-header,
.wco-header.app-header {
height: 32px;
min-height: 32px;
max-height: 32px;
padding: 0 0.5rem;
border-bottom: none;
overflow: visible;
}
.wco-header .header-left { gap: 0.35rem; overflow: visible; }
.wco-header .header-right { gap: 0.2rem; }
.wco-header .toolbar-toggle {
width: 22px;
height: 22px;
border-radius: 4px;
}
.wco-header .toolbar-toggle svg { width: 12px; height: 12px; }
.wco-header .header-sep { font-size: 0.65rem; }
.wco-header .header-canvas-name { font-size: 0.65rem; max-width: 100px; }
.wco-header .header-canvas-badge { font-size: 0.5rem; padding: 0 0.2rem; }
.wco-header :deep(.pwa-banner) { display: none; }
.wco-header .debug-btn {
padding: 1px 4px;
font-size: 8px;
border-radius: 3px;
gap: 2px;
}
.wco-header .debug-btn svg { width: 10px; height: 10px; }
.wco-header .log-count { font-size: 7px; padding: 0 2px; min-width: 10px; }
.wco-header .refresh-btn {
width: 22px;
height: 22px;
border-radius: 4px;
}
.wco-header .refresh-btn svg { width: 12px; height: 12px; }
/* TorchButton compact via :deep */
.wco-header :deep(.trigger-split) { border-radius: 4px; font-size: 0.65rem; }
.wco-header :deep(.trigger-main) { padding: 0.1rem 0.25rem 0.1rem 0.35rem; gap: 0.25rem; }
.wco-header :deep(.trigger-chevron) { padding: 0.1rem 0.25rem; }
.wco-header :deep(.status-dot) { width: 5px; height: 5px; }
.wco-header :deep(.chevron) { width: 10px; height: 10px; }
.wco-header :deep(.trigger-name) { max-width: 60px; font-size: 0.65rem; }
/* Window Controls Overlay — real PWA titlebar */
@media (display-mode: window-controls-overlay) {
.app-header {
position: fixed;
top: env(titlebar-area-y, 0);
left: env(titlebar-area-x, 0);
width: env(titlebar-area-width, 100%);
height: env(titlebar-area-height, 32px);
min-height: unset;
max-height: env(titlebar-area-height, 32px);
padding: 0 0.5rem;
z-index: 10000;
border-bottom: none;
box-sizing: border-box;
background: var(--bg-primary);
}
.app-container {
padding-top: env(titlebar-area-height, 32px);
}
/* All the same compacting as .wco-header */
.header-left { gap: 0.35rem; }
.header-right { gap: 0.2rem; }
.toolbar-toggle { width: 22px; height: 22px; border-radius: 4px; }
.toolbar-toggle svg { width: 12px; height: 12px; }
.header-sep { font-size: 0.65rem; }
.header-canvas-name { font-size: 0.65rem; max-width: 100px; }
.header-canvas-badge { font-size: 0.5rem; padding: 0 0.2rem; }
.debug-btn { padding: 1px 4px; font-size: 8px; border-radius: 3px; gap: 2px; }
.debug-btn svg { width: 10px; height: 10px; }
.log-count { font-size: 7px; padding: 0 2px; min-width: 10px; }
.refresh-btn { width: 22px; height: 22px; border-radius: 4px; }
.refresh-btn svg { width: 12px; height: 12px; }
}
/* Tiny hidden toggle dot */
.wco-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--border-color);
opacity: 0.18;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
}
.wco-dot:hover {
opacity: 0.5;
transform: scale(1.8);
}
.wco-dot.on {
background: #6366f1;
opacity: 0.6;
box-shadow: 0 0 4px rgba(99, 102, 241, 0.5);
}
.toolbar-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
background: transparent;
border: 1px solid var(--border-color);
border-radius: 5px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
}
.toolbar-toggle:hover {
background: var(--bg-hover);
color: var(--accent, #6366f1);
border-color: var(--accent, #6366f1);
}
.toolbar-toggle svg {
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.toolbar-toggle.collapsed svg {
transform: scaleX(-1);
}
.toolbar-toggle.collapsed {
color: var(--accent, #6366f1);
border-color: rgba(99, 102, 241, 0.3);
background: rgba(99, 102, 241, 0.08);
}
.header-sep {
color: var(--text-muted);
font-size: 1.1rem;
font-size: 0.85rem;
opacity: 0.4;
}
.header-canvas-name {
font-size: 0.9rem;
font-size: 0.8rem;
font-weight: 500;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 160px;
}
.header-canvas-badge {
padding: 0.125rem 0.5rem;
padding: 0.0625rem 0.375rem;
background: rgba(99, 102, 241, 0.15);
color: #6366f1;
font-size: 0.6875rem;
font-size: 0.625rem;
font-weight: 500;
border-radius: 999px;
}
@@ -597,12 +773,12 @@ watch(() => route.name, (newPage) => {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
width: 28px;
height: 28px;
padding: 0;
background: transparent;
border: 1px solid var(--border-color);
border-radius: 6px;
border-radius: 5px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s ease;
@@ -1115,13 +1291,13 @@ watch(() => route.name, (newPage) => {
.debug-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
gap: 3px;
padding: 3px 6px;
background: rgba(100, 100, 100, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
border-radius: 5px;
color: #888;
font-size: 11px;
font-size: 10px;
cursor: pointer;
transition: all 0.2s;
}
@@ -1140,11 +1316,12 @@ watch(() => route.name, (newPage) => {
.log-count {
background: #ef4444;
color: white;
font-size: 9px;
padding: 1px 5px;
font-size: 8px;
padding: 1px 4px;
border-radius: 10px;
min-width: 16px;
min-width: 14px;
text-align: center;
line-height: 1.2;
}
/* Debug Console Panel */