Skip to content

Commit e4e0cee

Browse files
authored
Merge pull request #17 from dailker/main
chore(repo): rename master to main & feat(ui): dark mode and top bar improvements
2 parents 4391cfd + 01520a0 commit e4e0cee

11 files changed

Lines changed: 1152 additions & 128 deletions

src/components/Desktop.js

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { useApp } from '@/context/AppContext';
55
import { useTheme } from '@/context/ThemeContext';
66
import AppIcon from './AppIcon';
77
import Window from './Window';
8+
import EnhancedWindow from './EnhancedWindow';
89
import Taskbar from './Taskbar';
10+
import TopBarService from '@/system/services/TopBarService';
11+
import TopBarDropdownService from '@/system/services/TopBarDropdownService';
12+
import TopBarInfoAboutService from '@/system/services/TopBarInfoAboutService';
13+
import WindowTopBarKeyShortcutRegistrationService from '@/system/services/WindowTopBarKeyShortcutRegistrationService';
914

1015
// --- (CHANGED) ---
1116
// We no longer import the app components here directly for the list.
@@ -66,11 +71,94 @@ export default function Desktop() {
6671
}));
6772
};
6873

69-
// --- (UNCHANGED) ---
70-
// This function works perfectly as is.
71-
const renderAppContent = (app) => {
74+
// Create services for enhanced apps
75+
const createAppServices = (app) => {
76+
if (app.id === 'notes') {
77+
const topBarService = new TopBarService(app.id, app.name, '/icons/notes.png');
78+
const dropdownService = new TopBarDropdownService();
79+
const keyShortcutService = new WindowTopBarKeyShortcutRegistrationService();
80+
81+
// Pre-populate dropdowns to avoid delay
82+
const menuHandlers = {
83+
onNew: () => {},
84+
onOpen: () => {},
85+
onOpenLocal: () => {},
86+
onSave: () => {},
87+
onSaveAs: () => {},
88+
onPrint: () => window.print(),
89+
onUndo: () => document.execCommand('undo'),
90+
onRedo: () => document.execCommand('redo'),
91+
onCut: () => document.execCommand('cut'),
92+
onCopy: () => document.execCommand('copy'),
93+
onPaste: () => document.execCommand('paste'),
94+
onFind: () => {},
95+
onReplace: () => {},
96+
onFindInFiles: () => {},
97+
onFindNext: () => {},
98+
onFindPrevious: () => {},
99+
onSelectFindNext: () => {},
100+
onSelectFindPrevious: () => {},
101+
onFindVolatileNext: () => {},
102+
onFindVolatilePrevious: () => {},
103+
onIncrementalSearch: () => {},
104+
onSearchResults: () => {},
105+
onNextSearchResult: () => {},
106+
onPreviousSearchResult: () => {},
107+
onGoTo: () => {},
108+
onGoToMatchingBrace: () => {},
109+
onSelectBetween: () => {},
110+
onStyleAllOccurrences: () => {},
111+
onStyleOneToken: () => {},
112+
onClearStyle: () => {},
113+
onJumpUp: () => {},
114+
onJumpDown: () => {},
115+
onCopyStyledText: () => {},
116+
onFindInRange: () => {},
117+
hasChanges: false,
118+
hasSelection: false
119+
};
120+
121+
const fileDropdown = dropdownService.createFileDropdown(menuHandlers);
122+
const editDropdown = dropdownService.createEditDropdown(menuHandlers);
123+
const searchDropdown = dropdownService.createSearchDropdown(menuHandlers);
124+
dropdownService.addDropdown(fileDropdown.label, fileDropdown.items);
125+
dropdownService.addDropdown(editDropdown.label, editDropdown.items);
126+
dropdownService.addDropdown(searchDropdown.label, searchDropdown.items);
127+
128+
const infoService = new TopBarInfoAboutService({
129+
onShowAbout: () => {}
130+
});
131+
132+
return { topBarService, dropdownService, infoService, keyShortcutService };
133+
}
134+
return null;
135+
};
136+
137+
const renderWindow = (app) => {
138+
const services = createAppServices(app);
72139
const AppComponent = appComponents[app.component];
73-
return AppComponent ? <AppComponent /> : <div>App not found</div>;
140+
141+
if (!AppComponent) return <div key={app.id}>App not found</div>;
142+
143+
if (services) {
144+
return (
145+
<EnhancedWindow
146+
key={app.id}
147+
app={app}
148+
topBarService={services.topBarService}
149+
dropdownService={services.dropdownService}
150+
infoService={services.infoService}
151+
keyShortcutService={services.keyShortcutService}
152+
>
153+
<AppComponent {...services} />
154+
</EnhancedWindow>
155+
);
156+
}
157+
return (
158+
<Window key={app.id} app={app}>
159+
<AppComponent />
160+
</Window>
161+
);
74162
};
75163

76164
return (
@@ -99,13 +187,8 @@ export default function Desktop() {
99187
/>
100188
))}
101189

