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:
@@ -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 {
|
||||
|
||||
407
frontend/src/components/WindowContainer.vue
Normal file
407
frontend/src/components/WindowContainer.vue
Normal 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>
|
||||
Reference in New Issue
Block a user