feat: US-001 — scaffold NeuralDesk Electron + React app

- electron-forge 7 + Vite plugin (vite-typescript template)
- React 19 + TypeScript 5 strict mode
- TanStack Router with file-based routing (4 routes: /, /timeline, /tasks, /projects)
- Tailwind CSS 3 + PostCSS with Figma design tokens (sidebar, primary, muted)
- Framer Motion, Lucide React, shadcn/ui utilities (cn, CVA, clsx, twMerge)
- AppShell layout: 240px sidebar with collapse toggle, active route highlighting
- Vite configs use .mts extension to avoid ESM/CJS conflict with electron-forge
- Full package build verified (linux x64)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Roberto Musso
2026-02-19 15:28:31 +01:00
commit f6cc8bb23a
28 changed files with 14134 additions and 0 deletions

63
src/main/index.ts Normal file
View File

@@ -0,0 +1,63 @@
import { app, BrowserWindow } from 'electron';
import path from 'node:path';
import started from 'electron-squirrel-startup';
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) {
app.quit();
}
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1280,
height: 800,
minWidth: 900,
minHeight: 600,
titleBarStyle: 'hiddenInset',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
// and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
} else {
mainWindow.loadFile(
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
);
}
// Open DevTools in development.
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools();
}
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.

2
src/preload/index.ts Normal file
View File

@@ -0,0 +1,2 @@
// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts

View File

@@ -0,0 +1,113 @@
import { useState } from 'react';
import { Link, useRouterState } from '@tanstack/react-router';
import {
House,
ChartGantt,
ClipboardCheck,
FolderKanban,
PanelLeft,
} from 'lucide-react';
import { cn } from '@/lib/utils';
const NAV_ITEMS = [
{ to: '/', icon: House, label: 'Home' },
{ to: '/timeline', icon: ChartGantt, label: 'Timeline' },
{ to: '/tasks', icon: ClipboardCheck, label: 'Tasks' },
{ to: '/projects', icon: FolderKanban, label: 'Projects' },
] as const;
interface AppShellProps {
children: React.ReactNode;
}
export function AppShell({ children }: AppShellProps) {
const [collapsed, setCollapsed] = useState(false);
const routerState = useRouterState();
const currentPath = routerState.location.pathname;
return (
<div className="flex h-screen w-screen overflow-hidden bg-background">
{/* Sidebar */}
<aside
className={cn(
'flex flex-col h-full bg-sidebar border-r border-sidebar-border transition-all duration-200 overflow-hidden shrink-0',
collapsed ? 'w-16' : 'w-60',
)}
>
{/* Logo */}
<div
className={cn(
'flex items-center gap-3 px-3 py-3 shrink-0',
collapsed && 'justify-center',
)}
>
<div className="size-7 rounded-lg bg-primary flex items-center justify-center shrink-0">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
className="text-primary-foreground"
>
<path
d="M12 2L13.5 8.5L20 10L13.5 11.5L12 18L10.5 11.5L4 10L10.5 8.5L12 2Z"
fill="currentColor"
/>
</svg>
</div>
{!collapsed && (
<span className="font-semibold text-sm text-foreground">
NeuralDesk
</span>
)}
</div>
{/* Nav */}
<nav className="flex flex-col gap-0.5 px-2 flex-1 mt-2">
{NAV_ITEMS.map(({ to, icon: Icon, label }) => {
const isActive =
to === '/'
? currentPath === '/'
: currentPath.startsWith(to);
return (
<Link
key={to}
to={to}
className={cn(
'flex items-center gap-2 h-8 px-3 rounded-md text-sm text-sidebar-foreground transition-colors',
'hover:bg-sidebar-accent',
isActive && 'bg-sidebar-accent font-medium',
)}
>
<Icon size={16} className="shrink-0" />
{!collapsed && <span className="truncate">{label}</span>}
</Link>
);
})}
</nav>
{/* Collapse toggle */}
<div className="px-2 pb-3 shrink-0">
<button
onClick={() => setCollapsed((c) => !c)}
className={cn(
'flex items-center gap-2 h-8 px-3 rounded-md text-sm text-sidebar-foreground w-full',
'hover:bg-sidebar-accent transition-colors',
collapsed && 'justify-center',
)}
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
>
<PanelLeft size={16} className="shrink-0" />
{!collapsed && <span>Collapse</span>}
</button>
</div>
</aside>
{/* Main content */}
<main className="flex-1 min-w-0 overflow-hidden relative">
{children}
</main>
</div>
);
}

48
src/renderer/globals.css Normal file
View File

