Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/components/Dock.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
Expand All @@ -98,6 +103,9 @@ const Dock = () => {
className={canOpen ? "" : "opacity-60"}
/>
</button>
{windows[id]?.isOpen && (
<div className="absolute -bottom-1 w-1 h-1 bg-white rounded-full" />
)}
Comment on lines +106 to +108
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indicator dot is shown when isOpen is true, but it should also consider the isMinimized state. According to typical window management UX patterns, the indicator should remain visible for minimized windows. However, verify if this matches the intended behavior - if minimized windows should not show the indicator, then the condition should be isOpen && !isMinimized.

Copilot uses AI. Check for mistakes.
</div>
))}

Expand Down
6 changes: 3 additions & 3 deletions src/components/WindowControls.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import useWindowStore from "#store/window"

const WindowControls = ({ target }) => {
const { closeWindow } = useWindowStore();
const { closeWindow, minimizeWindow, maximizeWindow } = useWindowStore();
return (
<div id="window-controls">
<div className="close" onClick={() => closeWindow(target)}/>
<div className="minimize" />
<div className="maximize" />
<div className="minimize" onClick={() => minimizeWindow(target)}/>
<div className="maximize" onClick={() => maximizeWindow(target)}/>
</div>
)
};
Expand Down
16 changes: 8 additions & 8 deletions src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
100 changes: 92 additions & 8 deletions src/hoc/WindowWrapper.jsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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;
Expand All @@ -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"),
Comment on lines +45 to +46
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Draggable trigger queries for '#window-header' on mount, but if the Component is not yet rendered or doesn't contain this element, the querySelector will return null and dragging may not work. Consider adding error handling or validation to ensure the trigger element exists before creating the Draggable instance.

Suggested change
const [instance] = Draggable.create(el, {
trigger: el.querySelector("#window-header"),
const headerEl = el.querySelector("#window-header");
if (!headerEl) return;
const [instance] = Draggable.create(el, {
trigger: headerEl,

Copilot uses AI. Check for mistakes.
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 (
<section id={windowKey} ref={ref} style={{ zIndex }}
className="absolute">
<section
id={windowKey}
ref={ref}
style={{ zIndex }}
className={`absolute ${isMaximized ? 'maximized' : ''}`}>
<Component {...props} />
</section>
);
Expand Down
127 changes: 112 additions & 15 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The z-index value of 40 for the nav is hardcoded here, but window z-indexes start at INITIAL_Z_INDEX (1000) and increment from there. This could cause windows to appear above the navigation bar. Consider using a z-index value higher than the maximum possible window z-index, or defining navigation z-index as a constant.

Suggested change
@apply flex justify-between items-center bg-white/50 backdrop-blur-3xl p-2 px-5 select-none relative z-40;
@apply flex justify-between items-center bg-white/50 backdrop-blur-3xl p-2 px-5 select-none relative z-[9999];

Copilot uses AI. Check for mistakes.
transition: background-color 0.2s ease;
--navbar-height: 48px;

div {
@apply flex items-center max-sm:w-full max-sm:justify-center gap-5;
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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;
}
}
}
}
Expand All @@ -201,7 +227,7 @@ body {
@apply flex items-center;

svg {
@apply w-5 me-5;
@apply w-5 me-5 flex-shrink-0;
}
}
}
Expand Down Expand Up @@ -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;
}
Loading