Add Dockview layout manager and update dependencies; remove GoldenLayout

This commit is contained in:
2025-09-28 00:10:45 +02:00
parent f9f0651628
commit 719acff4fa
9 changed files with 322 additions and 185 deletions
+139
View File
@@ -0,0 +1,139 @@
import { useEffect, useRef, useState } from 'react';
import {
DockviewReact,
DockviewReadyEvent,
DockviewApi,
themeAbyss,
themeLight,
} from 'dockview';
import { useMantineColorScheme, useComputedColorScheme } from '@mantine/core';
import { useDockviewStore } from '../stores/useDockviewStore';
import { Welcome } from './Welcome/Welcome';
import { ColorSchemeToggle } from './ColorSchemeToggle/ColorSchemeToggle';
import 'dockview/dist/styles/dockview.css';
export function DockviewLayoutManager() {
const hostRef = useRef<HTMLDivElement | null>(null);
const apiRef = useRef<DockviewApi | null>(null);
const { layoutConfig, setLayoutConfig } = useDockviewStore();
// 🎨 Detecta automáticamente el tema de Mantine (light / dark / system)
const { colorScheme } = useMantineColorScheme();
const computedScheme = useComputedColorScheme('light', {
getInitialValueInEffect: true,
});
// ⚡ Inicializa Dockview al estar listo
const onReady = (event: DockviewReadyEvent) => {
const api = event.api;
apiRef.current = api;
if (layoutConfig) {
try {
api.fromJSON(layoutConfig);
} catch {
console.warn('Error loading saved Dockview layout, using default');
createDefaultLayout(api);
}
} else {
createDefaultLayout(api);
}
// 🧠 Guarda el layout cuando cambia
api.onDidLayoutChange(() => {
const saved = api.toJSON();
setLayoutConfig(saved);
});
};
// 🌗 Sincroniza Dockview con el tema de Mantine automáticamente
useEffect(() => {
const el = hostRef.current;
if (!el) return;
// Remueve las clases anteriores
el.classList.remove('dockview-theme-dark', 'dockview-theme-light');
// Aplica el tema adecuado
const isDark = computedScheme === 'dark';
el.classList.add(isDark ? 'dockview-theme-dark' : 'dockview-theme-light');
el.style.backgroundColor = isDark ? '#1A1B1E' : '#f5f6f7';
}, [computedScheme]);
// 🧱 Layout inicial por defecto
const createDefaultLayout = (api: DockviewApi) => {
const welcome = api.addPanel({
id: 'welcome',
component: 'welcome',
title: 'Welcome',
});
api.addPanel({
id: 'toggle',
component: 'toggle',
title: 'Theme',
position: {
referencePanel: welcome,
direction: 'right',
},
});
};
// ⚙️ Tema de Dockview sincronizado con Mantine
const dockviewTheme = computedScheme === 'dark' ? themeAbyss : themeLight;
return (
<div
ref={hostRef}
style={{
flex: 1,
width: '100%',
height: '100%',
display: 'flex',
overflow: 'hidden',
transition: 'background-color 0.3s ease-in-out',
}}
>
<DockviewReact
theme={dockviewTheme} // 🔥 Tema dinámico oficial de Dockview
components={{
welcome: (props) => (
<ResponsivePanel component="welcome" {...props} />
),
toggle: (props) => (
<ResponsivePanel component="toggle" {...props} />
),
}}
onReady={onReady}
/>
</div>
);
}
// ✅ Panel adaptable al tamaño
function ResponsivePanel({
component,
api,
}: {
component: 'welcome' | 'toggle';
api: any;
}) {
const [size, setSize] = useState({ width: api.width, height: api.height });
useEffect(() => {
const unsubscribe = api.onDidDimensionsChange(() => {
setSize({ width: api.width, height: api.height });
});
return () => unsubscribe.dispose();
}, [api]);
if (component === 'welcome') {
return <Welcome width={size.width} height={size.height} />;
}
if (component === 'toggle') {
return <ColorSchemeToggle width={size.width} height={size.height} />;
}
return null;
}
-151
View File
@@ -1,151 +0,0 @@
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { GoldenLayout } from 'golden-layout';
import { useComputedColorScheme } from '@mantine/core';
import 'golden-layout/dist/css/goldenlayout-base.css';
import glLightHref from 'golden-layout/dist/css/themes/goldenlayout-light-theme.css?url';
import glDarkHref from 'golden-layout/dist/css/themes/goldenlayout-dark-theme.css?url';
import { useGoldenLayoutStore } from '../stores/useGoldenLayoutStore';
import { Welcome } from '../components/Welcome/Welcome';
import { ColorSchemeToggle } from '../components/ColorSchemeToggle/ColorSchemeToggle';
type Mount = { id: string; el: HTMLElement; type: 'welcome' | 'toggle' };
export function GoldenLayoutManager() {
const hostRef = useRef<HTMLDivElement | null>(null);
const [mounts, setMounts] = useState<Mount[]>([]);
const { layoutConfig, setLayoutConfig } = useGoldenLayoutStore();
const computedScheme = useComputedColorScheme('light');
const themeLinkRef = useRef<HTMLLinkElement | null>(null);
const glRef = useRef<GoldenLayout | null>(null);
useEffect(() => {
if (!hostRef.current) return;
const gl = new GoldenLayout(hostRef.current, {
settings: { constrainDragToContainer: true },
});
glRef.current = gl;
const registerPortal = (type: Mount['type']) => {
gl.registerComponentFactoryFunction(type, (container) => {
const el = container.element;
const id =
typeof crypto !== 'undefined' && 'randomUUID' in crypto
? (crypto as any).randomUUID()
: Math.random().toString(36).slice(2);
setMounts((prev) => [...prev, { id, el, type }]);
const obs = new MutationObserver(() => {
if (!document.body.contains(el)) {
setMounts((prev) => prev.filter((m) => m.id !== id));
obs.disconnect();
}
});
obs.observe(document.body, { childList: true, subtree: true });
});
};
registerPortal('welcome');
registerPortal('toggle');
if (layoutConfig) {
gl.loadLayout(layoutConfig);
} else {
gl.loadLayout({
root: {
type: 'row',
content: [
{ type: 'component', componentType: 'welcome', title: 'Welcome' },
{ type: 'component', componentType: 'toggle', title: 'Theme' },
],
},
});
}
gl.on('stateChanged', () => {
const saved = gl.saveLayout();
setLayoutConfig(saved);
});
return () => {
setMounts([]);
gl.destroy();
};
}, []);
useEffect(() => {
const link = document.createElement('link');
link.rel = 'stylesheet';
document.head.appendChild(link);
themeLinkRef.current = link;
return () => link.remove();
}, []);
useEffect(() => {
if (themeLinkRef.current) {
themeLinkRef.current.href =
computedScheme === 'dark' ? glDarkHref : glLightHref;
}
}, [computedScheme]);
useEffect(() => {
if (!hostRef.current || !glRef.current) return;
const ro = new ResizeObserver(() => {
if (hostRef.current && glRef.current) {
glRef.current.updateSize(hostRef.current.clientWidth, hostRef.current.clientHeight);
}
});
ro.observe(hostRef.current);
return () => ro.disconnect();
}, []);
// 🚀 Clip del drag proxy a los bordes
useEffect(() => {
const host = hostRef.current;
if (!host) return;
const onMouseMove = () => {
const proxy = document.querySelector<HTMLElement>('.lm_dragProxy');
if (!proxy) return;
const bounds = host.getBoundingClientRect();
const proxyBounds = proxy.getBoundingClientRect();
let left = proxyBounds.left;
let top = proxyBounds.top;
if (left < bounds.left) left = bounds.left;
if (top < bounds.top) top = bounds.top;
if (left + proxyBounds.width > bounds.right) {
left = bounds.right - proxyBounds.width;
}
if (top + proxyBounds.height > bounds.bottom) {
top = bounds.bottom - proxyBounds.height;
}
proxy.style.position = 'absolute';
proxy.style.left = `${left - bounds.left}px`;
proxy.style.top = `${top - bounds.top}px`;
};
window.addEventListener('mousemove', onMouseMove);
return () => window.removeEventListener('mousemove', onMouseMove);
}, []);
return (
<>
<div ref={hostRef} style={{ width: '100%', height: '100%' }} />
{mounts.map((m) =>
createPortal(
m.type === 'welcome' ? <Welcome /> : <ColorSchemeToggle />,
m.el
)
)}
</>
);
}
+94 -13
View File
@@ -1,23 +1,104 @@
import { Anchor, Text, Title } from '@mantine/core';
import classes from './Welcome.module.css';
import { useEffect, useState } from 'react';
export function Welcome({ width, height }: { width: number; height: number }) {
// 📏 Clasificamos el tamaño del panel
const small = width < 400;
const medium = width >= 400 && width < 800;
const large = width >= 800;
// 🔹 Ajustes según tamaño del panel
const padding = small ? 8 : medium ? 16 : 32;
const textSize = small ? 'sm' : medium ? 'md' : 'lg';
const titleOrder = small ? 3 : medium ? 2 : 1;
const maxWidth = small ? 300 : medium ? 500 : 700;
// 🎨 Tamaños de fuente personalizados por modo
const titleFontSize = small
? '2rem' // pequeño
: medium
? '3.5rem' // mediano
: '6rem'; // grande
const subtitleFontSize = small
? '0.85rem'
: medium
? '1rem'
: '1.25rem';
// ⏰ Estado para la hora actual
const [currentTime, setCurrentTime] = useState(new Date());
// 🔄 Actualizamos la hora cada segundo
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
export function Welcome() {
return (
<>
<Title className={classes.title} ta="center" mt={100}>
Welcome to{' '}
<Text inherit variant="gradient" component="span" gradient={{ from: 'pink', to: 'yellow' }}>
Mantine
<div
style={{
width,
height,
overflow: 'auto',
padding,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Title
className={classes.title}
ta="center"
order={titleOrder}
style={{
fontSize: titleFontSize,
lineHeight: 1.1,
fontWeight: 900,
letterSpacing: '-1.5px',
}}
>
Welcome to the{' '}
<Text
inherit
variant="gradient"
component="span"
gradient={{ from: 'pink', to: 'yellow' }}
>
Machine
</Text>
</Title>
<Text c="dimmed" ta="center" size="lg" maw={580} mx="auto" mt="xl">
This starter Vite project includes a minimal setup, if you want to learn more on Mantine +
Vite integration follow{' '}
<Anchor href="https://mantine.dev/guides/vite/" size="lg">
this guide
<Text
c="dimmed"
ta="center"
size={textSize}
maw={maxWidth}
mx="auto"
mt={medium || large ? 'xl' : 'sm'}
style={{ fontSize: subtitleFontSize }}
>
Este es el boilerplate de Enmanuel. Para conocer Mantine usa{' '}
<Anchor href="https://mantine.dev/guides/vite/" size={textSize}>
esta guia
</Anchor>
. To get started edit pages/Home.page.tsx file.
. Para comenzar edita <code>pages/Home.page.tsx</code>
</Text>
</>
<Text c="gray" ta="center" mt="md" size="sm">
Panel size: {Math.round(width)} × {Math.round(height)} ({small ? 'small' : medium ? 'medium' : 'large'})
</Text>
{/* ⏰ Mostramos la hora actual */}
<Text c="gray" ta="center" mt="sm" size="sm">
{currentTime.toLocaleTimeString([], { hour12: false })}
</Text>
</div>
);
}