102-
{/* --- (UNCHANGED) --- */}
103-
{/* This logic is based on the global context and doesn't need to change. */}
104-
{state.openApps.map((app) => (
105-
<Window key={app.id} app={app}>
106-
{renderAppContent(app)}
107-
</Window>
108-
))}
190+
{/* Render windows with appropriate services */}
191+
{state.openApps.map((app) => renderWindow(app))}
109192

110193
<Taskbar />
111194
</div>

src/components/EnhancedWindow.js

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
// src/components/EnhancedWindow.js
2+
3+
import React, { useState, useRef, useEffect } from 'react';
4+
import { useApp } from '@/context/AppContext';
5+
import { useTheme } from '@/context/ThemeContext';
6+
import { motion, AnimatePresence } from 'framer-motion';
7+
import { InformationCircleIcon } from '@heroicons/react/24/outline';
8+
9+
export default function EnhancedWindow({ app, children, topBarService, dropdownService, infoService, keyShortcutService }) {
10+
const { dispatch } = useApp();
11+
const { theme } = useTheme();
12+
const [position, setPosition] = useState({
13+
x: 100 + Math.random() * 200,
14+
y: 100 + Math.random() * 100,
15+
});
16+
const [isDragging, setIsDragging] = useState(false);
17+
const [activeDropdown, setActiveDropdown] = useState(null);
18+
const [markdownActions, setMarkdownActions] = useState(null);
19+
const dragRef = useRef({ startX: 0, startY: 0 });
20+
const windowRef = useRef(null);
21+
22+
const handleMouseDown = (e) => {
23+
setIsDragging(true);
24+
dragRef.current = {
25+
startX: e.clientX - position.x,
26+
startY: e.clientY - position.y,
27+
};
28+
dispatch({ type: 'SET_ACTIVE_APP', payload: app.id });
29+
if (windowRef.current) {
30+
windowRef.current.focus();
31+
}
32+
};
33+
34+
const handleMouseMove = (e) => {
35+
if (!isDragging) return;
36+
setPosition({
37+
x: e.clientX - dragRef.current.startX,
38+
y: e.clientY - dragRef.current.startY,
39+
});
40+
};
41+
42+
const handleMouseUp = () => {
43+
setIsDragging(false);
44+
};
45+
46+
const handleClose = () => {
47+
dispatch({ type: 'CLOSE_APP', payload: app.id });
48+
};
49+
50+
const handleMinimize = () => {
51+
dispatch({ type: 'MINIMIZE_APP', payload: app.id });
52+
};
53+
54+
const toggleDropdown = (dropdownLabel) => {
55+
setActiveDropdown(activeDropdown === dropdownLabel ? null : dropdownLabel);
56+
};
57+
58+
const handleMenuItemClick = (action) => {
59+
setActiveDropdown(null);
60+
if (action) action();
61+
};
62+
63+
// Close dropdown when clicking outside
64+
useEffect(() => {
65+
const handleClickOutside = () => {
66+
setActiveDropdown(null);
67+
};
68+
69+
if (activeDropdown) {
70+
document.addEventListener('click', handleClickOutside);
71+
return () => document.removeEventListener('click', handleClickOutside);
72+
}
73+
}, [activeDropdown]);
74+
75+
// Set window element for shortcut service
76+
useEffect(() => {
77+
if (keyShortcutService && windowRef.current) {
78+
keyShortcutService.windowElement = windowRef.current;
79+
}
80+
}, [keyShortcutService]);
81+
82+
return (
83+
<AnimatePresence>
84+
{!app.minimized && (
85+
<motion.div
86+
ref={windowRef}
87+
initial={{ scale: 0.8, opacity: 0, y: 50 }}
88+
animate={{ scale: 1, opacity: 1, y: 0 }}
89+
exit={{ scale: 0.8, opacity: 0, y: 50 }}
90+
transition={{ duration: 0.2, ease: 'easeOut' }}
91+
className={`absolute ${theme.window.bg} ${theme.window.border} border rounded-lg shadow-lg overflow-hidden`}
92+
style={{
93+
left: position.x,
94+
top: position.y,
95+
width: 700,
96+
height: 500,
97+
zIndex: app.zIndex,
98+
}}
99+
onMouseMove={handleMouseMove}
100+
onMouseUp={handleMouseUp}
101+
onMouseLeave={handleMouseUp}
102+
tabIndex={-1}
103+
>
104+
{/* Top Window Bar */}
105+
<div
106+
className={`${theme.window.header} px-3 py-2 flex justify-between items-center window-drag border-b`}
107+
style={{ height: '32px' }}
108+
onMouseDown={handleMouseDown}
109+
>
110+
<div className="flex items-center gap-2">
111+
{topBarService?.icon && (
112+
<img src={topBarService.icon} alt="" className="w-4 h-4" />
113+
)}
114+
<span className={`font-medium text-sm ${theme.window.text}`}>
115+
{topBarService?.getDisplayTitle() || app.name}
116+
</span>
117+
</div>
118+
<div className="flex items-center gap-1">
119+
<button
120+
onClick={handleMinimize}
121+
className="w-3 h-3 bg-orange-500 rounded-full hover:bg-orange-600"
122+
title="Minimize"
123+
/>
124+
<button
125+
className="w-3 h-3 bg-blue-500 rounded-full hover:bg-blue-600"
126+
title="Maximize"
127+
/>
128+
<button
129+
onClick={handleClose}
130+
className="w-3 h-3 bg-red-500 rounded-full hover:bg-red-600"
131+
title="Close"
132+
/>
133+
</div>
134+
</div>
135+
136+
{/* Menu Bar */}
137+
{dropdownService && (
138+
<div className={`${theme.window.header} px-3 py-1 flex items-center gap-4 border-b text-sm relative`}>
139+
{dropdownService.getDropdowns().map((dropdown) => (
140+
<div key={dropdown.label} className="relative">
141+
<button
142+
onMouseDown={(e) => e.preventDefault()}
143+
onClick={(e) => {
144+
e.stopPropagation();
145+
e.preventDefault();
146+
toggleDropdown(dropdown.label);
147+
}}
148+
className={`px-2 py-1 rounded hover:bg-gray-100 ${
149+
activeDropdown === dropdown.label ? 'bg-gray-100' : ''
150+
}`}
151+
>
152+
{dropdown.label}
153+
</button>
154+
155+
{activeDropdown === dropdown.label && (
156+
<div className="absolute top-full left-0 mt-1 bg-white border rounded shadow-lg min-w-48 z-50">
157+
{dropdown.items.map((item, index) => (
158+
item.type === 'separator' ? (
159+
<hr key={index} className="my-1" />
160+
) : (
161+
<button
162+
key={index}
163+
onMouseDown={(e) => e.preventDefault()}
164+
onClick={(e) => {
165+
e.stopPropagation();
166+
e.preventDefault();
167+
handleMenuItemClick(item.action);
168+
}}
169+
disabled={item.disabled}
170+
className={`w-full text-left px-3 py-2 hover:bg-gray-100 flex justify-between items-center ${
171+
item.disabled ? 'text-gray-400 cursor-not-allowed' : ''
172+
}`}
173+
>
174+
<span>{item.label}</span>
175+
{item.shortcut && (
176+
<span className="text-xs text-gray-500 ml-4">{item.shortcut}</span>
177+
)}
178+
</button>
179+
)
180+
))}
181+
</div>
182+
)}
183+
</div>
184+
))}
185+
186+
{/* Markdown Formatting Buttons */}
187+
{app.id === 'notes' && markdownActions && (
188+
<div className="flex items-center gap-1 ml-4 border-l pl-4">
189+
<button
190+
onMouseDown={(e) => e.preventDefault()}
191+
onClick={markdownActions.handleBold}
192+
className="p-1 hover:bg-gray-100 rounded font-bold text-xs"
193+
title="Bold"
194+
>
195+
B
196+
</button>
197+
<button
198+
onMouseDown={(e) => e.preventDefault()}
199+
onClick={markdownActions.handleItalic}
200+
className="p-1 hover:bg-gray-100 rounded italic text-xs"
201+
title="Italic"
202+
>
203+
I
204+
</button>
205+
<button
206+
onMouseDown={(e) => e.preventDefault()}
207+
onClick={markdownActions.handleStrikethrough}
208+
className="p-1 hover:bg-gray-100 rounded line-through text-xs"
209+
title="Strikethrough"
210+
>
211+
S
212+
</button>
213+
<button
214+
onMouseDown={(e) => e.preventDefault()}
215+
onClick={markdownActions.handleCode}
216+
className="p-1 hover:bg-gray-100 rounded font-mono bg-gray-50 text-xs"
217+
title="Code"
218+
>
219+
&lt;/&gt;
220+
</button>
221+
<button
222+
onMouseDown={(e) => e.preventDefault()}
223+
onClick={markdownActions.handleHeader}
224+
className="p-1 hover:bg-gray-100 rounded font-bold text-xs"
225+
title="Header"
226+
>
227+
H
228+
</button>
229+
</div>
230+
)}
231+
232+
{infoService && (
233+
<div className="ml-auto">
234+
<button
235+
onMouseDown={(e) => e.preventDefault()}
236+
onClick={(e) => {
237+
e.preventDefault();
238+
infoService.getInfoIcon().onClick();
239+
}}
240+
className="p-1 hover:bg-gray-100 rounded"
241+
title="About"
242+
>
243+
<InformationCircleIcon className="w-4 h-4" />
244+
</button>
245+
</div>
246+
)}
247+
</div>
248+
)}
249+
250+
{/* Content Area */}
251+
<div className="flex-1 overflow-hidden" style={{ height: 'calc(100% - 64px)' }}>
252+
{React.cloneElement(children, { setMarkdownActions })}
253+
</div>
254+
</motion.div>
255+
)}
256+
</AnimatePresence>
257+
);
258+
}

0 commit comments

Comments
 (0)