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..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 } = 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 e87e81a..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 }, - 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, 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 1581a73..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 } from "react" +import { useLayoutEffect, useRef, useState, useEffect } from "react" import gsap from "gsap"; import { Draggable } from "gsap/Draggable"; @@ -20,9 +20,11 @@ import { Draggable } from "gsap/Draggable"; */ const WindowWrapper = (Component, windowKey) => { const Wrapped = (props) => { - const { focusWindow, windows } = useWindowStore(); - const { isOpen, zIndex } = windows[windowKey]; + const { focusWindow, windows, maximizeWindow, setWindowPosition } = useWindowStore(); + const { isOpen, zIndex, isMinimized, isMaximized, savedPosition } = windows[windowKey]; const ref = useRef(null); + const [shouldHighlightNav, setShouldHighlightNav] = useState(false); + const draggableInstanceRef = useRef(null); useGSAP(() => { const el = ref.current; @@ -40,21 +42,103 @@ 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), + 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 ? "block" : "none"; - }, [isOpen]); + el.style.display = isOpen && !isMinimized ? "block" : "none"; + }, [isOpen, isMinimized]); + + // 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'); + } + } + }, [shouldHighlightNav]); return ( -
+
); diff --git a/src/index.css b/src/index.css index f16b318..5cbdee0 100644 --- a/src/index.css +++ b/src/index.css @@ -37,7 +37,9 @@ 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; + transition: background-color 0.2s ease; + --navbar-height: 48px; div { @apply flex items-center max-sm:w-full max-sm:justify-center gap-5; @@ -106,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; } } @@ -163,33 +165,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 +227,7 @@ body { @apply flex items-center; svg { - @apply w-5 me-5; + @apply w-5 me-5 flex-shrink-0; } } } @@ -393,4 +419,75 @@ body { } } } + + /* Navbar highlight when dragging window to maximize */ + nav.maximize-highlight { + @apply bg-blue-500/30 backdrop-blur-3xl; + } +} + +/* 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; +} + +/* 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 ddc926a..0684ee2 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,9 @@ const useWindowStore = create(immer((set) => ({ const win = state.windows[windowKey]; if(!win) return; win.isOpen = false; + win.isMinimized = false; + win.isMaximized = false; + win.savedPosition = null; win.zIndex = INITIAL_Z_INDEX; win.data = null; }), @@ -30,6 +34,27 @@ 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; + }), + + maximizeWindow: (windowKey) => + set((state) => { + const win = state.windows[windowKey]; + if(!win) return; + win.isMaximized = !win.isMaximized; + }), + + setWindowPosition: (windowKey, position) => + set((state) => { + const win = state.windows[windowKey]; + if(!win) return; + win.savedPosition = position; + }), }))); 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 +

+
+
;