diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml new file mode 100644 index 000000000..741edacc8 --- /dev/null +++ b/.github/workflows/desktop-build.yml @@ -0,0 +1,117 @@ +name: Desktop App Build + +on: + push: + tags: + - 'desktop-v*' + workflow_dispatch: + +jobs: + build: + strategy: + matrix: + include: + - os: macos-latest + platform: mac + arch: universal + - os: windows-latest + platform: win + arch: x64 + - os: ubuntu-latest + platform: linux + arch: x64 + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.5.2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build packages + run: pnpm turbo run build --filter=@lightfast/desktop^... + + - name: Build Electron app for macOS + if: matrix.platform == 'mac' + run: pnpm dist:desktop:mac + env: + CSC_LINK: ${{ secrets.MAC_CERTS }} + CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + + - name: Build Electron app for Windows + if: matrix.platform == 'win' + run: pnpm dist:desktop:win + env: + CSC_LINK: ${{ secrets.WIN_CERTS }} + CSC_KEY_PASSWORD: ${{ secrets.WIN_CERTS_PASSWORD }} + + - name: Build Electron app for Linux + if: matrix.platform == 'linux' + run: pnpm dist:desktop:linux + + - name: Upload artifacts - macOS + if: matrix.platform == 'mac' + uses: actions/upload-artifact@v4 + with: + name: lightfast-desktop-mac + path: | + apps/desktop/out/*.dmg + apps/desktop/out/*.zip + + - name: Upload artifacts - Windows + if: matrix.platform == 'win' + uses: actions/upload-artifact@v4 + with: + name: lightfast-desktop-win + path: apps/desktop/out/*.exe + + - name: Upload artifacts - Linux + if: matrix.platform == 'linux' + uses: actions/upload-artifact@v4 + with: + name: lightfast-desktop-linux + path: | + apps/desktop/out/*.AppImage + apps/desktop/out/*.deb + + release: + needs: build + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: | + lightfast-desktop-mac/*.dmg + lightfast-desktop-mac/*.zip + lightfast-desktop-win/*.exe + lightfast-desktop-linux/*.AppImage + lightfast-desktop-linux/*.deb + draft: false + prerelease: false + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore new file mode 100644 index 000000000..5d648a073 --- /dev/null +++ b/apps/desktop/.gitignore @@ -0,0 +1,34 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +dist-electron/ +out/ + +# Electron +electron-builder.yml +*.unpacked + +# Environment +.env +.env.local + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Testing +coverage/ +.nyc_output/ \ No newline at end of file diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts new file mode 100644 index 000000000..327dd6040 --- /dev/null +++ b/apps/desktop/electron/main.ts @@ -0,0 +1,69 @@ +import { app, BrowserWindow, shell, ipcMain } from 'electron'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +let mainWindow: BrowserWindow | null = null; + +function createWindow(): void { + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + minWidth: 800, + minHeight: 600, + title: 'Lightfast', + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, 'preload.js'), + }, + show: false, + backgroundColor: '#000000', + titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', + }); + + mainWindow.once('ready-to-show', () => { + mainWindow?.show(); + }); + + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url); + return { action: 'deny' }; + }); + + if (process.env.NODE_ENV === 'development') { + mainWindow.loadURL('http://localhost:5173'); + } else { + mainWindow.loadFile(path.join(__dirname, '../index.html')); + } + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +app.whenReady().then(() => { + createWindow(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +ipcMain.handle('app:getVersion', (): string => { + return app.getVersion(); +}); + +ipcMain.handle('app:getName', (): string => { + return app.getName(); +}); \ No newline at end of file diff --git a/apps/desktop/electron/preload.ts b/apps/desktop/electron/preload.ts new file mode 100644 index 000000000..ff9c11f93 --- /dev/null +++ b/apps/desktop/electron/preload.ts @@ -0,0 +1,9 @@ +import { contextBridge, ipcRenderer } from 'electron'; + +contextBridge.exposeInMainWorld('electron', { + app: { + getVersion: () => ipcRenderer.invoke('app:getVersion'), + getName: () => ipcRenderer.invoke('app:getName'), + }, + platform: process.platform, +}); \ No newline at end of file diff --git a/apps/desktop/eslint.config.js b/apps/desktop/eslint.config.js new file mode 100644 index 000000000..6a1a86754 --- /dev/null +++ b/apps/desktop/eslint.config.js @@ -0,0 +1,8 @@ +import baseConfig from "@repo/eslint-config/base.js"; + +export default [ + ...baseConfig, + { + ignores: ["dist/**", "out/**", "*.config.ts", "*.config.js"], + }, +]; \ No newline at end of file diff --git a/apps/desktop/index.html b/apps/desktop/index.html new file mode 100644 index 000000000..779c4cb5b --- /dev/null +++ b/apps/desktop/index.html @@ -0,0 +1,13 @@ + + + + + + + Lightfast Desktop + + +
+ + + \ No newline at end of file diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 000000000..7ff21d1a5 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,94 @@ +{ + "name": "@lightfast/desktop", + "version": "0.1.0", + "private": true, + "description": "Lightfast Desktop Application", + "main": "dist-electron/main.js", + "scripts": { + "dev": "vite", + "build": "vite build", + "dist": "npm run build && electron-builder", + "dist:mac": "npm run build && electron-builder --mac", + "dist:win": "npm run build && electron-builder --win", + "dist:linux": "npm run build && electron-builder --linux", + "lint": "eslint . --max-warnings 0", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist out" + }, + "dependencies": { + "electron-log": "^5.4.2", + "electron-updater": "^6.6.2" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/prettier-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/node": "catalog:", + "@types/react": "catalog:react19", + "@types/react-dom": "catalog:react19", + "@vitejs/plugin-react": "^5.0.0", + "electron": "^37.2.6", + "electron-builder": "^26.0.12", + "eslint": "catalog:", + "npm-run-all": "^4.1.5", + "prettier": "catalog:", + "react": "catalog:react19", + "react-dom": "catalog:react19", + "typescript": "catalog:", + "vite": "^7.1.2", + "vite-plugin-electron": "^0.29.0", + "vite-plugin-electron-renderer": "^0.14.6", + "wait-on": "^8.0.4" + }, + "build": { + "appId": "com.lightfast.desktop", + "productName": "Lightfast", + "directories": { + "output": "out" + }, + "files": [ + "dist", + "dist-electron", + "!node_modules/**/*" + ], + "mac": { + "category": "public.app-category.developer-tools", + "target": [ + { + "target": "dmg", + "arch": ["x64", "arm64"] + }, + { + "target": "zip", + "arch": ["x64", "arm64"] + } + ] + }, + "win": { + "target": [ + { + "target": "nsis", + "arch": ["x64"] + } + ] + }, + "linux": { + "target": [ + { + "target": "AppImage", + "arch": ["x64"] + }, + { + "target": "deb", + "arch": ["x64"] + } + ], + "category": "Development" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true + } + }, + "prettier": "@repo/prettier-config" +} \ No newline at end of file diff --git a/apps/desktop/public/icon.png b/apps/desktop/public/icon.png new file mode 100644 index 000000000..92e9d4795 --- /dev/null +++ b/apps/desktop/public/icon.png @@ -0,0 +1,2 @@ +# Placeholder for icon.png +# Replace this with your actual icon file (512x512 PNG recommended) \ No newline at end of file diff --git a/apps/desktop/src/renderer/App.tsx b/apps/desktop/src/renderer/App.tsx new file mode 100644 index 000000000..f46786828 --- /dev/null +++ b/apps/desktop/src/renderer/App.tsx @@ -0,0 +1,94 @@ +import React, { useEffect, useState } from 'react'; + +interface AppInfo { + name: string; + version: string; + platform: string; +} + +declare global { + interface Window { + electron: { + app: { + getName: () => Promise; + getVersion: () => Promise; + }; + platform: string; + }; + } +} + +function App(): React.ReactElement { + const [appInfo, setAppInfo] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadAppInfo = async (): Promise => { + try { + const [name, version] = await Promise.all([ + window.electron.app.getName(), + window.electron.app.getVersion(), + ]); + + setAppInfo({ + name, + version, + platform: window.electron.platform, + }); + } catch (error) { + console.error('Failed to load app info:', error); + } finally { + setLoading(false); + } + }; + + loadAppInfo(); + }, []); + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+
+
+

