diff --git a/package-lock.json b/package-lock.json index 3415d80..8316efb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "@mantine/core": "8.3.1", "@mantine/hooks": "8.3.1", + "@phosphor-icons/react": "^2.1.10", "@react-three/fiber": "^9.3.0", + "dockview": "^4.9.0", "golden-layout": "^2.6.0", "phosphor-react": "^1.4.1", "react": "^19.1.1", @@ -1504,6 +1506,19 @@ "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": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3658,6 +3673,24 @@ "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", diff --git a/package.json b/package.json index 97d8027..a20f24c 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "dependencies": { "@mantine/core": "8.3.1", "@mantine/hooks": "8.3.1", + "@phosphor-icons/react": "^2.1.10", "@react-three/fiber": "^9.3.0", + "dockview": "^4.9.0", "golden-layout": "^2.6.0", "phosphor-react": "^1.4.1", "react": "^19.1.1", diff --git a/src/components/DockviewLayoutManager.tsx b/src/components/DockviewLayoutManager.tsx new file mode 100644 index 0000000..681e809 --- /dev/null +++ b/src/components/DockviewLayoutManager.tsx @@ -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(null); + const apiRef = useRef(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 ( +
+ ( + + ), + toggle: (props) => ( + + ), + }} + onReady={onReady} + /> +
+ ); +} + +// ✅ 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 ; + } + + if (component === 'toggle') { + return ; + } + + return null; +} diff --git a/src/components/GoldenLayoutManager.tsx b/src/components/GoldenLayoutManager.tsx deleted file mode 100644 index 74c2ee0..0000000 --- a/src/components/GoldenLayoutManager.tsx +++ /dev/null @@ -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(null); - const [mounts, setMounts] = useState([]); - const { layoutConfig, setLayoutConfig } = useGoldenLayoutStore(); - - const computedScheme = useComputedColorScheme('light'); - const themeLinkRef = useRef(null); - const glRef = useRef(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('.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 ( - <> -
- {mounts.map((m) => - createPortal( - m.type === 'welcome' ? : , - m.el - ) - )} - - ); -} diff --git a/src/components/Welcome/Welcome.tsx b/src/components/Welcome/Welcome.tsx index 7d90297..d6384ec 100644 --- a/src/components/Welcome/Welcome.tsx +++ b/src/components/Welcome/Welcome.tsx @@ -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 ( - <> - - 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> - - This starter Vite project includes a minimal setup, if you want to learn more on Mantine + - Vite integration follow{' '} - - this guide + + + Este es el boilerplate de Enmanuel. Para conocer Mantine usa{' '} + + esta guia - . To get started edit pages/Home.page.tsx file. + . Para comenzar edita pages/Home.page.tsx - + + + Panel size: {Math.round(width)} × {Math.round(height)} ({small ? 'small' : medium ? 'medium' : 'large'}) + + + {/* ⏰ Mostramos la hora actual */} + + {currentTime.toLocaleTimeString([], { hour12: false })} + + +
); } diff --git a/src/pages/Home.page.tsx b/src/pages/Home.page.tsx index c2a201a..f42c1b6 100644 --- a/src/pages/Home.page.tsx +++ b/src/pages/Home.page.tsx @@ -1,13 +1,19 @@ -import { MantineProvider } from '@mantine/core'; -import { GoldenLayoutManager } from '../components/GoldenLayoutManager'; +// src/pages/HomePage.tsx +import { Box } from '@mantine/core'; +import { DockviewLayoutManager } from '../components/DockviewLayoutManager'; export function HomePage() { return ( - - {/* Este div será el contenedor: puede ser pantalla completa o un panel en tu layout */} -
- -
-
+ + + ); } diff --git a/src/stores/useDockviewStore.ts b/src/stores/useDockviewStore.ts new file mode 100644 index 0000000..8feee88 --- /dev/null +++ b/src/stores/useDockviewStore.ts @@ -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((set) => ({ + layoutConfig: null, + + // Guarda la configuración serializada + setLayoutConfig: (config) => set({ layoutConfig: config }), + + // Limpia el layout almacenado + resetLayoutConfig: () => set({ layoutConfig: null }), +})); diff --git a/src/stores/useGoldenLayoutStore.ts b/src/stores/useGoldenLayoutStore.ts deleted file mode 100644 index aea8805..0000000 --- a/src/stores/useGoldenLayoutStore.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { create } from 'zustand'; - -type GoldenLayoutState = { - layoutConfig: any | null; - setLayoutConfig: (config: any) => void; -}; - -export const useGoldenLayoutStore = create((set) => ({ - layoutConfig: null, - setLayoutConfig: (config) => set({ layoutConfig: config }), -})); diff --git a/yarn.lock b/yarn.lock index c2ab40b..32c9d9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -493,6 +493,11 @@ "@nodelib/fs.scandir" "2.1.5" 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": version "0.11.0" resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" @@ -1457,6 +1462,18 @@ dir-glob@^3.0.1: dependencies: 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: version "2.1.0" resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" @@ -3203,7 +3220,7 @@ react-docgen@^8.0.0: resolve "^1.22.1" 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" resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz" 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" 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" resolved "https://registry.npmjs.org/react/-/react-19.1.1.tgz" integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==