Skip to content

Commit 015f471

Browse files
committed
feat: new sidepanels
1 parent bb36840 commit 015f471

File tree

3 files changed

+303
-142
lines changed

3 files changed

+303
-142
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { motion, AnimatePresence } from 'framer-motion';
3+
import { XIcon, InfoIcon } from '@phosphor-icons/react';
4+
import { useSidePanel } from '../context/SidePanelContext';
5+
import GenerativeArt from './GenerativeArt';
6+
7+
const BrutalistSidePanel = () => {
8+
const {
9+
isOpen,
10+
closeSidePanel,
11+
panelTitle,
12+
panelContent,
13+
panelWidth,
14+
setPanelWidth,
15+
} = useSidePanel();
16+
const [isResizing, setIsResizing] = useState(false);
17+
18+
// Close on escape key
19+
useEffect(() => {
20+
const handleKeyDown = (event) => {
21+
if (event.key === 'Escape' && isOpen) {
22+
closeSidePanel();
23+
}
24+
};
25+
window.addEventListener('keydown', handleKeyDown);
26+
return () => window.removeEventListener('keydown', handleKeyDown);
27+
}, [isOpen, closeSidePanel]);
28+
29+
// Handle resizing
30+
useEffect(() => {
31+
const handleMouseMove = (e) => {
32+
if (!isResizing) return;
33+
const newWidth = window.innerWidth - e.clientX;
34+
// Constrain width: min 300px, max 90% of screen
35+
if (newWidth > 300 && newWidth < window.innerWidth * 0.9) {
36+
setPanelWidth(newWidth);
37+
}
38+
};
39+
40+
const handleMouseUp = () => {
41+
setIsResizing(false);
42+
};
43+
44+
if (isResizing) {
45+
window.addEventListener('mousemove', handleMouseMove);
46+
window.addEventListener('mouseup', handleMouseUp);
47+
document.body.style.cursor = 'ew-resize';
48+
document.body.style.userSelect = 'none';
49+
} else {
50+
document.body.style.cursor = 'default';
51+
document.body.style.userSelect = 'auto';
52+
}
53+
54+
return () => {
55+
window.removeEventListener('mousemove', handleMouseMove);
56+
window.removeEventListener('mouseup', handleMouseUp);
57+
document.body.style.cursor = 'default';
58+
document.body.style.userSelect = 'auto';
59+
};
60+
}, [isResizing, setPanelWidth]);
61+
62+
const variants = {
63+
open: { x: 0, opacity: 1 },
64+
closed: { x: '100%', opacity: 1 },
65+
};
66+
67+
return (
68+
<AnimatePresence>
69+
{isOpen && (
70+
<>
71+
{/* Backdrop with blur */}
72+
<motion.div
73+
initial={{ opacity: 0 }}
74+
animate={{ opacity: 1 }}
75+
exit={{ opacity: 0 }}
76+
onClick={closeSidePanel}
77+
className="fixed inset-0 bg-black/80 backdrop-blur-md z-[100]"
78+
/>
79+
80+
<motion.div
81+
initial="closed"
82+
animate="open"
83+
exit="closed"
84+
variants={variants}
85+
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
86+
style={{ width: panelWidth }}
87+
className="fixed top-0 right-0 h-full bg-[#050505] border-l border-white/10 shadow-2xl z-[110] flex flex-col overflow-hidden"
88+
>
89+
{/* Background Art */}
90+
<div className="absolute inset-0 opacity-[0.03] pointer-events-none grayscale">
91+
<GenerativeArt seed={panelTitle} className="w-full h-full" />
92+
</div>
93+
94+
{/* Resize Handle */}
95+
<div
96+
onMouseDown={(e) => {
97+
setIsResizing(true);
98+
e.preventDefault();
99+
}}
100+
className="absolute left-0 top-0 bottom-0 w-1 cursor-ew-resize hover:bg-emerald-500/50 transition-colors z-[120]"
101+
title="DRAG_TO_RESIZE"
102+
/>
103+
104+
{/* Header */}
105+
<div className="relative z-10 flex items-center justify-between p-8 border-b border-white/10 bg-black/40">
106+
<div className="flex flex-col gap-1 min-w-0 pr-4">
107+
<span className="font-mono text-[9px] text-emerald-500 uppercase tracking-[0.3em] font-bold">
108+
System_Panel_Node
109+
</span>
110+
<h2 className="text-2xl font-black font-mono tracking-tighter text-white uppercase italic leading-none pr-2">
111+
{panelTitle}
112+
</h2>
113+
</div>
114+
<button
115+
onClick={closeSidePanel}
116+
className="p-2 text-gray-500 hover:text-white transition-colors flex-shrink-0"
117+
>
118+
<XIcon size={24} weight="bold" />
119+
</button>
120+
</div>
121+
122+
{/* Content Area */}
123+
<div className="relative z-10 flex-1 overflow-y-auto p-8 custom-scrollbar text-white">
124+
<div className="space-y-8">
125+
{panelContent}
126+
</div>
127+
</div>
128+
129+
{/* Panel Footer */}
130+
<div className="relative z-10 p-6 border-t border-white/10 bg-black/40 flex items-center justify-between">
131+
<div className="flex items-center gap-3 text-gray-600 font-mono text-[9px] uppercase tracking-[0.2em]">
132+
<InfoIcon weight="fill" size={14} className="text-emerald-500/50" />
133+
<span>Buffer_Active // {Math.round(panelWidth)}PX</span>
134+
</div>
135+
<div className="flex gap-2">
136+
<div className="w-1.5 h-1.5 bg-emerald-500 animate-pulse rounded-full" />
137+
<div className="w-1.5 h-1.5 bg-white/10 rounded-full" />
138+
</div>
139+
</div>
140+
</motion.div>
141+
</>
142+
)}
143+
</AnimatePresence>
144+
);
145+
};
146+
147+
export default BrutalistSidePanel;