@@ -0,0 +1,48 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 83.1%;
--radius: 0.5rem;
/* Sidebar tokens — matching Figma exactly */
--sidebar: 0 0% 98%;
--sidebar-border: 0 0% 89.8%;
--sidebar-accent: 0 0% 96.1%;
--sidebar-foreground: 0 0% 25.1%;
}
* {
border-color: hsl(var(--border));
}
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-family: 'Geist', 'Inter', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
margin: 0;
overflow: hidden; /* Electron: no OS scrollbars */
}
#root {
height: 100vh;
width: 100vw;
display: flex;
overflow: hidden;
}
}
/* Geist font — loaded via CDN in index.html */

14
src/renderer/index.tsx Normal file
View File

@@ -0,0 +1,14 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from '@tanstack/react-router';
import { router } from './router';
import './globals.css';
const rootElement = document.getElementById('root');
if (!rootElement) throw new Error('Root element not found');
createRoot(rootElement).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
);

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,113 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as TimelineRouteImport } from './routes/timeline'
import { Route as TasksRouteImport } from './routes/tasks'
import { Route as ProjectsRouteImport } from './routes/projects'
import { Route as IndexRouteImport } from './routes/index'
const TimelineRoute = TimelineRouteImport.update({
id: '/timeline',
path: '/timeline',
getParentRoute: () => rootRouteImport,
} as any)
const TasksRoute = TasksRouteImport.update({
id: '/tasks',
path: '/tasks',
getParentRoute: () => rootRouteImport,
} as any)
const ProjectsRoute = ProjectsRouteImport.update({
id: '/projects',
path: '/projects',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/projects': typeof ProjectsRoute
'/tasks': typeof TasksRoute
'/timeline': typeof TimelineRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/projects': typeof ProjectsRoute
'/tasks': typeof TasksRoute
'/timeline': typeof TimelineRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/projects': typeof ProjectsRoute
'/tasks': typeof TasksRoute
'/timeline': typeof TimelineRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/projects' | '/tasks' | '/timeline'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/projects' | '/tasks' | '/timeline'
id: '__root__' | '/' | '/projects' | '/tasks' | '/timeline'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ProjectsRoute: typeof ProjectsRoute
TasksRoute: typeof TasksRoute
TimelineRoute: typeof TimelineRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/timeline': {
id: '/timeline'
path: '/timeline'
fullPath: '/timeline'
preLoaderRoute: typeof TimelineRouteImport
parentRoute: typeof rootRouteImport
}
'/tasks': {
id: '/tasks'
path: '/tasks'
fullPath: '/tasks'
preLoaderRoute: typeof TasksRouteImport
parentRoute: typeof rootRouteImport
}
'/projects': {
id: '/projects'
path: '/projects'
fullPath: '/projects'
preLoaderRoute: typeof ProjectsRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ProjectsRoute: ProjectsRoute,
TasksRoute: TasksRoute,
TimelineRoute: TimelineRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

10
src/renderer/router.ts Normal file
View File

@@ -0,0 +1,10 @@
import { createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
export const router = createRouter({ routeTree });
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}

View File

@@ -0,0 +1,10 @@
import { createRootRoute, Outlet } from '@tanstack/react-router';
import { AppShell } from '@/components/layout/AppShell';
export const Route = createRootRoute({
component: () => (
<AppShell>
<Outlet />
</AppShell>
),
});

View File

@@ -0,0 +1,32 @@
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/')({
component: HomePage,
});
function HomePage() {
return (
<div className="flex flex-col items-center justify-center h-full gap-4">
<div className="flex items-center gap-3">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
className="text-foreground"
>
<path
d="M12 2L13.5 8.5L20 10L13.5 11.5L12 18L10.5 11.5L4 10L10.5 8.5L12 2Z"
fill="currentColor"
/>
</svg>
<h1 className="text-3xl font-semibold tracking-tight">
Hello, Roberto
</h1>
</div>
<p className="text-muted-foreground text-sm">
NeuralDesk is ready. Start building.
</p>
</div>
);
}

View File

@@ -0,0 +1,13 @@
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/projects')({
component: ProjectsPage,
});
function ProjectsPage() {
return (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
Projects coming in US-006
</div>
);
}

View File

@@ -0,0 +1,13 @@
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/tasks')({
component: TasksPage,
});
function TasksPage() {
return (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
Tasks coming in US-007
</div>
);
}

View File

@@ -0,0 +1,13 @@
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/timeline')({
component: TimelinePage,
});
function TimelinePage() {
return (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
Timeline coming in US-008
</div>
);
}