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
+33
View File
@@ -10,7 +10,9 @@
"dependencies": { "dependencies": {
"@mantine/core": "8.3.1", "@mantine/core": "8.3.1",
"@mantine/hooks": "8.3.1", "@mantine/hooks": "8.3.1",
"@phosphor-icons/react": "^2.1.10",
"@react-three/fiber": "^9.3.0", "@react-three/fiber": "^9.3.0",
"dockview": "^4.9.0",
"golden-layout": "^2.6.0", "golden-layout": "^2.6.0",
"phosphor-react": "^1.4.1", "phosphor-react": "^1.4.1",
"react": "^19.1.1", "react": "^19.1.1",
@@ -1504,6 +1506,19 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@phosphor-icons/react": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz",
"integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">= 16.8",
"react-dom": ">= 16.8"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -3658,6 +3673,24 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dockview": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/dockview/-/dockview-4.9.0.tgz",
"integrity": "sha512-iqPQQiiHmCAw6jS2HmjYM5yvTeSWi6wpxqnK2pEdtv94jb8iAw/Bjwj1o50IDMvAbI9euizFjdZ3XV/FRRn/Ew==",
"license": "MIT",
"dependencies": {
"dockview-core": "^4.9.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/dockview-core": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/dockview-core/-/dockview-core-4.9.0.tgz",
"integrity": "sha512-T23JiOMG14WjHGFeiMvVRCj6gOHy69YOj/VDfF1727rDOA/Ht9SCZjzsGTKSaWoBVZW5wYxCZaDwTX5hfP0zyw==",
"license": "MIT"
},
"node_modules/doctrine": { "node_modules/doctrine": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+2
View File
@@ -22,7 +22,9 @@
"dependencies": { "dependencies": {
"@mantine/core": "8.3.1", "@mantine/core": "8.3.1",
"@mantine/hooks": "8.3.1", "@mantine/hooks": "8.3.1",
"@phosphor-icons/react": "^2.1.10",
"@react-three/fiber": "^9.3.0", "@react-three/fiber": "^9.3.0",
"dockview": "^4.9.0",
"golden-layout": "^2.6.0", "golden-layout": "^2.6.0",
"phosphor-react": "^1.4.1", "phosphor-react": "^1.4.1",
"react": "^19.1.1", "react": "^19.1.1",
+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 { Anchor, Text, Title } from '@mantine/core';
import classes from './Welcome.module.css'; 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 ( return (
<> <div
<Title className={classes.title} ta="center" mt={100}> style={{
Welcome to{' '} width,
<Text inherit variant="gradient" component="span" gradient={{ from: 'pink', to: 'yellow' }}> height,
Mantine 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> </Text>
</Title> </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 + <Text
Vite integration follow{' '} c="dimmed"
<Anchor href="https://mantine.dev/guides/vite/" size="lg"> ta="center"
this guide 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> </Anchor>
. To get started edit pages/Home.page.tsx file. . Para comenzar edita <code>pages/Home.page.tsx</code>
</Text> </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>
); );
} }
+14 -8
View File
@@ -1,13 +1,19 @@
import { MantineProvider } from '@mantine/core'; // src/pages/HomePage.tsx
import { GoldenLayoutManager } from '../components/GoldenLayoutManager'; import { Box } from '@mantine/core';
import { DockviewLayoutManager } from '../components/DockviewLayoutManager';
export function HomePage() { export function HomePage() {
return ( return (
<MantineProvider defaultColorScheme="light"> <Box
{/* Este div será el contenedor: puede ser pantalla completa o un panel en tu layout */} style={{
<div style={{ width: '100%', height: '100vh' }}> width: '100vw', // ocupa todo el ancho del viewport
<GoldenLayoutManager /> height: '100vh', // ocupa todo el alto del viewport
</div> display: 'flex',
</MantineProvider> flexDirection: 'column',
overflow: 'hidden',
}}
>
<DockviewLayoutManager />
</Box>
); );
} }
+21
View File
@@ -0,0 +1,21 @@
import { create } from 'zustand';
interface DockviewStore {
layoutConfig: any | null;
setLayoutConfig: (config: any) => void;
resetLayoutConfig: () => void;
}
/**
* Store global para manejar el estado del layout de Dockview.
* Guarda y restaura la configuración del layout (panels, groups, floating, etc).
*/
export const useDockviewStore = create<DockviewStore>((set) => ({
layoutConfig: null,
// Guarda la configuración serializada
setLayoutConfig: (config) => set({ layoutConfig: config }),
// Limpia el layout almacenado
resetLayoutConfig: () => set({ layoutConfig: null }),
}));
-11
View File
@@ -1,11 +0,0 @@
import { create } from 'zustand';
type GoldenLayoutState = {
layoutConfig: any | null;
setLayoutConfig: (config: any) => void;
};
export const useGoldenLayoutStore = create<GoldenLayoutState>((set) => ({
layoutConfig: null,
setLayoutConfig: (config) => set({ layoutConfig: config }),
}));
+19 -2
View File
@@ -493,6 +493,11 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@phosphor-icons/react@^2.1.10":
version "2.1.10"
resolved "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz"
integrity sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==
"@pkgjs/parseargs@^0.11.0": "@pkgjs/parseargs@^0.11.0":
version "0.11.0" version "0.11.0"
resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz"
@@ -1457,6 +1462,18 @@ dir-glob@^3.0.1:
dependencies: dependencies:
path-type "^4.0.0" path-type "^4.0.0"
dockview-core@^4.9.0:
version "4.9.0"
resolved "https://registry.npmjs.org/dockview-core/-/dockview-core-4.9.0.tgz"
integrity sha512-T23JiOMG14WjHGFeiMvVRCj6gOHy69YOj/VDfF1727rDOA/Ht9SCZjzsGTKSaWoBVZW5wYxCZaDwTX5hfP0zyw==
dockview@^4.9.0:
version "4.9.0"
resolved "https://registry.npmjs.org/dockview/-/dockview-4.9.0.tgz"
integrity sha512-iqPQQiiHmCAw6jS2HmjYM5yvTeSWi6wpxqnK2pEdtv94jb8iAw/Bjwj1o50IDMvAbI9euizFjdZ3XV/FRRn/Ew==
dependencies:
dockview-core "^4.9.0"
doctrine@^2.1.0: doctrine@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz"
@@ -3203,7 +3220,7 @@ react-docgen@^8.0.0:
resolve "^1.22.1" resolve "^1.22.1"
strip-indent "^4.0.0" strip-indent "^4.0.0"
"react-dom@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom@^18.0.0 || ^19.0.0", "react-dom@^18.x || ^19.x", react-dom@^19.0.0, react-dom@^19.1.1, react-dom@>=16.13, react-dom@>=16.8.0, react-dom@>=17.0.0, react-dom@>=18: "react-dom@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom@^18.0.0 || ^19.0.0", "react-dom@^18.x || ^19.x", react-dom@^19.0.0, react-dom@^19.1.1, "react-dom@>= 16.8", react-dom@>=16.13, react-dom@>=16.8.0, react-dom@>=17.0.0, react-dom@>=18:
version "19.1.1" version "19.1.1"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz" resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz"
integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw== integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==
@@ -3293,7 +3310,7 @@ react-use-measure@^2.1.7:
resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz" resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz"
integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg== integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==
"react@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react@^18.0.0 || ^19.0.0", "react@^18.x || ^19.x", react@^19.0.0, react@^19.1.1, react@>=16, react@>=16.13, react@>=16.8.0, react@>=17.0, react@>=17.0.0, react@>=18, react@>=18.0.0: "react@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react@^18.0.0 || ^19.0.0", "react@^18.x || ^19.x", react@^19.0.0, react@^19.1.1, "react@>= 16.8", react@>=16, react@>=16.13, react@>=16.8.0, react@>=17.0, react@>=17.0.0, react@>=18, react@>=18.0.0:
version "19.1.1" version "19.1.1"
resolved "https://registry.npmjs.org/react/-/react-19.1.1.tgz" resolved "https://registry.npmjs.org/react/-/react-19.1.1.tgz"
integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ== integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==