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.
Files changed (64) hide show
  1. package/agents/elsabro-orchestrator.md +113 -0
  2. package/commands/elsabro/add-phase.md +17 -0
  3. package/commands/elsabro/add-todo.md +111 -53
  4. package/commands/elsabro/audit-milestone.md +19 -0
  5. package/commands/elsabro/check-todos.md +210 -31
  6. package/commands/elsabro/complete-milestone.md +20 -1
  7. package/commands/elsabro/debug.md +19 -0
  8. package/commands/elsabro/discuss-phase.md +18 -1
  9. package/commands/elsabro/execute.md +511 -58
  10. package/commands/elsabro/insert-phase.md +18 -1
  11. package/commands/elsabro/list-phase-assumptions.md +17 -0
  12. package/commands/elsabro/new-milestone.md +19 -0
  13. package/commands/elsabro/new.md +19 -0
  14. package/commands/elsabro/pause-work.md +19 -0
  15. package/commands/elsabro/plan-milestone-gaps.md +20 -1
  16. package/commands/elsabro/plan.md +264 -36
  17. package/commands/elsabro/progress.md +203 -79
  18. package/commands/elsabro/quick.md +19 -0
  19. package/commands/elsabro/remove-phase.md +17 -0
  20. package/commands/elsabro/research-phase.md +18 -1
  21. package/commands/elsabro/resume-work.md +19 -0
  22. package/commands/elsabro/start.md +399 -98
  23. package/commands/elsabro/verify-work.md +138 -5
  24. package/hooks/confirm-destructive.sh +145 -0
  25. package/hooks/hooks-config.json +81 -0
  26. package/hooks/lint-check.sh +238 -0
  27. package/hooks/post-edit-test.sh +189 -0
  28. package/package.json +3 -2
  29. package/references/SYSTEM_INDEX.md +241 -0
  30. package/references/command-flow.md +352 -0
  31. package/references/enforcement-rules.md +331 -0
  32. package/references/error-contracts-tests.md +1171 -0
  33. package/references/error-contracts.md +3102 -0
  34. package/references/error-handling-instructions.md +26 -12
  35. package/references/parallel-worktrees.md +293 -0
  36. package/references/state-sync.md +381 -0
  37. package/references/task-dispatcher.md +388 -0
  38. package/references/tasks-integration.md +380 -0
  39. package/scripts/setup-parallel-worktrees.sh +319 -0
  40. package/skills/api-microservice.md +765 -0
  41. package/skills/api-setup.md +76 -3
  42. package/skills/auth-setup.md +46 -6
  43. package/skills/chrome-extension.md +584 -0
  44. package/skills/cicd-setup.md +1206 -0
  45. package/skills/cli-tool.md +884 -0
  46. package/skills/database-setup.md +41 -5
  47. package/skills/desktop-app.md +1351 -0
  48. package/skills/expo-app.md +35 -2
  49. package/skills/full-stack-app.md +543 -0
  50. package/skills/memory-update.md +207 -0
  51. package/skills/mobile-app.md +813 -0
  52. package/skills/nextjs-app.md +33 -2
  53. package/skills/payments-setup.md +76 -1
  54. package/skills/review.md +331 -0
  55. package/skills/saas-starter.md +639 -0
  56. package/skills/sentry-setup.md +41 -7
  57. package/skills/techdebt.md +289 -0
  58. package/skills/testing-setup.md +1218 -0
  59. package/skills/tutor.md +219 -0
  60. package/templates/.planning/notes/.gitkeep +0 -0
  61. package/templates/CLAUDE.md.template +48 -0
  62. package/templates/error-handling-config.json +79 -2
  63. package/templates/mistakes.md.template +52 -0
  64. 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>