feat: Add floating window system for canvas components

- Add WindowContainer.vue with Liquid Glass styling, drag, resize, close
- Add windows store for managing window state (position, size, z-index)
- Modify dynamicComponents.ts to wrap Vue components in floating windows
- Add MCP tools: move_window, resize_window, close_window, list_windows
- Add isolated Claude profiles (ejecutor, nucleo000) with versioned configs
This commit is contained in:
2026-02-14 20:04:11 -06:00
parent 39faf4bf77
commit d9eaba393b
15 changed files with 1008 additions and 23 deletions

View File

@@ -63,13 +63,15 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
background: var(--bg-primary);
overflow: auto;
overflow: hidden;
position: relative;
}
.canvas-content {
flex: 1;
padding: 1.5rem;
position: relative;
min-height: 100%;
overflow: hidden;
}
.canvas-placeholder {

View File

@@ -0,0 +1,407 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = withDefaults(defineProps<{
id: string
title?: string
x?: number
y?: number
width?: number
height?: number
minWidth?: number
minHeight?: number
}>(), {
title: 'Window',
x: 50,
y: 50,
width: 400,
height: 300,
minWidth: 200,
minHeight: 100
})
const emit = defineEmits<{
close: []
move: [pos: { x: number; y: number }]
resize: [size: { width: number; height: number }]
focus: []
}>()
// Estado interno de la ventana
const currentX = ref(props.x)
const currentY = ref(props.y)
const currentWidth = ref(props.width)
const currentHeight = ref(props.height)
const zIndex = ref(100)
// Estado de drag
const isDragging = ref(false)
const dragStartX = ref(0)
const dragStartY = ref(0)
const dragOffsetX = ref(0)
const dragOffsetY = ref(0)
// Estado de resize
const isResizing = ref(false)
const resizeDirection = ref('')
const resizeStartX = ref(0)
const resizeStartY = ref(0)
const resizeStartWidth = ref(0)
const resizeStartHeight = ref(0)
const windowStyle = computed(() => ({
left: `${currentX.value}px`,
top: `${currentY.value}px`,
width: `${currentWidth.value}px`,
height: `${currentHeight.value}px`,
zIndex: zIndex.value
}))
// Drag handlers
function startDrag(e: MouseEvent) {
if ((e.target as HTMLElement).closest('.window-controls')) return
isDragging.value = true
dragStartX.value = e.clientX
dragStartY.value = e.clientY
dragOffsetX.value = currentX.value
dragOffsetY.value = currentY.value
bringToFront()
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
}
function onDrag(e: MouseEvent) {
if (!isDragging.value) return
const deltaX = e.clientX - dragStartX.value
const deltaY = e.clientY - dragStartY.value
currentX.value = Math.max(0, dragOffsetX.value + deltaX)
currentY.value = Math.max(0, dragOffsetY.value + deltaY)
}
function stopDrag() {
if (isDragging.value) {
isDragging.value = false
emit('move', { x: currentX.value, y: currentY.value })
}
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
}
// Resize handlers
function startResize(direction: string, e: MouseEvent) {
e.preventDefault()
e.stopPropagation()
isResizing.value = true
resizeDirection.value = direction
resizeStartX.value = e.clientX
resizeStartY.value = e.clientY
resizeStartWidth.value = currentWidth.value
resizeStartHeight.value = currentHeight.value
bringToFront()
document.addEventListener('mousemove', onResize)
document.addEventListener('mouseup', stopResize)
}
function onResize(e: MouseEvent) {
if (!isResizing.value) return
const deltaX = e.clientX - resizeStartX.value
const deltaY = e.clientY - resizeStartY.value
if (resizeDirection.value.includes('e')) {
currentWidth.value = Math.max(props.minWidth, resizeStartWidth.value + deltaX)
}
if (resizeDirection.value.includes('s')) {
currentHeight.value = Math.max(props.minHeight, resizeStartHeight.value + deltaY)
}
if (resizeDirection.value.includes('w')) {
const newWidth = Math.max(props.minWidth, resizeStartWidth.value - deltaX)
if (newWidth !== currentWidth.value) {
currentX.value = resizeStartX.value + (resizeStartWidth.value - newWidth) + (e.clientX - resizeStartX.value) - deltaX
currentWidth.value = newWidth
}
}
if (resizeDirection.value.includes('n')) {
const newHeight = Math.max(props.minHeight, resizeStartHeight.value - deltaY)
if (newHeight !== currentHeight.value) {
currentY.value = resizeStartY.value + (resizeStartHeight.value - newHeight) + (e.clientY - resizeStartY.value) - deltaY
currentHeight.value = newHeight
}
}
}
function stopResize() {
if (isResizing.value) {
isResizing.value = false
emit('resize', { width: currentWidth.value, height: currentHeight.value })
}
document.removeEventListener('mousemove', onResize)
document.removeEventListener('mouseup', stopResize)
}
// Z-index management
let maxZIndex = 100
function bringToFront() {
maxZIndex++
zIndex.value = maxZIndex
emit('focus')
}
function handleWindowClick() {
bringToFront()
}
// Cleanup
onUnmounted(() => {
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
document.removeEventListener('mousemove', onResize)
document.removeEventListener('mouseup', stopResize)
})
</script>
<template>
<div
class="window-container"
:style="windowStyle"
:data-window-id="id"
@mousedown="handleWindowClick"
>
<!-- Header / Title bar -->
<div class="window-header" @mousedown="startDrag">
<span class="window-title">{{ title }}</span>
<div class="window-controls">
<button @click.stop="emit('close')" class="btn-close" title="Cerrar">
<svg width="10" height="10" viewBox="0 0 10 10" fill="currentColor">
<path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
</div>
</div>
<!-- Content area -->
<div class="window-content">
<slot></slot>
</div>
<!-- Resize handles -->
<div class="resize-handle resize-n" @mousedown="startResize('n', $event)"></div>
<div class="resize-handle resize-e" @mousedown="startResize('e', $event)"></div>
<div class="resize-handle resize-s" @mousedown="startResize('s', $event)"></div>
<div class="resize-handle resize-w" @mousedown="startResize('w', $event)"></div>
<div class="resize-handle resize-ne" @mousedown="startResize('ne', $event)"></div>
<div class="resize-handle resize-se" @mousedown="startResize('se', $event)"></div>
<div class="resize-handle resize-sw" @mousedown="startResize('sw', $event)"></div>
<div class="resize-handle resize-nw" @mousedown="startResize('nw', $event)"></div>
</div>
</template>
<style>
.window-container {
position: absolute;
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 14px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.2),
inset 0 0.5px 0 rgba(255, 255, 255, 0.06);
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 200px;
min-height: 100px;
}
.window-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 3px 8px;
background: transparent;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
cursor: move;
user-select: none;
flex-shrink: 0;
height: 20px;
}
.window-title {
font-size: 11px;
font-weight: 400;
color: rgba(255, 255, 255, 0.4);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.window-controls {
display: flex;
margin-left: 4px;
}
.btn-close {
width: 14px;
height: 14px;
border: none;
border-radius: 50%;
background: rgba(255, 59, 48, 0.7);
color: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
padding: 0;
}
.btn-close svg {
width: 7px;
height: 7px;
opacity: 0;
transition: opacity 0.15s ease;
}
.btn-close:hover {
background: rgba(255, 59, 48, 1);
color: white;
}
.btn-close:hover svg {
opacity: 1;
}
.window-content {
flex: 1;
overflow: auto;
padding: 10px;
background: transparent;
position: relative;
z-index: 1;
}
.window-content::-webkit-scrollbar {
width: 3px;
height: 3px;
}
.window-content::-webkit-scrollbar-track {
background: transparent;
}
.window-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.window-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
.window-content::-webkit-scrollbar-corner {
background: transparent;
}
/* Resize handles */
.resize-handle {
position: absolute;
background: transparent;
z-index: 10;
}
.resize-n {
top: 0;
left: 8px;
right: 8px;
height: 4px;
cursor: n-resize;
}
.resize-e {
right: 0;
top: 8px;
bottom: 8px;
width: 4px;
cursor: e-resize;
}
.resize-s {
bottom: 0;
left: 8px;
right: 8px;
height: 4px;
cursor: s-resize;
}
.resize-w {
left: 0;
top: 8px;
bottom: 8px;
width: 4px;
cursor: w-resize;
}
.resize-ne {
top: 0;
right: 0;
width: 12px;
height: 12px;
cursor: ne-resize;
}
.resize-se {
bottom: 0;
right: 0;
width: 12px;
height: 12px;
cursor: se-resize;
}
.resize-sw {
bottom: 0;
left: 0;
width: 12px;
height: 12px;
cursor: sw-resize;
}
.resize-nw {
top: 0;
left: 0;
width: 12px;
height: 12px;
cursor: nw-resize;
}
/* Indicador visual de resize en esquinas */
.resize-se::after {
content: '';
position: absolute;
bottom: 3px;
right: 3px;
width: 6px;
height: 6px;
border-right: 2px solid #444;
border-bottom: 2px solid #444;
opacity: 0.5;
}
.window-container:hover .resize-se::after {
opacity: 0.8;
}
</style>