src/components/LuxeSidePanel.jsx

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { motion, AnimatePresence } from 'framer-motion';
3+
import { XIcon, InfoIcon } from '@phosphor-icons/react';
4+
import { useSidePanel } from '../context/SidePanelContext';
5+
import LuxeArt from './LuxeArt';
6+
7+
const LuxeSidePanel = () => {
8+
const {
9+
isOpen,
10+
closeSidePanel,
11+
panelTitle,
12+
panelContent,
13+
panelWidth,
14+
setPanelWidth,
15+
} = useSidePanel();
16+
const [isResizing, setIsResizing] = useState(false);
17+
18+
// Close on escape key
19+
useEffect(() => {
20+
const handleKeyDown = (event) => {
21+
if (event.key === 'Escape' && isOpen) {
22+
closeSidePanel();
23+
}
24+
};
25+
window.addEventListener('keydown', handleKeyDown);
26+
return () => window.removeEventListener('keydown', handleKeyDown);
27+
}, [isOpen, closeSidePanel]);
28+
29+
// Handle resizing
30+
useEffect(() => {
31+
const handleMouseMove = (e) => {
32+
if (!isResizing) return;
33+
const newWidth = window.innerWidth - e.clientX;
34+
if (newWidth > 350 && newWidth < window.innerWidth * 0.85) {
35+
setPanelWidth(newWidth);
36+
}
37+
};
38+
39+
const handleMouseUp = () => {
40+
setIsResizing(false);
41+
};
42+
43+
if (isResizing) {
44+
window.addEventListener('mousemove', handleMouseMove);
45+
window.addEventListener('mouseup', handleMouseUp);
46+
document.body.style.cursor = 'ew-resize';
47+
document.body.style.userSelect = 'none';
48+
} else {
49+
document.body.style.cursor = 'default';
50+
document.body.style.userSelect = 'auto';
51+
}
52+
53+
return () => {
54+
window.removeEventListener('mousemove', handleMouseMove);
55+
window.removeEventListener('mouseup', handleMouseUp);
56+
document.body.style.cursor = 'default';
57+
document.body.style.userSelect = 'auto';
58+
};
59+
}, [isResizing, setPanelWidth]);
60+
61+
const variants = {
62+
open: { x: 0, opacity: 1 },
63+
closed: { x: '100%', opacity: 1 },
64+
};
65+
66+
return (
67+
<AnimatePresence>
68+
{isOpen && (
69+
<>
70+
{/* Backdrop with Luxe blur */}
71+
<motion.div
72+
initial={{ opacity: 0 }}
73+
animate={{ opacity: 1 }}
74+
exit={{ opacity: 0 }}
75+
onClick={closeSidePanel}
76+
className="fixed inset-0 bg-[#F5F5F0]/80 backdrop-blur-sm z-[100]"
77+
/>
78+
79+
<motion.div
80+
initial="closed"
81+
animate="open"
82+
exit="closed"
83+
variants={variants}
84+
transition={{ type: 'spring', stiffness: 200, damping: 35 }}
85+
style={{ width: panelWidth }}
86+
className="fixed top-0 right-0 h-full bg-white border-l border-black/5 shadow-2xl z-[110] flex flex-col overflow-hidden"
87+
>
88+
{/* Background Texture */}
89+
<div className="absolute inset-0 opacity-[0.2] pointer-events-none">
90+
<LuxeArt seed={panelTitle} className="w-full h-full" transparent={true} />
91+
</div>
92+
93+
{/* Resize Handle */}
94+
<div
95+
onMouseDown={(e) => {
96+
setIsResizing(true);
97+
e.preventDefault();
98+
}}
99+
className="absolute left-0 top-0 bottom-0 w-1.5 cursor-ew-resize hover:bg-[#8D4004]/20 transition-all z-[120]"
100+
/>
101+
102+
{/* Header */}
103+
<div className="relative z-10 flex items-center justify-between p-10 border-b border-black/5 bg-[#FAFAF8]">
104+
<div className="flex flex-col gap-1.5 min-w-0 pr-4">
105+
<span className="font-outfit text-[9px] text-[#8D4004] uppercase tracking-[0.4em] font-bold">
106+
Reference Panel
107+
</span>
108+
<h2 className="text-3xl font-playfairDisplay italic text-[#1A1A1A] leading-tight">
109+
{panelTitle}
110+
</h2>
111+
</div>
112+
<button
113+
onClick={closeSidePanel}
114+
className="p-2 text-[#1A1A1A]/20 hover:text-[#8D4004] transition-colors flex-shrink-0"
115+
>
116+
<XIcon size={24} weight="light" />
117+
</button>
118+
</div>
119+
120+
{/* Content Area */}
121+
<div className="relative z-10 flex-1 overflow-y-auto p-10 custom-scrollbar text-[#1A1A1A]/80 font-outfit leading-relaxed text-base">
122+
<div className="space-y-8">
123+
{panelContent}
124+
</div>
125+
</div>
126+
127+
{/* Panel Footer */}
128+
<div className="relative z-10 p-6 border-t border-black/5 bg-[#FAFAF8] flex items-center justify-between">
129+
<div className="flex items-center gap-3 text-black/20 font-outfit text-[9px] uppercase tracking-[0.3em]">
130+
<InfoIcon weight="light" size={14} className="text-[#8D4004]/40" />
131+
<span>Structural Node // {Math.round(panelWidth)}PX</span>
132+
</div>
133+
<div className="flex gap-2">
134+
<div className="w-1.5 h-1.5 bg-[#8D4004]/40 rounded-full animate-pulse" />
135+
<div className="w-1.5 h-1.5 bg-black/5 rounded-full" />
136+
</div>
137+
</div>
138+
</motion.div>
139+
</>
140+
)}
141+
</AnimatePresence>
142+
);
143+
};
144+
145+
export default LuxeSidePanel;

0 commit comments

Comments
 (0)