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