⚡ Lightfast Desktop

+
+ +
+

Hello World!

+

Welcome to the Lightfast Desktop Application

+ + {appInfo && ( +
+

App: {appInfo.name}

+

Version: {appInfo.version}

+

Platform: {appInfo.platform}

+
+ )} + +
+

Auto-update enabled

+
+ +
+

Built with:

+
    +
  • Electron + TypeScript
  • +
  • React 18
  • +
  • Vite
  • +
  • Type-safe IPC
  • +
+
+
+
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/apps/desktop/src/renderer/index.css b/apps/desktop/src/renderer/index.css new file mode 100644 index 000000000..d156a25e8 --- /dev/null +++ b/apps/desktop/src/renderer/index.css @@ -0,0 +1,155 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + height: 100vh; + overflow: hidden; +} + +.app { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + padding: 20px; +} + +.loading { + font-size: 24px; + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.app-header { + text-align: center; + max-width: 600px; + width: 100%; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border-radius: 20px; + padding: 40px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2); +} + +.logo-container h1 { + font-size: 36px; + margin-bottom: 30px; + background: linear-gradient(to right, #ffd700, #ffed4e); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.info-container h2 { + font-size: 28px; + margin-bottom: 10px; +} + +.info-container p { + font-size: 16px; + margin-bottom: 20px; + opacity: 0.9; +} + +.app-info { + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + padding: 20px; + margin: 20px 0; +} + +.app-info p { + margin: 5px 0; + font-size: 14px; +} + +.actions { + margin: 30px 0; +} + +.update-button { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +.update-button:hover { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3); +} + +.update-button:active { + transform: translateY(0); +} + +.update-status { + margin-top: 15px; + font-size: 14px; +} + +.status-checking { + color: #ffd700; +} + +.status-available { + color: #4ade80; +} + +.status-downloaded { + color: #60a5fa; +} + +.status-error { + color: #f87171; +} + +.tech-stack { + margin-top: 30px; + text-align: left; +} + +.tech-stack h3 { + font-size: 18px; + margin-bottom: 10px; + text-align: center; +} + +.tech-stack ul { + list-style: none; + padding: 0; +} + +.tech-stack li { + padding: 8px 15px; + margin: 5px 0; + background: rgba(255, 255, 255, 0.1); + border-radius: 5px; + transition: background 0.2s; +} + +.tech-stack li:hover { + background: rgba(255, 255, 255, 0.15); +} \ No newline at end of file diff --git a/apps/desktop/src/renderer/main.tsx b/apps/desktop/src/renderer/main.tsx new file mode 100644 index 000000000..fb7690c50 --- /dev/null +++ b/apps/desktop/src/renderer/main.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +const root = document.getElementById('root'); + +if (!root) { + throw new Error('Root element not found'); +} + +ReactDOM.createRoot(root).render( + + + +); \ No newline at end of file diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json new file mode 100644 index 000000000..99a8ac497 --- /dev/null +++ b/apps/desktop/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "@repo/typescript-config/nextjs.json", + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "types": ["vite/client", "node"], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*", "electron/**/*", "vite.config.mts"], + "exclude": ["node_modules", "dist", "dist-electron", "out"] +} \ No newline at end of file diff --git a/apps/desktop/vite.config.mts b/apps/desktop/vite.config.mts new file mode 100644 index 000000000..7111dbd8f --- /dev/null +++ b/apps/desktop/vite.config.mts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import electron from 'vite-plugin-electron/simple'; +import { fileURLToPath } from 'node:url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); + +export default defineConfig({ + plugins: [ + react(), + electron({ + main: { + entry: 'electron/main.ts', + }, + preload: { + input: __dirname + 'electron/preload.ts', + }, + renderer: {}, + }), + ], + resolve: { + alias: { + '@': __dirname + 'src', + }, + }, +}); \ No newline at end of file diff --git a/apps/www/src/app/(app)/download/page.tsx b/apps/www/src/app/(app)/download/page.tsx new file mode 100644 index 000000000..86e39cd94 --- /dev/null +++ b/apps/www/src/app/(app)/download/page.tsx @@ -0,0 +1,161 @@ +import type { Metadata } from "next"; +import { DownloadIcon, MonitorIcon, AppleIcon, WindowsIcon, LinuxIcon } from "lucide-react"; + +export const metadata: Metadata = { + title: "Download Lightfast Desktop", + description: "Download Lightfast Desktop app for macOS, Windows, and Linux", +}; + +interface Platform { + name: string; + icon: React.ComponentType<{ className?: string }>; + primary: string; + secondary?: string; + downloadUrl: string; + version: string; + requirements: string[]; +} + +const platforms: Platform[] = [ + { + name: "macOS", + icon: AppleIcon, + primary: "Download for macOS", + secondary: "Universal (Intel + Apple Silicon)", + downloadUrl: "https://github.com/lightfastai/lightfast/releases/latest/download/Lightfast.dmg", + version: "0.1.0", + requirements: ["macOS 10.15 or later", "64-bit processor"], + }, + { + name: "Windows", + icon: WindowsIcon, + primary: "Download for Windows", + secondary: "64-bit", + downloadUrl: "https://github.com/lightfastai/lightfast/releases/latest/download/Lightfast-Setup.exe", + version: "0.1.0", + requirements: ["Windows 10 or later", "64-bit processor"], + }, + { + name: "Linux", + icon: LinuxIcon, + primary: "Download for Linux", + secondary: "AppImage", + downloadUrl: "https://github.com/lightfastai/lightfast/releases/latest/download/Lightfast.AppImage", + version: "0.1.0", + requirements: ["64-bit Linux distribution", "GLIBC 2.29 or later"], + }, +]; + +export default function DownloadPage() { + return ( +
+
+
+ +
+

Download Lightfast Desktop

+

+ Experience the power of Lightfast with our native desktop application. + Available for macOS, Windows, and Linux. +

+
+ +
+ {platforms.map((platform) => { + const Icon = platform.icon; + return ( +
+
+ +
+

{platform.name}

+

Version {platform.version}

+
+ + + + {platform.primary} + + + {platform.secondary && ( +

{platform.secondary}

+ )} + +
+

Requirements:

+
    + {platform.requirements.map((req) => ( +
  • {req}
  • + ))} +
+
+
+
+ ); + })} +
+ +
+

Features

+
+
+
+
+

Native Performance

+

+ Built with Electron for optimal performance on your desktop +

+
+
+
+
+
+

Type-Safe Architecture

+

+ Fully typed with TypeScript for reliability and maintainability +

+
+
+
+
+
+

Auto Updates

+

+ Automatic updates ensure you always have the latest features +

+
+
+
+
+
+

Cross-Platform

+

+ Consistent experience across macOS, Windows, and Linux +

+
+
+
+
+ +
+

+ Need help? Check out our{" "} + + desktop documentation + {" "} + or{" "} + + report an issue + + . +

+
+
+ ); +} \ No newline at end of file diff --git a/apps/www/src/components/site-header.tsx b/apps/www/src/components/site-header.tsx index c2396e158..a141d0799 100644 --- a/apps/www/src/components/site-header.tsx +++ b/apps/www/src/components/site-header.tsx @@ -19,6 +19,12 @@ export function SiteHeader() { > Docs + + Download +