Skip to content

Commit 8f33fd7

Browse files
committed
polish ui controls and lock layout against overflow
restyled window controls with clearer spacing and themed hover states prevented layout shifts from long paths/log lines with min-width and overflow fixes improved terminal wrapping/scrolling to keep UI stable
1 parent 89a8018 commit 8f33fd7

8 files changed

Lines changed: 382 additions & 165 deletions

File tree

electron/main.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,16 @@ function createWindow() {
4444
minHeight: 680,
4545
backgroundColor: '#0b1020',
4646
title: 'RoboCopy Pro',
47+
frame: false,
48+
titleBarStyle: 'hidden',
4749
webPreferences: {
4850
contextIsolation: true,
4951
preload: path.join(__dirname, 'preload.js'),
5052
},
5153
});
5254

55+
mainWindow.setMenuBarVisibility(false);
56+
5357
if (isDev) {
5458
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
5559
mainWindow.webContents.openDevTools({ mode: 'detach' });
@@ -90,6 +94,26 @@ ipcMain.on('robocopy:ui-progress', (event, value) => {
9094
window.setProgressBar(value);
9195
});
9296

97+
ipcMain.on('window:minimize', (event) => {
98+
const window = BrowserWindow.fromWebContents(event.sender);
99+
if (window) window.minimize();
100+
});
101+
102+
ipcMain.on('window:maximize', (event) => {
103+
const window = BrowserWindow.fromWebContents(event.sender);
104+
if (!window) return;
105+
if (window.isMaximized()) {
106+
window.unmaximize();
107+
} else {
108+
window.maximize();
109+
}
110+
});
111+
112+
ipcMain.on('window:close', (event) => {
113+
const window = BrowserWindow.fromWebContents(event.sender);
114+
if (window) window.close();
115+
});
116+
93117
ipcMain.handle('robocopy:run', async (event, payload) => {
94118
const { args, command } = payload;
95119
const window = BrowserWindow.fromWebContents(event.sender);

electron/preload.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ contextBridge.exposeInMainWorld('electron', {
44
selectFolder: () => ipcRenderer.invoke('robocopy:select-folder'),
55
});
66

7+
contextBridge.exposeInMainWorld('windowControls', {
8+
minimize: () => ipcRenderer.send('window:minimize'),
9+
maximize: () => ipcRenderer.send('window:maximize'),
10+
close: () => ipcRenderer.send('window:close'),
11+
});
12+
713
contextBridge.exposeInMainWorld('robocopy', {
814
run: ({ args, command, onProgress }) => {
915
const logListener = (_, line) => {

src/App.jsx

Lines changed: 190 additions & 115 deletions
Large diffs are not rendered by default.

src/components/Button.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
'inline-flex items-center justify-center gap-2 rounded-xl px-5 py-3 text-sm font-semibold transition focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/70 disabled:cursor-not-allowed';
44
const variants = {
55
primary:
6-
'accent-gradient text-white shadow-card hover:shadow-card-hover hover:-translate-y-0.5 disabled:opacity-60',
6+
'accent-gradient text-white shadow-card hover:shadow-card-hover hover:-translate-y-0.5 disabled:opacity-60 relative before:absolute before:inset-0 before:rounded-xl before:border before:border-white/20 before:opacity-60 before:pointer-events-none',
77
ghost:
88
'border border-white/10 bg-white/5 text-white/80 hover:border-cyan-400/60 hover:text-white',
99
};

src/components/ConsoleOutput.jsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
export default function ConsoleOutput({ lines }) {
1+
export default function ConsoleOutput({ lines, highlightedLines }) {
22
return (
3-
<div className="crystal-shell h-52 overflow-hidden">
3+
<div className="terminal-surface h-64 min-w-0 overflow-hidden rounded-2xl">
44
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2">
55
<span className="text-xs font-semibold text-white/60">Robocopy Output</span>
66
<span className="glass-pill">Live</span>
77
</div>
8-
<div className="h-full overflow-y-auto px-4 py-3 font-mono text-xs text-emerald-200/90">
8+
<div className="terminal-text h-[calc(100%-40px)] overflow-y-auto overflow-x-auto px-4 py-3 text-xs">
99
{lines.length === 0 ? (
1010
<p className="text-white/50">Waiting for run...</p>
1111
) : (
12-
lines.map((line, index) => (
13-
<p key={`${line}-${index}`} className="leading-relaxed">
14-
{line}
12+
(highlightedLines || lines).map((line, index) => (
13+
<p
14+
key={`${line.text || line}-${index}`}
15+
className={`leading-relaxed whitespace-pre ${line.className || ''}`}
16+
>
17+
{line.text || line}
1518
</p>
1619
))
1720
)}

src/components/DropZone.jsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { useState } from 'react';
2-
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
2+
import { ArrowDownTrayIcon, FolderOpenIcon, InboxArrowDownIcon } from '@heroicons/react/24/outline';
33

4-
export default function DropZone({ label, description, value, onDropPath, onBrowse }) {
4+
const ICONS = {
5+
source: FolderOpenIcon,
6+
destination: InboxArrowDownIcon,
7+
};
8+
9+
export default function DropZone({ label, description, value, onDropPath, onBrowse, kind }) {
510
const [active, setActive] = useState(false);
11+
const Icon = kind ? ICONS[kind] : null;
12+
const displayText = active ? `Drop to set as ${label}` : value || 'Drop a folder here';
613

714
return (
815
<div
9-
className={`crystal-item relative flex h-full min-h-[150px] flex-col justify-between gap-3 p-4 transition ${
10-
active ? 'border-cyan-400/60 shadow-card-hover' : ''
16+
className={`crystal-item squircle relative flex min-h-[240px] min-w-0 flex-col justify-between gap-4 p-6 text-center transition ${
17+
active ? 'drop-magnet' : ''
1118
}`}
1219
onDragEnter={(event) => {
1320
event.preventDefault();
@@ -26,14 +33,21 @@ export default function DropZone({ label, description, value, onDropPath, onBrow
2633
const path = file.path || file.name;
2734
if (path) onDropPath(path);
2835
}}
36+
tabIndex={0}
2937
>
30-
<div className="space-y-2">
31-
<p className="text-sm font-semibold text-white">{label}</p>
38+
<div className="space-y-3">
39+
<div className="flex items-center justify-center gap-2 text-sm font-semibold text-white">
40+
{Icon ? <Icon className="h-5 w-5 text-cyan-200" /> : null}
41+
<span>{label}</span>
42+
</div>
3243
<p className="text-xs text-white/60">{description}</p>
3344
</div>
34-
<div className="space-y-3">
35-
<div className="rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs text-white/70 break-all">
36-
{value || 'Drop a folder here'}
45+
<div className="min-w-0 space-y-4">
46+
{Icon ? <Icon className="mx-auto h-12 w-12 text-cyan-200/80" /> : null}
47+
<div className="min-w-0 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs text-white/70">
48+
<p className="truncate" title={displayText}>
49+
{displayText}
50+
</p>
3751
</div>
3852
<button type="button" className="glass-btn w-full" onClick={onBrowse}>
3953
<ArrowDownTrayIcon className="h-4 w-4" />

src/components/Panel.jsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
export default function Panel({ title, description, children, actions }) {
22
return (
3-
<section className="crystal-shell space-y-4 p-5 sm:p-6">
4-
<div className="flex flex-wrap items-start justify-between gap-3">
5-
<div>
3+
<section className="crystal-shell squircle shadow-float min-w-0 space-y-5 p-6">
4+
<div className="flex min-w-0 flex-wrap items-start justify-between gap-3">
5+
<div className="min-w-0">
66
<h2 className="text-base font-semibold text-white">{title}</h2>
77
{description ? <p className="text-sm text-white/60">{description}</p> : null}
88
</div>
9-
{actions ? <div className="flex items-center gap-2">{actions}</div> : null}
9+
{actions ? <div className="flex min-w-0 items-center gap-2">{actions}</div> : null}
1010
</div>
11-
<div>{children}</div>
11+
<div className="min-w-0">{children}</div>
1212
</section>
1313
);
1414
}

src/index.css

Lines changed: 124 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Sora:wght@500;600;700;800&display=swap');
1+
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Sora:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
22

33
@tailwind base;
44
@tailwind components;
@@ -90,34 +90,45 @@
9090
box-shadow: var(--shadow-card-hover);
9191
}
9292

93+
.shadow-float {
94+
box-shadow:
95+
0 4px 12px -8px hsl(0 0% 0% / 0.45),
96+
0 18px 36px -20px hsl(232 95% 66% / 0.25),
97+
0 36px 60px -30px hsl(196 89% 62% / 0.2);
98+
}
99+
100+
.squircle {
101+
border-radius: 28px;
102+
}
103+
93104
.glass-crystal {
94105
background:
95-
linear-gradient(135deg, hsl(var(--card) / 0.7), hsl(var(--card) / 0.45)),
106+
linear-gradient(135deg, hsl(var(--card) / 0.55), hsl(var(--card) / 0.35)),
96107
radial-gradient(120% 140% at 0% 0%, hsl(var(--primary) / 0.2), transparent 45%),
97108
radial-gradient(120% 140% at 100% 100%, hsl(var(--accent) / 0.18), transparent 50%);
98-
backdrop-filter: blur(20px) saturate(135%);
99-
-webkit-backdrop-filter: blur(20px) saturate(135%);
100-
border: 1px solid hsl(var(--border) / 0.7);
109+
backdrop-filter: blur(22px) saturate(140%);
110+
-webkit-backdrop-filter: blur(22px) saturate(140%);
111+
border: 1px solid hsl(var(--border) / 0.65);
101112
box-shadow:
102113
inset 0 1px 0 hsl(0 0% 100% / 0.12),
103114
0 18px 40px -24px hsl(var(--primary) / 0.45);
104115
}
105116

106117
.crystal-shell {
107118
background:
108-
linear-gradient(145deg, hsl(var(--card) / 0.82), hsl(var(--card) / 0.6)),
119+
linear-gradient(145deg, hsl(var(--card) / 0.7), hsl(var(--card) / 0.45)),
109120
radial-gradient(160% 120% at 0% 0%, hsl(var(--primary) / 0.16), transparent 48%),
110121
radial-gradient(160% 120% at 100% 100%, hsl(var(--accent) / 0.12), transparent 54%);
111122
backdrop-filter: blur(22px) saturate(140%);
112123
-webkit-backdrop-filter: blur(22px) saturate(140%);
113-
border: 1px solid hsl(var(--border) / 0.72);
124+
border: 1px solid hsl(var(--border) / 0.7);
114125
box-shadow:
115126
inset 0 1px 0 hsl(0 0% 100% / 0.1),
116127
0 22px 46px -26px hsl(var(--primary) / 0.4);
117128
}
118129

119130
.crystal-item {
120-
@apply rounded-2xl border border-white/10 bg-white/5 backdrop-blur-xl transition-all duration-300;
131+
@apply border border-white/10 bg-white/5 backdrop-blur-xl transition-all duration-200;
121132
box-shadow: inset 0 1px 0 hsl(0 0% 100% / 0.08);
122133
}
123134

@@ -126,22 +137,29 @@
126137
box-shadow:
127138
inset 0 1px 0 hsl(0 0% 100% / 0.14),
128139
0 18px 44px -26px hsl(var(--primary) / 0.42);
140+
transform: translateY(-2px);
129141
}
130142

131143
.glass-input {
132-
@apply w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/90 outline-none transition;
144+
@apply w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/90 outline-none transition duration-150;
133145
}
134146

135147
.glass-input:focus {
136148
@apply border-cyan-400/60 ring-1 ring-cyan-400/30;
149+
box-shadow: 0 0 0 3px hsl(var(--accent) / 0.18);
137150
}
138151

139152
.glass-btn {
140-
@apply inline-flex items-center justify-center gap-2 rounded-xl border border-white/10 bg-white/10 px-4 py-3 text-sm font-semibold text-white/80 transition;
153+
@apply inline-flex items-center justify-center gap-2 rounded-xl border border-white/10 bg-white/10 px-4 py-3 text-sm font-semibold text-white/80 transition duration-150;
141154
}
142155

143156
.glass-btn:hover {
144157
@apply border-cyan-400/60 text-white;
158+
transform: translateY(-1px);
159+
}
160+
161+
.glass-btn:active {
162+
transform: translateY(0);
145163
}
146164

147165
.glass-chip {
@@ -152,36 +170,113 @@
152170
@apply inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/10 px-3 py-1 text-xs font-semibold text-white/70;
153171
}
154172

173+
.window-controls {
174+
@apply rounded-2xl border border-white/10 bg-white/10 p-2;
175+
box-shadow: inset 0 1px 0 hsl(0 0% 100% / 0.08), 0 10px 24px -18px hsl(0 0% 0% / 0.5);
176+
}
177+
178+
.window-btn {
179+
@apply flex h-9 w-9 items-center justify-center rounded-xl border border-white/15 bg-white/10 text-white/70 transition duration-150;
180+
}
181+
182+
.window-btn:hover {
183+
@apply border-cyan-400/60 text-white;
184+
box-shadow: 0 6px 18px -12px hsl(var(--accent) / 0.6);
185+
}
186+
187+
.window-btn:active {
188+
transform: translateY(0);
189+
}
190+
191+
.window-btn-close:hover {
192+
@apply border-rose-400/70 text-rose-200;
193+
box-shadow: 0 6px 18px -12px hsl(0 84% 60% / 0.5);
194+
}
195+
155196
.accent-gradient {
156197
background: var(--gradient-primary);
157198
}
158199

159-
.fade-in {
160-
animation: fadeIn 0.6s ease both;
200+
.focus-glow:focus-visible {
201+
box-shadow: 0 0 0 3px hsl(var(--accent) / 0.25);
161202
}
162203

163-
.float-slow {
164-
animation: floatSlow 6s ease-in-out infinite;
204+
.drop-magnet {
205+
border-style: dashed;
206+
border-color: hsl(var(--accent) / 0.6);
207+
box-shadow: 0 0 30px hsl(var(--accent) / 0.25);
208+
transform: scale(1.02);
165209
}
166210

167-
@keyframes fadeIn {
168-
from {
169-
opacity: 0;
170-
transform: translateY(8px);
171-
}
172-
to {
173-
opacity: 1;
174-
transform: translateY(0);
175-
}
211+
.gradient-ring {
212+
position: relative;
213+
isolation: isolate;
176214
}
177215

178-
@keyframes floatSlow {
179-
0%,
180-
100% {
181-
transform: translateY(0px);
216+
.gradient-ring::before {
217+
content: '';
218+
position: absolute;
219+
inset: -1px;
220+
border-radius: inherit;
221+
padding: 1px;
222+
background: linear-gradient(120deg, rgba(56, 189, 248, 0.7), rgba(139, 92, 246, 0.7), rgba(56, 189, 248, 0.7));
223+
background-size: 200% 200%;
224+
animation: ringFlow 6s linear infinite;
225+
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
226+
-webkit-mask-composite: xor;
227+
mask-composite: exclude;
228+
z-index: -1;
229+
}
230+
231+
@keyframes ringFlow {
232+
0% {
233+
background-position: 0% 50%;
182234
}
183-
50% {
184-
transform: translateY(-10px);
235+
100% {
236+
background-position: 200% 50%;
185237
}
186238
}
239+
240+
.terminal-surface {
241+
position: relative;
242+
background: linear-gradient(180deg, rgba(7, 12, 24, 0.9), rgba(4, 8, 18, 0.95));
243+
border: 1px solid rgba(56, 189, 248, 0.2);
244+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03), 0 18px 40px -30px rgba(56, 189, 248, 0.5);
245+
}
246+
247+
.terminal-surface::after {
248+
content: '';
249+
position: absolute;
250+
inset: 0;
251+
background: repeating-linear-gradient(
252+
0deg,
253+
rgba(255, 255, 255, 0.03) 0px,
254+
rgba(255, 255, 255, 0.03) 1px,
255+
transparent 1px,
256+
transparent 3px
257+
);
258+
opacity: 0.2;
259+
pointer-events: none;
260+
}
261+
262+
.terminal-text {
263+
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
264+
font-variant-ligatures: contextual;
265+
}
266+
267+
.log-success {
268+
color: #7ee787;
269+
}
270+
271+
.log-skip {
272+
color: rgba(255, 255, 255, 0.45);
273+
}
274+
275+
.log-error {
276+
color: #ff7b72;
277+
}
278+
279+
.log-folder {
280+
color: #79c0ff;
281+
}
187282
}

0 commit comments

Comments
 (0)