From aff4cda0a2813329e6df7de54f5cb349e13c2057 Mon Sep 17 00:00:00 2001 From: christopher-ling Date: Sun, 21 Dec 2025 22:35:29 -0500 Subject: [PATCH 1/4] minimize functionality --- src/components/Dock.jsx | 10 +++++++++- src/components/WindowControls.jsx | 4 ++-- src/constants/index.js | 16 ++++++++-------- src/hoc/WindowWrapper.jsx | 6 +++--- src/index.css | 2 +- src/store/window.js | 9 +++++++++ 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/components/Dock.jsx b/src/components/Dock.jsx index 21d5ad4..bb9a90e 100644 --- a/src/components/Dock.jsx +++ b/src/components/Dock.jsx @@ -70,9 +70,14 @@ const Dock = () => { return; } - if(window.isOpen) { + if(window.isOpen && window.isMinimized) { + // Restore minimized window + openWindow(app.id); + } else if(window.isOpen) { + // Close open window closeWindow(app.id); } else { + // Open closed window openWindow(app.id); } }; @@ -98,6 +103,9 @@ const Dock = () => { className={canOpen ? "" : "opacity-60"} /> + {windows[id]?.isOpen && ( +
+ )}
))} diff --git a/src/components/WindowControls.jsx b/src/components/WindowControls.jsx index e3dc4a5..7b07207 100644 --- a/src/components/WindowControls.jsx +++ b/src/components/WindowControls.jsx @@ -1,11 +1,11 @@ import useWindowStore from "#store/window" const WindowControls = ({ target }) => { - const { closeWindow } = useWindowStore(); + const { closeWindow, minimizeWindow } = useWindowStore(); return (
closeWindow(target)}/> -
+
minimizeWindow(target)}/>
) diff --git a/src/constants/index.js b/src/constants/index.js index e87e81a..1345465 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -493,14 +493,14 @@ export const locations = { const INITIAL_Z_INDEX = 1000; const WINDOW_CONFIG = { - finder: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null }, - contact: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null }, - resume: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null }, - safari: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null }, - photos: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null }, - terminal: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null }, - txtfile: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null }, - imgfile: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null }, + finder: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, + contact: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, + resume: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, + safari: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, + photos: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, + terminal: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, + txtfile: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, + imgfile: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, }; export { INITIAL_Z_INDEX, WINDOW_CONFIG }; \ No newline at end of file diff --git a/src/hoc/WindowWrapper.jsx b/src/hoc/WindowWrapper.jsx index 1581a73..52bd18f 100644 --- a/src/hoc/WindowWrapper.jsx +++ b/src/hoc/WindowWrapper.jsx @@ -21,7 +21,7 @@ import { Draggable } from "gsap/Draggable"; const WindowWrapper = (Component, windowKey) => { const Wrapped = (props) => { const { focusWindow, windows } = useWindowStore(); - const { isOpen, zIndex } = windows[windowKey]; + const { isOpen, zIndex, isMinimized } = windows[windowKey]; const ref = useRef(null); useGSAP(() => { @@ -48,8 +48,8 @@ const WindowWrapper = (Component, windowKey) => { useLayoutEffect(() => { const el = ref.current; if(!el) return; - el.style.display = isOpen ? "block" : "none"; - }, [isOpen]); + el.style.display = isOpen && !isMinimized ? "block" : "none"; + }, [isOpen, isMinimized]); return ( diff --git a/src/index.css b/src/index.css index f16b318..54f1b9b 100644 --- a/src/index.css +++ b/src/index.css @@ -37,7 +37,7 @@ body { } nav { - @apply flex justify-between items-center bg-white/50 backdrop-blur-3xl p-2 px-5 select-none; + @apply flex justify-between items-center bg-white/50 backdrop-blur-3xl p-2 px-5 select-none relative z-40; div { @apply flex items-center max-sm:w-full max-sm:justify-center gap-5; diff --git a/src/store/window.js b/src/store/window.js index ddc926a..030492c 100644 --- a/src/store/window.js +++ b/src/store/window.js @@ -11,6 +11,7 @@ const useWindowStore = create(immer((set) => ({ const win = state.windows[windowKey]; if(!win) return; win.isOpen = true; + win.isMinimized = false; win.zIndex = state.nextZIndex; win.data = data ?? window.data; state.nextZIndex++; @@ -21,6 +22,7 @@ const useWindowStore = create(immer((set) => ({ const win = state.windows[windowKey]; if(!win) return; win.isOpen = false; + win.isMinimized = false; win.zIndex = INITIAL_Z_INDEX; win.data = null; }), @@ -30,6 +32,13 @@ const useWindowStore = create(immer((set) => ({ const win = state.windows[windowKey]; win.zIndex = state.nextZIndex++; }), + + minimizeWindow: (windowKey) => + set((state) => { + const win = state.windows[windowKey]; + if(!win) return; + win.isMinimized = true; + }), }))); export default useWindowStore; \ No newline at end of file From 63767c2fe0da80b14c362e173ce8522008c88ae2 Mon Sep 17 00:00:00 2001 From: christopher-ling Date: Sun, 21 Dec 2025 22:50:33 -0500 Subject: [PATCH 2/4] close improvement and draggable sizing --- src/constants/index.js | 16 +++--- src/hoc/WindowWrapper.jsx | 109 +++++++++++++++++++++++++++++++++++--- src/index.css | 102 ++++++++++++++++++++++++++++++----- src/store/window.js | 10 ++++ src/windows/Terminal.jsx | 72 +++++++++++++------------ 5 files changed, 249 insertions(+), 60 deletions(-) diff --git a/src/constants/index.js b/src/constants/index.js index 1345465..0298e9f 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -493,14 +493,14 @@ export const locations = { const INITIAL_Z_INDEX = 1000; const WINDOW_CONFIG = { - finder: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, - contact: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, - resume: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, - safari: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, - photos: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, - terminal: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, - txtfile: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, - imgfile: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false }, + finder: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, + contact: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, + resume: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, + safari: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, + photos: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, + terminal: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, + txtfile: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, + imgfile: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, }; export { INITIAL_Z_INDEX, WINDOW_CONFIG }; \ No newline at end of file diff --git a/src/hoc/WindowWrapper.jsx b/src/hoc/WindowWrapper.jsx index 52bd18f..390bc11 100644 --- a/src/hoc/WindowWrapper.jsx +++ b/src/hoc/WindowWrapper.jsx @@ -1,6 +1,6 @@ import useWindowStore from "#store/window"; import { useGSAP } from "@gsap/react"; -import { useLayoutEffect, useRef } from "react" +import { useLayoutEffect, useRef, useState } from "react" import gsap from "gsap"; import { Draggable } from "gsap/Draggable"; @@ -20,9 +20,10 @@ import { Draggable } from "gsap/Draggable"; */ const WindowWrapper = (Component, windowKey) => { const Wrapped = (props) => { - const { focusWindow, windows } = useWindowStore(); - const { isOpen, zIndex, isMinimized } = windows[windowKey]; + const { focusWindow, windows, resizeWindow } = useWindowStore(); + const { isOpen, zIndex, isMinimized, width, height } = windows[windowKey]; const ref = useRef(null); + const [isResizing, setIsResizing] = useState(false); useGSAP(() => { const el = ref.current; @@ -40,7 +41,10 @@ const WindowWrapper = (Component, windowKey) => { const el = ref.current; if (!el) return; - const [instance] = Draggable.create(el, { onPress: () => focusWindow(windowKey) }); + const [instance] = Draggable.create(el, { + trigger: el.querySelector("#window-header"), + onPress: () => focusWindow(windowKey) + }); return () => instance.kill(); }, []); @@ -51,11 +55,104 @@ const WindowWrapper = (Component, windowKey) => { el.style.display = isOpen && !isMinimized ? "block" : "none"; }, [isOpen, isMinimized]); + useLayoutEffect(() => { + const el = ref.current; + if (!el) return; + + const handleMouseDown = (e, edge) => { + e.preventDefault(); + e.stopPropagation(); + setIsResizing(true); + focusWindow(windowKey); + + const startX = e.clientX; + const startY = e.clientY; + const startWidth = el.offsetWidth; + const startHeight = el.offsetHeight; + const startLeft = el.offsetLeft; + const startTop = el.offsetTop; + + const handleMouseMove = (e) => { + let newWidth = startWidth; + let newHeight = startHeight; + let newLeft = startLeft; + let newTop = startTop; + + if (edge.includes('right')) { + newWidth = Math.max(300, startWidth + (e.clientX - startX)); + } + if (edge.includes('left')) { + const delta = e.clientX - startX; + newWidth = Math.max(300, startWidth - delta); + if (newWidth > 300) newLeft = startLeft + delta; + } + if (edge.includes('bottom')) { + newHeight = Math.max(200, startHeight + (e.clientY - startY)); + } + if (edge.includes('top')) { + const delta = e.clientY - startY; + newHeight = Math.max(200, startHeight - delta); + if (newHeight > 200) newTop = startTop + delta; + } + + el.style.width = `${newWidth}px`; + el.style.height = `${newHeight}px`; + el.style.left = `${newLeft}px`; + el.style.top = `${newTop}px`; + }; + + const handleMouseUp = () => { + setIsResizing(false); + resizeWindow(windowKey, el.offsetWidth, el.offsetHeight); + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }; + + const edges = [ + { selector: '.resize-right', edge: 'right' }, + { selector: '.resize-left', edge: 'left' }, + { selector: '.resize-bottom', edge: 'bottom' }, + { selector: '.resize-top', edge: 'top' }, + { selector: '.resize-corner-br', edge: 'bottom-right' }, + { selector: '.resize-corner-bl', edge: 'bottom-left' }, + { selector: '.resize-corner-tr', edge: 'top-right' }, + { selector: '.resize-corner-tl', edge: 'top-left' }, + ]; + + edges.forEach(({ selector, edge }) => { + const handle = el.querySelector(selector); + if (handle) { + handle.addEventListener('mousedown', (e) => handleMouseDown(e, edge)); + } + }); + }, [windowKey, focusWindow, resizeWindow]); + return ( -
+
+ + {/* Resize handles */} +
+
+
+
+
+
+
+
); }; diff --git a/src/index.css b/src/index.css index 54f1b9b..981fd24 100644 --- a/src/index.css +++ b/src/index.css @@ -163,33 +163,57 @@ body { } #terminal { - @apply w-xl absolute top-32 left-1/12 bg-white shadow-2xl drop-shadow-2xl rounded-xl overflow-hidden; + @apply w-xl absolute top-32 left-1/12 bg-white shadow-2xl drop-shadow-2xl rounded-xl overflow-hidden flex flex-col; h2 { @apply font-bold text-sm text-center w-full; } - .techstack { - @apply text-sm font-roboto p-5; + .terminal-content { + @apply flex-1 overflow-hidden flex flex-col; + } + + .terminal-body { + @apply flex-1 overflow-y-auto text-sm font-roboto p-5; + } + + .terminal-prompt { + @apply text-black mb-4; + } + .terminal-output { .label { - @apply flex items-center ms-10 mt-7; + @apply flex items-center gap-4 ms-10 mt-7 text-gray-600 text-xs; + + .label-category { + @apply w-32 flex-shrink-0; + } + + .label-tech { + @apply flex-1 min-w-0; + } } .content { - @apply py-5 my-5 border-y border-dashed space-y-1; + @apply py-5 my-5 border-y border-dashed border-gray-300 space-y-2; + + .tech-row { + @apply flex items-start gap-2; - li { .check { - @apply text-[#00A154] w-5; + @apply text-[#00A154] w-5 flex-shrink-0 mt-0.5; } - h3 { - @apply font-semibold text-[#00A154] w-32 ms-5; + .category-name { + @apply font-semibold text-[#00A154] w-32 flex-shrink-0; } - ul { - @apply flex items-center gap-3; + .tech-items { + @apply flex flex-wrap gap-x-3 gap-y-1 flex-1 min-w-0 text-black; + + .tech-item { + @apply break-words; + } } } } @@ -201,7 +225,7 @@ body { @apply flex items-center; svg { - @apply w-5 me-5; + @apply w-5 me-5 flex-shrink-0; } } } @@ -393,4 +417,58 @@ body { } } } +} + +/* Window resize handles */ +section.absolute { + position: relative; +} + +section .resize-right, +section .resize-left { + @apply absolute top-0 bottom-0 w-1 cursor-ew-resize; +} + +section .resize-right { + @apply right-0; +} + +section .resize-left { + @apply left-0; +} + +section .resize-bottom, +section .resize-top { + @apply absolute left-0 right-0 h-1 cursor-ns-resize; +} + +section .resize-bottom { + @apply bottom-0; +} + +section .resize-top { + @apply top-0; +} + +section .resize-corner-br, +section .resize-corner-bl, +section .resize-corner-tr, +section .resize-corner-tl { + @apply absolute w-3 h-3 z-10; +} + +section .resize-corner-br { + @apply bottom-0 right-0 cursor-nwse-resize; +} + +section .resize-corner-bl { + @apply bottom-0 left-0 cursor-nesw-resize; +} + +section .resize-corner-tr { + @apply top-0 right-0 cursor-nesw-resize; +} + +section .resize-corner-tl { + @apply top-0 left-0 cursor-nwse-resize; } \ No newline at end of file diff --git a/src/store/window.js b/src/store/window.js index 030492c..df8a057 100644 --- a/src/store/window.js +++ b/src/store/window.js @@ -25,6 +25,8 @@ const useWindowStore = create(immer((set) => ({ win.isMinimized = false; win.zIndex = INITIAL_Z_INDEX; win.data = null; + win.width = null; + win.height = null; }), focusWindow: (windowKey, data = null) => @@ -39,6 +41,14 @@ const useWindowStore = create(immer((set) => ({ if(!win) return; win.isMinimized = true; }), + + resizeWindow: (windowKey, width, height) => + set((state) => { + const win = state.windows[windowKey]; + if(!win) return; + win.width = width; + win.height = height; + }), }))); export default useWindowStore; \ No newline at end of file diff --git a/src/windows/Terminal.jsx b/src/windows/Terminal.jsx index 36a9011..edcfcd3 100644 --- a/src/windows/Terminal.jsx +++ b/src/windows/Terminal.jsx @@ -10,40 +10,44 @@ const Terminal = () => {

Tech Stack

-
-

- @christopher % - show tech stack -

- -
-

Category

-

Technologies

-
- -
    - {techStack.map(({ category, items }) => ( -
  • - -

    {category}

    -
      - {items.map((item, i) => ( -
    • {item}{i < items.length - 1 ? "," : ""}
    • - ))} -
    -
  • - ))} -
- -
-

- {techStack.length} of {techStack.length} stacks loaded successfully (100%) -

- -

- - Render time: 6ms -

+
+
+

+ @christopher % + show tech stack +

+ +
+
+

Category

+

Technologies

+
+ +
    + {techStack.map(({ category, items }) => ( +
  • + +

    {category}

    +
      + {items.map((item, i) => ( +
    • {item}{i < items.length - 1 ? "," : ""}
    • + ))} +
    +
  • + ))} +
+ +
+

+ {techStack.length} of {techStack.length} stacks loaded successfully (100%) +

+ +

+ + Render time: 6ms +

+
+
; From 1add7dc1144759551f474404b41c96ffd8bd4d0b Mon Sep 17 00:00:00 2001 From: christopher-ling Date: Sun, 21 Dec 2025 23:37:51 -0500 Subject: [PATCH 3/4] feat: add window controls with minimize and maximize functionality - Implement minimize window feature with white dot indicator in dock - Add maximize functionality triggered by green button or drag-to-top - Add navbar highlight effect when dragging window to top - Make only window header draggable (not entire window) - Maximized windows cover dock and fill screen below navbar - Windows reset to original size when closed - Fix minimize restore behavior in dock click handler --- src/components/WindowControls.jsx | 4 +- src/constants/index.js | 16 +-- src/hoc/WindowWrapper.jsx | 177 ++++++++++++++---------------- src/index.css | 19 ++++ src/store/window.js | 16 ++- 5 files changed, 122 insertions(+), 110 deletions(-) diff --git a/src/components/WindowControls.jsx b/src/components/WindowControls.jsx index 7b07207..d526daa 100644 --- a/src/components/WindowControls.jsx +++ b/src/components/WindowControls.jsx @@ -1,12 +1,12 @@ import useWindowStore from "#store/window" const WindowControls = ({ target }) => { - const { closeWindow, minimizeWindow } = useWindowStore(); + const { closeWindow, minimizeWindow, maximizeWindow } = useWindowStore(); return (
closeWindow(target)}/>
minimizeWindow(target)}/> -
+
maximizeWindow(target)}/>
) }; diff --git a/src/constants/index.js b/src/constants/index.js index 0298e9f..625b315 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -493,14 +493,14 @@ export const locations = { const INITIAL_Z_INDEX = 1000; const WINDOW_CONFIG = { - finder: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, - contact: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, - resume: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, - safari: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, - photos: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, - terminal: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, - txtfile: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, - imgfile: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, width: null, height: null }, + finder: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, isMaximized: false, savedPosition: null }, + contact: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, isMaximized: false, savedPosition: null }, + resume: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, isMaximized: false, savedPosition: null }, + safari: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, isMaximized: false, savedPosition: null }, + photos: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, isMaximized: false, savedPosition: null }, + terminal: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, isMaximized: false, savedPosition: null }, + txtfile: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, isMaximized: false, savedPosition: null }, + imgfile: { isOpen: false, zIndex: INITIAL_Z_INDEX, data: null, isMinimized: false, isMaximized: false, savedPosition: null }, }; export { INITIAL_Z_INDEX, WINDOW_CONFIG }; \ No newline at end of file diff --git a/src/hoc/WindowWrapper.jsx b/src/hoc/WindowWrapper.jsx index 390bc11..5af622b 100644 --- a/src/hoc/WindowWrapper.jsx +++ b/src/hoc/WindowWrapper.jsx @@ -1,6 +1,6 @@ import useWindowStore from "#store/window"; import { useGSAP } from "@gsap/react"; -import { useLayoutEffect, useRef, useState } from "react" +import { useLayoutEffect, useRef, useState, useEffect } from "react" import gsap from "gsap"; import { Draggable } from "gsap/Draggable"; @@ -20,10 +20,11 @@ import { Draggable } from "gsap/Draggable"; */ const WindowWrapper = (Component, windowKey) => { const Wrapped = (props) => { - const { focusWindow, windows, resizeWindow } = useWindowStore(); - const { isOpen, zIndex, isMinimized, width, height } = windows[windowKey]; + const { focusWindow, windows, maximizeWindow, setWindowPosition } = useWindowStore(); + const { isOpen, zIndex, isMinimized, isMaximized, savedPosition } = windows[windowKey]; const ref = useRef(null); - const [isResizing, setIsResizing] = useState(false); + const [shouldHighlightNav, setShouldHighlightNav] = useState(false); + const draggableInstanceRef = useRef(null); useGSAP(() => { const el = ref.current; @@ -43,116 +44,102 @@ const WindowWrapper = (Component, windowKey) => { const [instance] = Draggable.create(el, { trigger: el.querySelector("#window-header"), - onPress: () => focusWindow(windowKey) + onPress: () => focusWindow(windowKey), + onDrag: function() { + const navbar = document.querySelector('nav'); + if (navbar) { + const rect = el.getBoundingClientRect(); + const windowTop = rect.top; + + // Highlight if window top is at or above navbar (0 or negative) + if (windowTop <= 5) { + setShouldHighlightNav(true); + } else { + setShouldHighlightNav(false); + } + } + }, + onDragEnd: function() { + const navbar = document.querySelector('nav'); + if (navbar) { + const rect = el.getBoundingClientRect(); + const windowTop = rect.top; + + // Maximize if window top is at or above navbar top + if (windowTop <= 5) { + // Save position before maximizing + setWindowPosition(windowKey, { + left: el.style.left, + top: el.style.top, + width: el.style.width, + height: el.style.height, + }); + maximizeWindow(windowKey); + } + } + setShouldHighlightNav(false); + } }); + draggableInstanceRef.current = instance; + return () => instance.kill(); }, []); + // Handle maximize state changes + useEffect(() => { + const el = ref.current; + if (!el || !draggableInstanceRef.current) return; + + if (isMaximized) { + // Reset GSAP transform to fix offset + gsap.set(el, { x: 0, y: 0 }); + // Disable dragging when maximized + draggableInstanceRef.current.disable(); + } else { + // Re-enable dragging when restored + draggableInstanceRef.current.enable(); + + // Restore saved position if available + if (savedPosition) { + gsap.set(el, { + x: 0, + y: 0, + left: savedPosition.left, + top: savedPosition.top, + width: savedPosition.width, + height: savedPosition.height, + }); + } + } + }, [isMaximized, savedPosition]); + useLayoutEffect(() => { const el = ref.current; if(!el) return; el.style.display = isOpen && !isMinimized ? "block" : "none"; }, [isOpen, isMinimized]); - useLayoutEffect(() => { - const el = ref.current; - if (!el) return; - - const handleMouseDown = (e, edge) => { - e.preventDefault(); - e.stopPropagation(); - setIsResizing(true); - focusWindow(windowKey); - - const startX = e.clientX; - const startY = e.clientY; - const startWidth = el.offsetWidth; - const startHeight = el.offsetHeight; - const startLeft = el.offsetLeft; - const startTop = el.offsetTop; - - const handleMouseMove = (e) => { - let newWidth = startWidth; - let newHeight = startHeight; - let newLeft = startLeft; - let newTop = startTop; - - if (edge.includes('right')) { - newWidth = Math.max(300, startWidth + (e.clientX - startX)); - } - if (edge.includes('left')) { - const delta = e.clientX - startX; - newWidth = Math.max(300, startWidth - delta); - if (newWidth > 300) newLeft = startLeft + delta; - } - if (edge.includes('bottom')) { - newHeight = Math.max(200, startHeight + (e.clientY - startY)); - } - if (edge.includes('top')) { - const delta = e.clientY - startY; - newHeight = Math.max(200, startHeight - delta); - if (newHeight > 200) newTop = startTop + delta; - } - - el.style.width = `${newWidth}px`; - el.style.height = `${newHeight}px`; - el.style.left = `${newLeft}px`; - el.style.top = `${newTop}px`; - }; - - const handleMouseUp = () => { - setIsResizing(false); - resizeWindow(windowKey, el.offsetWidth, el.offsetHeight); - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - }; - - const edges = [ - { selector: '.resize-right', edge: 'right' }, - { selector: '.resize-left', edge: 'left' }, - { selector: '.resize-bottom', edge: 'bottom' }, - { selector: '.resize-top', edge: 'top' }, - { selector: '.resize-corner-br', edge: 'bottom-right' }, - { selector: '.resize-corner-bl', edge: 'bottom-left' }, - { selector: '.resize-corner-tr', edge: 'top-right' }, - { selector: '.resize-corner-tl', edge: 'top-left' }, - ]; - - edges.forEach(({ selector, edge }) => { - const handle = el.querySelector(selector); - if (handle) { - handle.addEventListener('mousedown', (e) => handleMouseDown(e, edge)); + // Add/remove highlight class to navbar + useEffect(() => { + const navbar = document.querySelector('nav'); + if (navbar) { + if (shouldHighlightNav) { + navbar.classList.add('maximize-highlight'); + } else { + navbar.classList.remove('maximize-highlight'); } - }); - }, [windowKey, focusWindow, resizeWindow]); + } + }, [shouldHighlightNav]); return (
+ style={{ zIndex }} + className={`absolute ${isMaximized ? 'maximized' : ''}`}> - - {/* Resize handles */} -
-
-
-
-
-
-
-
); }; diff --git a/src/index.css b/src/index.css index 981fd24..553c683 100644 --- a/src/index.css +++ b/src/index.css @@ -38,6 +38,8 @@ body { nav { @apply flex justify-between items-center bg-white/50 backdrop-blur-3xl p-2 px-5 select-none relative z-40; + transition: background-color 0.2s ease; + --navbar-height: 48px; div { @apply flex items-center max-sm:w-full max-sm:justify-center gap-5; @@ -417,6 +419,11 @@ body { } } } + + /* Navbar highlight when dragging window to maximize */ + nav.maximize-highlight { + @apply bg-blue-500/30 backdrop-blur-3xl; + } } /* Window resize handles */ @@ -471,4 +478,16 @@ section .resize-corner-tr { section .resize-corner-tl { @apply top-0 left-0 cursor-nwse-resize; +} + +/* Maximized window state */ +section.maximized { + @apply !fixed !rounded-xl; + top: calc(var(--navbar-height, 48px) + 8px) !important; + left: 8px !important; + width: calc(100vw - 16px) !important; + height: calc(100vh - var(--navbar-height, 48px) - 16px) !important; + margin: 0 !important; + z-index: 50 !important; + transform: none !important; } \ No newline at end of file diff --git a/src/store/window.js b/src/store/window.js index df8a057..0684ee2 100644 --- a/src/store/window.js +++ b/src/store/window.js @@ -23,10 +23,10 @@ const useWindowStore = create(immer((set) => ({ if(!win) return; win.isOpen = false; win.isMinimized = false; + win.isMaximized = false; + win.savedPosition = null; win.zIndex = INITIAL_Z_INDEX; win.data = null; - win.width = null; - win.height = null; }), focusWindow: (windowKey, data = null) => @@ -42,12 +42,18 @@ const useWindowStore = create(immer((set) => ({ win.isMinimized = true; }), - resizeWindow: (windowKey, width, height) => + maximizeWindow: (windowKey) => set((state) => { const win = state.windows[windowKey]; if(!win) return; - win.width = width; - win.height = height; + win.isMaximized = !win.isMaximized; + }), + + setWindowPosition: (windowKey, position) => + set((state) => { + const win = state.windows[windowKey]; + if(!win) return; + win.savedPosition = position; }), }))); From 52a4f185802ccd9a9f6ec78b3546bd9cbefee814 Mon Sep 17 00:00:00 2001 From: christopher-ling Date: Sun, 21 Dec 2025 23:52:40 -0500 Subject: [PATCH 4/4] cursor pointers --- src/index.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.css b/src/index.css index 553c683..5cbdee0 100644 --- a/src/index.css +++ b/src/index.css @@ -108,11 +108,11 @@ body { } .minimize { - @apply size-3.5 rounded-full bg-[#ffc030]; + @apply size-3.5 rounded-full bg-[#ffc030] cursor-pointer; } .maximize { - @apply size-3.5 rounded-full bg-[#2acb42]; + @apply size-3.5 rounded-full bg-[#2acb42] cursor-pointer; } }