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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "java-runner-client",
"version": "2.1.3",
"version": "2.1.4",
"description": "Run and manage Java processes with profiles, console I/O, and system tray support",
"main": "dist/main/main.js",
"scripts": {
Expand Down
32 changes: 16 additions & 16 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import { HashRouter, Navigate, Route, Routes } from 'react-router-dom';
import { TitleBar } from './components/common/TitleBar';
import { DevModeGate } from './components/developer/DevModeGate';
import { MainLayout } from './components/MainLayout';
import { AppProvider } from './store/AppStore';
import { TitleBar } from './components/layout/TitleBar';
import { MainLayout } from './components/MainLayout';
import { DevModeGate } from './components/developer/DevModeGate';

function JavaRunnerFallback() {
function Fallback() {
return (
<div className="flex flex-col items-center justify-center h-screen bg-gray-900 text-gray-100 p-8 text-center select-none">
<div className="mb-6 text-6xl">!</div>
Expand All @@ -24,22 +25,21 @@ function JavaRunnerFallback() {
}

export default function App() {
if (!window.api) return <JavaRunnerFallback />;
if (!window.api) return <Fallback />;

return (
<AppProvider>
<HashRouter
future={{
v7_startTransition: false,
v7_relativeSplatPath: false,
}}
>
<div className="flex flex-col h-screen bg-base-900 text-text-primary min-h-0 select-none">
<HashRouter future={{ v7_startTransition: false, v7_relativeSplatPath: false }}>
{/* Root: full viewport, flex column, no overflow */}
<div className="flex flex-col h-screen bg-base-900 text-text-primary select-none overflow-hidden">
<TitleBar />
<Routes>
<Route path="/" element={<Navigate to="/console" replace />} />
<Route path="/*" element={<MainLayout />} />
</Routes>
{/* Content area: takes remaining height, no overflow — children manage their own */}
<div className="flex flex-1 min-h-0 overflow-hidden">
<Routes>
<Route path="/" element={<Navigate to="/console" replace />} />
<Route path="/*" element={<MainLayout />} />
</Routes>
</div>
</div>
<DevModeGate />
</HashRouter>
Expand Down
100 changes: 32 additions & 68 deletions src/renderer/components/MainLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
import { ProfileSidebar } from './profiles/ProfileSidebar';
import { ConsoleTab } from './console/ConsoleTab';
Expand All @@ -8,34 +8,36 @@ import { SettingsTab } from './settings/SettingsTab';
import { UtilitiesTab } from './utils/UtilitiesTab';
import { FaqPanel } from './faq/FaqPanel';
import { DeveloperTab } from './developer/DeveloperTab';
import { PanelHeader } from './layout/PanelHeader';
import { useApp } from '../store/AppStore';
import { useDevMode } from '../hooks/useDevMode';
import { VscTerminal, VscAccount } from 'react-icons/vsc';
import { LuList } from 'react-icons/lu';
import { JRCEnvironment } from 'src/main/shared/types/App.types';

const MAIN_TABS = [
{ path: 'console', label: 'Console', Icon: VscTerminal },
{ path: 'config', label: 'Configure', Icon: LuList },
{ path: 'profile', label: 'Profile', Icon: VscAccount },
] as const;

// Panels rendered in the side-panel view (replace main tabs area)
const SIDE_PANELS = ['settings', 'faq', 'utilities', 'developer'] as const;
type SidePanel = (typeof SIDE_PANELS)[number];

function isSidePanel(seg: string): seg is SidePanel {
return (SIDE_PANELS as readonly string[]).includes(seg);
}

const PANEL_LABELS: Record<SidePanel, string> = {
settings: 'Application Settings',
faq: 'FAQ',
utilities: 'Utilities',
developer: 'Developer',
};

function isSidePanel(seg: string): seg is SidePanel {
return (SIDE_PANELS as readonly string[]).includes(seg);
}

// Profile-specific tabs shown in the tab bar
const PROFILE_TABS = [
{ path: 'console', label: 'Console', Icon: VscTerminal },
{ path: 'config', label: 'Configure', Icon: LuList },
{ path: 'profile', label: 'Profile', Icon: VscAccount },
] as const;

export function MainLayout() {
const { state, activeProfile, isRunning, setActiveProfile } = useApp();
const { state, activeProfile, isRunning } = useApp();
const devMode = useDevMode();
const navigate = useNavigate();
const location = useLocation();
Expand All @@ -48,89 +50,51 @@ export function MainLayout() {
const color = activeProfile?.color ?? '#4ade80';
const running = activeProfile ? isRunning(activeProfile.id) : false;

// Redirect away from developer panel if dev mode is turned off
// Redirect away from developer panel when dev mode is disabled
useEffect(() => {
if (!devMode && activePanel === 'developer') {
navigate('console', { replace: true });
navigate('/console', { replace: true });
}
}, [devMode, activePanel, navigate]);

// When profile changes, go to console
// Navigate to console when active profile changes
const prevIdRef = React.useRef(state.activeProfileId);
useEffect(() => {
if (state.activeProfileId !== prevIdRef.current) {
prevIdRef.current = state.activeProfileId;
if (!activePanel) navigate('console', { replace: true });
if (!activePanel) navigate('/console', { replace: true });
}
}, [state.activeProfileId, activePanel, navigate]);

const openPanel = (panel: SidePanel) => {
navigate(activePanel === panel ? 'console' : panel);
};

const handleProfileClick = () => {
if (activePanel) navigate('console');
};

return (
<div className="flex flex-1 min-h-0">
<ProfileSidebar
onOpenSettings={() => openPanel('settings')}
onOpenFaq={() => openPanel('faq')}
onOpenUtilities={() => openPanel('utilities')}
onOpenDeveloper={() => openPanel('developer')}
onProfileClick={handleProfileClick}
activeSidePanel={activePanel}
/>
<div className="flex flex-1 min-h-0 overflow-hidden">
<ProfileSidebar activeSidePanel={activePanel} />

<div className="flex flex-col flex-1 min-h-0">
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
{activePanel ? (
// Side panel view
<>
<div className="shrink-0">
<div className="flex items-center gap-3 px-4 h-10 bg-base-900">
<button
onClick={() => navigate('console')}
className="flex items-center gap-1.5 text-xs text-text-muted hover:text-text-primary transition-colors"
>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
>
<path d="M8 2L4 6l4 4" />
</svg>
Back
</button>
<div className="w-px h-4 bg-surface-border" />
<span className="text-xs font-medium text-text-secondary capitalize">
{PANEL_LABELS[activePanel]}
</span>
</div>
<div className="border-b border-surface-border" />
</div>

<div className="flex-1 min-h-0 bg-base-800 animate-fade-in">
<PanelHeader title={PANEL_LABELS[activePanel]} />
<div className="flex-1 min-h-0 overflow-hidden animate-fade-in">
<Routes>
<Route path="settings" element={<SettingsTab />} />
<Route path="faq" element={<FaqPanel />} />
<Route path="utilities" element={<UtilitiesTab />} />
<Route path="developer" element={<DeveloperTab />} />
<Route path="*" element={<Navigate to="/console" replace />} />
</Routes>
</div>
</>
) : (
// Profile tab view
<>
<div className="flex items-center px-4 pt-2 border-b border-surface-border bg-base-900 shrink-0">
{MAIN_TABS.map((tab) => {
<div className="flex items-center px-4 border-b border-surface-border bg-base-900 shrink-0">
{PROFILE_TABS.map((tab) => {
const isActive = activeTab === tab.path;
return (
<button
key={tab.path}
onClick={() => navigate(tab.path)}
onClick={() => navigate(`/${tab.path}`)}
className={[
'flex items-center gap-1.5 px-3 py-2 text-xs border-b-2 -mb-px transition-all duration-150',
isActive
Expand Down Expand Up @@ -158,12 +122,12 @@ export function MainLayout() {
)}
</div>

<div className="flex-1 min-h-0 bg-base-800 animate-fade-in">
<div className="flex-1 min-h-0 overflow-hidden animate-fade-in">
<Routes>
<Route path="console" element={<ConsoleTab />} />
<Route path="config" element={<ConfigTab />} />
<Route path="profile" element={<ProfileTab />} />
<Route path="*" element={<Navigate to="console" replace />} />
<Route path="*" element={<Navigate to="/console" replace />} />
</Routes>
</div>
</>
Expand Down
15 changes: 2 additions & 13 deletions src/renderer/components/common/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,22 @@ export function ContextMenu({ x, y, items, onClose }: Props) {

useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (!ref.current) return;

// only close if clicking OUTSIDE
if (!ref.current.contains(e.target as Node)) {
onClose();
}
if (!ref.current?.contains(e.target as Node)) onClose();
};

const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};

document.addEventListener('click', handleClick);
document.addEventListener('keydown', handleKey);

return () => {
document.removeEventListener('click', handleClick);
document.removeEventListener('keydown', handleKey);
};
}, [onClose]);

const style: React.CSSProperties = { position: 'fixed', zIndex: 1000 };

if (x + 180 > window.innerWidth) style.right = window.innerWidth - x;
else style.left = x;

if (y + items.length * 32 > window.innerHeight) style.bottom = window.innerHeight - y;
else style.top = y;

Expand All @@ -57,12 +47,11 @@ export function ContextMenu({ x, y, items, onClose }: Props) {
if (item.type === 'separator') {
return <div key={i} className="my-1 border-t border-surface-border/60" />;
}

return (
<button
key={i}
disabled={item.disabled}
onMouseDown={(e) => e.preventDefault()} // prevent focus jump
onMouseDown={(e) => e.preventDefault()}
onClick={(e) => {
if (item.disabled || !item.onClick) return;
item.onClick(e);
Expand Down
15 changes: 15 additions & 0 deletions src/renderer/components/common/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';

interface Props {
icon: React.ReactNode;
text: string;
}

export function EmptyState({ icon, text }: Props) {
return (
<div className="flex flex-col items-center gap-3 py-12 text-text-muted">
{icon}
<p className="text-xs font-mono text-center max-w-xs leading-relaxed">{text}</p>
</div>
);
}
5 changes: 1 addition & 4 deletions src/renderer/components/common/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export function Modal({ open, title, onClose, width = 'md', children }: Props) {
WIDTHS[width],
].join(' ')}
>
{/* Header */}
<div className="flex items-center justify-between px-5 py-3.5 border-b border-surface-border shrink-0">
<h2 className="text-sm font-semibold text-text-primary">{title}</h2>
<button
Expand All @@ -54,9 +53,7 @@ export function Modal({ open, title, onClose, width = 'md', children }: Props) {
<VscClose size={15} />
</button>
</div>

{/* Scrollable body */}
<div className="flex-1 overflow-y-auto">{children}</div>
<div className="flex-1 overflow-y-auto min-h-0">{children}</div>
</div>
</div>
);
Expand Down
18 changes: 9 additions & 9 deletions src/renderer/components/common/PropList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function PropList({ items, onChange, onPendingChange }: Props) {

const notify = (k: string, v: string) =>
onPendingChange?.(k.trim().length > 0 || v.trim().length > 0);

const setKey = (v: string) => {
setDraftKey(v);
notify(v, draftValue);
Expand All @@ -44,6 +45,12 @@ export function PropList({ items, onChange, onPendingChange }: Props) {
const editValue = (i: number, value: string) =>
onChange(items.map((it, idx) => (idx === i ? { ...it, value } : it)));

const inputCls = (disabled?: boolean) =>
[
'flex-1 bg-base-900 border border-surface-border rounded-md px-2.5 py-1.5 text-xs font-mono text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/40 transition-colors',
disabled ? 'opacity-40' : '',
].join(' ');

return (
<div className="space-y-1.5">
{items.length > 0 && (
Expand All @@ -61,7 +68,6 @@ export function PropList({ items, onChange, onPendingChange }: Props) {
<div key={i} className="flex items-center gap-2 group">
<button
onClick={() => toggle(i)}
title={item.enabled ? 'Disable' : 'Enable'}
className={[
'w-4 h-4 rounded border transition-colors shrink-0',
item.enabled
Expand All @@ -87,21 +93,15 @@ export function PropList({ items, onChange, onPendingChange }: Props) {
value={item.key}
onChange={(e) => editKey(i, e.target.value)}
placeholder="property.key"
className={[
'flex-1 bg-base-900 border border-surface-border rounded-md px-2.5 py-1.5 text-xs font-mono text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/40 transition-colors',
!item.enabled ? 'opacity-40' : '',
].join(' ')}
className={inputCls(!item.enabled)}
/>
<span className="text-text-muted font-mono text-xs">=</span>
<input
type="text"
value={item.value}
onChange={(e) => editValue(i, e.target.value)}
placeholder="value (optional)"
className={[
'flex-1 bg-base-900 border border-surface-border rounded-md px-2.5 py-1.5 text-xs font-mono text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/40 transition-colors',
!item.enabled ? 'opacity-40' : '',
].join(' ')}
className={inputCls(!item.enabled)}
/>
<button
onClick={() => remove(i)}
Expand Down
Loading
Loading