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>

View File

@@ -19,6 +19,8 @@ import {
import { setActivePinia, type Pinia } from 'pinia'
import { useCanvasStore } from '../stores/canvas'
import { useThemeStore } from '../stores/theme'
import { useWindowsStore } from '../stores/windows'
import WindowContainer from '../components/WindowContainer.vue'
// Uses relative URLs - works with Vite proxy in dev and Traefik in production
const API_URL = ''
@@ -183,6 +185,12 @@ function getThemeStore() {
return useThemeStore()
}
function getWindowsStore() {
const globalPinia = (window as any).__pinia as Pinia | undefined
if (globalPinia) setActivePinia(globalPinia)
return useWindowsStore()
}
const dynamicHelpers = {
$emit: (event: string, ...args: any[]) => eventBus.emit(event, ...args),
$on: (event: string, cb: EventCallback) => eventBus.on(event, cb),
@@ -271,25 +279,47 @@ export function buildComponent(definition: VueComponentDefinition): Component {
const renderedContainers: Map<string, HTMLElement> = new Map()
export interface LayoutOptions {
x?: number
y?: number
width?: number
height?: number
}
export function renderInlineComponent(
definition: VueComponentDefinition,
target: HTMLElement,
props: Record<string, any> = {},
append: boolean = false
append: boolean = false,
layout?: LayoutOptions
): { unmount: () => void } {
const scopeId = generateScopeId(definition.id)
const windowsStore = getWindowsStore()
// Registrar ventana en el store
const windowState = windowsStore.register(definition.id, {
title: definition.name,
x: layout?.x,
y: layout?.y,
width: layout?.width ?? 400,
height: layout?.height ?? 300
})
const container = document.createElement('div')
container.id = `inline-${definition.id}`
container.className = 'dynamic-component-wrapper'
container.setAttribute(`data-${scopeId}`, '')
// Siempre hacer append para ventanas flotantes
if (append) {
target.appendChild(container)
} else {
// En modo replace, limpiar contenedor anterior si existe
const oldContainer = renderedContainers.get(definition.id)
if (oldContainer) render(null, oldContainer)
target.innerHTML = ''
if (oldContainer) {
render(null, oldContainer)
oldContainer.remove()
}
target.appendChild(container)
}
@@ -300,25 +330,52 @@ export function renderInlineComponent(
const isAsync = definition.setup ? /\bawait\b/.test(definition.setup) : false
const component = buildComponent(definition)
const vnode = isAsync
// Función unmount que se llamará al cerrar la ventana
const unmount = () => {
render(null, container)
renderedContainers.delete(definition.id)
windowsStore.remove(definition.id)
container.remove()
document.getElementById(`style-${definition.id}`)?.remove()
}
// Crear el componente interno
const innerComponent = isAsync
? createVNode(Suspense, null, {
default: () => createVNode(component, props),
fallback: () => createVNode('div', { class: 'loading' }, 'Loading...')
})
: createVNode(component, props)
const mainApp = (window as any).__vueApp as App | undefined
if (mainApp?._context) vnode.appContext = mainApp._context
// Envolver en WindowContainer
const windowVNode = createVNode(
WindowContainer,
{
id: definition.id,
title: definition.name,
x: windowState.x,
y: windowState.y,
width: windowState.width,
height: windowState.height,
onClose: unmount,
onMove: (pos: { x: number; y: number }) => {
windowsStore.updatePosition(definition.id, pos.x, pos.y)
},
onResize: (size: { width: number; height: number }) => {
windowsStore.updateSize(definition.id, size.width, size.height)
},
onFocus: () => {
windowsStore.bringToFront(definition.id)
}
},
{ default: () => innerComponent }
)
render(vnode, container)
const mainApp = (window as any).__vueApp as App | undefined
if (mainApp?._context) windowVNode.appContext = mainApp._context
render(windowVNode, container)
renderedContainers.set(definition.id, container)
return {
unmount: () => {
render(null, container)
renderedContainers.delete(definition.id)
container.remove()
document.getElementById(`style-${definition.id}`)?.remove()
}
}
return { unmount }
}

View File

@@ -140,7 +140,7 @@ function getToolConfigs(): Map<string, ToolConfig> {
// Category to tool names mapping
const categoryTools: Record<ToolCategory, string[]> = {
global: ['get_current_page', 'navigate_to', 'list_available_tools', 'activate_tool', 'deactivate_tool', 'pin_tool', 'page_refresh'],
canvas: ['render_html', 'render_vue_component'],
canvas: ['render_html', 'render_vue_component', 'move_window', 'resize_window', 'close_window', 'list_windows'],
component: ['save_vue_component', 'load_vue_component', 'list_vue_components', 'delete_vue_component'],
theme: ['get_design_tokens', 'get_active_theme', 'set_theme_variable', 'save_theme', 'list_themes', 'switch_theme', 'reset_theme'],
database: ['list_tables', 'get_table_schema', 'get_table_data', 'get_database_stats', 'execute_query'],

View File

@@ -1,5 +1,6 @@
import type { ToolConfig } from './index'
import { useCanvasStore } from '../../../stores/canvas'
import { useWindowsStore } from '../../../stores/windows'
import {
renderInlineComponent,
type VueComponentDefinition
@@ -82,7 +83,7 @@ export function createCanvasHandlers(): ToolConfig[] {
},
{
name: 'render_vue_component',
description: 'Renderiza un componente Vue 3 completo con ref, reactive, computed, etc.',
description: 'Renderiza un componente Vue 3 en una ventana flotante con drag, resize y close.',
category: 'canvas',
schema: {
type: 'object',
@@ -95,7 +96,11 @@ export function createCanvasHandlers(): ToolConfig[] {
props: { type: 'array', items: { type: 'string' }, description: 'Lista de props' },
imports: { type: 'array', items: { type: 'string' }, description: 'Funciones de Vue a importar' },
componentProps: { type: 'object', description: 'Valores para las props' },
mode: { type: 'string', enum: ['replace', 'append'], description: 'Modo de renderizado' }
mode: { type: 'string', enum: ['replace', 'append'], description: 'Modo de renderizado' },
x: { type: 'number', description: 'Posicion X inicial de la ventana' },
y: { type: 'number', description: 'Posicion Y inicial de la ventana' },
width: { type: 'number', description: 'Ancho inicial de la ventana' },
height: { type: 'number', description: 'Alto inicial de la ventana' }
},
required: ['id', 'name', 'template']
},
@@ -109,6 +114,10 @@ export function createCanvasHandlers(): ToolConfig[] {
imports?: string[]
componentProps?: Record<string, any>
mode?: string
x?: number
y?: number
width?: number
height?: number
}) => {
const container = getCanvasContainer()
if (!container) return 'Error: canvas no encontrado'
@@ -126,7 +135,14 @@ export function createCanvasHandlers(): ToolConfig[] {
}
const isAppend = args.mode === 'append'
const result = renderInlineComponent(definition, container, args.componentProps || {}, isAppend)
const layout = {
x: args.x,
y: args.y,
width: args.width,
height: args.height
}
const result = renderInlineComponent(definition, container, args.componentProps || {}, isAppend, layout)
;(window as any).__vueComponentUnmount = result.unmount
@@ -134,7 +150,128 @@ export function createCanvasHandlers(): ToolConfig[] {
const canvasStore = useCanvasStore()
canvasStore.addToHistory({ tool: 'render_vue_component', args, timestamp: Date.now() })
return `Componente Vue "${args.name}" renderizado`
return `Componente Vue "${args.name}" renderizado en ventana flotante`
}
},
{
name: 'move_window',
description: 'Mueve una ventana a una posicion especifica en el canvas.',
category: 'canvas',
schema: {
type: 'object',
properties: {
id: { type: 'string', description: 'ID de la ventana a mover' },
x: { type: 'number', description: 'Nueva posicion X' },
y: { type: 'number', description: 'Nueva posicion Y' }
},
required: ['id', 'x', 'y']
},
handler: (args: { id: string; x: number; y: number }) => {
const windowsStore = useWindowsStore()
if (!windowsStore.has(args.id)) {
return `Error: Ventana "${args.id}" no encontrada`
}
windowsStore.updatePosition(args.id, args.x, args.y)
// Actualizar el DOM directamente
const windowEl = document.querySelector(`[data-window-id="${args.id}"]`) as HTMLElement
if (windowEl) {
windowEl.style.left = `${args.x}px`
windowEl.style.top = `${args.y}px`
}
return `Ventana "${args.id}" movida a (${args.x}, ${args.y})`
}
},
{
name: 'resize_window',
description: 'Cambia el tamano de una ventana en el canvas.',
category: 'canvas',
schema: {
type: 'object',
properties: {
id: { type: 'string', description: 'ID de la ventana a redimensionar' },
width: { type: 'number', description: 'Nuevo ancho' },
height: { type: 'number', description: 'Nuevo alto' }
},
required: ['id', 'width', 'height']
},
handler: (args: { id: string; width: number; height: number }) => {
const windowsStore = useWindowsStore()
if (!windowsStore.has(args.id)) {
return `Error: Ventana "${args.id}" no encontrada`
}
windowsStore.updateSize(args.id, args.width, args.height)
// Actualizar el DOM directamente
const windowEl = document.querySelector(`[data-window-id="${args.id}"]`) as HTMLElement
if (windowEl) {
windowEl.style.width = `${args.width}px`
windowEl.style.height = `${args.height}px`
}
return `Ventana "${args.id}" redimensionada a ${args.width}x${args.height}`
}
},
{
name: 'close_window',
description: 'Cierra una ventana del canvas.',
category: 'canvas',
schema: {
type: 'object',
properties: {
id: { type: 'string', description: 'ID de la ventana a cerrar' }
},
required: ['id']
},
handler: (args: { id: string }) => {
const windowsStore = useWindowsStore()
if (!windowsStore.has(args.id)) {
return `Error: Ventana "${args.id}" no encontrada`
}
// Buscar y eliminar el contenedor
const container = document.getElementById(`inline-${args.id}`)
if (container) {
container.remove()
}
// Eliminar estilos
document.getElementById(`style-${args.id}`)?.remove()
// Eliminar del store
windowsStore.remove(args.id)
return `Ventana "${args.id}" cerrada`
}
},
{
name: 'list_windows',
description: 'Lista todas las ventanas abiertas en el canvas.',
category: 'canvas',
schema: {
type: 'object',
properties: {},
required: []
},
handler: () => {
const windowsStore = useWindowsStore()
const windows = windowsStore.windowsList
if (windows.length === 0) {
return 'No hay ventanas abiertas'
}
const list = windows.map(w =>
`- ${w.id}: "${w.title}" en (${w.x}, ${w.y}) - ${w.width}x${w.height}`
).join('\n')
return `Ventanas abiertas (${windows.length}):\n${list}`
}
}
]

View File

@@ -20,6 +20,10 @@ export const ALL_TOOL_METAS: ToolMeta[] = [
// Canvas tools
{ name: 'render_html', description: 'Renderiza HTML en el canvas', category: 'canvas' },
{ name: 'render_vue_component', description: 'Renderiza un componente Vue 3 completo', category: 'canvas' },
{ name: 'move_window', description: 'Mueve una ventana a una posicion especifica', category: 'canvas' },
{ name: 'resize_window', description: 'Cambia el tamano de una ventana', category: 'canvas' },
{ name: 'close_window', description: 'Cierra una ventana del canvas', category: 'canvas' },
{ name: 'list_windows', description: 'Lista todas las ventanas abiertas', category: 'canvas' },
// Component tools
{ name: 'save_vue_component', description: 'Guarda un componente Vue en la base de datos', category: 'component' },
@@ -110,5 +114,6 @@ export const CATEGORY_INFO: Record<ToolCategory, { label: string; color: string;
source: { label: 'Source', color: '#8b5cf6', icon: 'M16 18l6-6-6-6M8 6l-6 6 6 6' },
project: { label: 'Project', color: '#06b6d4', icon: 'M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z' },
terminal: { label: 'Terminal', color: '#22c55e', icon: 'M4 17l6-6-6-6M12 19h8' },
git: { label: 'Git', color: '#f97316', icon: 'M6 3v12M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM18 9a9 9 0 0 1-9 9' }
git: { label: 'Git', color: '#f97316', icon: 'M6 3v12M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM18 9a9 9 0 0 1-9 9' },
torch: { label: 'Torch', color: '#eab308', icon: 'M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z' }
}

View File

@@ -0,0 +1,125 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface WindowState {
id: string
title: string
x: number
y: number
width: number
height: number
zIndex: number
}
export const useWindowsStore = defineStore('windows', () => {
const windows = ref<Map<string, WindowState>>(new Map())
const maxZIndex = ref(100)
const windowOffset = ref(0)
// Getters
const windowsList = computed(() => Array.from(windows.value.values()))
const windowCount = computed(() => windows.value.size)
// Obtener siguiente posición para ventana nueva (cascada)
function getNextPosition() {
const offset = windowOffset.value * 30
windowOffset.value = (windowOffset.value + 1) % 10
return { x: 50 + offset, y: 50 + offset }
}
// Registrar nueva ventana
function register(id: string, state: Partial<WindowState> = {}) {
const pos = state.x !== undefined && state.y !== undefined
? { x: state.x, y: state.y }
: getNextPosition()
windows.value.set(id, {
id,
title: state.title ?? id,
x: pos.x,
y: pos.y,
width: state.width ?? 400,
height: state.height ?? 300,
zIndex: ++maxZIndex.value
})
return windows.value.get(id)!
}
// Traer ventana al frente
function bringToFront(id: string) {
const win = windows.value.get(id)
if (win) {
win.zIndex = ++maxZIndex.value
}
}
// Actualizar posición
function updatePosition(id: string, x: number, y: number) {
const win = windows.value.get(id)
if (win) {
win.x = x
win.y = y
}
}
// Actualizar tamaño
function updateSize(id: string, width: number, height: number) {
const win = windows.value.get(id)
if (win) {
win.width = width
win.height = height
}
}
// Actualizar título
function updateTitle(id: string, title: string) {
const win = windows.value.get(id)
if (win) {
win.title = title
}
}
// Eliminar ventana
function remove(id: string) {
windows.value.delete(id)
}
// Obtener ventana por ID
function get(id: string) {
return windows.value.get(id)
}
// Verificar si existe
function has(id: string) {
return windows.value.has(id)
}
// Limpiar todas las ventanas
function clear() {
windows.value.clear()
windowOffset.value = 0
}
return {
// State
windows,
maxZIndex,
// Getters
windowsList,
windowCount,
// Actions
register,
bringToFront,
updatePosition,
updateSize,
updateTitle,
remove,
get,
has,
clear,
getNextPosition
}
})