feat: add Electron desktop application support

- Add electron/ directory with main process, preload script, and types
- Add system tray integration and window management
- Add IPC communication for app info, external links, server controls
- Add useElectron React hooks for Next.js integration
- Add build scripts for Windows, macOS, and Linux
- Add development scripts for running Electron with Next.js

Ref: #149
This commit is contained in:
benzntech
2026-02-28 10:40:25 +05:30
parent 6ed98fb21c
commit b004d0472b
9 changed files with 6747 additions and 21 deletions
+150
View File
@@ -0,0 +1,150 @@
# OmniRoute Electron Desktop App
This directory contains the Electron desktop application wrapper for OmniRoute.
## Structure
```
electron/
├── main.js # Main process (window management, IPC)
├── preload.js # Preload script (secure bridge to renderer)
├── package.json # Electron-specific dependencies
├── types.d.ts # TypeScript definitions
└── assets/ # Application icons and resources
```
## Development
### Prerequisites
1. Build the Next.js app first:
```bash
npm run build
```
2. Install Electron dependencies:
```bash
cd electron
npm install
```
### Running in Development
1. Start the Next.js development server:
```bash
npm run dev
```
2. In another terminal, start Electron:
```bash
cd electron
npm run dev
```
### Running in Production Mode
1. Build Next.js in standalone mode:
```bash
npm run build
```
2. Start Electron:
```bash
cd electron
npm start
```
## Building
### Build for Current Platform
```bash
cd electron
npm run build
```
### Build for Specific Platforms
```bash
# Windows
npm run build:win
# macOS
npm run build:mac
# Linux
npm run build:linux
```
## Output
Built applications are placed in `dist-electron/`:
- Windows: `.exe` installer (NSIS)
- macOS: `.dmg` installer
- Linux: `.AppImage`
## Features
- **System Tray Integration**: Minimize to tray, quick actions
- **Native Notifications**: Desktop notifications
- **Window Management**: Minimize, maximize, close
- **Auto-start**: Option to launch on system startup
- **Offline Support**: Local server bundled with the app
## Configuration
### Environment Variables
The Electron app respects these environment variables:
- `OMNIROUTE_PORT`: Server port (default: 20128)
- `NODE_ENV`: Set to 'production' for production builds
### Custom Icon
Place your icons in `assets/`:
- `icon.ico` - Windows icon (256x256)
- `icon.icns` - macOS icon bundle
- `icon.png` - Linux/general use (512x512)
- `tray-icon.png` - System tray icon (16x16 or 32x32)
## IPC Channels
| Channel | Direction | Description |
|---------|-----------|-------------|
| `get-app-info` | Renderer → Main | Get app name, version, platform |
| `open-external` | Renderer → Main | Open URL in default browser |
| `get-data-dir` | Renderer → Main | Get data directory path |
| `restart-server` | Renderer → Main | Restart the internal server |
| `server-status` | Main → Renderer | Server status updates |
| `port-changed` | Main → Renderer | Port change notifications |
## Security
- `contextIsolation: true` - Isolates renderer from Node.js
- `nodeIntegration: false` - No direct Node.js access in renderer
- Preload script validates IPC channels
- No remote code execution
## Troubleshooting
### App Won't Start
1. Check if port 20128 is available
2. Check logs in the console
3. Verify the build output exists
### White Screen
1. Verify Next.js build exists
2. Check the server URL in main.js
3. Check for console errors
### Build Fails
1. Ensure you have build tools installed:
- Windows: Visual Studio Build Tools
- macOS: Xcode Command Line Tools
- Linux: build-essential, libsecret-1-dev
## License
MIT
+282
View File
@@ -0,0 +1,282 @@
/**
* OmniRoute Electron Desktop App - Main Process
*
* This is the entry point for the Electron desktop application.
* It manages the main window, system tray, and IPC communication.
*/
const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, shell } = require('electron');
const path = require('path');
const { spawn } = require('child_process');
const fs = require('fs');
// Environment detection
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
const isProduction = !isDev;
// Paths
const APP_PATH = app.getAppPath();
const RESOURCES_PATH = isProduction ? process.resourcesPath : APP_PATH;
const NEXT_SERVER_PATH = path.join(RESOURCES_PATH, 'app');
// State
let mainWindow = null;
let tray = null;
let nextServer = null;
let serverPort = 20128;
// Server URL
const getServerUrl = () => `http://localhost:${serverPort}`;
/**
* Create the main application window
*/
function createWindow() {
mainWindow = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1024,
minHeight: 700,
title: 'OmniRoute',
icon: path.join(RESOURCES_PATH, 'assets', 'icon.png'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
webSecurity: true,
},
show: false,
backgroundColor: '#0a0a0a',
titleBarStyle: 'hiddenInset',
trafficLightPosition: { x: 16, y: 16 },
});
// Load the Next.js app
if (isDev) {
mainWindow.loadURL(getServerUrl());
mainWindow.webContents.openDevTools({ mode: 'detach' });
} else {
mainWindow.loadURL(getServerUrl());
}
// Show window when ready
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
// Handle external links
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
// Handle window close
mainWindow.on('close', (event) => {
if (!app.isQuitting) {
event.preventDefault();
mainWindow.hide();
}
return false;
});
mainWindow.on('closed', () => {
mainWindow = null;
});
}
/**
* Create system tray icon
*/
function createTray() {
const iconPath = path.join(RESOURCES_PATH, 'assets', 'tray-icon.png');
let icon;
try {
icon = nativeImage.createFromPath(iconPath);
if (icon.isEmpty()) {
icon = nativeImage.createEmpty();
}
} catch {
icon = nativeImage.createEmpty();
}
tray = new Tray(icon);
const contextMenu = Menu.buildFromTemplate([
{
label: 'Open OmniRoute',
click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
},
},
{
label: 'Open Dashboard',
click: () => {
shell.openExternal(getServerUrl());
},
},
{ type: 'separator' },
{
label: 'Server Port',
submenu: [
{ label: `Port: ${serverPort}`, enabled: false },
{ type: 'separator' },
{ label: '20128', click: () => changePort(20128) },
{ label: '3000', click: () => changePort(3000) },
{ label: '8080', click: () => changePort(8080) },
],
},
{ type: 'separator' },
{
label: 'Quit',
click: () => {
app.isQuitting = true;
app.quit();
},
},
]);
tray.setToolTip('OmniRoute');
tray.setContextMenu(contextMenu);
tray.on('double-click', () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
});
}
/**
* Change the server port
*/
function changePort(port) {
if (port === serverPort) return;
serverPort = port;
if (mainWindow) {
mainWindow.loadURL(getServerUrl());
}
createTray();
}
/**
* Start the Next.js server (production mode)
*/
function startNextServer() {
if (isDev) {
console.log('Development mode: Connect to existing Next.js server');
return;
}
const serverScript = path.join(NEXT_SERVER_PATH, 'server.js');
if (!fs.existsSync(serverScript)) {
console.error('Server script not found:', serverScript);
return;
}
console.log('Starting Next.js server...');
nextServer = spawn('node', [serverScript], {
cwd: NEXT_SERVER_PATH,
env: {
...process.env,
PORT: String(serverPort),
NODE_ENV: 'production',
},
stdio: 'inherit',
});
nextServer.on('error', (err) => {
console.error('Failed to start server:', err);
});
nextServer.on('exit', (code) => {
console.log('Server exited with code:', code);
});
}
/**
* Stop the Next.js server
*/
function stopNextServer() {
if (nextServer) {
nextServer.kill();
nextServer = null;
}
}
/**
* IPC Handlers
*/
function setupIpcHandlers() {
// Get app info
ipcMain.handle('get-app-info', () => ({
name: app.getName(),
version: app.getVersion(),
platform: process.platform,
isDev,
port: serverPort,
}));
// Open external URL
ipcMain.handle('open-external', (event, url) => {
shell.openExternal(url);
});
// Get data directory
ipcMain.handle('get-data-dir', () => {
const home = require('os').homedir();
return path.join(home, '.omniroute');
});
// Restart server
ipcMain.handle('restart-server', async () => {
stopNextServer();
await new Promise(resolve => setTimeout(resolve, 1000));
startNextServer();
return { success: true };
});
}
// App lifecycle events
app.whenReady().then(() => {
startNextServer();
createWindow();
createTray();
setupIpcHandlers();
// macOS: recreate window when dock icon clicked
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
} else if (mainWindow) {
mainWindow.show();
}
});
});
// Quit when all windows are closed (except on macOS)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// Clean up before quit
app.on('before-quit', () => {
app.isQuitting = true;
stopNextServer();
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled Rejection:', reason);
});
+5278
View File
File diff suppressed because it is too large Load Diff
+105
View File
@@ -0,0 +1,105 @@
{
"name": "omniroute-desktop",
"version": "1.0.0",
"description": "OmniRoute Desktop Application",
"main": "main.js",
"author": "OmniRoute Team",
"license": "MIT",
"homepage": "https://omniroute.online",
"scripts": {
"start": "electron .",
"dev": "electron . --no-sandbox",
"build": "electron-builder",
"build:win": "electron-builder --win",
"build:mac": "electron-builder --mac",
"build:linux": "electron-builder --linux",
"pack": "electron-builder --dir"
},
"dependencies": {},
"devDependencies": {
"electron": "^33.0.0",
"electron-builder": "^25.1.8"
},
"build": {
"appId": "online.omniroute.desktop",
"productName": "OmniRoute",
"copyright": "Copyright © 2025 OmniRoute",
"directories": {
"output": "dist-electron",
"buildResources": "assets"
},
"files": [
"main.js",
"preload.js",
"package.json"
],
"extraResources": [
{
"from": "../.next/standalone",
"to": "app",
"filter": ["**/*"]
},
{
"from": "../.next/static",
"to": "app/.next/static",
"filter": ["**/*"]
},
{
"from": "../public",
"to": "app/public",
"filter": ["**/*"]
}
],
"win": {
"target": [
{
"target": "nsis",
"arch": ["x64"]
}
],
"icon": "assets/icon.ico"
},
"mac": {
"target": [
{
"target": "dmg",
"arch": ["x64", "arm64"]
}
],
"icon": "assets/icon.icns",
"category": "public.app-category.productivity"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": ["x64"]
}
],
"icon": "assets/icons",
"category": "Utility"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"installerIcon": "assets/icon.ico",
"uninstallerIcon": "assets/icon.ico"
},
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
]
}
}
}
+140
View File
@@ -0,0 +1,140 @@
/**
* OmniRoute Electron Desktop App - Preload Script
*
* This script runs in a separate context before the web page loads.
* It provides a secure bridge between the renderer process (Next.js app)
* and the main process (Electron).
*
* Security: Uses contextIsolation: true for maximum security.
*/
const { contextBridge, ipcRenderer } = require('electron');
// Valid IPC channels for security
const VALID_CHANNELS = {
invoke: [
'get-app-info',
'open-external',
'get-data-dir',
'restart-server',
],
send: [
'window-minimize',
'window-maximize',
'window-close',
],
receive: [
'server-status',
'port-changed',
],
};
/**
* Expose a limited API to the renderer process
*/
contextBridge.exposeInMainWorld('electronAPI', {
/**
* Get application information
* @returns {Promise<{name: string, version: string, platform: string, isDev: boolean, port: number}>}
*/
getAppInfo: () => ipcRenderer.invoke('get-app-info'),
/**
* Open an external URL in the default browser
* @param {string} url - The URL to open
*/
openExternal: (url) => ipcRenderer.invoke('open-external', url),
/**
* Get the data directory path
* @returns {Promise<string>}
*/
getDataDir: () => ipcRenderer.invoke('get-data-dir'),
/**
* Restart the server
* @returns {Promise<{success: boolean}>}
*/
restartServer: () => ipcRenderer.invoke('restart-server'),
/**
* Minimize the window
*/
minimizeWindow: () => ipcRenderer.send('window-minimize'),
/**
* Maximize/unmaximize the window
*/
maximizeWindow: () => ipcRenderer.send('window-maximize'),
/**
* Close the window
*/
closeWindow: () => ipcRenderer.send('window-close'),
/**
* Listen for server status updates
* @param {function} callback - Callback function
*/
onServerStatus: (callback) => {
ipcRenderer.on('server-status', (event, data) => callback(data));
},
/**
* Remove server status listener
*/
removeServerStatusListener: () => {
ipcRenderer.removeAllListeners('server-status');
},
/**
* Listen for port changes
* @param {function} callback - Callback function
*/
onPortChanged: (callback) => {
ipcRenderer.on('port-changed', (event, port) => callback(port));
},
/**
* Remove port changed listener
*/
removePortChangedListener: () => {
ipcRenderer.removeAllListeners('port-changed');
},
/**
* Check if running in Electron
* @returns {boolean}
*/
isElectron: true,
/**
* Get the platform
* @returns {string}
*/
platform: process.platform,
});
/**
* Type definition for the exposed API (for TypeScript support)
* This can be referenced in the Next.js app for type safety.
*/
// declare global {
// interface Window {
// electronAPI: {
// getAppInfo: () => Promise<AppInfo>;
// openExternal: (url: string) => Promise<void>;
// getDataDir: () => Promise<string>;
// restartServer: () => Promise<{success: boolean}>;
// minimizeWindow: () => void;
// maximizeWindow: () => void;
// closeWindow: () => void;
// onServerStatus: (callback: (data: any) => void) => void;
// removeServerStatusListener: () => void;
// onPortChanged: (callback: (port: number) => void) => void;
// removePortChangedListener: () => void;
// isElectron: boolean;
// platform: string;
// };
// }
// }
+88
View File
@@ -0,0 +1,88 @@
/**
* OmniRoute Electron Types
*
* TypeScript definitions for the Electron API exposed to the renderer process.
*/
export interface AppInfo {
name: string;
version: string;
platform: 'win32' | 'darwin' | 'linux';
isDev: boolean;
port: number;
}
export interface ElectronAPI {
/**
* Get application information
*/
getAppInfo(): Promise<AppInfo>;
/**
* Open an external URL in the default browser
*/
openExternal(url: string): Promise<void>;
/**
* Get the data directory path
*/
getDataDir(): Promise<string>;
/**
* Restart the server
*/
restartServer(): Promise<{ success: boolean }>;
/**
* Minimize the window
*/
minimizeWindow(): void;
/**
* Maximize/unmaximize the window
*/
maximizeWindow(): void;
/**
* Close the window
*/
closeWindow(): void;
/**
* Listen for server status updates
*/
onServerStatus(callback: (data: { status: string; port: number }) => void): void;
/**
* Remove server status listener
*/
removeServerStatusListener(): void;
/**
* Listen for port changes
*/
onPortChanged(callback: (port: number) => void): void;
/**
* Remove port changed listener
*/
removePortChangedListener(): void;
/**
* Check if running in Electron
*/
isElectron: boolean;
/**
* Get the platform
*/
platform: 'win32' | 'darwin' | 'linux';
}
declare global {
interface Window {
electronAPI: ElectronAPI;
}
}
export {};
+503 -20
View File
@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "1.6.2",
"version": "1.6.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "1.6.2",
"version": "1.6.3",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
@@ -53,6 +53,8 @@
"@types/node": "^25.2.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"eslint": "^9.39.2",
"eslint-config-next": "16.1.6",
"husky": "^9.1.7",
@@ -61,7 +63,8 @@
"tailwindcss": "^4",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.0"
"typescript-eslint": "^8.56.0",
"wait-on": "^9.0.4"
},
"engines": {
"node": ">=18.0.0 <24.0.0"
@@ -366,6 +369,13 @@
"tslib": "^2.4.0"
}
},
"node_modules/@epic-web/invariant": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
"dev": true,
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
@@ -1004,6 +1014,60 @@
"tslib": "^2.8.1"
}
},
"node_modules/@hapi/address": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",
"integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^11.0.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@hapi/formula": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz",
"integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@hapi/hoek": {
"version": "11.0.7",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz",
"integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@hapi/pinpoint": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz",
"integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@hapi/tlds": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz",
"integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@hapi/topo": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz",
"integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^11.0.2"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -2281,7 +2345,7 @@
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
@@ -3011,7 +3075,7 @@
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -3918,6 +3982,13 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
@@ -3953,6 +4024,18 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -4337,6 +4420,94 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/cliui/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/cliui/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -4372,6 +4543,19 @@
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
@@ -4389,6 +4573,47 @@
"dev": true,
"license": "MIT"
},
"node_modules/concurrently": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "4.1.2",
"rxjs": "7.8.2",
"shell-quote": "1.8.3",
"supports-color": "8.1.1",
"tree-kill": "1.2.2",
"yargs": "17.7.2"
},
"bin": {
"conc": "dist/bin/concurrently.js",
"concurrently": "dist/bin/concurrently.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/concurrently/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/content-disposition": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
@@ -4436,6 +4661,24 @@
"node": ">=6.6.0"
}
},
"node_modules/cross-env": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@epic-web/invariant": "^1.0.0",
"cross-spawn": "^7.0.6"
},
"bin": {
"cross-env": "dist/bin/cross-env.js",
"cross-env-shell": "dist/bin/cross-env-shell.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4455,7 +4698,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/d3-array": {
@@ -4785,6 +5028,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -5859,6 +6112,46 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -5958,6 +6251,16 @@
"node": ">=6.9.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
@@ -7034,6 +7337,25 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/joi": {
"version": "18.0.2",
"resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz",
"integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/address": "^5.1.1",
"@hapi/formula": "^3.0.2",
"@hapi/hoek": "^11.0.7",
"@hapi/pinpoint": "^2.0.1",
"@hapi/tlds": "^1.1.1",
"@hapi/topo": "^6.0.2",
"@standard-schema/spec": "^1.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/jose": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
@@ -7506,6 +7828,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -7953,17 +8282,6 @@
}
}
},
"node_modules/next-intl/node_modules/@swc/helpers": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
"integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -8508,7 +8826,7 @@
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
@@ -8527,7 +8845,7 @@
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@@ -8686,6 +9004,13 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true,
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
@@ -8839,6 +9164,7 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/react-redux": {
@@ -8988,6 +9314,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -9127,6 +9463,16 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-array-concat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -9443,6 +9789,19 @@
"node": ">=8"
}
},
"node_modules/shell-quote": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@@ -10084,6 +10443,16 @@
"node": ">=0.6"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true,
"license": "MIT",
"bin": {
"tree-kill": "cli.js"
}
},
"node_modules/ts-api-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
@@ -10290,7 +10659,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -10523,6 +10892,26 @@
"d3-timer": "^3.0.1"
}
},
"node_modules/wait-on": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz",
"integrity": "sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"axios": "^1.13.5",
"joi": "^18.0.2",
"lodash": "^4.17.23",
"minimist": "^1.2.8",
"rxjs": "^7.8.2"
},
"bin": {
"wait-on": "bin/wait-on"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -10734,6 +11123,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -10757,6 +11156,90 @@
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/yargs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/yargs/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+9 -1
View File
@@ -48,6 +48,11 @@
"build:cli": "node scripts/prepublish.mjs",
"start": "node scripts/run-next.mjs start",
"lint": "eslint .",
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:20128 && cd electron && npm run dev\"",
"electron:build": "npm run build && cd electron && npm run build",
"electron:build:win": "npm run build && cd electron && npm run build:win",
"electron:build:mac": "npm run build && cd electron && npm run build:mac",
"electron:build:linux": "npm run build && cd electron && npm run build:linux",
"test": "node --import tsx/esm --test tests/unit/*.test.mjs",
"test:unit": "node --import tsx/esm --test tests/unit/*.test.mjs",
"test:plan3": "node --test tests/unit/plan3-p0.test.mjs",
@@ -98,6 +103,8 @@
"@types/node": "^25.2.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"eslint": "^9.39.2",
"eslint-config-next": "16.1.6",
"husky": "^9.1.7",
@@ -106,7 +113,8 @@
"tailwindcss": "^4",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.0"
"typescript-eslint": "^8.56.0",
"wait-on": "^9.0.4"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,mjs}": [
+192
View File
@@ -0,0 +1,192 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
/**
* Check if the app is running in Electron
*/
export function useIsElectron(): boolean {
const [isElectron, setIsElectron] = useState(false);
useEffect(() => {
setIsElectron(typeof window !== 'undefined' && window.electronAPI?.isElectron === true);
}, []);
return isElectron;
}
/**
* Get Electron app information
*/
export function useElectronAppInfo() {
const [appInfo, setAppInfo] = useState<{
name: string;
version: string;
platform: string;
isDev: boolean;
port: number;
} | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (typeof window === 'undefined' || !window.electronAPI) {
setLoading(false);
return;
}
window.electronAPI
.getAppInfo()
.then((info) => {
setAppInfo(info);
setLoading(false);
})
.catch((err) => {
setError(err);
setLoading(false);
});
}, []);
return { appInfo, loading, error };
}
/**
* Get the data directory path
*/
export function useDataDir() {
const [dataDir, setDataDir] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (typeof window === 'undefined' || !window.electronAPI) {
setLoading(false);
return;
}
window.electronAPI
.getDataDir()
.then((dir) => {
setDataDir(dir);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
}, []);
return { dataDir, loading };
}
/**
* Window controls for Electron
*/
export function useWindowControls() {
const isElectron = useIsElectron();
const minimize = useCallback(() => {
if (isElectron && window.electronAPI) {
window.electronAPI.minimizeWindow();
}
}, [isElectron]);
const maximize = useCallback(() => {
if (isElectron && window.electronAPI) {
window.electronAPI.maximizeWindow();
}
}, [isElectron]);
const close = useCallback(() => {
if (isElectron && window.electronAPI) {
window.electronAPI.closeWindow();
}
}, [isElectron]);
return {
isElectron,
minimize,
maximize,
close,
};
}
/**
* Open external URL in default browser
*/
export function useOpenExternal() {
const isElectron = useIsElectron();
const openExternal = useCallback(
async (url: string) => {
if (isElectron && window.electronAPI) {
await window.electronAPI.openExternal(url);
} else {
window.open(url, '_blank', 'noopener,noreferrer');
}
},
[isElectron]
);
return { openExternal };
}
/**
* Server controls for Electron
*/
export function useServerControls() {
const isElectron = useIsElectron();
const [restarting, setRestarting] = useState(false);
const restart = useCallback(async () => {
if (!isElectron || !window.electronAPI) {
return { success: false };
}
setRestarting(true);
try {
const result = await window.electronAPI.restartServer();
return result;
} finally {
setRestarting(false);
}
}, [isElectron]);
return {
isElectron,
restart,
restarting,
};
}
/**
* Listen for server status updates
*/
export function useServerStatus(onStatus: (status: { status: string; port: number }) => void) {
const isElectron = useIsElectron();
useEffect(() => {
if (!isElectron || !window.electronAPI) return;
window.electronAPI.onServerStatus(onStatus);
return () => {
window.electronAPI.removeServerStatusListener();
};
}, [isElectron, onStatus]);
}
/**
* Listen for port changes
*/
export function usePortChanged(onPortChanged: (port: number) => void) {
const isElectron = useIsElectron();
useEffect(() => {
if (!isElectron || !window.electronAPI) return;
window.electronAPI.onPortChanged(onPortChanged);
return () => {
window.electronAPI.removePortChangedListener();
};
}, [isElectron, onPortChanged]);
}