elsabro 2.1.0 → 2.3.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/agents/elsabro-orchestrator.md +113 -0
- package/commands/elsabro/add-phase.md +17 -0
- package/commands/elsabro/add-todo.md +111 -53
- package/commands/elsabro/audit-milestone.md +19 -0
- package/commands/elsabro/check-todos.md +210 -31
- package/commands/elsabro/complete-milestone.md +20 -1
- package/commands/elsabro/debug.md +19 -0
- package/commands/elsabro/discuss-phase.md +18 -1
- package/commands/elsabro/execute.md +511 -58
- package/commands/elsabro/insert-phase.md +18 -1
- package/commands/elsabro/list-phase-assumptions.md +17 -0
- package/commands/elsabro/new-milestone.md +19 -0
- package/commands/elsabro/new.md +19 -0
- package/commands/elsabro/pause-work.md +19 -0
- package/commands/elsabro/plan-milestone-gaps.md +20 -1
- package/commands/elsabro/plan.md +264 -36
- package/commands/elsabro/progress.md +203 -79
- package/commands/elsabro/quick.md +19 -0
- package/commands/elsabro/remove-phase.md +17 -0
- package/commands/elsabro/research-phase.md +18 -1
- package/commands/elsabro/resume-work.md +19 -0
- package/commands/elsabro/start.md +399 -98
- package/commands/elsabro/verify-work.md +138 -5
- package/hooks/confirm-destructive.sh +145 -0
- package/hooks/hooks-config.json +81 -0
- package/hooks/lint-check.sh +238 -0
- package/hooks/post-edit-test.sh +189 -0
- package/package.json +3 -2
- package/references/SYSTEM_INDEX.md +241 -0
- package/references/command-flow.md +352 -0
- package/references/enforcement-rules.md +331 -0
- package/references/error-contracts-tests.md +1171 -0
- package/references/error-contracts.md +3102 -0
- package/references/error-handling-instructions.md +26 -12
- package/references/parallel-worktrees.md +293 -0
- package/references/state-sync.md +381 -0
- package/references/task-dispatcher.md +388 -0
- package/references/tasks-integration.md +380 -0
- package/scripts/setup-parallel-worktrees.sh +319 -0
- package/skills/api-microservice.md +765 -0
- package/skills/api-setup.md +76 -3
- package/skills/auth-setup.md +46 -6
- package/skills/chrome-extension.md +584 -0
- package/skills/cicd-setup.md +1206 -0
- package/skills/cli-tool.md +884 -0
- package/skills/database-setup.md +41 -5
- package/skills/desktop-app.md +1351 -0
- package/skills/expo-app.md +35 -2
- package/skills/full-stack-app.md +543 -0
- package/skills/memory-update.md +207 -0
- package/skills/mobile-app.md +813 -0
- package/skills/nextjs-app.md +33 -2
- package/skills/payments-setup.md +76 -1
- package/skills/review.md +331 -0
- package/skills/saas-starter.md +639 -0
- package/skills/sentry-setup.md +41 -7
- package/skills/techdebt.md +289 -0
- package/skills/testing-setup.md +1218 -0
- package/skills/tutor.md +219 -0
- package/templates/.planning/notes/.gitkeep +0 -0
- package/templates/CLAUDE.md.template +48 -0
- package/templates/error-handling-config.json +79 -2
- package/templates/mistakes.md.template +52 -0
- package/templates/patterns.md.template +114 -0
|
@@ -0,0 +1,1351 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: desktop-app
|
|
3
|
+
description: Crear aplicaciones de escritorio multiplataforma con Tauri, React y Vite
|
|
4
|
+
tags: [desktop, tauri, react, cross-platform, windows, macos, linux, native]
|
|
5
|
+
difficulty: intermediate
|
|
6
|
+
estimated_time: 30min
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Skill: Desktop App
|
|
10
|
+
|
|
11
|
+
<when_to_use>
|
|
12
|
+
Usar cuando el usuario menciona:
|
|
13
|
+
- "aplicacion de escritorio"
|
|
14
|
+
- "app nativa"
|
|
15
|
+
- "tauri"
|
|
16
|
+
- "electron alternative"
|
|
17
|
+
- "desktop multiplataforma"
|
|
18
|
+
- "app para Windows/Mac/Linux"
|
|
19
|
+
- "aplicacion instalable"
|
|
20
|
+
</when_to_use>
|
|
21
|
+
|
|
22
|
+
<pre_requisites>
|
|
23
|
+
## Pre-requisitos
|
|
24
|
+
- Node.js 20+
|
|
25
|
+
- Rust (instalado via rustup)
|
|
26
|
+
- Dependencias del sistema:
|
|
27
|
+
- **macOS:** Xcode Command Line Tools
|
|
28
|
+
- **Windows:** Visual Studio Build Tools, WebView2
|
|
29
|
+
- **Linux:** webkit2gtk, libayatana-appindicator
|
|
30
|
+
</pre_requisites>
|
|
31
|
+
|
|
32
|
+
<tech_stack>
|
|
33
|
+
## Stack Tecnologico
|
|
34
|
+
| Categoria | Tecnologia | Version |
|
|
35
|
+
|-----------|------------|---------|
|
|
36
|
+
| Framework | Tauri | 2.x |
|
|
37
|
+
| Frontend | React | 19.x |
|
|
38
|
+
| Bundler | Vite | 6.x |
|
|
39
|
+
| Language | TypeScript | 5.x |
|
|
40
|
+
| Styling | Tailwind CSS | 4.x |
|
|
41
|
+
| State | Zustand | 5.x |
|
|
42
|
+
| Testing | Vitest + Playwright | 2.x |
|
|
43
|
+
| Backend | Rust | stable |
|
|
44
|
+
</tech_stack>
|
|
45
|
+
|
|
46
|
+
<project_structure>
|
|
47
|
+
## Estructura de Proyecto
|
|
48
|
+
```
|
|
49
|
+
my-desktop-app/
|
|
50
|
+
├── src/ # Frontend (React)
|
|
51
|
+
│ ├── components/
|
|
52
|
+
│ │ ├── ui/
|
|
53
|
+
│ │ │ ├── Button.tsx
|
|
54
|
+
│ │ │ └── Input.tsx
|
|
55
|
+
│ │ └── layout/
|
|
56
|
+
│ │ ├── Sidebar.tsx
|
|
57
|
+
│ │ └── Titlebar.tsx
|
|
58
|
+
│ ├── hooks/
|
|
59
|
+
│ │ ├── useTauri.ts
|
|
60
|
+
│ │ └── useStore.ts
|
|
61
|
+
│ ├── stores/
|
|
62
|
+
│ │ └── appStore.ts
|
|
63
|
+
│ ├── lib/
|
|
64
|
+
│ │ ├── tauri.ts # Tauri API wrappers
|
|
65
|
+
│ │ └── utils.ts
|
|
66
|
+
│ ├── App.tsx
|
|
67
|
+
│ ├── main.tsx
|
|
68
|
+
│ └── index.css
|
|
69
|
+
├── src-tauri/ # Backend (Rust)
|
|
70
|
+
│ ├── src/
|
|
71
|
+
│ │ ├── main.rs
|
|
72
|
+
│ │ ├── lib.rs
|
|
73
|
+
│ │ └── commands/
|
|
74
|
+
│ │ ├── mod.rs
|
|
75
|
+
│ │ └── file_ops.rs
|
|
76
|
+
│ ├── icons/
|
|
77
|
+
│ ├── capabilities/
|
|
78
|
+
│ │ └── default.json
|
|
79
|
+
│ ├── Cargo.toml
|
|
80
|
+
│ ├── tauri.conf.json
|
|
81
|
+
│ └── build.rs
|
|
82
|
+
├── tests/
|
|
83
|
+
│ ├── unit/
|
|
84
|
+
│ │ └── components.test.tsx
|
|
85
|
+
│ └── e2e/
|
|
86
|
+
│ └── app.spec.ts
|
|
87
|
+
├── package.json
|
|
88
|
+
├── vite.config.ts
|
|
89
|
+
├── tailwind.config.ts
|
|
90
|
+
├── tsconfig.json
|
|
91
|
+
└── README.md
|
|
92
|
+
```
|
|
93
|
+
</project_structure>
|
|
94
|
+
|
|
95
|
+
<setup_steps>
|
|
96
|
+
## Pasos de Setup
|
|
97
|
+
|
|
98
|
+
### 1. Instalar Prerrequisitos
|
|
99
|
+
|
|
100
|
+
**macOS:**
|
|
101
|
+
```bash
|
|
102
|
+
xcode-select --install
|
|
103
|
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Windows (PowerShell como Admin):**
|
|
107
|
+
```powershell
|
|
108
|
+
winget install Rustlang.Rustup
|
|
109
|
+
# Instalar VS Build Tools desde https://visualstudio.microsoft.com/visual-cpp-build-tools/
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Linux (Ubuntu/Debian):**
|
|
113
|
+
```bash
|
|
114
|
+
sudo apt update
|
|
115
|
+
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
|
|
116
|
+
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev
|
|
117
|
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 2. Crear Proyecto Tauri
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Usando create-tauri-app
|
|
124
|
+
npm create tauri-app@latest my-desktop-app -- --template react-ts
|
|
125
|
+
|
|
126
|
+
cd my-desktop-app
|
|
127
|
+
npm install
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 3. Instalar Dependencias Adicionales
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Frontend dependencies
|
|
134
|
+
npm install zustand @tauri-apps/api@2
|
|
135
|
+
|
|
136
|
+
# Styling
|
|
137
|
+
npm install -D tailwindcss@4 @tailwindcss/vite
|
|
138
|
+
|
|
139
|
+
# Testing
|
|
140
|
+
npm install -D vitest @testing-library/react @testing-library/jest-dom
|
|
141
|
+
npm install -D @playwright/test
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 4. Configurar Tailwind CSS v4
|
|
145
|
+
|
|
146
|
+
**vite.config.ts:**
|
|
147
|
+
```typescript
|
|
148
|
+
import { defineConfig } from 'vite';
|
|
149
|
+
import react from '@vitejs/plugin-react';
|
|
150
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
151
|
+
|
|
152
|
+
const host = process.env.TAURI_DEV_HOST;
|
|
153
|
+
|
|
154
|
+
export default defineConfig({
|
|
155
|
+
plugins: [react(), tailwindcss()],
|
|
156
|
+
clearScreen: false,
|
|
157
|
+
server: {
|
|
158
|
+
port: 1420,
|
|
159
|
+
strictPort: true,
|
|
160
|
+
host: host || false,
|
|
161
|
+
hmr: host
|
|
162
|
+
? {
|
|
163
|
+
protocol: 'ws',
|
|
164
|
+
host,
|
|
165
|
+
port: 1421,
|
|
166
|
+
}
|
|
167
|
+
: undefined,
|
|
168
|
+
watch: {
|
|
169
|
+
ignored: ['**/src-tauri/**'],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**src/index.css:**
|
|
176
|
+
```css
|
|
177
|
+
@import "tailwindcss";
|
|
178
|
+
|
|
179
|
+
@theme {
|
|
180
|
+
--color-primary: #3b82f6;
|
|
181
|
+
--color-secondary: #64748b;
|
|
182
|
+
--font-sans: 'Inter', system-ui, sans-serif;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* Custom titlebar styles */
|
|
186
|
+
.titlebar {
|
|
187
|
+
-webkit-app-region: drag;
|
|
188
|
+
user-select: none;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.titlebar-button {
|
|
192
|
+
-webkit-app-region: no-drag;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 5. Configurar Tauri
|
|
197
|
+
|
|
198
|
+
**src-tauri/tauri.conf.json:**
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"$schema": "https://schema.tauri.app/config/2",
|
|
202
|
+
"productName": "My Desktop App",
|
|
203
|
+
"identifier": "com.myapp.desktop",
|
|
204
|
+
"version": "0.1.0",
|
|
205
|
+
"build": {
|
|
206
|
+
"beforeDevCommand": "npm run dev",
|
|
207
|
+
"devUrl": "http://localhost:1420",
|
|
208
|
+
"beforeBuildCommand": "npm run build",
|
|
209
|
+
"frontendDist": "../dist"
|
|
210
|
+
},
|
|
211
|
+
"app": {
|
|
212
|
+
"withGlobalTauri": true,
|
|
213
|
+
"windows": [
|
|
214
|
+
{
|
|
215
|
+
"title": "My Desktop App",
|
|
216
|
+
"width": 1200,
|
|
217
|
+
"height": 800,
|
|
218
|
+
"minWidth": 800,
|
|
219
|
+
"minHeight": 600,
|
|
220
|
+
"resizable": true,
|
|
221
|
+
"fullscreen": false,
|
|
222
|
+
"decorations": true,
|
|
223
|
+
"transparent": false,
|
|
224
|
+
"center": true
|
|
225
|
+
}
|
|
226
|
+
],
|
|
227
|
+
"security": {
|
|
228
|
+
"csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost"
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
"bundle": {
|
|
232
|
+
"active": true,
|
|
233
|
+
"icon": [
|
|
234
|
+
"icons/32x32.png",
|
|
235
|
+
"icons/128x128.png",
|
|
236
|
+
"icons/128x128@2x.png",
|
|
237
|
+
"icons/icon.icns",
|
|
238
|
+
"icons/icon.ico"
|
|
239
|
+
],
|
|
240
|
+
"targets": "all",
|
|
241
|
+
"windows": {
|
|
242
|
+
"certificateThumbprint": null,
|
|
243
|
+
"digestAlgorithm": "sha256",
|
|
244
|
+
"timestampUrl": ""
|
|
245
|
+
},
|
|
246
|
+
"macOS": {
|
|
247
|
+
"entitlements": null,
|
|
248
|
+
"exceptionDomain": "",
|
|
249
|
+
"frameworks": [],
|
|
250
|
+
"minimumSystemVersion": "10.15",
|
|
251
|
+
"signingIdentity": null
|
|
252
|
+
},
|
|
253
|
+
"linux": {
|
|
254
|
+
"deb": {
|
|
255
|
+
"depends": []
|
|
256
|
+
},
|
|
257
|
+
"appimage": {
|
|
258
|
+
"bundleMediaFramework": true
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 6. Configurar Capabilities
|
|
266
|
+
|
|
267
|
+
**src-tauri/capabilities/default.json:**
|
|
268
|
+
```json
|
|
269
|
+
{
|
|
270
|
+
"$schema": "https://schema.tauri.app/config/2",
|
|
271
|
+
"identifier": "default",
|
|
272
|
+
"description": "Default permissions for the app",
|
|
273
|
+
"windows": ["main"],
|
|
274
|
+
"permissions": [
|
|
275
|
+
"core:default",
|
|
276
|
+
"core:window:default",
|
|
277
|
+
"core:window:allow-start-dragging",
|
|
278
|
+
"core:window:allow-close",
|
|
279
|
+
"core:window:allow-minimize",
|
|
280
|
+
"core:window:allow-maximize",
|
|
281
|
+
"core:window:allow-set-size",
|
|
282
|
+
"dialog:default",
|
|
283
|
+
"dialog:allow-open",
|
|
284
|
+
"dialog:allow-save",
|
|
285
|
+
"fs:default",
|
|
286
|
+
"fs:allow-read-text-file",
|
|
287
|
+
"fs:allow-write-text-file",
|
|
288
|
+
"shell:allow-open",
|
|
289
|
+
"notification:default"
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### 7. Crear Backend Rust
|
|
295
|
+
|
|
296
|
+
**src-tauri/src/main.rs:**
|
|
297
|
+
```rust
|
|
298
|
+
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
299
|
+
|
|
300
|
+
mod commands;
|
|
301
|
+
|
|
302
|
+
use commands::file_ops;
|
|
303
|
+
|
|
304
|
+
fn main() {
|
|
305
|
+
tauri::Builder::default()
|
|
306
|
+
.plugin(tauri_plugin_dialog::init())
|
|
307
|
+
.plugin(tauri_plugin_fs::init())
|
|
308
|
+
.plugin(tauri_plugin_shell::init())
|
|
309
|
+
.plugin(tauri_plugin_notification::init())
|
|
310
|
+
.invoke_handler(tauri::generate_handler![
|
|
311
|
+
file_ops::read_file,
|
|
312
|
+
file_ops::write_file,
|
|
313
|
+
file_ops::get_app_info,
|
|
314
|
+
])
|
|
315
|
+
.run(tauri::generate_context!())
|
|
316
|
+
.expect("error while running tauri application");
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**src-tauri/src/commands/mod.rs:**
|
|
321
|
+
```rust
|
|
322
|
+
pub mod file_ops;
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**src-tauri/src/commands/file_ops.rs:**
|
|
326
|
+
```rust
|
|
327
|
+
use std::fs;
|
|
328
|
+
use std::path::PathBuf;
|
|
329
|
+
use tauri::Manager;
|
|
330
|
+
|
|
331
|
+
#[derive(serde::Serialize)]
|
|
332
|
+
pub struct AppInfo {
|
|
333
|
+
pub name: String,
|
|
334
|
+
pub version: String,
|
|
335
|
+
pub data_dir: String,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#[tauri::command]
|
|
339
|
+
pub fn read_file(path: String) -> Result<String, String> {
|
|
340
|
+
fs::read_to_string(&path).map_err(|e| e.to_string())
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#[tauri::command]
|
|
344
|
+
pub fn write_file(path: String, contents: String) -> Result<(), String> {
|
|
345
|
+
fs::write(&path, contents).map_err(|e| e.to_string())
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#[tauri::command]
|
|
349
|
+
pub fn get_app_info(app: tauri::AppHandle) -> Result<AppInfo, String> {
|
|
350
|
+
let config = app.config();
|
|
351
|
+
let data_dir = app
|
|
352
|
+
.path()
|
|
353
|
+
.app_data_dir()
|
|
354
|
+
.map_err(|e| e.to_string())?
|
|
355
|
+
.to_string_lossy()
|
|
356
|
+
.to_string();
|
|
357
|
+
|
|
358
|
+
Ok(AppInfo {
|
|
359
|
+
name: config.product_name.clone().unwrap_or_default(),
|
|
360
|
+
version: config.version.clone().unwrap_or_default(),
|
|
361
|
+
data_dir,
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**src-tauri/Cargo.toml:**
|
|
367
|
+
```toml
|
|
368
|
+
[package]
|
|
369
|
+
name = "my-desktop-app"
|
|
370
|
+
version = "0.1.0"
|
|
371
|
+
description = "A Tauri App"
|
|
372
|
+
authors = ["you"]
|
|
373
|
+
edition = "2021"
|
|
374
|
+
|
|
375
|
+
[lib]
|
|
376
|
+
name = "my_desktop_app_lib"
|
|
377
|
+
crate-type = ["staticlib", "cdylib", "rlib"]
|
|
378
|
+
|
|
379
|
+
[build-dependencies]
|
|
380
|
+
tauri-build = { version = "2", features = [] }
|
|
381
|
+
|
|
382
|
+
[dependencies]
|
|
383
|
+
tauri = { version = "2", features = [] }
|
|
384
|
+
tauri-plugin-dialog = "2"
|
|
385
|
+
tauri-plugin-fs = "2"
|
|
386
|
+
tauri-plugin-shell = "2"
|
|
387
|
+
tauri-plugin-notification = "2"
|
|
388
|
+
serde = { version = "1", features = ["derive"] }
|
|
389
|
+
serde_json = "1"
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### 8. Crear Frontend React
|
|
393
|
+
|
|
394
|
+
**src/main.tsx:**
|
|
395
|
+
```typescript
|
|
396
|
+
import React from 'react';
|
|
397
|
+
import ReactDOM from 'react-dom/client';
|
|
398
|
+
import App from './App';
|
|
399
|
+
import './index.css';
|
|
400
|
+
|
|
401
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
402
|
+
<React.StrictMode>
|
|
403
|
+
<App />
|
|
404
|
+
</React.StrictMode>
|
|
405
|
+
);
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**src/App.tsx:**
|
|
409
|
+
```typescript
|
|
410
|
+
import { useState } from 'react';
|
|
411
|
+
import { Titlebar } from './components/layout/Titlebar';
|
|
412
|
+
import { Sidebar } from './components/layout/Sidebar';
|
|
413
|
+
import { Button } from './components/ui/Button';
|
|
414
|
+
import { useAppStore } from './stores/appStore';
|
|
415
|
+
import { useTauri } from './hooks/useTauri';
|
|
416
|
+
|
|
417
|
+
function App() {
|
|
418
|
+
const { appInfo, isLoading } = useTauri();
|
|
419
|
+
const { theme, toggleTheme } = useAppStore();
|
|
420
|
+
const [content, setContent] = useState('');
|
|
421
|
+
|
|
422
|
+
const handleOpenFile = async () => {
|
|
423
|
+
const { open } = await import('@tauri-apps/plugin-dialog');
|
|
424
|
+
const { readTextFile } = await import('@tauri-apps/plugin-fs');
|
|
425
|
+
|
|
426
|
+
const selected = await open({
|
|
427
|
+
multiple: false,
|
|
428
|
+
filters: [{ name: 'Text', extensions: ['txt', 'md', 'json'] }],
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
if (selected) {
|
|
432
|
+
const text = await readTextFile(selected as string);
|
|
433
|
+
setContent(text);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const handleSaveFile = async () => {
|
|
438
|
+
const { save } = await import('@tauri-apps/plugin-dialog');
|
|
439
|
+
const { writeTextFile } = await import('@tauri-apps/plugin-fs');
|
|
440
|
+
|
|
441
|
+
const path = await save({
|
|
442
|
+
filters: [{ name: 'Text', extensions: ['txt'] }],
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
if (path) {
|
|
446
|
+
await writeTextFile(path, content);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div className={`h-screen flex flex-col ${theme === 'dark' ? 'dark' : ''}`}>
|
|
452
|
+
<Titlebar />
|
|
453
|
+
|
|
454
|
+
<div className="flex flex-1 overflow-hidden">
|
|
455
|
+
<Sidebar />
|
|
456
|
+
|
|
457
|
+
<main className="flex-1 p-6 bg-white dark:bg-gray-900 overflow-auto">
|
|
458
|
+
<div className="max-w-4xl mx-auto">
|
|
459
|
+
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
|
460
|
+
{appInfo?.name || 'Loading...'}
|
|
461
|
+
</h1>
|
|
462
|
+
|
|
463
|
+
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
464
|
+
Version: {appInfo?.version || '-'}
|
|
465
|
+
</p>
|
|
466
|
+
|
|
467
|
+
<div className="flex gap-4 mb-6">
|
|
468
|
+
<Button onClick={handleOpenFile}>Open File</Button>
|
|
469
|
+
<Button onClick={handleSaveFile} variant="secondary">
|
|
470
|
+
Save File
|
|
471
|
+
</Button>
|
|
472
|
+
<Button onClick={toggleTheme} variant="ghost">
|
|
473
|
+
Toggle Theme
|
|
474
|
+
</Button>
|
|
475
|
+
</div>
|
|
476
|
+
|
|
477
|
+
<textarea
|
|
478
|
+
className="w-full h-64 p-4 border rounded-lg
|
|
479
|
+
bg-gray-50 dark:bg-gray-800
|
|
480
|
+
text-gray-900 dark:text-white
|
|
481
|
+
border-gray-200 dark:border-gray-700
|
|
482
|
+
focus:ring-2 focus:ring-primary focus:border-transparent"
|
|
483
|
+
value={content}
|
|
484
|
+
onChange={(e) => setContent(e.target.value)}
|
|
485
|
+
placeholder="Open a file or start typing..."
|
|
486
|
+
/>
|
|
487
|
+
</div>
|
|
488
|
+
</main>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export default App;
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**src/components/layout/Titlebar.tsx:**
|
|
498
|
+
```typescript
|
|
499
|
+
import { getCurrentWindow } from '@tauri-apps/api/window';
|
|
500
|
+
|
|
501
|
+
export function Titlebar() {
|
|
502
|
+
const appWindow = getCurrentWindow();
|
|
503
|
+
|
|
504
|
+
const handleMinimize = () => appWindow.minimize();
|
|
505
|
+
const handleMaximize = () => appWindow.toggleMaximize();
|
|
506
|
+
const handleClose = () => appWindow.close();
|
|
507
|
+
|
|
508
|
+
return (
|
|
509
|
+
<div className="titlebar h-8 bg-gray-100 dark:bg-gray-800 flex items-center justify-between px-4">
|
|
510
|
+
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
511
|
+
My Desktop App
|
|
512
|
+
</span>
|
|
513
|
+
|
|
514
|
+
<div className="flex gap-2">
|
|
515
|
+
<button
|
|
516
|
+
onClick={handleMinimize}
|
|
517
|
+
className="titlebar-button w-6 h-6 rounded hover:bg-gray-200 dark:hover:bg-gray-700
|
|
518
|
+
text-gray-600 dark:text-gray-400 flex items-center justify-center"
|
|
519
|
+
>
|
|
520
|
+
<MinusIcon />
|
|
521
|
+
</button>
|
|
522
|
+
<button
|
|
523
|
+
onClick={handleMaximize}
|
|
524
|
+
className="titlebar-button w-6 h-6 rounded hover:bg-gray-200 dark:hover:bg-gray-700
|
|
525
|
+
text-gray-600 dark:text-gray-400 flex items-center justify-center"
|
|
526
|
+
>
|
|
527
|
+
<SquareIcon />
|
|
528
|
+
</button>
|
|
529
|
+
<button
|
|
530
|
+
onClick={handleClose}
|
|
531
|
+
className="titlebar-button w-6 h-6 rounded hover:bg-red-500 hover:text-white
|
|
532
|
+
text-gray-600 dark:text-gray-400 flex items-center justify-center"
|
|
533
|
+
>
|
|
534
|
+
<XIcon />
|
|
535
|
+
</button>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const MinusIcon = () => (
|
|
542
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
543
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
|
|
544
|
+
</svg>
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
const SquareIcon = () => (
|
|
548
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
549
|
+
<rect x="4" y="4" width="16" height="16" rx="1" strokeWidth={2} />
|
|
550
|
+
</svg>
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const XIcon = () => (
|
|
554
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
555
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
556
|
+
</svg>
|
|
557
|
+
);
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**src/components/layout/Sidebar.tsx:**
|
|
561
|
+
```typescript
|
|
562
|
+
interface NavItem {
|
|
563
|
+
id: string;
|
|
564
|
+
label: string;
|
|
565
|
+
icon: React.ReactNode;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const navItems: NavItem[] = [
|
|
569
|
+
{ id: 'home', label: 'Home', icon: <HomeIcon /> },
|
|
570
|
+
{ id: 'files', label: 'Files', icon: <FolderIcon /> },
|
|
571
|
+
{ id: 'settings', label: 'Settings', icon: <SettingsIcon /> },
|
|
572
|
+
];
|
|
573
|
+
|
|
574
|
+
export function Sidebar() {
|
|
575
|
+
return (
|
|
576
|
+
<aside className="w-64 bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700">
|
|
577
|
+
<nav className="p-4">
|
|
578
|
+
<ul className="space-y-2">
|
|
579
|
+
{navItems.map((item) => (
|
|
580
|
+
<li key={item.id}>
|
|
581
|
+
<button
|
|
582
|
+
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg
|
|
583
|
+
text-gray-700 dark:text-gray-300
|
|
584
|
+
hover:bg-gray-100 dark:hover:bg-gray-700
|
|
585
|
+
transition-colors"
|
|
586
|
+
>
|
|
587
|
+
{item.icon}
|
|
588
|
+
<span>{item.label}</span>
|
|
589
|
+
</button>
|
|
590
|
+
</li>
|
|
591
|
+
))}
|
|
592
|
+
</ul>
|
|
593
|
+
</nav>
|
|
594
|
+
</aside>
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const HomeIcon = () => (
|
|
599
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
600
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
601
|
+
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
|
602
|
+
</svg>
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
const FolderIcon = () => (
|
|
606
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
607
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
608
|
+
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
609
|
+
</svg>
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
const SettingsIcon = () => (
|
|
613
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
614
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
615
|
+
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
616
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
617
|
+
</svg>
|
|
618
|
+
);
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
**src/components/ui/Button.tsx:**
|
|
622
|
+
```typescript
|
|
623
|
+
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
|
624
|
+
|
|
625
|
+
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
626
|
+
variant?: 'primary' | 'secondary' | 'ghost';
|
|
627
|
+
size?: 'sm' | 'md' | 'lg';
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
631
|
+
({ className = '', variant = 'primary', size = 'md', children, ...props }, ref) => {
|
|
632
|
+
const baseStyles = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
|
|
633
|
+
|
|
634
|
+
const variants = {
|
|
635
|
+
primary: 'bg-primary text-white hover:bg-blue-600 focus:ring-primary',
|
|
636
|
+
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
|
|
637
|
+
ghost: 'bg-transparent text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800',
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const sizes = {
|
|
641
|
+
sm: 'px-3 py-1.5 text-sm',
|
|
642
|
+
md: 'px-4 py-2 text-base',
|
|
643
|
+
lg: 'px-6 py-3 text-lg',
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
return (
|
|
647
|
+
<button
|
|
648
|
+
ref={ref}
|
|
649
|
+
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
|
|
650
|
+
{...props}
|
|
651
|
+
>
|
|
652
|
+
{children}
|
|
653
|
+
</button>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
Button.displayName = 'Button';
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**src/components/ui/Input.tsx:**
|
|
662
|
+
```typescript
|
|
663
|
+
import { InputHTMLAttributes, forwardRef } from 'react';
|
|
664
|
+
|
|
665
|
+
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
666
|
+
label?: string;
|
|
667
|
+
error?: string;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
671
|
+
({ className = '', label, error, ...props }, ref) => {
|
|
672
|
+
return (
|
|
673
|
+
<div className="flex flex-col gap-1">
|
|
674
|
+
{label && (
|
|
675
|
+
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
676
|
+
{label}
|
|
677
|
+
</label>
|
|
678
|
+
)}
|
|
679
|
+
<input
|
|
680
|
+
ref={ref}
|
|
681
|
+
className={`px-4 py-2 rounded-lg border
|
|
682
|
+
bg-white dark:bg-gray-800
|
|
683
|
+
text-gray-900 dark:text-white
|
|
684
|
+
border-gray-300 dark:border-gray-600
|
|
685
|
+
focus:ring-2 focus:ring-primary focus:border-transparent
|
|
686
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
687
|
+
${error ? 'border-red-500' : ''} ${className}`}
|
|
688
|
+
{...props}
|
|
689
|
+
/>
|
|
690
|
+
{error && <span className="text-sm text-red-500">{error}</span>}
|
|
691
|
+
</div>
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
Input.displayName = 'Input';
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### 9. Crear Hooks y Store
|
|
700
|
+
|
|
701
|
+
**src/hooks/useTauri.ts:**
|
|
702
|
+
```typescript
|
|
703
|
+
import { useState, useEffect } from 'react';
|
|
704
|
+
import { invoke } from '@tauri-apps/api/core';
|
|
705
|
+
|
|
706
|
+
interface AppInfo {
|
|
707
|
+
name: string;
|
|
708
|
+
version: string;
|
|
709
|
+
data_dir: string;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export function useTauri() {
|
|
713
|
+
const [appInfo, setAppInfo] = useState<AppInfo | null>(null);
|
|
714
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
715
|
+
const [error, setError] = useState<string | null>(null);
|
|
716
|
+
|
|
717
|
+
useEffect(() => {
|
|
718
|
+
async function fetchAppInfo() {
|
|
719
|
+
try {
|
|
720
|
+
const info = await invoke<AppInfo>('get_app_info');
|
|
721
|
+
setAppInfo(info);
|
|
722
|
+
} catch (err) {
|
|
723
|
+
setError(err as string);
|
|
724
|
+
} finally {
|
|
725
|
+
setIsLoading(false);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
fetchAppInfo();
|
|
730
|
+
}, []);
|
|
731
|
+
|
|
732
|
+
return { appInfo, isLoading, error };
|
|
733
|
+
}
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
**src/stores/appStore.ts:**
|
|
737
|
+
```typescript
|
|
738
|
+
import { create } from 'zustand';
|
|
739
|
+
import { persist } from 'zustand/middleware';
|
|
740
|
+
|
|
741
|
+
interface AppState {
|
|
742
|
+
theme: 'light' | 'dark';
|
|
743
|
+
sidebarOpen: boolean;
|
|
744
|
+
recentFiles: string[];
|
|
745
|
+
toggleTheme: () => void;
|
|
746
|
+
toggleSidebar: () => void;
|
|
747
|
+
addRecentFile: (path: string) => void;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export const useAppStore = create<AppState>()(
|
|
751
|
+
persist(
|
|
752
|
+
(set) => ({
|
|
753
|
+
theme: 'light',
|
|
754
|
+
sidebarOpen: true,
|
|
755
|
+
recentFiles: [],
|
|
756
|
+
|
|
757
|
+
toggleTheme: () =>
|
|
758
|
+
set((state) => ({
|
|
759
|
+
theme: state.theme === 'light' ? 'dark' : 'light',
|
|
760
|
+
})),
|
|
761
|
+
|
|
762
|
+
toggleSidebar: () =>
|
|
763
|
+
set((state) => ({
|
|
764
|
+
sidebarOpen: !state.sidebarOpen,
|
|
765
|
+
})),
|
|
766
|
+
|
|
767
|
+
addRecentFile: (path) =>
|
|
768
|
+
set((state) => ({
|
|
769
|
+
recentFiles: [path, ...state.recentFiles.filter((p) => p !== path)].slice(0, 10),
|
|
770
|
+
})),
|
|
771
|
+
}),
|
|
772
|
+
{
|
|
773
|
+
name: 'app-storage',
|
|
774
|
+
}
|
|
775
|
+
)
|
|
776
|
+
);
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
**src/lib/tauri.ts:**
|
|
780
|
+
```typescript
|
|
781
|
+
import { invoke } from '@tauri-apps/api/core';
|
|
782
|
+
|
|
783
|
+
export async function readFile(path: string): Promise<string> {
|
|
784
|
+
return invoke<string>('read_file', { path });
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export async function writeFile(path: string, contents: string): Promise<void> {
|
|
788
|
+
return invoke('write_file', { path, contents });
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export async function showNotification(title: string, body: string): Promise<void> {
|
|
792
|
+
const { sendNotification } = await import('@tauri-apps/plugin-notification');
|
|
793
|
+
await sendNotification({ title, body });
|
|
794
|
+
}
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### 10. Crear Tests
|
|
798
|
+
|
|
799
|
+
**tests/unit/components.test.tsx:**
|
|
800
|
+
```typescript
|
|
801
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
802
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
803
|
+
import { Button } from '../../src/components/ui/Button';
|
|
804
|
+
import { Input } from '../../src/components/ui/Input';
|
|
805
|
+
|
|
806
|
+
// Mock Tauri APIs
|
|
807
|
+
vi.mock('@tauri-apps/api/core', () => ({
|
|
808
|
+
invoke: vi.fn(),
|
|
809
|
+
}));
|
|
810
|
+
|
|
811
|
+
vi.mock('@tauri-apps/api/window', () => ({
|
|
812
|
+
getCurrentWindow: () => ({
|
|
813
|
+
minimize: vi.fn(),
|
|
814
|
+
toggleMaximize: vi.fn(),
|
|
815
|
+
close: vi.fn(),
|
|
816
|
+
}),
|
|
817
|
+
}));
|
|
818
|
+
|
|
819
|
+
describe('Button Component', () => {
|
|
820
|
+
it('renders with children', () => {
|
|
821
|
+
render(<Button>Click me</Button>);
|
|
822
|
+
expect(screen.getByText('Click me')).toBeInTheDocument();
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('handles click events', () => {
|
|
826
|
+
const handleClick = vi.fn();
|
|
827
|
+
render(<Button onClick={handleClick}>Click me</Button>);
|
|
828
|
+
|
|
829
|
+
fireEvent.click(screen.getByText('Click me'));
|
|
830
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('applies variant styles', () => {
|
|
834
|
+
const { rerender } = render(<Button variant="primary">Primary</Button>);
|
|
835
|
+
expect(screen.getByText('Primary')).toHaveClass('bg-primary');
|
|
836
|
+
|
|
837
|
+
rerender(<Button variant="secondary">Secondary</Button>);
|
|
838
|
+
expect(screen.getByText('Secondary')).toHaveClass('bg-gray-200');
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('applies size styles', () => {
|
|
842
|
+
const { rerender } = render(<Button size="sm">Small</Button>);
|
|
843
|
+
expect(screen.getByText('Small')).toHaveClass('text-sm');
|
|
844
|
+
|
|
845
|
+
rerender(<Button size="lg">Large</Button>);
|
|
846
|
+
expect(screen.getByText('Large')).toHaveClass('text-lg');
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
describe('Input Component', () => {
|
|
851
|
+
it('renders with label', () => {
|
|
852
|
+
render(<Input label="Email" />);
|
|
853
|
+
expect(screen.getByText('Email')).toBeInTheDocument();
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('shows error message', () => {
|
|
857
|
+
render(<Input error="Invalid input" />);
|
|
858
|
+
expect(screen.getByText('Invalid input')).toBeInTheDocument();
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it('forwards ref', () => {
|
|
862
|
+
const ref = { current: null };
|
|
863
|
+
render(<Input ref={ref} />);
|
|
864
|
+
expect(ref.current).toBeInstanceOf(HTMLInputElement);
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
**tests/e2e/app.spec.ts:**
|
|
870
|
+
```typescript
|
|
871
|
+
import { test, expect, _electron as electron } from '@playwright/test';
|
|
872
|
+
import { join } from 'path';
|
|
873
|
+
|
|
874
|
+
test.describe('Desktop App E2E', () => {
|
|
875
|
+
test.skip('should launch and display main window', async () => {
|
|
876
|
+
// This test requires a built app
|
|
877
|
+
const appPath = join(__dirname, '../../src-tauri/target/release/my-desktop-app');
|
|
878
|
+
|
|
879
|
+
const electronApp = await electron.launch({ executablePath: appPath });
|
|
880
|
+
const window = await electronApp.firstWindow();
|
|
881
|
+
|
|
882
|
+
// Wait for the app to load
|
|
883
|
+
await window.waitForSelector('text=My Desktop App');
|
|
884
|
+
|
|
885
|
+
// Take screenshot
|
|
886
|
+
await window.screenshot({ path: 'tests/screenshots/main-window.png' });
|
|
887
|
+
|
|
888
|
+
await electronApp.close();
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
**vitest.config.ts:**
|
|
894
|
+
```typescript
|
|
895
|
+
import { defineConfig } from 'vitest/config';
|
|
896
|
+
import react from '@vitejs/plugin-react';
|
|
897
|
+
|
|
898
|
+
export default defineConfig({
|
|
899
|
+
plugins: [react()],
|
|
900
|
+
test: {
|
|
901
|
+
globals: true,
|
|
902
|
+
environment: 'jsdom',
|
|
903
|
+
setupFiles: ['./tests/setup.ts'],
|
|
904
|
+
include: ['tests/unit/**/*.test.{ts,tsx}'],
|
|
905
|
+
coverage: {
|
|
906
|
+
provider: 'v8',
|
|
907
|
+
reporter: ['text', 'json', 'html'],
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
**tests/setup.ts:**
|
|
914
|
+
```typescript
|
|
915
|
+
import '@testing-library/jest-dom';
|
|
916
|
+
```
|
|
917
|
+
</setup_steps>
|
|
918
|
+
|
|
919
|
+
<verification>
|
|
920
|
+
## Verificacion
|
|
921
|
+
|
|
922
|
+
### Desarrollo Local
|
|
923
|
+
|
|
924
|
+
```bash
|
|
925
|
+
# Iniciar en modo desarrollo
|
|
926
|
+
npm run tauri dev
|
|
927
|
+
|
|
928
|
+
# Solo frontend (sin Tauri)
|
|
929
|
+
npm run dev
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
### Ejecutar Tests
|
|
933
|
+
|
|
934
|
+
```bash
|
|
935
|
+
# Tests unitarios
|
|
936
|
+
npm run test
|
|
937
|
+
|
|
938
|
+
# Tests con coverage
|
|
939
|
+
npm run test:coverage
|
|
940
|
+
|
|
941
|
+
# Tests E2E (requiere build)
|
|
942
|
+
npm run test:e2e
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
### Build de Produccion
|
|
946
|
+
|
|
947
|
+
```bash
|
|
948
|
+
# Build para la plataforma actual
|
|
949
|
+
npm run tauri build
|
|
950
|
+
|
|
951
|
+
# El instalador estara en:
|
|
952
|
+
# - macOS: src-tauri/target/release/bundle/dmg/
|
|
953
|
+
# - Windows: src-tauri/target/release/bundle/msi/
|
|
954
|
+
# - Linux: src-tauri/target/release/bundle/deb/ o appimage/
|
|
955
|
+
```
|
|
956
|
+
</verification>
|
|
957
|
+
|
|
958
|
+
<building>
|
|
959
|
+
## Build Multiplataforma
|
|
960
|
+
|
|
961
|
+
### Build Local
|
|
962
|
+
|
|
963
|
+
```bash
|
|
964
|
+
# Build para tu plataforma actual
|
|
965
|
+
npm run tauri build
|
|
966
|
+
|
|
967
|
+
# Build con debug info
|
|
968
|
+
npm run tauri build -- --debug
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
### Build para Windows
|
|
972
|
+
|
|
973
|
+
```bash
|
|
974
|
+
# Desde Windows
|
|
975
|
+
npm run tauri build
|
|
976
|
+
|
|
977
|
+
# Cross-compile desde Linux/macOS (requiere toolchain)
|
|
978
|
+
rustup target add x86_64-pc-windows-msvc
|
|
979
|
+
npm run tauri build -- --target x86_64-pc-windows-msvc
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
### Build para macOS
|
|
983
|
+
|
|
984
|
+
```bash
|
|
985
|
+
# Desde macOS
|
|
986
|
+
npm run tauri build
|
|
987
|
+
|
|
988
|
+
# Universal binary (Intel + Apple Silicon)
|
|
989
|
+
npm run tauri build -- --target universal-apple-darwin
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
### Build para Linux
|
|
993
|
+
|
|
994
|
+
```bash
|
|
995
|
+
# Desde Linux
|
|
996
|
+
npm run tauri build
|
|
997
|
+
|
|
998
|
+
# Targets especificos
|
|
999
|
+
npm run tauri build -- --bundles deb
|
|
1000
|
+
npm run tauri build -- --bundles appimage
|
|
1001
|
+
npm run tauri build -- --bundles rpm
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### GitHub Actions CI/CD
|
|
1005
|
+
|
|
1006
|
+
**.github/workflows/build.yml:**
|
|
1007
|
+
```yaml
|
|
1008
|
+
name: Build
|
|
1009
|
+
|
|
1010
|
+
on:
|
|
1011
|
+
push:
|
|
1012
|
+
tags:
|
|
1013
|
+
- 'v*'
|
|
1014
|
+
|
|
1015
|
+
jobs:
|
|
1016
|
+
build:
|
|
1017
|
+
strategy:
|
|
1018
|
+
fail-fast: false
|
|
1019
|
+
matrix:
|
|
1020
|
+
include:
|
|
1021
|
+
- platform: macos-latest
|
|
1022
|
+
args: --target universal-apple-darwin
|
|
1023
|
+
- platform: ubuntu-22.04
|
|
1024
|
+
args: ''
|
|
1025
|
+
- platform: windows-latest
|
|
1026
|
+
args: ''
|
|
1027
|
+
|
|
1028
|
+
runs-on: ${{ matrix.platform }}
|
|
1029
|
+
|
|
1030
|
+
steps:
|
|
1031
|
+
- uses: actions/checkout@v4
|
|
1032
|
+
|
|
1033
|
+
- name: Setup Node
|
|
1034
|
+
uses: actions/setup-node@v4
|
|
1035
|
+
with:
|
|
1036
|
+
node-version: 20
|
|
1037
|
+
|
|
1038
|
+
- name: Install Rust
|
|
1039
|
+
uses: dtolnay/rust-action@stable
|
|
1040
|
+
|
|
1041
|
+
- name: Install dependencies (Ubuntu)
|
|
1042
|
+
if: matrix.platform == 'ubuntu-22.04'
|
|
1043
|
+
run: |
|
|
1044
|
+
sudo apt-get update
|
|
1045
|
+
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
|
1046
|
+
|
|
1047
|
+
- name: Install frontend dependencies
|
|
1048
|
+
run: npm ci
|
|
1049
|
+
|
|
1050
|
+
- name: Build Tauri app
|
|
1051
|
+
uses: tauri-apps/tauri-action@v0
|
|
1052
|
+
env:
|
|
1053
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
1054
|
+
with:
|
|
1055
|
+
tagName: v__VERSION__
|
|
1056
|
+
releaseName: 'v__VERSION__'
|
|
1057
|
+
releaseBody: 'See the assets to download this version.'
|
|
1058
|
+
releaseDraft: true
|
|
1059
|
+
prerelease: false
|
|
1060
|
+
args: ${{ matrix.args }}
|
|
1061
|
+
```
|
|
1062
|
+
</building>
|
|
1063
|
+
|
|
1064
|
+
<common_issues>
|
|
1065
|
+
## Problemas Comunes
|
|
1066
|
+
|
|
1067
|
+
### WebView2 no instalado (Windows)
|
|
1068
|
+
|
|
1069
|
+
**Problema:** App no inicia en Windows.
|
|
1070
|
+
|
|
1071
|
+
**Solucion:**
|
|
1072
|
+
```bash
|
|
1073
|
+
# Bundlear WebView2 con la app
|
|
1074
|
+
# En tauri.conf.json:
|
|
1075
|
+
{
|
|
1076
|
+
"bundle": {
|
|
1077
|
+
"windows": {
|
|
1078
|
+
"webviewInstallMode": {
|
|
1079
|
+
"type": "embedBootstrapper"
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
### Permisos de archivo denegados
|
|
1087
|
+
|
|
1088
|
+
**Problema:** No puede leer/escribir archivos.
|
|
1089
|
+
|
|
1090
|
+
**Solucion:** Verificar capabilities en `src-tauri/capabilities/default.json`:
|
|
1091
|
+
```json
|
|
1092
|
+
{
|
|
1093
|
+
"permissions": [
|
|
1094
|
+
"fs:default",
|
|
1095
|
+
"fs:allow-read-text-file",
|
|
1096
|
+
"fs:allow-write-text-file"
|
|
1097
|
+
]
|
|
1098
|
+
}
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
### Build falla en Linux
|
|
1102
|
+
|
|
1103
|
+
**Problema:** Missing webkit2gtk.
|
|
1104
|
+
|
|
1105
|
+
**Solucion:**
|
|
1106
|
+
```bash
|
|
1107
|
+
# Ubuntu/Debian
|
|
1108
|
+
sudo apt install libwebkit2gtk-4.1-dev
|
|
1109
|
+
|
|
1110
|
+
# Fedora
|
|
1111
|
+
sudo dnf install webkit2gtk4.1-devel
|
|
1112
|
+
|
|
1113
|
+
# Arch
|
|
1114
|
+
sudo pacman -S webkit2gtk-4.1
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
### App muy grande
|
|
1118
|
+
|
|
1119
|
+
**Problema:** El bundle es muy pesado.
|
|
1120
|
+
|
|
1121
|
+
**Solucion:**
|
|
1122
|
+
```toml
|
|
1123
|
+
# Cargo.toml - Release profile
|
|
1124
|
+
[profile.release]
|
|
1125
|
+
lto = true
|
|
1126
|
+
opt-level = "s"
|
|
1127
|
+
codegen-units = 1
|
|
1128
|
+
panic = "abort"
|
|
1129
|
+
strip = true
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
### Hot reload no funciona
|
|
1133
|
+
|
|
1134
|
+
**Problema:** Cambios no se reflejan.
|
|
1135
|
+
|
|
1136
|
+
**Solucion:**
|
|
1137
|
+
```typescript
|
|
1138
|
+
// vite.config.ts
|
|
1139
|
+
export default defineConfig({
|
|
1140
|
+
server: {
|
|
1141
|
+
watch: {
|
|
1142
|
+
// No ignorar src-tauri si necesitas recargar el backend
|
|
1143
|
+
ignored: ['**/src-tauri/target/**'],
|
|
1144
|
+
},
|
|
1145
|
+
},
|
|
1146
|
+
});
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
### Invoke no encuentra comando
|
|
1150
|
+
|
|
1151
|
+
**Problema:** `invoke('my_command')` falla.
|
|
1152
|
+
|
|
1153
|
+
**Solucion:**
|
|
1154
|
+
```rust
|
|
1155
|
+
// Verificar que el comando esta registrado en main.rs
|
|
1156
|
+
.invoke_handler(tauri::generate_handler![
|
|
1157
|
+
my_command, // <- Debe estar aqui
|
|
1158
|
+
])
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
### Iconos no aparecen
|
|
1162
|
+
|
|
1163
|
+
**Problema:** App sin icono en dock/taskbar.
|
|
1164
|
+
|
|
1165
|
+
**Solucion:**
|
|
1166
|
+
```bash
|
|
1167
|
+
# Generar iconos en todos los tamanos
|
|
1168
|
+
npm run tauri icon /path/to/icon.png
|
|
1169
|
+
|
|
1170
|
+
# O manualmente agregar en src-tauri/icons/:
|
|
1171
|
+
# - 32x32.png
|
|
1172
|
+
# - 128x128.png
|
|
1173
|
+
# - 128x128@2x.png
|
|
1174
|
+
# - icon.icns (macOS)
|
|
1175
|
+
# - icon.ico (Windows)
|
|
1176
|
+
```
|
|
1177
|
+
</common_issues>
|
|
1178
|
+
|
|
1179
|
+
<best_practices>
|
|
1180
|
+
## Mejores Practicas
|
|
1181
|
+
|
|
1182
|
+
### Seguridad
|
|
1183
|
+
|
|
1184
|
+
```rust
|
|
1185
|
+
// Validar paths antes de operaciones de archivo
|
|
1186
|
+
#[tauri::command]
|
|
1187
|
+
fn read_file(app: tauri::AppHandle, path: String) -> Result<String, String> {
|
|
1188
|
+
let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
|
1189
|
+
|
|
1190
|
+
// Solo permitir archivos dentro del directorio de la app
|
|
1191
|
+
let canonical = std::fs::canonicalize(&path).map_err(|e| e.to_string())?;
|
|
1192
|
+
if !canonical.starts_with(&app_dir) {
|
|
1193
|
+
return Err("Access denied".to_string());
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
std::fs::read_to_string(&path).map_err(|e| e.to_string())
|
|
1197
|
+
}
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
### Performance
|
|
1201
|
+
|
|
1202
|
+
```typescript
|
|
1203
|
+
// Lazy load modulos pesados
|
|
1204
|
+
const loadHeavyModule = async () => {
|
|
1205
|
+
const { HeavyComponent } = await import('./HeavyComponent');
|
|
1206
|
+
return HeavyComponent;
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
// Usar web workers para operaciones pesadas
|
|
1210
|
+
const worker = new Worker(new URL('./worker.ts', import.meta.url));
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
### Estado Persistente
|
|
1214
|
+
|
|
1215
|
+
```typescript
|
|
1216
|
+
// Usar Zustand persist para guardar estado
|
|
1217
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
1218
|
+
|
|
1219
|
+
const useStore = create(
|
|
1220
|
+
persist(
|
|
1221
|
+
(set) => ({
|
|
1222
|
+
// state
|
|
1223
|
+
}),
|
|
1224
|
+
{
|
|
1225
|
+
name: 'app-storage',
|
|
1226
|
+
storage: createJSONStorage(() => localStorage),
|
|
1227
|
+
}
|
|
1228
|
+
)
|
|
1229
|
+
);
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
### Comunicacion IPC Eficiente
|
|
1233
|
+
|
|
1234
|
+
```rust
|
|
1235
|
+
// Usar eventos para comunicacion continua
|
|
1236
|
+
#[tauri::command]
|
|
1237
|
+
fn start_watching(window: tauri::Window) {
|
|
1238
|
+
std::thread::spawn(move || {
|
|
1239
|
+
loop {
|
|
1240
|
+
window.emit("update", serde_json::json!({ "status": "ok" })).unwrap();
|
|
1241
|
+
std::thread::sleep(std::time::Duration::from_secs(1));
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
```typescript
|
|
1248
|
+
// Frontend - escuchar eventos
|
|
1249
|
+
import { listen } from '@tauri-apps/api/event';
|
|
1250
|
+
|
|
1251
|
+
const unlisten = await listen('update', (event) => {
|
|
1252
|
+
console.log('Update:', event.payload);
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
// Cleanup
|
|
1256
|
+
unlisten();
|
|
1257
|
+
```
|
|
1258
|
+
</best_practices>
|
|
1259
|
+
|
|
1260
|
+
<examples>
|
|
1261
|
+
## Ejemplos de Uso
|
|
1262
|
+
|
|
1263
|
+
### App Basica
|
|
1264
|
+
|
|
1265
|
+
```bash
|
|
1266
|
+
# Crear proyecto
|
|
1267
|
+
npm create tauri-app@latest my-app -- --template react-ts
|
|
1268
|
+
cd my-app
|
|
1269
|
+
|
|
1270
|
+
# Desarrollo
|
|
1271
|
+
npm run tauri dev
|
|
1272
|
+
|
|
1273
|
+
# Build
|
|
1274
|
+
npm run tauri build
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
### Con Base de Datos SQLite
|
|
1278
|
+
|
|
1279
|
+
```toml
|
|
1280
|
+
# Cargo.toml
|
|
1281
|
+
[dependencies]
|
|
1282
|
+
rusqlite = { version = "0.31", features = ["bundled"] }
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
```rust
|
|
1286
|
+
use rusqlite::Connection;
|
|
1287
|
+
|
|
1288
|
+
#[tauri::command]
|
|
1289
|
+
fn init_db(app: tauri::AppHandle) -> Result<(), String> {
|
|
1290
|
+
let db_path = app.path().app_data_dir()
|
|
1291
|
+
.map_err(|e| e.to_string())?
|
|
1292
|
+
.join("data.db");
|
|
1293
|
+
|
|
1294
|
+
let conn = Connection::open(db_path).map_err(|e| e.to_string())?;
|
|
1295
|
+
|
|
1296
|
+
conn.execute(
|
|
1297
|
+
"CREATE TABLE IF NOT EXISTS items (
|
|
1298
|
+
id INTEGER PRIMARY KEY,
|
|
1299
|
+
name TEXT NOT NULL,
|
|
1300
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
1301
|
+
)",
|
|
1302
|
+
[],
|
|
1303
|
+
).map_err(|e| e.to_string())?;
|
|
1304
|
+
|
|
1305
|
+
Ok(())
|
|
1306
|
+
}
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
### Con System Tray
|
|
1310
|
+
|
|
1311
|
+
```json
|
|
1312
|
+
// tauri.conf.json
|
|
1313
|
+
{
|
|
1314
|
+
"app": {
|
|
1315
|
+
"trayIcon": {
|
|
1316
|
+
"iconPath": "icons/tray-icon.png",
|
|
1317
|
+
"iconAsTemplate": true
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
```rust
|
|
1324
|
+
// main.rs
|
|
1325
|
+
use tauri::{
|
|
1326
|
+
menu::{Menu, MenuItem},
|
|
1327
|
+
tray::TrayIconBuilder,
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
fn main() {
|
|
1331
|
+
tauri::Builder::default()
|
|
1332
|
+
.setup(|app| {
|
|
1333
|
+
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
|
|
1334
|
+
let menu = Menu::with_items(app, &[&quit])?;
|
|
1335
|
+
|
|
1336
|
+
TrayIconBuilder::new()
|
|
1337
|
+
.icon(app.default_window_icon().unwrap().clone())
|
|
1338
|
+
.menu(&menu)
|
|
1339
|
+
.on_menu_event(|app, event| match event.id.as_ref() {
|
|
1340
|
+
"quit" => app.exit(0),
|
|
1341
|
+
_ => {}
|
|
1342
|
+
})
|
|
1343
|
+
.build(app)?;
|
|
1344
|
+
|
|
1345
|
+
Ok(())
|
|
1346
|
+
})
|
|
1347
|
+
.run(tauri::generate_context!())
|
|
1348
|
+
.expect("error while running tauri application");
|
|
1349
|
+
}
|
|
1350
|
+
```
|
|
1351
|
+
</examples>
|