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:
16
.eslintrc.json
Normal file
16
.eslintrc.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"plugin:import/electron",
|
||||||
|
"plugin:import/typescript"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser"
|
||||||
|
}
|
||||||
92
.gitignore
vendored
Normal file
92
.gitignore
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# nuxt.js build output
|
||||||
|
.nuxt
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# Webpack
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
# Electron-Forge
|
||||||
|
out/
|
||||||
59
forge.config.ts
Normal file
59
forge.config.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import type { ForgeConfig } from '@electron-forge/shared-types';
|
||||||
|
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
|
||||||
|
import { MakerZIP } from '@electron-forge/maker-zip';
|
||||||
|
import { MakerDeb } from '@electron-forge/maker-deb';
|
||||||
|
import { MakerRpm } from '@electron-forge/maker-rpm';
|
||||||
|
import { VitePlugin } from '@electron-forge/plugin-vite';
|
||||||
|
import { FusesPlugin } from '@electron-forge/plugin-fuses';
|
||||||
|
import { FuseV1Options, FuseVersion } from '@electron/fuses';
|
||||||
|
|
||||||
|
const config: ForgeConfig = {
|
||||||
|
packagerConfig: {
|
||||||
|
asar: true,
|
||||||
|
},
|
||||||
|
rebuildConfig: {},
|
||||||
|
makers: [
|
||||||
|
new MakerSquirrel({}),
|
||||||
|
new MakerZIP({}, ['darwin']),
|
||||||
|
new MakerRpm({}),
|
||||||
|
new MakerDeb({}),
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
new VitePlugin({
|
||||||
|
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||||
|
// If you are familiar with Vite configuration, it will look really familiar.
|
||||||
|
build: [
|
||||||
|
{
|
||||||
|
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
|
||||||
|
entry: 'src/main/index.ts',
|
||||||
|
config: 'vite.main.config.mts',
|
||||||
|
target: 'main',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: 'src/preload/index.ts',
|
||||||
|
config: 'vite.preload.config.mts',
|
||||||
|
target: 'preload',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
renderer: [
|
||||||
|
{
|
||||||
|
name: 'main_window',
|
||||||
|
config: 'vite.renderer.config.mts',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
// Fuses are used to enable/disable various Electron functionality
|
||||||
|
// at package time, before code signing the application
|
||||||
|
new FusesPlugin({
|
||||||
|
version: FuseVersion.V1,
|
||||||
|
[FuseV1Options.RunAsNode]: false,
|
||||||
|
[FuseV1Options.EnableCookieEncryption]: true,
|
||||||
|
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||||
|
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||||
|
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
||||||
|
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
1
forge.env.d.ts
vendored
Normal file
1
forge.env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="@electron-forge/plugin-vite/forge-vite-env" />
|
||||||
15
index.html
Normal file
15
index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>NeuralDesk</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/renderer/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12850
package-lock.json
generated
Normal file
12850
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
package.json
Normal file
55
package.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "neuraldesk",
|
||||||
|
"productName": "NeuralDesk",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Local-first intelligent desktop workspace",
|
||||||
|
"main": ".vite/build/main.js",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron-forge start",
|
||||||
|
"package": "electron-forge package",
|
||||||
|
"make": "electron-forge make",
|
||||||
|
"publish": "electron-forge publish",
|
||||||
|
"lint": "eslint --ext .ts,.tsx ."
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "rmusso",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@electron-forge/cli": "^7.11.1",
|
||||||
|
"@electron-forge/maker-deb": "^7.11.1",
|
||||||
|
"@electron-forge/maker-rpm": "^7.11.1",
|
||||||
|
"@electron-forge/maker-squirrel": "^7.11.1",
|
||||||
|
"@electron-forge/maker-zip": "^7.11.1",
|
||||||
|
"@electron-forge/plugin-auto-unpack-natives": "^7.11.1",
|
||||||
|
"@electron-forge/plugin-fuses": "^7.11.1",
|
||||||
|
"@electron-forge/plugin-vite": "^7.11.1",
|
||||||
|
"@electron/fuses": "^1.8.0",
|
||||||
|
"@tanstack/router-vite-plugin": "^1.161.1",
|
||||||
|
"@types/electron-squirrel-startup": "^1.0.2",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
|
"autoprefixer": "^10.4.24",
|
||||||
|
"electron": "40.6.0",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.19",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^5.4.21"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/react-router": "^1.161.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
|
"framer-motion": "^12.34.2",
|
||||||
|
"lucide-react": "^0.575.0",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
|
"tailwind-merge": "^3.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
63
src/main/index.ts
Normal file
63
src/main/index.ts
Normal 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
2
src/preload/index.ts
Normal 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
|
||||||
113
src/renderer/components/layout/AppShell.tsx
Normal file
113
src/renderer/components/layout/AppShell.tsx
Normal 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
48
src/renderer/globals.css
Normal 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
14
src/renderer/index.tsx
Normal 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>,
|
||||||
|
);
|
||||||
6
src/renderer/lib/utils.ts
Normal file
6
src/renderer/lib/utils.ts
Normal 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));
|
||||||
|
}
|
||||||
113
src/renderer/routeTree.gen.ts
Normal file
113
src/renderer/routeTree.gen.ts
Normal 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
10
src/renderer/router.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/renderer/routes/__root.tsx
Normal file
10
src/renderer/routes/__root.tsx
Normal 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>
|
||||||
|
),
|
||||||
|
});
|
||||||
32
src/renderer/routes/index.tsx
Normal file
32
src/renderer/routes/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/renderer/routes/projects.tsx
Normal file
13
src/renderer/routes/projects.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/renderer/routes/tasks.tsx
Normal file
13
src/renderer/routes/tasks.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/renderer/routes/timeline.tsx
Normal file
13
src/renderer/routes/timeline.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
tailwind.config.js
Normal file
42
tailwind.config.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ['./index.html', './src/renderer/**/*.{ts,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Geist', 'Inter', 'system-ui', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
DEFAULT: 'hsl(var(--sidebar))',
|
||||||
|
border: 'hsl(var(--sidebar-border))',
|
||||||
|
accent: 'hsl(var(--sidebar-accent))',
|
||||||
|
foreground: 'hsl(var(--sidebar-foreground))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
486
tasks/prd-neuraldesk.md
Normal file
486
tasks/prd-neuraldesk.md
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
# PRD: NeuralDesk — MVP Implementation
|
||||||
|
|
||||||
|
> **Status:** APPROVED / READY FOR DEV
|
||||||
|
> **Version:** 1.0 (MVP)
|
||||||
|
> **Date:** 2026-02-19
|
||||||
|
> **Stack:** Electron · React · TypeScript · shadcn/ui · Tailwind · Drizzle ORM · SQLite · LanceDB · GitHub Copilot SDK
|
||||||
|
> **Figma:** [Full File](https://www.figma.com/design/FxyJG9kpou4DfD7jM9WHKP/Desk)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
NeuralDesk is a local-first desktop workspace acting as a "Digital Executive Secretary." It centralizes notes, tasks, and project context into a local SQLite database and exposes a multi-agent AI layer (via GitHub Copilot SDK) that proactively surfaces insights and drafts actions. Data never leaves the machine, making it safe for enterprise environments.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Ship a working Electron desktop app with five sections: Home, Timeline, Tasks, Projects, Notes.
|
||||||
|
- All data persisted locally in SQLite (zero cloud dependency for data storage).
|
||||||
|
- Hierarchical Client → Sub-Client → Project structure fully navigable from a sidebar tree.
|
||||||
|
- "Fluid Curtain" pull-down gesture that transitions any view into a full-screen AI chat scoped to the current context.
|
||||||
|
- Multi-agent AI system (@Orchestrator, @ProjectAgent, @EmailAgent, @KnowledgeAgent) integrated via GitHub Copilot SDK.
|
||||||
|
- Milestone completion = every section functional at the level described in User Stories below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UI Summary (from Figma)
|
||||||
|
|
||||||
|
### Shared Shell
|
||||||
|
- **Left sidebar:** 240px, `#fafafa` background, border-right `#e5e5e5`.
|
||||||
|
- Top: NeuralDesk logo/wordmark.
|
||||||
|
- Nav items: Home (house icon), Timeline (chart-gantt icon), Tasks (clipboard-check icon), Projects (folder-kanban icon). Active item gets `#f5f5f5` accent + no extra border.
|
||||||
|
- Bottom: Collapse button (panel-left icon).
|
||||||
|
- **Right edge:** Vertical rotated label "keep scrolling for AI / next section" + chevron-down. This is the visual affordance for the Fluid Curtain pull-down gesture.
|
||||||
|
- **Font:** Geist (Regular 400, Medium 500, Semibold 600). Sizes: sm=14px, base=16px.
|
||||||
|
- **Colors:** bg=`#ffffff`, foreground=`#0a0a0a`, muted=`#737373`, border=`#e5e5e5`, sidebar=`#fafafa`, sidebar-accent=`#f5f5f5`, primary=`#171717`, primary-fg=`#fafafa`.
|
||||||
|
|
||||||
|
### HOME
|
||||||
|
- Top-right corner: stat chip showing "N Task due" count.
|
||||||
|
- Main area (centered, max-w ~1088px): AI greeting `✦ Hello, {name}` (Heading 2, Geist Semibold 30px, -1px tracking).
|
||||||
|
- Below: AI-generated daily brief paragraph with **bold** key phrases inline.
|
||||||
|
- Chat input box: white, border `#d4d4d4`, shadow-lg, 109px tall, placeholder "Ask me anythings...", Send button (black, icon + label) bottom-right of the box.
|
||||||
|
- Below chat: 4 suggestion chips (`Item` component) — icon badge + short prompt text — in a 4-column flex row.
|
||||||
|
|
||||||
|
### TIMELINE
|
||||||
|
- Main content area: placeholder background `#fef2f2` (the Gantt chart is not yet designed in Figma; implementation is free to choose a library).
|
||||||
|
- Same sidebar + right-edge Fluid Curtain affordance.
|
||||||
|
|
||||||
|
### TASKS
|
||||||
|
- Header row: 4 stat cards — "Total task", "To Do", "In Progress", "Completed" — each with an icon and count.
|
||||||
|
- Below: search bar (full-width, placeholder "Search tasks or projects...") + "Order by" dropdown (right) + status filter tabs (All | To Do | In Progress | Completed).
|
||||||
|
- Task list rows (flat, full-width):
|
||||||
|
- Checkbox (left)
|
||||||
|
- Title (bold, 14px) + description subtitle (gray, 14px)
|
||||||
|
- Priority chip: `HIGH` (up-arrow, red-toned) | `MEDIUM` (right-arrow, gray) | `LOW` (down-arrow, green-toned)
|
||||||
|
- Due date chip (calendar icon + "Due Mon DD")
|
||||||
|
- Breadcrumb path (Client > Sub-Client > Project, chevron-separated)
|
||||||
|
- Assignee (person icon + name string)
|
||||||
|
- Completed tasks show row with green-tinted background.
|
||||||
|
|
||||||
|
### PROJECTS
|
||||||
|
- **Left panel (tree):** "Projects" heading + `+` new button + search input. Hierarchical tree: Client (folder, bold) → Sub-Client (folder) → Project (circle/file). Expand/collapse chevrons. Active project highlighted.
|
||||||
|
- **Right panel (project detail):**
|
||||||
|
- Breadcrumb (Client > Sub-Client) at top.
|
||||||
|
- Project name as H1.
|
||||||
|
- 3 stat cards: Notes count | Tasks Complete (x/y fraction) | Checkpoints (x/y fraction).
|
||||||
|
- AI Project Summary card (sparkle icon + generated paragraph).
|
||||||
|
- **Project Timeline:** inline Gantt — months across top (Feb 2026, Mar 2026 …), horizontal bar with dot markers for each checkpoint. Legend: To Do (dark) / Completed (green). "+ Add" button top-right.
|
||||||
|
- **Tasks (Kanban):** 3 columns — To Do / In progress / Completed. Task cards: title, description, priority chip, due date, assignee. "+ Add" per column header.
|
||||||
|
- **Notes list:** flat list of note entries + "+ Add" button.
|
||||||
|
|
||||||
|
### NOTES
|
||||||
|
- Milkdown editor (standalone route) for writing/editing a single note.
|
||||||
|
- Markdown-native, full-screen editor style.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// clients — hierarchical (self-referencing parentId)
|
||||||
|
export const clients = sqliteTable('clients', {
|
||||||
|
id: text('id').primaryKey(), // UUID
|
||||||
|
parentId: text('parent_id'), // null = top-level client
|
||||||
|
name: text('name').notNull(),
|
||||||
|
industry: text('industry'),
|
||||||
|
createdAt: integer('created_at').notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// projects — attached to a client/sub-client, or orphan
|
||||||
|
export const projects = sqliteTable('projects', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
clientId: text('client_id').references(() => clients.id), // nullable
|
||||||
|
name: text('name').notNull(),
|
||||||
|
status: text('status').default('active'), // active | archived
|
||||||
|
aiSummary: text('ai_summary'), // AI-generated paragraph
|
||||||
|
createdAt: integer('created_at').notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// tasks — belong to a project (or global/orphan if projectId null)
|
||||||
|
export const tasks = sqliteTable('tasks', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
projectId: text('project_id').references(() => projects.id), // nullable
|
||||||
|
title: text('title').notNull(),
|
||||||
|
description: text('description'),
|
||||||
|
status: text('status').default('todo'), // todo | in_progress | done
|
||||||
|
priority: text('priority').default('medium'), // high | medium | low
|
||||||
|
assignee: text('assignee'), // plain string name
|
||||||
|
dueDate: integer('due_date'), // unix timestamp
|
||||||
|
createdAt: integer('created_at').notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// checkpoints — milestones on the per-project timeline
|
||||||
|
export const checkpoints = sqliteTable('checkpoints', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
projectId: text('project_id').references(() => projects.id).notNull(),
|
||||||
|
title: text('title').notNull(),
|
||||||
|
date: integer('date').notNull(), // unix timestamp
|
||||||
|
isAiSuggested: integer('is_ai_suggested').default(0), // 0=manual, 1=AI
|
||||||
|
isApproved: integer('is_approved').default(1), // 0=pending AI approval
|
||||||
|
createdAt: integer('created_at').notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// notes — markdown content attached to a project
|
||||||
|
export const notes = sqliteTable('notes', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
projectId: text('project_id').references(() => projects.id), // nullable
|
||||||
|
title: text('title').notNull(),
|
||||||
|
content: text('content').notNull(), // raw Markdown
|
||||||
|
createdAt: integer('created_at').notNull(),
|
||||||
|
updatedAt: integer('updated_at').notNull(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Stories
|
||||||
|
|
||||||
|
### PHASE 1 — Foundation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-001: Electron + React scaffold
|
||||||
|
**Description:** As a developer, I need a working Electron app with React+TypeScript and a shared main/renderer process setup so that all other features have a platform to run on.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] `electron-builder` or `electron-vite` scaffold with hot-reload in dev.
|
||||||
|
- [ ] Main process can open a `BrowserWindow` serving the React app.
|
||||||
|
- [ ] TypeScript strict mode enabled, `tsconfig.json` configured.
|
||||||
|
- [ ] `package.json` scripts: `dev`, `build`, `preview`.
|
||||||
|
- [ ] App opens without errors on Linux, macOS, Windows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-002: SQLite database + Drizzle ORM setup
|
||||||
|
**Description:** As a developer, I need the SQLite database initialized with Drizzle ORM so that all CRUD operations use a typed, schema-driven interface.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] `better-sqlite3` (or `@electric-sql/pglite` alternative) installed in main process.
|
||||||
|
- [ ] Drizzle schema file defines all 5 tables (clients, projects, tasks, checkpoints, notes).
|
||||||
|
- [ ] Migration runs on app start; DB file created at `~/.neuraldesk/data.db` (or `app.getPath('userData')`).
|
||||||
|
- [ ] Drizzle Studio accessible in dev mode (`drizzle-kit studio`).
|
||||||
|
- [ ] TypeScript types inferred from schema (no manual type duplication).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-003: App shell — sidebar navigation
|
||||||
|
**Description:** As a user, I want a persistent left sidebar so that I can navigate between all sections of the app.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] Sidebar renders at 240px with `#fafafa` background and right border.
|
||||||
|
- [ ] Nav items: Home (house), Timeline (chart-gantt), Tasks (clipboard-check), Projects (folder-kanban). Each uses Lucide icon + label.
|
||||||
|
- [ ] Active route highlights item with `#f5f5f5` accent background.
|
||||||
|
- [ ] Collapse button at bottom toggles sidebar to icon-only mode (64px).
|
||||||
|
- [ ] Router renders correct view for each nav item (React Router or TanStack Router).
|
||||||
|
- [ ] Verify in browser using dev-browser skill.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-004: Client CRUD (hierarchical)
|
||||||
|
**Description:** As a user, I want to create, rename, and delete Clients and Sub-Clients so that I can mirror real-world corporate structures.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] "New Client" action creates a top-level client (parentId = null).
|
||||||
|
- [ ] "New Sub-Client" action (available on a selected client) creates a child (parentId = selected client's id).
|
||||||
|
- [ ] Client and Sub-Client names are editable via inline rename or modal.
|
||||||
|
- [ ] Deleting a client warns if it has child clients or projects; cascade delete is opt-in.
|
||||||
|
- [ ] Changes are immediately persisted to SQLite.
|
||||||
|
- [ ] TypeScript types pass; no `any`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-005: Project CRUD
|
||||||
|
**Description:** As a user, I want to create projects attached to a client/sub-client or as standalone orphans so that I can track both client and internal work.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] "New Project" dialog asks for name and optionally a client (dropdown, searchable).
|
||||||
|
- [ ] Projects with no client appear under an "Internal / No Client" group in the tree.
|
||||||
|
- [ ] Project can be re-parented (moved to a different client) via edit dialog.
|
||||||
|
- [ ] Project status toggled between `active` and `archived`.
|
||||||
|
- [ ] Archived projects hidden by default; toggle to show.
|
||||||
|
- [ ] Persisted to SQLite immediately.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-006: Projects sidebar tree view
|
||||||
|
**Description:** As a user, I want to see all clients, sub-clients, and projects in a collapsible tree in the Projects section so that I can navigate the hierarchy at a glance.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] Tree renders: Client (folder icon) → Sub-Client (folder icon) → Project (circle icon).
|
||||||
|
- [ ] Clients and sub-clients expand/collapse independently.
|
||||||
|
- [ ] Search input filters tree in real-time (client name, sub-client name, project name).
|
||||||
|
- [ ] Clicking a project loads the Project Detail panel on the right.
|
||||||
|
- [ ] Active project is highlighted in the tree.
|
||||||
|
- [ ] Verify in browser using dev-browser skill.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-007: Task CRUD (global Tasks view)
|
||||||
|
**Description:** As a user, I want a global task list where I can create, filter, search, and update tasks so that I can manage all work across projects in one place.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] 4 stat cards at top: Total, To Do, In Progress, Completed — update reactively.
|
||||||
|
- [ ] Search filters tasks by title or description, case-insensitive.
|
||||||
|
- [ ] Status filter tabs (All | To Do | In Progress | Completed) filter list.
|
||||||
|
- [ ] "Order by" dropdown supports: Due Date, Priority, Created Date.
|
||||||
|
- [ ] Task rows show: checkbox, title, description, priority chip (HIGH/MEDIUM/LOW with color), due date chip, breadcrumb (Client > Sub-Client > Project), assignee.
|
||||||
|
- [ ] Clicking checkbox toggles status: todo → done (skip in_progress for quick-complete).
|
||||||
|
- [ ] Inline "New Task" button opens a creation modal with fields: title, description, priority, due date, project (optional), assignee (optional).
|
||||||
|
- [ ] All changes persisted to SQLite.
|
||||||
|
- [ ] Verify in browser using dev-browser skill.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-008: Manual Timeline (global & per-project)
|
||||||
|
**Description:** As a user, I want to view and manually create timeline checkpoints on a Gantt-style view so that I have full control over milestone dates.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] Timeline view renders a horizontal time axis (months) with dot markers for each checkpoint.
|
||||||
|
- [ ] Global Timeline shows checkpoints from all projects (color-coded by project or status).
|
||||||
|
- [ ] Per-project timeline (in Project Detail) scoped to that project's checkpoints only.
|
||||||
|
- [ ] "+ Add" button opens a dialog: title, date picker, project (in global view).
|
||||||
|
- [ ] Checkpoint dots distinguish status: To Do (dark/filled) vs Completed (green).
|
||||||
|
- [ ] "Today" marker line displayed on the timeline.
|
||||||
|
- [ ] Clicking a checkpoint dot shows a popover with title, date, and delete action.
|
||||||
|
- [ ] Persisted to SQLite.
|
||||||
|
- [ ] Verify in browser using dev-browser skill.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-009: Project Detail view
|
||||||
|
**Description:** As a user, I want a rich project detail panel that shows notes count, tasks summary, AI summary, timeline, Kanban, and notes list in one scrollable view.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] Breadcrumb (Client > Sub-Client) rendered at top.
|
||||||
|
- [ ] Stat cards: Notes count | Tasks Complete (x/y) | Checkpoints (x/y).
|
||||||
|
- [ ] AI Project Summary card shows sparkle icon + placeholder text ("AI summary will appear here") until agent generates it.
|
||||||
|
- [ ] Inline Project Timeline (same Gantt component as US-008, scoped).
|
||||||
|
- [ ] Kanban board: To Do / In Progress / Completed columns. Task cards show title, description, priority, due date, assignee. Drag between columns updates `status`.
|
||||||
|
- [ ] Notes list: title + creation date for each note. Click opens Milkdown editor. "+ Add" creates new note.
|
||||||
|
- [ ] Verify in browser using dev-browser skill.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-010: Notes editor (Milkdown)
|
||||||
|
**Description:** As a user, I want a full-screen Markdown editor for each note so that I can write rich content without leaving the app.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] Milkdown editor renders in a dedicated route (`/notes/:noteId`).
|
||||||
|
- [ ] Supports: headings, bold, italic, code blocks, bullet lists, ordered lists, blockquotes.
|
||||||
|
- [ ] Auto-saves content to SQLite on change (debounced, 500ms).
|
||||||
|
- [ ] Back navigation returns to the project detail view (or previous location).
|
||||||
|
- [ ] Note title editable at the top (separate from Milkdown content).
|
||||||
|
- [ ] Verify in browser using dev-browser skill.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PHASE 2 — The Fluid Curtain & Agents
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-011: Fluid Curtain — pull-down gesture + animation
|
||||||
|
**Description:** As a user, I want to pull down from the top of any view to slide the app off-screen and reveal the AI chat layer beneath.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] Scrolling up past the top of content (overscroll) OR pressing a keyboard shortcut (e.g., `Cmd/Ctrl+K` or `⌥↓`) triggers the curtain.
|
||||||
|
- [ ] App panel slides down using Framer Motion spring animation, exiting the bottom of the viewport.
|
||||||
|
- [ ] AI Chat view is fully revealed below (full-screen, no sidebar obstruction).
|
||||||
|
- [ ] Pulling the app back up (swipe/scroll from bottom or shortcut) re-covers the chat.
|
||||||
|
- [ ] Animation is smooth (no jank). Spring config: stiffness 300, damping 30.
|
||||||
|
- [ ] Right-edge "keep scrolling for AI" label and chevron are visible in every section as the affordance.
|
||||||
|
- [ ] Verify in browser using dev-browser skill.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-012: Context-scoped AI chat
|
||||||
|
**Description:** As a user, I want the AI chat (revealed by the curtain) to know the context I was in so that answers are scoped to the right project or global scope.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] When curtain is pulled from a Project Detail view, a context header displays "Chatting about: [Project Name]".
|
||||||
|
- [ ] When pulled from Home, context is global (all data).
|
||||||
|
- [ ] Context is passed as a system message to the GitHub Copilot SDK call (project notes, tasks, checkpoints as structured JSON).
|
||||||
|
- [ ] Agent responses reference only documents within the scoped project.
|
||||||
|
- [ ] Chat history is session-only (not persisted in MVP).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-013: GitHub Copilot SDK integration + @Orchestrator
|
||||||
|
**Description:** As a developer, I need the GitHub Copilot SDK wired up with an Orchestrator agent that routes user messages to the correct specialist agent.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] SDK initialized in main process with enterprise credentials (from env/config).
|
||||||
|
- [ ] `@Orchestrator` reads user intent and calls `route_to_project`, `route_to_general`, or `route_to_email` tool.
|
||||||
|
- [ ] Routing result invokes the correct specialist agent and returns its response.
|
||||||
|
- [ ] Streaming responses supported (tokens shown incrementally in chat UI).
|
||||||
|
- [ ] Errors handled gracefully (SDK timeout, auth failure) with user-facing message.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-014: @ProjectAgent with project tools
|
||||||
|
**Description:** As a user, I want the AI to answer project-specific questions and take actions (add task, suggest checkpoints) within the scoped project.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] `read_project_notes` tool fetches all notes for the project from SQLite.
|
||||||
|
- [ ] `add_task` tool creates a task in the project (writes to SQLite) and confirms in chat.
|
||||||
|
- [ ] `suggest_checkpoints` tool returns a list of proposed checkpoints (title + date) as interactive cards in chat (Approve / Reject each).
|
||||||
|
- [ ] Approved checkpoints are inserted into `checkpoints` table with `is_ai_suggested=1, is_approved=1`.
|
||||||
|
- [ ] `get_summary` tool generates a 2-3 sentence project summary and updates `projects.ai_summary` in SQLite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PHASE 3 — Intelligence & RAG
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-015: LanceDB vector store setup + note embedding
|
||||||
|
**Description:** As a developer, I need notes and project content embedded into LanceDB so that semantic search is possible across all projects.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] LanceDB initialized in main process, storing vectors at `~/.neuraldesk/vectors/`.
|
||||||
|
- [ ] On note save (create or update), content is embedded via GitHub Copilot SDK embeddings endpoint and stored in LanceDB with `{noteId, projectId, content}` metadata.
|
||||||
|
- [ ] Existing notes are indexed on first startup (migration script).
|
||||||
|
- [ ] Embedding errors logged but do not block the save operation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-016: @KnowledgeAgent — semantic search across all projects
|
||||||
|
**Description:** As a user, I want to ask "what did we decide about X?" and get answers pulled from across all past project notes, not just the current one.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] `vector_search_all` tool accepts a query string, returns top-5 semantically similar note chunks from LanceDB.
|
||||||
|
- [ ] Results include source note title and project name for attribution.
|
||||||
|
- [ ] @Orchestrator routes knowledge queries to @KnowledgeAgent.
|
||||||
|
- [ ] Response in chat includes inline citations ("From: Project A — Meeting Notes, Feb 12").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-017: AI checkpoint suggestions from notes
|
||||||
|
**Description:** As a user, I want the AI to proactively analyze my meeting notes and suggest timeline checkpoints I may have missed.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] Triggered manually ("Suggest checkpoints" button in Project Detail timeline header) or by @ProjectAgent tool call.
|
||||||
|
- [ ] @ProjectAgent reads all notes for the project, extracts date-anchored commitments, returns as suggested checkpoints.
|
||||||
|
- [ ] Suggestions appear as dismissible cards in the Timeline UI with `isAiSuggested=1, isApproved=0`.
|
||||||
|
- [ ] Approve → `isApproved` set to 1, checkpoint appears on timeline.
|
||||||
|
- [ ] Reject → checkpoint deleted.
|
||||||
|
- [ ] Verify in browser using dev-browser skill.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### US-018: Home dashboard — AI daily brief
|
||||||
|
**Description:** As a user, I want the Home screen to greet me with an AI-generated daily brief summarizing my tasks and suggesting actions.
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] On app open, @Orchestrator queries tasks due today/this week and recent project activity.
|
||||||
|
- [ ] AI generates a personalized paragraph with key highlights (tasks due, suggested calls/emails).
|
||||||
|
- [ ] Brief is displayed below the greeting with **bold** key phrases inline (as in Figma).
|
||||||
|
- [ ] 4 suggestion chips below the chat box are pre-populated with context-relevant queries.
|
||||||
|
- [ ] Chat box on Home is scoped globally (no project context).
|
||||||
|
- [ ] Verify in browser using dev-browser skill.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Functional Requirements
|
||||||
|
|
||||||
|
- **FR-01:** All data stored locally in SQLite at `app.getPath('userData')/neuraldesk.db`.
|
||||||
|
- **FR-02:** App functions fully offline; AI features degrade gracefully when network is unavailable.
|
||||||
|
- **FR-03:** Client tree supports unlimited nesting depth but UI only needs to display 3 levels (Client → Sub-Client → Project).
|
||||||
|
- **FR-04:** Tasks table has a nullable `projectId`; global Tasks view shows all tasks regardless.
|
||||||
|
- **FR-05:** The "Fluid Curtain" animation must not lose the underlying view state (app slides but remains mounted).
|
||||||
|
- **FR-06:** GitHub Copilot SDK credentials are stored in OS keychain (not plaintext config).
|
||||||
|
- **FR-07:** Milkdown auto-save uses a 500ms debounce; unsaved indicator shown if pending.
|
||||||
|
- **FR-08:** All IDs are UUIDs (use `crypto.randomUUID()`).
|
||||||
|
- **FR-09:** Drizzle migrations run automatically on startup; never destructive.
|
||||||
|
- **FR-10:** Kanban drag-and-drop updates `tasks.status` and `tasks.updatedAt` immediately in SQLite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Non-Goals (Out of Scope for MVP)
|
||||||
|
|
||||||
|
- Email client / inbox integration (EmailAgent tools are stubs only).
|
||||||
|
- Cloud sync or multi-device support.
|
||||||
|
- Real assignee accounts (assignee is a plain name string, not a user entity).
|
||||||
|
- Notifications or system tray alerts.
|
||||||
|
- Dark mode.
|
||||||
|
- Mobile or web version.
|
||||||
|
- Export to PDF/CSV.
|
||||||
|
- Notes version history.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Considerations
|
||||||
|
|
||||||
|
- **Font:** Geist via `@fontsource/geist` or CDN. Apply globally via CSS variable.
|
||||||
|
- **Icons:** Lucide React (house, chart-gantt, clipboard-check, folder-kanban, panel-left, send, sparkles, chevron-down).
|
||||||
|
- **Gantt:** ✅ Custom SVG component. Month labels on X axis, `<circle>` dots for checkpoints, `<line>` baseline, `<TodayMarker>`. Use `ResizeObserver` for responsive width.
|
||||||
|
- **Kanban:** ✅ `@hello-pangea/dnd` — `<DragDropContext>` wrapping 3 `<Droppable>` columns, each task a `<Draggable>`.
|
||||||
|
- **Fluid Curtain:** ✅ `framer-motion` `useMotionValue` + `useSpring`. Trigger: `wheel` event at `scrollTop === 0 && deltaY < 0` OR `Cmd/Ctrl+K`. Right-edge "keep scrolling for AI" label is a **visual hint only** (not interactive).
|
||||||
|
- **shadcn/ui components to reuse:** Button, Input, Badge, Card, Dialog, Separator, Tabs, Tooltip, DropdownMenu, Popover.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Considerations
|
||||||
|
|
||||||
|
- **IPC:** ✅ `electron-trpc`. Define a single `appRouter` in main process exposing all domains (`tasks`, `projects`, `clients`, `checkpoints`, `notes`, `ai`). Renderer uses `trpc.[domain].[procedure].useQuery/useMutation()`. Zod validates all inputs at the boundary.
|
||||||
|
- GitHub Copilot SDK may require enterprise SSO token; provide a settings screen for token input (US not in MVP scope, but infrastructure must exist).
|
||||||
|
- LanceDB Node.js binding (`vectordb` package) runs in main process only.
|
||||||
|
- Milkdown v7+ with React adapter. Plugin list: `commonmark`, `history`, `clipboard`, `math` (optional).
|
||||||
|
- Use `electron-store` or `conf` for lightweight app settings (user name for greeting, sidebar collapsed state, etc.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
- All 5 sections navigable and functional with real SQLite-persisted data.
|
||||||
|
- Fluid Curtain animation runs at 60fps with no layout shift on return.
|
||||||
|
- @ProjectAgent correctly scopes a context query (zero responses sourcing from another project).
|
||||||
|
- Note embedding + LanceDB retrieval returns relevant results for a simple semantic query.
|
||||||
|
- App cold-start < 3 seconds on a modern machine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. ~~**Gantt library vs. custom SVG?**~~ ✅ Resolved: custom SVG component.
|
||||||
|
2. ~~**GitHub Copilot SDK auth flow:**~~ ✅ Resolved: `keytar` (OS keychain). A minimal "Settings" screen for token input writes to keychain on save.
|
||||||
|
3. ~~**IPC architecture:**~~ ✅ Resolved: `electron-trpc` with Zod validation. All DB/AI operations exposed as tRPC procedures in main process; renderer uses typed React Query hooks.
|
||||||
|
4. **Milkdown vs. simpler editor:** Milkdown is powerful but has a learning curve. Is a simpler `CodeMirror`-based Markdown editor acceptable for MVP?
|
||||||
|
5. **"Fluid Curtain" on Linux:** Overscroll behavior differs across OS/window managers. What's the fallback trigger (keyboard shortcut only)?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Roadmap
|
||||||
|
|
||||||
|
### Phase 1 — Foundation (US-001 → US-010)
|
||||||
|
|
||||||
|
| Step | Story | Key Decision Point |
|
||||||
|
|------|-------|-------------------|
|
||||||
|
| 1.1 | US-001: Electron+React scaffold | ✅ **electron-forge + Vite plugin** (`npm init electron-app@latest -- --template=vite-typescript`) |
|
||||||
|
| 1.2 | US-002: SQLite + Drizzle setup | Schema finalized; migrations strategy |
|
||||||
|
| 1.3 | US-003: App shell + sidebar | ✅ **TanStack Router** (fully type-safe, `$projectId` params typed) |
|
||||||
|
| 1.4 | US-004 + US-005: Client & Project CRUD | Data model confirmed |
|
||||||
|
| 1.5 | US-006: Projects tree view | ✅ **Radix Collapsible + recursive `TreeNode`** (no extra dep, matches Figma) |
|
||||||
|
| 1.6 | US-007: Tasks global view | |
|
||||||
|
| 1.7 | US-008: Manual Timeline / Gantt | ✅ **Custom SVG component** (dot-on-axis, zero deps, matches Figma exactly) |
|
||||||
|
| 1.8 | US-009: Project Detail view | ✅ **@hello-pangea/dnd** for Kanban drag-and-drop |
|
||||||
|
| 1.9 | US-010: Milkdown editor | Plugin scope for MVP |
|
||||||
|
|
||||||
|
### Phase 2 — The Curtain & Agents (US-011 → US-014)
|
||||||
|
|
||||||
|
| Step | Story | Key Decision Point |
|
||||||
|
|------|-------|-------------------|
|
||||||
|
| 2.1 | US-011: Fluid Curtain animation | ✅ Wheel overscroll-up at `scrollTop=0` + `Cmd/Ctrl+K` shortcut. Right-edge label is visual-only (not a button). Framer Motion spring (`y` to viewport height). |
|
||||||
|
| 2.2 | US-012: Context-scoped chat UI | Chat bubble components, streaming UI |
|
||||||
|
| 2.3 | US-013: Copilot SDK + @Orchestrator | ✅ **`keytar`** for OS keychain token storage (main process only, IPC to renderer). |
|
||||||
|
| 2.4 | US-014: @ProjectAgent tools | Tool schema definition + SQLite write-back |
|
||||||
|
|
||||||
|
### Phase 3 — Intelligence & RAG (US-015 → US-018)
|
||||||
|
|
||||||
|
| Step | Story | Key Decision Point |
|
||||||
|
|------|-------|-------------------|
|
||||||
|
| 3.1 | US-015: LanceDB setup + embedding | ✅ **GitHub Copilot SDK embeddings** (`text-embedding-3-small`). Chunk notes by paragraph (~500 tokens). |
|
||||||
|
| 3.2 | US-016: @KnowledgeAgent search | Vector search tuning, k=5 default |
|
||||||
|
| 3.3 | US-017: AI checkpoint suggestions | Prompt engineering for date extraction |
|
||||||
|
| 3.4 | US-018: Home daily brief | Orchestrator routing for daily summary |
|
||||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/renderer/*"]
|
||||||
|
},
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src", "forge.config.ts", "forge.env.d.ts", "vite.*.config.ts", "vite.*.config.mts"]
|
||||||
|
}
|
||||||
4
tsr.config.json
Normal file
4
tsr.config.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"routesDirectory": "./src/renderer/routes",
|
||||||
|
"generatedRouteTree": "./src/renderer/routeTree.gen.ts"
|
||||||
|
}
|
||||||
12
vite.main.config.mts
Normal file
12
vite.main.config.mts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: 'main.js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
4
vite.preload.config.mts
Normal file
4
vite.preload.config.mts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config
|
||||||
|
export default defineConfig({});
|
||||||
20
vite.renderer.config.mts
Normal file
20
vite.renderer.config.mts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { TanStackRouterVite } from '@tanstack/router-vite-plugin';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
TanStackRouterVite({
|
||||||
|
routesDirectory: './src/renderer/routes',
|
||||||
|
generatedRouteTree: './src/renderer/routeTree.gen.ts',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src/renderer'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user