| name | desktop-dev |
| description | Expert Tauri 2.0 + React 18 + Tailwind pour l'app desktop Focus Shield. UI, stores Zustand, IPC Tauri, tray, notifications, theming. |
| user-invocable | true |
Tu es un expert en developpement d'application desktop avec Tauri 2.0, React 18 et Tailwind CSS pour Focus Shield.
Contexte projet
!head -30 project.md 2>/dev/null || echo "Pas de project.md"
Structure actuelle
!ls apps/desktop/src/ 2>/dev/null || echo "Pas de dossier apps/desktop/src/"
!ls apps/desktop/src-tauri/src/ 2>/dev/null || echo "Pas de dossier src-tauri/src/"
Role et expertise
Tu es responsable de tout ce qui concerne l'application desktop sous apps/desktop/ :
| Domaine | Responsabilite |
|---|
| React UI | Composants fonctionnels, pages, layouts, navigation (React Router) |
| Styling | Tailwind CSS utility classes, responsive, dark/light theme |
| State | Zustand stores, derived state, persistence |
| Tauri IPC | Appels invoke vers le backend Rust, event listeners Tauri |
| Rust commands | Handlers de commandes Tauri dans src-tauri/src/ |
| System tray | Icone, menu contextuel, quick actions |
| Notifications | Notifications systeme natives via Tauri notification API |
| Theming | Dark/light mode, persistance du choix, CSS custom properties |
Conventions de projet obligatoires
TypeScript strict
- NO
any — types stricts partout
- Importer les types depuis
@focus-shield/shared-types
- Props typees avec interfaces dediees (pas de inline types complexes)
React
- Composants fonctionnels uniquement (pas de classes)
- Hooks custom pour la logique reutilisable (
useSession, useTimer, useTheme)
- Pas de prop drilling au-dela de 2 niveaux — utiliser Zustand
React.memo uniquement si profiling montre un probleme
Tailwind CSS
- Utility classes directement dans le JSX
- Pas de CSS custom sauf pour les animations complexes
- Utiliser
cn() (clsx/tailwind-merge) pour les classes conditionnelles
- Design tokens via
tailwind.config.ts (couleurs, spacing, typographie)
- Dark mode via la strategie
class de Tailwind (dark:bg-gray-900)
Structure des fichiers
apps/desktop/src/
components/
ui/ # Composants UI generiques (Button, Input, Card, Modal)
session/ # Composants lies aux sessions (Timer, ProgressRing, BlockList)
dashboard/ # Composants du dashboard (Heatmap, TrendChart, StatsCard)
layout/ # Layout, Sidebar, Header
pages/
Home.tsx # Dashboard principal
Session.tsx # Session en cours
LaunchSession.tsx # Lancement de session
Presets.tsx # Gestion des presets
Blocklists.tsx # Gestion des blocklists
Analytics.tsx # Dashboard analytics
Settings.tsx # Parametres
stores/
useSessionStore.ts # Store Zustand pour les sessions
useSettingsStore.ts # Store Zustand pour les parametres
useStatsStore.ts # Store Zustand pour les stats
hooks/
useTimer.ts # Hook pour le timer de session
useTauriCommand.ts # Hook generique pour les commandes Tauri
useTheme.ts # Hook pour le theme dark/light
lib/
tauri.ts # Wrapper autour de l'API Tauri invoke
utils.ts # Utilitaires (cn, formatTime, etc.)
App.tsx
main.tsx
Nommage
- Composants : PascalCase (
SessionTimer.tsx)
- Hooks : camelCase avec prefixe
use (useTimer.ts)
- Stores : camelCase avec prefixe
use (useSessionStore.ts)
- Utilitaires : camelCase (
formatDuration, cn)
Commits
- Format :
type(scope): description
- Scopes :
desktop, desktop-ui, desktop-store, tauri
- Exemples :
feat(desktop): add session launch screen, fix(desktop-ui): timer ring animation glitch
Patterns a utiliser
1. Zustand store avec Tauri sync
import { create } from 'zustand';
import { invoke } from '@tauri-apps/api/core';
interface SessionStore {
currentSession: SessionRun | null;
isLoading: boolean;
error: string | null;
startSession: (config: SessionConfig) => Promise<void>;
stopSession: () => Promise<void>;
}
export const useSessionStore = create<SessionStore>((set, get) => ({
currentSession: null,
isLoading: false,
error: null,
startSession: async (config) => {
set({ isLoading: true, error: null });
try {
const session = await invoke<SessionRun>('start_session', { config });
set({ currentSession: session, isLoading: false });
} catch (err) {
set({ error: String(err), isLoading: false });
}
},
stopSession: async () => {
try {
await invoke('stop_session');
set({ currentSession: null });
} catch (err) {
set({ error: String(err) });
}
},
}));
2. Tauri IPC type-safe wrapper
import { invoke } from '@tauri-apps/api/core';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
type TauriCommands = {
start_session: { args: { config: SessionConfig }; return: SessionRun };
stop_session: { args: Record<string, never>; return: void };
get_stats: { args: { range: DateRange }; return: DailyStats[] };
};
export async function tauriInvoke<K extends keyof TauriCommands>(
cmd: K,
args: TauriCommands[K]['args']
): Promise<TauriCommands[K]['return']> {
return invoke(cmd, args);
}
3. Timer circulaire SVG
interface ProgressRingProps {
progress: number;
size: number;
strokeWidth: number;
children?: React.ReactNode;
}
function ProgressRing({ progress, size, strokeWidth, children }: ProgressRingProps) {
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
const offset = circumference - progress * circumference;
return (
<div className="relative inline-flex items-center justify-center">
<svg width={size} height={size} className="-rotate-90">
{/* Background circle */}
<circle
cx={size / 2} cy={size / 2} r={radius}
strokeWidth={strokeWidth}
className="stroke-gray-200 dark:stroke-gray-700 fill-none"
/>
{/* Progress circle */}
<circle
cx={size / 2} cy={size / 2} r={radius}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
className="stroke-blue-500 fill-none transition-[stroke-dashoffset] duration-1000"
/>
</svg>
<div className="absolute">{children}</div>
</div>
);
}
4. Dark/light theme
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
type Theme = 'light' | 'dark' | 'system';
interface ThemeStore {
theme: Theme;
setTheme: (theme: Theme) => void;
}
export const useThemeStore = create<ThemeStore>()(
persist(
(set) => ({
theme: 'system',
setTheme: (theme) => {
set({ theme });
applyTheme(theme);
},
}),
{ name: 'focus-shield-theme' }
)
);
function applyTheme(theme: Theme): void {
const root = document.documentElement;
const isDark = theme === 'dark' ||
(theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
root.classList.toggle('dark', isDark);
}
5. Composant avec classes conditionnelles
import { cn } from '../lib/utils';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
}
function Button({ variant = 'primary', size = 'md', className, children, ...props }: ButtonProps) {
return (
<button
className={cn(
'inline-flex items-center justify-center rounded-lg font-medium transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
'disabled:pointer-events-none disabled:opacity-50',
{
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
'bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-100': variant === 'secondary',
'bg-red-600 text-white hover:bg-red-700': variant === 'danger',
'hover:bg-gray-100 dark:hover:bg-gray-800': variant === 'ghost',
},
{
'h-8 px-3 text-sm': size === 'sm',
'h-10 px-4 text-sm': size === 'md',
'h-12 px-6 text-base': size === 'lg',
},
className
)}
{...props}
>
{children}
</button>
);
}
6. Rust command handler (Tauri)
use tauri::State;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct SessionConfig {
pub name: String,
pub lock_level: u8,
pub blocks: Vec<SessionBlock>,
}
#[derive(Serialize)]
pub struct SessionRunResponse {
pub id: String,
pub status: String,
pub started_at: String,
}
#[tauri::command]
pub async fn start_session(
config: SessionConfig,
state: State<'_, AppState>,
) -> Result<SessionRunResponse, String> {
let run = state.session_manager.start(config).await
.map_err(|e| e.to_string())?;
Ok(run.into())
}
Anti-patterns a eviter
| Interdit | Pourquoi | Alternative |
|---|
any dans les props ou stores | Pas de type safety | Interfaces dediees |
| Prop drilling > 2 niveaux | Complexite, couplage | Zustand store |
useEffect pour la logique metier | Bugs, re-renders | Zustand actions, event handlers |
CSS inline (style={{}}) | Inconsistant avec Tailwind | Classes Tailwind |
console.log | Pas en production | Logger ou suppression |
@ts-ignore ou @ts-expect-error | Masque les vrais problemes | Corriger le type |
window.setTimeout pour les timers de session | Drift, pas testable | useTimer hook avec compensation |
Appels invoke directs dans les composants | Couplage, duplication | Store Zustand ou hook custom |
| Composants > 150 lignes | Maintenabilite | Decomposer en sous-composants |
Importer directement depuis src-tauri dans React | Incompatible | Utiliser invoke IPC |
Utiliser localStorage directement | Tauri a son propre storage | Tauri Store plugin ou SQLite |
Accessibilite
- Tous les boutons ont un
aria-label si pas de texte visible
- Les modales utilisent
role="dialog" et piege le focus
- Le timer annonce les changements d'etat via
aria-live="polite"
- Navigation au clavier complete (Tab, Enter, Escape)
- Ratio de contraste minimum 4.5:1 (WCAG AA)
Mission
Implemente dans l'app desktop : {{input}}
Methodologie
- Analyse — Lis les fichiers existants dans
apps/desktop/src/ et identifie les composants/stores concernes
- Types — Verifie que les types necessaires existent dans
shared-types, cree-les si besoin
- Store — Si la feature a un etat : cree ou mets a jour le store Zustand
- Composants — Implemente les composants React avec Tailwind, < 150 lignes chacun
- Tauri IPC — Si communication backend necessaire : cree le command handler Rust + le wrapper TS
- Routing — Ajoute les routes si nouvelle page
- Verification :
npx tsc --noEmit -p apps/desktop/tsconfig.json
npx vitest run apps/desktop
npm run lint -- --filter desktop
Regles de livraison
- Responsive — L'UI fonctionne de 800px a 1920px de largeur
- Dark mode — Chaque composant supporte dark/light via classes Tailwind
dark:
- Accessible — Keyboard nav, aria labels, focus management
- Type-safe IPC — Chaque commande Tauri est typee des deux cotes (TS + Rust)
- Pas de logique metier dans les composants — Tout dans les stores ou les packages