create-athenea-app 0.1.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,36 @@
1
1
  # create-athenea-app
2
2
 
3
- Paquete CLI para generar un proyecto basado en esta plantilla.
3
+ CLI para generar una app de escritorio lista para laburar con **Electron + Preact + Vite**.
4
4
 
5
- Uso (cuando este publicado):
5
+ Usa [electron-vite](https://electron-vite.org/) para un flujo de desarrollo integrado:
6
+
7
+ - **HMR real** para el renderer (Preact)
8
+ - **Hot reload** para main y preload
9
+ - **Build unificado** de main, preload y renderer
10
+ - **electron-builder** listo para empaquetar
11
+
12
+ ## Uso
6
13
 
7
14
  ```bash
8
15
  npm create athenea-app@latest
9
16
  ```
10
17
 
11
- Tambien podes pasar el nombre directo:
18
+ O con nombre directo:
12
19
 
13
20
  ```bash
14
21
  npm create athenea-app@latest mi-proyecto
15
22
  ```
16
23
 
17
- Opciones:
24
+ ## Opciones
25
+
26
+ - `--no-install`: no ejecuta `npm install` automaticamente
27
+ - `--help`: muestra la ayuda
28
+
29
+ ## Stack incluido
18
30
 
19
- - `--no-install`: no ejecuta `npm install`
31
+ - **Electron** - App de escritorio multiplataforma
32
+ - **Preact** - UI rapida y liviana (3kB)
33
+ - **preact-iso** - Routing simple
34
+ - **Vite** - Dev server ultra rapido + build optimizado
35
+ - **electron-vite** - Integra Vite con Electron (main + preload + renderer)
36
+ - **electron-builder** - Empaquetado para Windows, macOS, Linux
package/bin/index.js CHANGED
@@ -189,34 +189,26 @@ function bmpSolid24(width, height, r, g, b) {
189
189
  return Buffer.concat([fileHeader, dib, pixels]);
190
190
  }
191
191
 
192
- async function ensureWhiteBuildAssets(projectDir) {
193
- const assetsDir = path.join(projectDir, "assets");
194
- const publicDir = path.join(projectDir, "public");
195
- await fsp.mkdir(assetsDir, { recursive: true });
196
- await fsp.mkdir(publicDir, { recursive: true });
192
+ async function ensureBuildAssets(projectDir) {
193
+ // electron-vite usa "resources" como directorio de assets para main/preload
194
+ const resourcesDir = path.join(projectDir, "resources");
195
+ await fsp.mkdir(resourcesDir, { recursive: true });
197
196
 
198
197
  const png256 = pngSolidRGBA(256, 256, 255, 255, 255, 255);
199
198
  const ico = icoFromPng(png256, 256);
200
- await fsp.writeFile(path.join(assetsDir, "build.ico"), ico);
199
+
200
+ // PNG para Linux, ICO para Windows
201
+ await fsp.writeFile(path.join(resourcesDir, "icon.png"), png256);
202
+ await fsp.writeFile(path.join(resourcesDir, "icon.ico"), ico);
201
203
 
202
204
  await fsp.writeFile(
203
- path.join(assetsDir, "installer-sidebar.bmp"),
205
+ path.join(resourcesDir, "installer-sidebar.bmp"),
204
206
  bmpSolid24(164, 314, 255, 255, 255),
205
207
  );
206
208
  await fsp.writeFile(
207
- path.join(assetsDir, "installer-header.bmp"),
209
+ path.join(resourcesDir, "installer-header.bmp"),
208
210
  bmpSolid24(150, 57, 255, 255, 255),
209
211
  );
210
-
211
- const viteSvgPath = path.join(publicDir, "vite.svg");
212
- if (!fs.existsSync(viteSvgPath)) {
213
- const svg =
214
- '<?xml version="1.0" encoding="UTF-8"?>\n' +
215
- '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">\n' +
216
- ' <rect width="64" height="64" fill="#ffffff"/>\n' +
217
- "</svg>\n";
218
- await fsp.writeFile(viteSvgPath, svg, "utf8");
219
- }
220
212
  }
221
213
 
222
214
  async function npmInstall(projectDir) {
@@ -282,24 +274,39 @@ async function main() {
282
274
  __APP_APP_ID__: appId,
283
275
  };
284
276
 
277
+ // Reemplazar tokens en los archivos correspondientes (nueva estructura electron-vite)
285
278
  await Promise.all([
286
- replaceTokensInFile(path.join(targetDir, "electron.js"), tokenMap),
287
- replaceTokensInFile(path.join(targetDir, "index.html"), tokenMap),
279
+ replaceTokensInFile(
280
+ path.join(targetDir, "src", "main", "index.js"),
281
+ tokenMap,
282
+ ),
283
+ replaceTokensInFile(
284
+ path.join(targetDir, "src", "renderer", "index.html"),
285
+ tokenMap,
286
+ ),
288
287
  replaceTokensInFile(path.join(targetDir, "README.md"), tokenMap),
289
288
  replaceTokensInFile(
290
- path.join(targetDir, "src", "routes", "Home", "Home.jsx"),
289
+ path.join(
290
+ targetDir,
291
+ "src",
292
+ "renderer",
293
+ "src",
294
+ "routes",
295
+ "Home",
296
+ "Home.jsx",
297
+ ),
291
298
  tokenMap,
292
299
  ),
293
300
  ]);
294
301
 
295
- await ensureWhiteBuildAssets(targetDir);
302
+ await ensureBuildAssets(targetDir);
296
303
 
297
304
  console.log("");
298
305
  console.log(`Proyecto creado en: ${targetDir}`);
299
306
  console.log("");
300
307
  console.log("Siguientes pasos:");
301
308
  console.log(` cd ${npmName}`);
302
- console.log(" npm run dev:electron");
309
+ console.log(" npm run dev");
303
310
 
304
311
  if (!noInstall) {
305
312
  console.log("");
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "create-athenea-app",
3
- "version": "0.1.1",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
- "description": "Creador de proyectos Athenea (Electron + Preact + Vite)",
5
+ "description": "CLI para scaffoldear apps de escritorio con Electron + Preact + Vite (DX rapida: dev server, hot reload, build y empaquetado)",
6
6
  "keywords": [
7
7
  "create",
8
8
  "starter",
9
9
  "template",
10
10
  "electron",
11
+ "electron-vite",
11
12
  "preact",
12
- "vite"
13
+ "vite",
14
+ "desktop"
13
15
  ],
14
16
  "bin": {
15
17
  "create-athenea-app": "bin/index.js"
@@ -1,19 +1,57 @@
1
1
  # **APP_TITLE**
2
2
 
3
- Proyecto generado con `create-athenea-app`.
3
+ Aplicación de escritorio creada con [Athenea](https://github.com/tu-usuario/athenea-desktop).
4
+
5
+ ## Stack
6
+
7
+ - **Electron** - Framework para apps de escritorio
8
+ - **electron-vite** - Build tool optimizado para Electron
9
+ - **Preact** - UI library liviana (3KB)
10
+ - **preact-iso** - Router minimalista
11
+
12
+ ## Estructura
13
+
14
+ ```
15
+ src/
16
+ ├── main/ # Proceso principal de Electron
17
+ │ └── index.js
18
+ ├── preload/ # Scripts de preload (bridge seguro)
19
+ │ └── index.js
20
+ └── renderer/ # Aplicación frontend
21
+ ├── index.html
22
+ ├── public/ # Assets estáticos
23
+ └── src/ # Código fuente React/Preact
24
+ ├── main.jsx
25
+ ├── app.jsx
26
+ ├── components/
27
+ └── routes/
28
+ ```
4
29
 
5
30
  ## Scripts
6
31
 
7
32
  ```bash
8
- npm run dev:electron
9
- npm run build
10
- npm run pack
11
- npm run dist
33
+ # Desarrollo
34
+ npm run dev # Inicia en modo desarrollo (Windows)
35
+ npm run dev:linux # Inicia en modo desarrollo (Linux)
36
+
37
+ # Build
38
+ npm run build # Compila la aplicación
39
+ npm run pack # Empaqueta sin crear instalador
40
+ npm run dist # Crea instaladores para la plataforma actual
41
+ npm run dist:win # Crea instalador para Windows
42
+ npm run dist:linux # Crea instalador para Linux
12
43
  ```
13
44
 
14
- ## Branding
45
+ ## IPC disponible
46
+
47
+ El preload expone `window.electronAPI` con:
15
48
 
16
- - Nombre del instalador/app: `package.json` -> `build.productName`
17
- - ID de la app: `package.json` -> `build.appId`
18
- - Titulos: `electron.js` y `index.html`
19
- - Recursos de instalador (placeholders blancos): `assets/build.ico` y `assets/installer-*.bmp`
49
+ ```javascript
50
+ // Guardar/leer configuración persistente
51
+ await window.electronAPI.settings.get();
52
+ await window.electronAPI.settings.set({ key: "value" });
53
+
54
+ // Abrir ventana secundaria con una ruta específica
55
+ window.electronAPI.window.openRoute("/mi-ruta");
56
+ window.electronAPI.window.openRoute({ route: "/mi-ruta", title: "Mi Ventana" });
57
+ ```
@@ -0,0 +1,43 @@
1
+ import { defineConfig, externalizeDepsPlugin } from "electron-vite";
2
+ import preact from "@preact/preset-vite";
3
+ import { resolve } from "path";
4
+
5
+ export default defineConfig({
6
+ main: {
7
+ plugins: [externalizeDepsPlugin()],
8
+ build: {
9
+ rollupOptions: {
10
+ input: {
11
+ index: resolve(__dirname, "src/main/index.js"),
12
+ },
13
+ },
14
+ },
15
+ },
16
+ preload: {
17
+ plugins: [externalizeDepsPlugin()],
18
+ build: {
19
+ rollupOptions: {
20
+ input: {
21
+ index: resolve(__dirname, "src/preload/index.js"),
22
+ },
23
+ },
24
+ },
25
+ },
26
+ renderer: {
27
+ root: "src/renderer",
28
+ publicDir: "public",
29
+ build: {
30
+ rollupOptions: {
31
+ input: {
32
+ index: resolve(__dirname, "src/renderer/index.html"),
33
+ },
34
+ },
35
+ },
36
+ plugins: [preact()],
37
+ resolve: {
38
+ alias: {
39
+ "@": resolve(__dirname, "src/renderer/src"),
40
+ },
41
+ },
42
+ },
43
+ });
@@ -1,103 +1,55 @@
1
1
  {
2
- "name": "__APP_TITLE__",
2
+ "name": "my-athenea-app",
3
3
  "private": true,
4
- "version": "0.1.1",
4
+ "version": "0.1.0",
5
+ "description": "Aplicación de escritorio creada con Athenea",
6
+ "author": "Developer",
5
7
  "type": "module",
6
- "main": "electron.js",
8
+ "main": "./out/main/index.js",
7
9
  "engines": {
8
10
  "node": ">=22.0.0"
9
11
  },
10
12
  "scripts": {
11
- "dev": "vite --host --port 0",
12
- "electron": "cross-env NODE_OPTIONS=\"--experimental-require-module --no-warnings\" electron .",
13
- "dev:electron": "node scripts/dev-electron.mjs",
14
- "copy-preload": "node -e \"const fs=require('fs'); if(!fs.existsSync('dist')) fs.mkdirSync('dist'); fs.copyFileSync('preload.cjs','dist/preload.cjs')\"",
15
- "build": "vite build && npm run copy-preload",
16
- "preview": "vite preview",
13
+ "dev": "electron-vite dev",
14
+ "dev:linux": "electron-vite dev -- --no-sandbox",
15
+ "build": "electron-vite build",
16
+ "preview": "electron-vite preview",
17
17
  "pack": "npm run build && electron-builder --dir",
18
18
  "dist": "npm run build && electron-builder",
19
19
  "dist:linux": "npm run build && electron-builder --linux",
20
- "dist:win": "npm run build && electron-builder --win",
21
- "lint": "eslint .",
22
- "format": "prettier --write .",
23
- "typecheck": "tsc --noEmit",
24
- "test": "vitest",
25
- "postinstall": "electron-builder install-app-deps"
20
+ "dist:win": "npm run build && electron-builder --win"
26
21
  },
27
22
  "dependencies": {
28
- "@preact/signals": "^2.5.1",
29
- "@tanstack/react-table": "^8.21.3",
30
- "dayjs": "^1.11.13",
31
- "electron-log": "^5.4.3",
32
- "electron-updater": "^6.6.2",
33
- "jwt-decode": "^4.0.0",
34
- "keytar": "7.9.0",
35
- "ky": "^1.2.4",
36
- "pdf-to-printer": "5.6.1",
37
23
  "preact": "^10.26.2",
38
- "preact-iso": "^2.9.1",
39
- "sweetalert2": "^11.26.10",
40
- "zod": "^3.23.8",
41
- "zustand": "^5.0.3"
24
+ "preact-iso": "^2.9.1"
42
25
  },
43
26
  "devDependencies": {
44
- "@eslint/eslintrc": "^3.3.1",
45
- "@eslint/js": "^9.39.1",
46
27
  "@preact/preset-vite": "^2.10.1",
47
- "@testing-library/preact": "3.2.3",
48
- "@types/node": "^22.7.8",
49
- "@typescript-eslint/eslint-plugin": "^8.46.4",
50
- "@typescript-eslint/parser": "^8.46.4",
51
- "autoprefixer": "^10.4.20",
52
- "concurrently": "^8.2.0",
53
- "cross-env": "^7.0.3",
54
- "dotenv": "^16.4.5",
55
28
  "electron": "^35.1.4",
56
29
  "electron-builder": "^24.13.1",
57
- "eslint": "^9.39.1",
58
- "eslint-config-prettier": "^10.1.8",
59
- "eslint-plugin-prettier": "^5.5.4",
60
- "eslint-plugin-react-hooks": "^7.0.1",
61
- "postcss": "^8.4.47",
62
- "prettier": "^3.6.2",
63
- "tailwindcss": "^3.4.14",
64
- "typescript": "^5.9.3",
65
- "vite": "^7.2.2",
66
- "vite-tsconfig-paths": "^5.0.0",
67
- "vitest": "^4.0.8",
68
- "wait-on": "^7.0.1"
69
- },
70
- "overrides": {
71
- "tar": "7.5.7"
30
+ "electron-vite": "^5.0.0",
31
+ "vite": "^7.2.2"
72
32
  },
73
33
  "build": {
74
- "appId": "__APP_APP_ID__",
75
- "productName": "__APP_TITLE__",
34
+ "appId": "com.example.app",
35
+ "productName": "App",
76
36
  "asar": true,
77
- "asarUnpack": [
78
- "**/*.node",
79
- "preload.cjs",
80
- "node_modules/pdf-to-printer/**/*"
81
- ],
82
37
  "directories": {
83
- "buildResources": "assets",
84
- "output": "../release"
38
+ "buildResources": "resources",
39
+ "output": "release"
85
40
  },
86
- "extraResources": [],
87
41
  "files": [
88
- "dist/**/*",
89
- "electron.js",
90
- "preload.cjs",
42
+ "out/**/*",
91
43
  "!**/*.map",
92
- "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
93
- "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}"
44
+ "!src/**/*",
45
+ "!electron.vite.config.*"
94
46
  ],
95
47
  "win": {
96
48
  "target": [
97
49
  "nsis"
98
50
  ],
99
51
  "signAndEditExecutable": false,
100
- "icon": "assets/build.ico"
52
+ "icon": "resources/icon.ico"
101
53
  },
102
54
  "nsis": {
103
55
  "oneClick": false,
@@ -105,19 +57,18 @@
105
57
  "allowToChangeInstallationDirectory": true,
106
58
  "createDesktopShortcut": true,
107
59
  "createStartMenuShortcut": true,
108
- "installerIcon": "assets/build.ico",
109
- "uninstallerIcon": "assets/build.ico",
110
- "installerSidebar": "assets/installer-sidebar.bmp",
111
- "uninstallerSidebar": "assets/installer-sidebar.bmp",
112
- "installerHeader": "assets/installer-header.bmp"
60
+ "installerIcon": "resources/icon.ico",
61
+ "uninstallerIcon": "resources/icon.ico",
62
+ "installerSidebar": "resources/installer-sidebar.bmp",
63
+ "uninstallerSidebar": "resources/installer-sidebar.bmp",
64
+ "installerHeader": "resources/installer-header.bmp"
113
65
  },
114
66
  "linux": {
115
67
  "target": [
116
- "AppImage",
117
- "deb"
68
+ "AppImage"
118
69
  ],
119
- "category": "Office",
120
- "maintainer": "tu-email@ejemplo.com"
70
+ "icon": "resources/icon.png",
71
+ "category": "Office"
121
72
  }
122
73
  }
123
74
  }
@@ -0,0 +1,2 @@
1
+ # Este directorio contiene los recursos para electron-builder
2
+ # Los iconos se generan automáticamente al crear el proyecto
@@ -0,0 +1,152 @@
1
+ import { app, BrowserWindow, ipcMain, screen } from "electron";
2
+ import path, { join } from "path";
3
+ import fs from "fs";
4
+
5
+ // ------------------- VENTANA PRINCIPAL -------------------
6
+
7
+ /** @type {BrowserWindow | null} */
8
+ let mainWindow = null;
9
+
10
+ /** @type {Map<string, BrowserWindow>} */
11
+ const childWindowsByRoute = new Map();
12
+
13
+ const settingsPath = path.join(app.getPath("userData"), "settings.json");
14
+
15
+ function createWindow() {
16
+ const { width: screenWidth, height: screenHeight } =
17
+ screen.getPrimaryDisplay().workAreaSize;
18
+
19
+ mainWindow = new BrowserWindow({
20
+ width: screenWidth,
21
+ height: screenHeight,
22
+ title: __APP_TITLE_JSON__,
23
+ webPreferences: {
24
+ preload: join(__dirname, "../preload/index.mjs"),
25
+ sandbox: false,
26
+ contextIsolation: true,
27
+ nodeIntegration: false,
28
+ },
29
+ });
30
+
31
+ mainWindow.setMenuBarVisibility(false);
32
+ mainWindow.setAutoHideMenuBar(true);
33
+ mainWindow.maximize();
34
+
35
+ // electron-vite: usa ELECTRON_RENDERER_URL en dev
36
+ if (process.env.ELECTRON_RENDERER_URL) {
37
+ mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
38
+ mainWindow.webContents.openDevTools();
39
+ } else {
40
+ mainWindow.loadFile(join(__dirname, "../renderer/index.html"));
41
+ }
42
+
43
+ mainWindow.on("minimize", () => {
44
+ for (const [, child] of childWindowsByRoute) {
45
+ if (!child.isDestroyed() && !child.isMinimized()) {
46
+ child.minimize();
47
+ }
48
+ }
49
+ });
50
+
51
+ mainWindow.on("restore", () => {
52
+ for (const [, child] of childWindowsByRoute) {
53
+ if (!child.isDestroyed()) {
54
+ child.restore();
55
+ }
56
+ }
57
+ });
58
+ }
59
+
60
+ app.whenReady().then(() => {
61
+ createWindow();
62
+ });
63
+
64
+ app.on("window-all-closed", () => {
65
+ if (process.platform !== "darwin") app.quit();
66
+ });
67
+
68
+ app.on("activate", () => {
69
+ if (BrowserWindow.getAllWindows().length === 0) {
70
+ createWindow();
71
+ }
72
+ });
73
+
74
+ // ------------------- HELPERS SETTINGS -------------------
75
+
76
+ function readSettings() {
77
+ try {
78
+ if (!fs.existsSync(settingsPath)) return {};
79
+ return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
80
+ } catch {
81
+ return {};
82
+ }
83
+ }
84
+
85
+ function writeSettings(data) {
86
+ try {
87
+ fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2), "utf-8");
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ // ------------------- IPC -------------------
95
+
96
+ ipcMain.handle("settings:get", () => readSettings());
97
+
98
+ ipcMain.handle("settings:set", (_event, data) => {
99
+ const current = readSettings();
100
+ const merged = { ...current, ...data };
101
+ writeSettings(merged);
102
+ return merged;
103
+ });
104
+
105
+ ipcMain.on("window:openRoute", (_event, options) => {
106
+ const route = typeof options === "string" ? options : options?.route;
107
+ const title = typeof options === "object" ? options.title : null;
108
+ if (!route) return;
109
+
110
+ const routeKey = String(route);
111
+ const existing = childWindowsByRoute.get(routeKey);
112
+ if (existing && !existing.isDestroyed()) {
113
+ if (existing.isMinimized()) existing.restore();
114
+ existing.focus();
115
+ return;
116
+ }
117
+
118
+ const child = new BrowserWindow({
119
+ width: 1200,
120
+ height: 700,
121
+ title: title || __APP_TITLE_JSON__,
122
+ webPreferences: {
123
+ preload: join(__dirname, "../preload/index.mjs"),
124
+ sandbox: false,
125
+ contextIsolation: true,
126
+ nodeIntegration: false,
127
+ },
128
+ });
129
+
130
+ child.setMenuBarVisibility(false);
131
+ child.setAutoHideMenuBar(true);
132
+ if (mainWindow && !mainWindow.isDestroyed()) {
133
+ child.setParentWindow(mainWindow);
134
+ }
135
+ childWindowsByRoute.set(routeKey, child);
136
+
137
+ if (process.env.ELECTRON_RENDERER_URL) {
138
+ child.loadURL(
139
+ `${process.env.ELECTRON_RENDERER_URL}?route=${encodeURIComponent(route)}&child=1`,
140
+ );
141
+ child.webContents.openDevTools();
142
+ } else {
143
+ child.loadFile(join(__dirname, "../renderer/index.html"), {
144
+ query: { route, child: "1" },
145
+ });
146
+ }
147
+
148
+ child.on("closed", () => {
149
+ const current = childWindowsByRoute.get(routeKey);
150
+ if (current === child) childWindowsByRoute.delete(routeKey);
151
+ });
152
+ });
@@ -0,0 +1,11 @@
1
+ import { contextBridge, ipcRenderer } from "electron";
2
+
3
+ contextBridge.exposeInMainWorld("electronAPI", {
4
+ settings: {
5
+ get: () => ipcRenderer.invoke("settings:get"),
6
+ set: (data) => ipcRenderer.invoke("settings:set", data),
7
+ },
8
+ window: {
9
+ openRoute: (options) => ipcRenderer.send("window:openRoute", options),
10
+ },
11
+ });
@@ -2,12 +2,12 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="vite.svg" />
5
+ <link rel="icon" type="image/svg+xml" href="./public/logo.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>__APP_TITLE__</title>
8
8
  </head>
9
9
  <body>
10
10
  <div id="app"></div>
11
- <script type="module" src="src/main.jsx"></script>
11
+ <script type="module" src="./src/main.jsx"></script>
12
12
  </body>
13
13
  </html>
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
2
+ <defs>
3
+ <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
5
+ <stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
6
+ </linearGradient>
7
+ </defs>
8
+ <circle cx="50" cy="50" r="45" fill="url(#grad)"/>
9
+ <text x="50" y="62" font-family="Arial, sans-serif" font-size="36" font-weight="bold" fill="white" text-anchor="middle">A</text>
10
+ </svg>
@@ -0,0 +1,22 @@
1
+ #app {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ }
12
+ .logo:hover {
13
+ filter: drop-shadow(0 0 2em #646cffaa);
14
+ }
15
+
16
+ .card {
17
+ padding: 2em;
18
+ }
19
+
20
+ .read-the-docs {
21
+ color: #888;
22
+ }
@@ -1,16 +1,13 @@
1
- import { render } from "preact";
2
1
  import { LocationProvider, Router, Route } from "preact-iso";
3
2
  import Home from "./routes/Home/Home";
4
- import "./index.css";
5
3
 
6
- function App() {
4
+ export default function App() {
7
5
  return (
8
6
  <LocationProvider>
9
7
  <Router>
10
8
  <Route path="/" component={Home} />
9
+ <Route default component={Home} />
11
10
  </Router>
12
11
  </LocationProvider>
13
12
  );
14
13
  }
15
-
16
- render(<App />, document.getElementById("app"));
@@ -0,0 +1,14 @@
1
+ import { useState } from "preact/hooks";
2
+ import "./style.css";
3
+
4
+ export function Counter() {
5
+ const [count, setCount] = useState(0);
6
+
7
+ return (
8
+ <div class="counter">
9
+ <button onClick={() => setCount(count - 1)}>-</button>
10
+ <p>{count}</p>
11
+ <button onClick={() => setCount(count + 1)}>+</button>
12
+ </div>
13
+ );
14
+ }
@@ -0,0 +1,46 @@
1
+ /* counter.css */
2
+ .counter {
3
+ display: flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ gap: 1rem;
7
+ padding: 1.5rem;
8
+ background-color: #f8f9fa;
9
+ border: 1px solid #e0e0e0;
10
+ border-radius: 12px;
11
+ width: fit-content;
12
+ margin: 2rem auto;
13
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
14
+ }
15
+
16
+ .counter p {
17
+ margin: 0;
18
+ font-size: 1.5rem;
19
+ font-weight: 500;
20
+ color: #333;
21
+ min-width: 2rem;
22
+ text-align: center;
23
+ }
24
+
25
+ .counter button {
26
+ background-color: #ffffff;
27
+ border: 1px solid #ccc;
28
+ border-radius: 8px;
29
+ width: 2.5rem;
30
+ height: 2.5rem;
31
+ font-size: 1.2rem;
32
+ font-weight: 600;
33
+ color: #333;
34
+ cursor: pointer;
35
+ transition: all 0.2s ease-in-out;
36
+ }
37
+
38
+ .counter button:hover {
39
+ background-color: #f0f0f0;
40
+ border-color: #999;
41
+ }
42
+
43
+ .counter button:active {
44
+ background-color: #e0e0e0;
45
+ transform: scale(0.96);
46
+ }
@@ -0,0 +1,23 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ html,
8
+ body {
9
+ height: 100%;
10
+ overflow: hidden;
11
+ }
12
+
13
+ body {
14
+ font-family:
15
+ -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
16
+ "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
17
+ -webkit-font-smoothing: antialiased;
18
+ -moz-osx-font-smoothing: grayscale;
19
+ }
20
+
21
+ #app {
22
+ height: 100%;
23
+ }
@@ -0,0 +1,5 @@
1
+ import { render } from "preact";
2
+ import App from "./app.jsx";
3
+ import "./index.css";
4
+
5
+ render(<App />, document.getElementById("app"));
@@ -1,11 +1,12 @@
1
1
  import { Counter } from "../../components/Counter/Counter";
2
+
2
3
  import "./style.css";
3
4
 
4
5
  export default function Home() {
5
6
  return (
6
7
  <div className="home">
7
- <h1 className="home-title">Bienvenido a __APP_TITLE__</h1>
8
- <img className="home-logo" src="/logo.svg" alt="" />
8
+ <h1 className="home-title"Bienvenido a __APP_TITLE__!</h1>
9
+ <img className="home-logo" src="../../public/logo.svg" alt="" />
9
10
  <Counter />
10
11
  </div>
11
12
  );
@@ -0,0 +1,48 @@
1
+ .home {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ justify-content: center;
6
+ padding: 2rem;
7
+ background-color: #f8f9fa;
8
+ height: 100vh;
9
+ box-sizing: border-box;
10
+ font-family: "Inter", system-ui, sans-serif;
11
+ color: #333;
12
+ }
13
+
14
+ .home-title {
15
+ font-size: 2.4rem;
16
+ font-weight: 600;
17
+ margin-bottom: 1.5rem;
18
+ color: #2c3e50;
19
+ letter-spacing: 0.5px;
20
+ text-align: center;
21
+ }
22
+
23
+ .home-logo {
24
+ width: 180px;
25
+ height: auto;
26
+ margin-bottom: 1.5rem;
27
+ animation: pulse-logo 8s ease-in-out infinite;
28
+ filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.1));
29
+ transition: transform 0.3s ease;
30
+ }
31
+
32
+ @keyframes pulse-logo {
33
+ 0% {
34
+ transform: scale(1);
35
+ }
36
+ 25% {
37
+ transform: scale(1.1);
38
+ }
39
+ 50% {
40
+ transform: scale(1.25);
41
+ }
42
+ 75% {
43
+ transform: scale(1.1);
44
+ }
45
+ 100% {
46
+ transform: scale(1);
47
+ }
48
+ }
@@ -1,230 +0,0 @@
1
- import { app, BrowserWindow, ipcMain, dialog, screen } from "electron";
2
- import path from "path";
3
- import { fileURLToPath } from "url";
4
- import fs from "fs";
5
- import os from "os";
6
- import keytar from "keytar";
7
- import pdfPrinter from "pdf-to-printer";
8
- import { spawn } from "child_process";
9
- import log from "electron-log";
10
-
11
- // Inicializar logger
12
- log.initialize();
13
- log.transports.file.level = "info";
14
- log.info("Iniciando aplicacion...");
15
-
16
- const { print: printPDF, getPrinters } = pdfPrinter;
17
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
-
19
- app.commandLine.appendSwitch("disable-features", "Autofill");
20
- if (!app.isPackaged || process.env.ELECTRON_DISABLE_SANDBOX === "1") {
21
- app.commandLine.appendSwitch("no-sandbox");
22
- }
23
-
24
- let backendProcess = null;
25
-
26
- function getBackendPath() {
27
- const isWin = process.platform === "win32";
28
- const executableName = isWin
29
- ? "api_billing_software.exe"
30
- : "api_billing_software";
31
-
32
- if (app.isPackaged) {
33
- return path.join(
34
- process.resourcesPath,
35
- "api_billing_software",
36
- executableName,
37
- );
38
- }
39
-
40
- return path.join(
41
- __dirname,
42
- "..",
43
- "backend",
44
- "dist",
45
- "api_billing_software",
46
- executableName,
47
- );
48
- }
49
-
50
- function startBackend() {
51
- const backendPath = getBackendPath();
52
- log.info("Intentando iniciar backend desde:", backendPath);
53
-
54
- if (!fs.existsSync(backendPath)) {
55
- log.error("No se encontro el ejecutable del backend en:", backendPath);
56
- return;
57
- }
58
-
59
- backendProcess = spawn(backendPath, [], {
60
- cwd: path.dirname(backendPath),
61
- stdio: "pipe",
62
- windowsHide: true,
63
- });
64
-
65
- backendProcess.on("close", () => {
66
- backendProcess = null;
67
- });
68
- }
69
-
70
- function stopBackend() {
71
- if (backendProcess) {
72
- backendProcess.kill();
73
- backendProcess = null;
74
- }
75
- }
76
-
77
- /** @type {BrowserWindow | null} */
78
- let mainWindow = null;
79
-
80
- /** @type {Map<string, BrowserWindow>} */
81
- const childWindowsByRoute = new Map();
82
-
83
- const settingsPath = path.join(app.getPath("userData"), "settings.json");
84
-
85
- function createWindow() {
86
- const isDev = !app.isPackaged;
87
- const devServerUrl = process.env.DEV_SERVER_URL || "http://localhost:5171";
88
- const { width: screenWidth, height: screenHeight } =
89
- screen.getPrimaryDisplay().workAreaSize;
90
-
91
- mainWindow = new BrowserWindow({
92
- width: screenWidth,
93
- height: screenHeight,
94
- title: __APP_TITLE_JSON__,
95
- webPreferences: {
96
- preload: path.join(__dirname, "preload.cjs"),
97
- devTools: isDev,
98
- contextIsolation: true,
99
- nodeIntegration: false,
100
- },
101
- });
102
-
103
- mainWindow.setMenuBarVisibility(false);
104
- mainWindow.setAutoHideMenuBar(true);
105
- mainWindow.maximize();
106
-
107
- if (isDev) {
108
- mainWindow.loadURL(devServerUrl);
109
- mainWindow.webContents.openDevTools();
110
- } else {
111
- const indexPath = path.join(__dirname, "dist", "index.html");
112
- mainWindow.loadFile(indexPath, { query: { route: "/" } });
113
- }
114
-
115
- mainWindow.on("minimize", () => {
116
- for (const [, child] of childWindowsByRoute) {
117
- if (!child.isDestroyed() && !child.isMinimized()) child.minimize();
118
- }
119
- });
120
-
121
- mainWindow.on("restore", () => {
122
- for (const [, child] of childWindowsByRoute) {
123
- if (!child.isDestroyed()) child.restore();
124
- }
125
- });
126
- }
127
-
128
- app.whenReady().then(() => {
129
- startBackend();
130
- createWindow();
131
- });
132
-
133
- app.on("will-quit", () => {
134
- stopBackend();
135
- });
136
-
137
- app.on("window-all-closed", () => {
138
- if (process.platform !== "darwin") app.quit();
139
- });
140
-
141
- app.on("activate", () => {
142
- if (BrowserWindow.getAllWindows().length === 0) createWindow();
143
- });
144
-
145
- function readSettings() {
146
- try {
147
- if (!fs.existsSync(settingsPath)) return {};
148
- return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
149
- } catch {
150
- return {};
151
- }
152
- }
153
-
154
- function writeSettings(data) {
155
- try {
156
- fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2), "utf-8");
157
- return true;
158
- } catch {
159
- return false;
160
- }
161
- }
162
-
163
- ipcMain.handle("settings:get", () => readSettings());
164
- ipcMain.handle("settings:set", (_event, data) => {
165
- const current = readSettings();
166
- const merged = { ...current, ...data };
167
- writeSettings(merged);
168
- return merged;
169
- });
170
-
171
- ipcMain.handle("printer:getPrinters", async () => {
172
- return await getPrinters();
173
- });
174
- ipcMain.handle("printer:printPDF", async (_event, { filePath, options }) => {
175
- try {
176
- await printPDF(filePath, options || {});
177
- return { ok: true };
178
- } catch (err) {
179
- return { ok: false, error: err?.message || String(err) };
180
- }
181
- });
182
-
183
- ipcMain.on("window:openRoute", (_event, options) => {
184
- const route = typeof options === "string" ? options : options?.route;
185
- const title = typeof options === "object" ? options.title : null;
186
- if (!route) return;
187
-
188
- const routeKey = String(route);
189
- const existing = childWindowsByRoute.get(routeKey);
190
- if (existing && !existing.isDestroyed()) {
191
- if (existing.isMinimized()) existing.restore();
192
- existing.focus();
193
- return;
194
- }
195
-
196
- const isDev = !app.isPackaged;
197
- const devServerUrl = process.env.DEV_SERVER_URL || "http://localhost:5171";
198
- const preloadPath = path.join(__dirname, "preload.cjs");
199
-
200
- const child = new BrowserWindow({
201
- width: 1200,
202
- height: 700,
203
- title: title || __APP_TITLE_JSON__,
204
- webPreferences: {
205
- preload: preloadPath,
206
- devTools: isDev,
207
- contextIsolation: true,
208
- nodeIntegration: false,
209
- },
210
- });
211
-
212
- child.setMenuBarVisibility(false);
213
- child.setAutoHideMenuBar(true);
214
- if (mainWindow && !mainWindow.isDestroyed())
215
- child.setParentWindow(mainWindow);
216
- childWindowsByRoute.set(routeKey, child);
217
-
218
- if (isDev) {
219
- child.loadURL(`${devServerUrl}?route=${encodeURIComponent(route)}&child=1`);
220
- child.webContents.openDevTools();
221
- } else {
222
- const indexPath = path.join(__dirname, "dist", "index.html");
223
- child.loadFile(indexPath, { query: { route, child: "1" } });
224
- }
225
-
226
- child.on("closed", () => {
227
- const current = childWindowsByRoute.get(routeKey);
228
- if (current === child) childWindowsByRoute.delete(routeKey);
229
- });
230
- });
@@ -1,3 +0,0 @@
1
- import js from "@eslint/js";
2
-
3
- export default [js.configs.recommended];
@@ -1,9 +0,0 @@
1
- const { contextBridge, ipcRenderer } = require("electron");
2
-
3
- contextBridge.exposeInMainWorld("electronAPI", {
4
- settingsGet: () => ipcRenderer.invoke("settings:get"),
5
- settingsSet: (data) => ipcRenderer.invoke("settings:set", data),
6
- openRoute: (options) => ipcRenderer.send("window:openRoute", options),
7
- getPrinters: () => ipcRenderer.invoke("printer:getPrinters"),
8
- printPDF: (payload) => ipcRenderer.invoke("printer:printPDF", payload),
9
- });
@@ -1 +0,0 @@
1
- // Compat (si alguien lo usa desde ESM). El preload real es preload.cjs
@@ -1,4 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
3
- <rect width="256" height="256" fill="#ffffff"/>
4
- </svg>
@@ -1,71 +0,0 @@
1
- import { createServer } from "vite";
2
- import { spawn } from "child_process";
3
- import path from "path";
4
- import { fileURLToPath } from "url";
5
- import fs from "fs";
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
-
9
- async function start() {
10
- const server = await createServer({
11
- server: {
12
- host: "127.0.0.1",
13
- port: 0,
14
- },
15
- });
16
-
17
- await server.listen();
18
-
19
- const url =
20
- server.resolvedUrls?.local?.[0] ||
21
- `http://localhost:${server.config.server.port || 5173}`;
22
-
23
- console.log(`[dev-electron] Vite dev server en ${url}`);
24
-
25
- const isWin = process.platform === "win32";
26
- const localElectronBin = path.resolve(
27
- __dirname,
28
- "..",
29
- "node_modules",
30
- ".bin",
31
- isWin ? "electron.cmd" : "electron",
32
- );
33
- const electronBin = fs.existsSync(localElectronBin)
34
- ? localElectronBin
35
- : "electron";
36
-
37
- const electronProcess = spawn(electronBin, [".", "--no-sandbox"], {
38
- stdio: "inherit",
39
- shell: isWin,
40
- env: {
41
- ...process.env,
42
- DEV_SERVER_URL: url,
43
- NODE_OPTIONS: "--experimental-require-module --no-warnings",
44
- ELECTRON_DISABLE_SANDBOX: "1",
45
- },
46
- cwd: path.resolve(__dirname, ".."),
47
- });
48
-
49
- const clean = () => {
50
- electronProcess.kill();
51
- server.close();
52
- };
53
-
54
- electronProcess.on("close", (code) => {
55
- clean();
56
- process.exit(code ?? 0);
57
- });
58
-
59
- const handleSignal = () => {
60
- clean();
61
- process.exit(0);
62
- };
63
-
64
- process.on("SIGINT", handleSignal);
65
- process.on("SIGTERM", handleSignal);
66
- }
67
-
68
- start().catch((err) => {
69
- console.error("[dev-electron] Error:", err);
70
- process.exit(1);
71
- });
@@ -1,13 +0,0 @@
1
- import { useState } from "preact/hooks";
2
- import "./style.css";
3
-
4
- export function Counter() {
5
- const [count, setCount] = useState(0);
6
- return (
7
- <div className="counter">
8
- <button onClick={() => setCount((c) => c - 1)}>-</button>
9
- <span>{count}</span>
10
- <button onClick={() => setCount((c) => c + 1)}>+</button>
11
- </div>
12
- );
13
- }
@@ -1,9 +0,0 @@
1
- .counter {
2
- display: inline-flex;
3
- gap: 12px;
4
- align-items: center;
5
- }
6
-
7
- .counter button {
8
- padding: 8px 12px;
9
- }
@@ -1,7 +0,0 @@
1
- :root {
2
- font-family: system-ui, sans-serif;
3
- }
4
-
5
- body {
6
- margin: 0;
7
- }
@@ -1,13 +0,0 @@
1
- .home {
2
- min-height: 100vh;
3
- display: grid;
4
- place-content: center;
5
- gap: 16px;
6
- text-align: center;
7
- }
8
-
9
- .home-logo {
10
- width: 120px;
11
- height: 120px;
12
- margin: 0 auto;
13
- }
@@ -1,18 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "useDefineForClassFields": true,
5
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
- "module": "ESNext",
7
- "skipLibCheck": true,
8
- "moduleResolution": "Bundler",
9
- "resolveJsonModule": true,
10
- "isolatedModules": true,
11
- "noEmit": true,
12
- "jsx": "react-jsx",
13
- "jsxImportSource": "preact",
14
- "strict": true,
15
- "types": ["node"]
16
- },
17
- "include": ["src"]
18
- }
@@ -1,25 +0,0 @@
1
- import { defineConfig } from "vite";
2
- import preact from "@preact/preset-vite";
3
- import path from "path";
4
-
5
- export default defineConfig({
6
- plugins: [preact()],
7
- base: "./",
8
- publicDir: "public",
9
- build: {
10
- outDir: "dist",
11
- assetsDir: "assets",
12
- rollupOptions: {
13
- output: {
14
- assetFileNames: "assets/[name].[hash][extname]",
15
- chunkFileNames: "assets/[name].[hash].js",
16
- entryFileNames: "assets/[name].[hash].js",
17
- },
18
- },
19
- },
20
- resolve: {
21
- alias: {
22
- "@": path.resolve(__dirname, "./src"),
23
- },
24
- },
25
- });